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