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