1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5use dokuwiki\plugin\sqlite\SQLiteDB; 6use dokuwiki\plugin\struct\types\AbstractBaseType; 7use dokuwiki\Utf8\PhpString; 8 9/** 10 * Class Schema 11 * 12 * Represents the schema of a single data table and all its properties. It defines what can be stored in 13 * the represented data table and how those contents are formatted. 14 * 15 * It can be initialized with a timestamp to access the schema as it looked at that particular point in time. 16 * 17 * @package dokuwiki\plugin\struct\meta 18 */ 19class Schema 20{ 21 use TranslationUtilities; 22 23 /** @var SQLiteDB|null */ 24 protected $sqlite; 25 26 /** @var int The ID of this schema */ 27 protected $id = 0; 28 29 /** @var string the user who last edited this schema */ 30 protected $user = ''; 31 32 /** @var string name of the associated table */ 33 protected $table = ''; 34 35 /** @var Column[] all the colums */ 36 protected $columns = array(); 37 38 /** @var int */ 39 protected $maxsort = 0; 40 41 /** @var int */ 42 protected $ts = 0; 43 44 /** @var string struct version info */ 45 protected $structversion = '?'; 46 47 /** @var array config array with label translations */ 48 protected $config = array(); 49 50 /** 51 * Schema constructor 52 * 53 * @param string $table The table this schema is for 54 * @param int $ts The timestamp for when this schema was valid, 0 for current 55 */ 56 public function __construct($table, $ts = 0) 57 { 58 $baseconfig = array('allowed editors' => '', 'internal' => false); 59 60 /** @var \helper_plugin_struct_db $helper */ 61 $helper = plugin_load('helper', 'struct_db'); 62 $info = $helper->getInfo(); 63 $this->structversion = $info['date']; 64 $this->sqlite = $helper->getDB(); 65 $table = self::cleanTableName($table); 66 $this->table = $table; 67 $this->ts = $ts; 68 69 // load info about the schema itself 70 if ($ts) { 71 $sql = "SELECT * 72 FROM schemas 73 WHERE tbl = ? 74 AND ts <= ? 75 ORDER BY ts DESC 76 LIMIT 1"; 77 $opt = array($table, $ts); 78 } else { 79 $sql = "SELECT * 80 FROM schemas 81 WHERE tbl = ? 82 ORDER BY ts DESC 83 LIMIT 1"; 84 $opt = array($table); 85 } 86 $schema = $this->sqlite->queryAll($sql, $opt); 87 $config = array(); 88 89 if (!empty($schema)) { 90 $result = array_shift($schema); 91 $this->id = $result['id']; 92 $this->user = $result['user']; 93 $this->ts = $result['ts']; 94 $config = json_decode($result['config'], true); 95 } 96 $this->config = array_merge($baseconfig, $config); 97 $this->initTransConfig(array('label')); 98 if (!$this->id) return; 99 100 // load existing columns 101 $sql = "SELECT SC.*, T.* 102 FROM schema_cols SC, 103 types T 104 WHERE SC.sid = ? 105 AND SC.tid = T.id 106 ORDER BY SC.sort"; 107 $rows = $this->sqlite->queryAll($sql, [$this->id]); 108 109 $typeclasses = Column::allTypes(); 110 foreach ($rows as $row) { 111 if ($row['class'] == 'Integer') { 112 $row['class'] = 'Decimal'; 113 } 114 115 $class = $typeclasses[$row['class']]; 116 if (!class_exists($class)) { 117 // This usually never happens, except during development 118 msg('Unknown type "' . hsc($row['class']) . '" falling back to Text', -1); 119 $class = 'dokuwiki\\plugin\\struct\\types\\Text'; 120 } 121 122 $config = json_decode($row['config'], true); 123 /** @var AbstractBaseType $type */ 124 $type = new $class($config, $row['label'], $row['ismulti'], $row['tid']); 125 $column = new Column( 126 $row['sort'], 127 $type, 128 $row['colref'], 129 $row['enabled'], 130 $table 131 ); 132 $type->setContext($column); 133 134 $this->columns[] = $column; 135 if ($row['sort'] > $this->maxsort) $this->maxsort = $row['sort']; 136 } 137 } 138 139 /** 140 * @return string identifer for debugging purposes 141 */ 142 public function __toString() 143 { 144 return __CLASS__ . ' ' . $this->table . ' (' . $this->id . ') '; 145 } 146 147 /** 148 * Cleans any unwanted stuff from table names 149 * 150 * @param string $table 151 * @return string 152 */ 153 public static function cleanTableName($table) 154 { 155 $table = strtolower($table); 156 $table = preg_replace('/[^a-z0-9_]+/', '', $table); 157 $table = preg_replace('/^[0-9_]+/', '', $table); 158 $table = trim($table); 159 return $table; 160 } 161 162 163 /** 164 * Gets a list of all available schemas 165 * 166 * @return \string[] 167 */ 168 public static function getAll() 169 { 170 /** @var \helper_plugin_struct_db $helper */ 171 $helper = plugin_load('helper', 'struct_db'); 172 $db = $helper->getDB(false); 173 if (!$db) return array(); 174 175 $tables = $db->queryAll("SELECT DISTINCT tbl FROM schemas ORDER BY tbl"); 176 177 $result = array(); 178 foreach ($tables as $row) { 179 $result[] = $row['tbl']; 180 } 181 return $result; 182 } 183 184 /** 185 * Delete all data associated with this schema 186 * 187 * This is really all data ever! Be careful! 188 */ 189 public function delete() 190 { 191 if (!$this->id) throw new StructException('can not delete unsaved schema'); 192 193 $this->sqlite->query('BEGIN TRANSACTION'); 194 195 $sql = "DROP TABLE "; 196 $this->sqlite->query($sql . 'data_' . $this->table); 197 $this->sqlite->query($sql . 'multi_' . $this->table); 198 199 $sql = "DELETE FROM schema_assignments WHERE tbl = '$this->table'"; 200 $this->sqlite->query($sql); 201 202 $sql = "DELETE FROM schema_assignments_patterns WHERE tbl = '$this->table'"; 203 $this->sqlite->query($sql); 204 205 $sql = "SELECT T.id 206 FROM types T, schema_cols SC, schemas S 207 WHERE T.id = SC.tid 208 AND SC.sid = S.id 209 AND S.tbl = ?"; 210 $sql = "DELETE FROM types WHERE id IN ($sql)"; 211 $this->sqlite->query($sql, [$this->table]); 212 213 $sql = "SELECT id 214 FROM schemas 215 WHERE tbl = ?"; 216 $sql = "DELETE FROM schema_cols WHERE sid IN ($sql)"; 217 $this->sqlite->query($sql, [$this->table]); 218 219 $sql = "DELETE FROM schemas WHERE tbl = ?"; 220 $this->sqlite->query($sql, [$this->table]); 221 222 $this->sqlite->query('COMMIT TRANSACTION'); 223 $this->sqlite->query('VACUUM'); 224 225 // a deleted schema should not be used anymore, but let's make sure it's somewhat sane anyway 226 $this->id = 0; 227 $this->columns = array(); 228 $this->maxsort = 0; 229 $this->ts = 0; 230 } 231 232 233 /** 234 * Clear all data of a schema, but retain the schema itself 235 */ 236 public function clear() 237 { 238 if (!$this->id) throw new StructException('can not clear data of unsaved schema'); 239 240 $this->sqlite->query('BEGIN TRANSACTION'); 241 $sql = 'DELETE FROM '; 242 $this->sqlite->query($sql . 'data_' . $this->table); 243 $this->sqlite->query($sql . 'multi_' . $this->table); 244 $this->sqlite->query('COMMIT TRANSACTION'); 245 $this->sqlite->query('VACUUM'); 246 } 247 248 /** 249 * @return int 250 */ 251 public function getId() 252 { 253 return $this->id; 254 } 255 256 /** 257 * @return int returns the timestamp this Schema was created at 258 */ 259 public function getTimeStamp() 260 { 261 return $this->ts; 262 } 263 264 /** 265 * @return string 266 */ 267 public function getUser() 268 { 269 return $this->user; 270 } 271 272 public function getConfig() 273 { 274 return $this->config; 275 } 276 277 /** 278 * Returns the translated label for this schema 279 * 280 * Uses the current language as determined by $conf['lang']. Falls back to english 281 * and then to the Schema label 282 * 283 * @return string 284 */ 285 public function getTranslatedLabel() 286 { 287 return $this->getTranslatedKey('label', $this->table); 288 } 289 290 /** 291 * Checks if the current user may edit data in this schema 292 * 293 * @return bool 294 */ 295 public function isEditable() 296 { 297 global $USERINFO; 298 global $INPUT; 299 if ($this->config['allowed editors'] === '') return true; 300 if ($INPUT->server->str('REMOTE_USER') === '') return false; 301 if (auth_isadmin()) return true; 302 return auth_isMember($this->config['allowed editors'], $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 303 } 304 305 /** 306 * 307 * @return bool 308 */ 309 public function isInternal() 310 { 311 return (bool) $this->config['internal']; 312 } 313 314 /** 315 * Returns a list of columns in this schema 316 * 317 * @param bool $withDisabled if false, disabled columns will not be returned 318 * @return Column[] 319 */ 320 public function getColumns($withDisabled = true) 321 { 322 if (!$withDisabled) { 323 return array_filter( 324 $this->columns, 325 function (Column $col) { 326 return $col->isEnabled(); 327 } 328 ); 329 } 330 331 return $this->columns; 332 } 333 334 /** 335 * Find a column in the schema by its label 336 * 337 * Only enabled columns are returned! 338 * 339 * @param $name 340 * @return bool|Column 341 */ 342 public function findColumn($name) 343 { 344 foreach ($this->columns as $col) { 345 if ($col->isEnabled() && PhpString::strtolower($col->getLabel()) == PhpString::strtolower($name)) { 346 return $col; 347 } 348 } 349 return false; 350 } 351 352 /** 353 * @return string 354 */ 355 public function getTable() 356 { 357 return $this->table; 358 } 359 360 /** 361 * @return int the highest sort number used in this schema 362 */ 363 public function getMaxsort() 364 { 365 return $this->maxsort; 366 } 367 368 /** 369 * @return string the JSON representing this schema 370 */ 371 public function toJSON() 372 { 373 $data = array( 374 'structversion' => $this->structversion, 375 'schema' => $this->getTable(), 376 'id' => $this->getId(), 377 'user' => $this->getUser(), 378 'config' => $this->getConfig(), 379 'columns' => array() 380 ); 381 382 foreach ($this->columns as $column) { 383 $data['columns'][] = array( 384 'colref' => $column->getColref(), 385 'ismulti' => $column->isMulti(), 386 'isenabled' => $column->isEnabled(), 387 'sort' => $column->getSort(), 388 'label' => $column->getLabel(), 389 'class' => $column->getType()->getClass(), 390 'config' => $column->getType()->getConfig(), 391 ); 392 } 393 394 return json_encode($data, JSON_PRETTY_PRINT); 395 } 396} 397