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