1083afc55SAndreas Gohr<?php 2083afc55SAndreas Gohr 3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\meta; 4d486d6d7SAndreas Gohr 5ba766201SAndreas Gohruse dokuwiki\plugin\struct\types\AbstractBaseType; 6a91bbca2SAndreas Gohruse dokuwiki\Utf8\PhpString; 7083afc55SAndreas Gohr 87182938bSAndreas Gohr/** 97182938bSAndreas Gohr * Class Schema 107182938bSAndreas Gohr * 117182938bSAndreas Gohr * Represents the schema of a single data table and all its properties. It defines what can be stored in 127182938bSAndreas Gohr * the represented data table and how those contents are formatted. 137182938bSAndreas Gohr * 147182938bSAndreas Gohr * It can be initialized with a timestamp to access the schema as it looked at that particular point in time. 157182938bSAndreas Gohr * 16ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta 177182938bSAndreas Gohr */ 18d6d97f60SAnna Dabrowskaclass Schema 19d6d97f60SAnna Dabrowska{ 20f800af69SMichael Große use TranslationUtilities; 21f800af69SMichael Große 22083afc55SAndreas Gohr /** @var \helper_plugin_sqlite|null */ 23083afc55SAndreas Gohr protected $sqlite; 24083afc55SAndreas Gohr 25083afc55SAndreas Gohr /** @var int The ID of this schema */ 26083afc55SAndreas Gohr protected $id = 0; 27083afc55SAndreas Gohr 28fa7b96aaSMichael Grosse /** @var string the user who last edited this schema */ 29fa7b96aaSMichael Grosse protected $user = ''; 30fa7b96aaSMichael Grosse 31083afc55SAndreas Gohr /** @var string name of the associated table */ 32083afc55SAndreas Gohr protected $table = ''; 33083afc55SAndreas Gohr 341c502704SAndreas Gohr /** @var Column[] all the colums */ 35083afc55SAndreas Gohr protected $columns = array(); 36083afc55SAndreas Gohr 37083afc55SAndreas Gohr /** @var int */ 38083afc55SAndreas Gohr protected $maxsort = 0; 39083afc55SAndreas Gohr 40250c83c2SAndreas Gohr /** @var int */ 41250c83c2SAndreas Gohr protected $ts = 0; 42250c83c2SAndreas Gohr 43d486d6d7SAndreas Gohr /** @var string struct version info */ 44d486d6d7SAndreas Gohr protected $structversion = '?'; 45d486d6d7SAndreas Gohr 46127d6bacSMichael Große /** @var array config array with label translations */ 47127d6bacSMichael Große protected $config = array(); 48e2c90eebSAndreas Gohr 49083afc55SAndreas Gohr /** 50083afc55SAndreas Gohr * Schema constructor 517182938bSAndreas Gohr * 52083afc55SAndreas Gohr * @param string $table The table this schema is for 53083afc55SAndreas Gohr * @param int $ts The timestamp for when this schema was valid, 0 for current 54083afc55SAndreas Gohr */ 55d6d97f60SAnna Dabrowska public function __construct($table, $ts = 0) 56d6d97f60SAnna Dabrowska { 576c9d1a10SAnna Dabrowska $baseconfig = array('allowed editors' => '', 'internal' => false); 58127d6bacSMichael Große 59083afc55SAndreas Gohr /** @var \helper_plugin_struct_db $helper */ 60083afc55SAndreas Gohr $helper = plugin_load('helper', 'struct_db'); 61d486d6d7SAndreas Gohr $info = $helper->getInfo(); 62d486d6d7SAndreas Gohr $this->structversion = $info['date']; 63083afc55SAndreas Gohr $this->sqlite = $helper->getDB(); 64083afc55SAndreas Gohr $table = self::cleanTableName($table); 65083afc55SAndreas Gohr $this->table = $table; 66250c83c2SAndreas Gohr $this->ts = $ts; 67083afc55SAndreas Gohr 68083afc55SAndreas Gohr // load info about the schema itself 69083afc55SAndreas Gohr if ($ts) { 70083afc55SAndreas Gohr $sql = "SELECT * 71083afc55SAndreas Gohr FROM schemas 72083afc55SAndreas Gohr WHERE tbl = ? 73083afc55SAndreas Gohr AND ts <= ? 74083afc55SAndreas Gohr ORDER BY ts DESC 75083afc55SAndreas Gohr LIMIT 1"; 76083afc55SAndreas Gohr $opt = array($table, $ts); 77083afc55SAndreas Gohr } else { 78083afc55SAndreas Gohr $sql = "SELECT * 79083afc55SAndreas Gohr FROM schemas 80083afc55SAndreas Gohr WHERE tbl = ? 81083afc55SAndreas Gohr ORDER BY ts DESC 82083afc55SAndreas Gohr LIMIT 1"; 83083afc55SAndreas Gohr $opt = array($table); 84083afc55SAndreas Gohr } 85*79b29326SAnna Dabrowska $schema = $this->sqlite->queryAll($sql, $opt); 86127d6bacSMichael Große $config = array(); 87*79b29326SAnna Dabrowska 88*79b29326SAnna Dabrowska if (!empty($schema)) { 894e2abec0SMichael Große $result = array_shift($schema); 90083afc55SAndreas Gohr $this->id = $result['id']; 91fa7b96aaSMichael Grosse $this->user = $result['user']; 92587e314dSAndreas Gohr $this->ts = $result['ts']; 93127d6bacSMichael Große $config = json_decode($result['config'], true); 94083afc55SAndreas Gohr } 95127d6bacSMichael Große $this->config = array_merge($baseconfig, $config); 96f800af69SMichael Große $this->initTransConfig(array('label')); 97083afc55SAndreas Gohr if (!$this->id) return; 98083afc55SAndreas Gohr 99083afc55SAndreas Gohr // load existing columns 100083afc55SAndreas Gohr $sql = "SELECT SC.*, T.* 101083afc55SAndreas Gohr FROM schema_cols SC, 102083afc55SAndreas Gohr types T 1031c502704SAndreas Gohr WHERE SC.sid = ? 1041c502704SAndreas Gohr AND SC.tid = T.id 105083afc55SAndreas Gohr ORDER BY SC.sort"; 106*79b29326SAnna Dabrowska $rows = $this->sqlite->queryAll($sql, [$this->id]); 107083afc55SAndreas Gohr 108636c8abaSAndreas Gohr $typeclasses = Column::allTypes(); 109083afc55SAndreas Gohr foreach ($rows as $row) { 110328db41bSAndreas Gohr if ($row['class'] == 'Integer') { 111328db41bSAndreas Gohr $row['class'] = 'Decimal'; 112328db41bSAndreas Gohr } 113328db41bSAndreas Gohr 114636c8abaSAndreas Gohr $class = $typeclasses[$row['class']]; 11598eaa57dSAndreas Gohr if (!class_exists($class)) { 11698eaa57dSAndreas Gohr // This usually never happens, except during development 11798eaa57dSAndreas Gohr msg('Unknown type "' . hsc($row['class']) . '" falling back to Text', -1); 118ba766201SAndreas Gohr $class = 'dokuwiki\\plugin\\struct\\types\\Text'; 11998eaa57dSAndreas Gohr } 12098eaa57dSAndreas Gohr 121083afc55SAndreas Gohr $config = json_decode($row['config'], true); 122bbf3d6aaSAndreas Gohr /** @var AbstractBaseType $type */ 123bbf3d6aaSAndreas Gohr $type = new $class($config, $row['label'], $row['ismulti'], $row['tid']); 124bbf3d6aaSAndreas Gohr $column = new Column( 1251c502704SAndreas Gohr $row['sort'], 126bbf3d6aaSAndreas Gohr $type, 1271c502704SAndreas Gohr $row['colref'], 12863d51bbfSAndreas Gohr $row['enabled'], 12963d51bbfSAndreas Gohr $table 1301c502704SAndreas Gohr ); 131bbf3d6aaSAndreas Gohr $type->setContext($column); 1321c502704SAndreas Gohr 1337629557eSAndreas Gohr $this->columns[] = $column; 134083afc55SAndreas Gohr if ($row['sort'] > $this->maxsort) $this->maxsort = $row['sort']; 135083afc55SAndreas Gohr } 136083afc55SAndreas Gohr } 137083afc55SAndreas Gohr 138083afc55SAndreas Gohr /** 13967641668SAndreas Gohr * @return string identifer for debugging purposes 14067641668SAndreas Gohr */ 141748e747fSAnna Dabrowska public function __toString() 142d6d97f60SAnna Dabrowska { 1435b808f9fSAnna Dabrowska return __CLASS__ . ' ' . $this->table . ' (' . $this->id . ') '; 14467641668SAndreas Gohr } 14567641668SAndreas Gohr 14667641668SAndreas Gohr /** 147083afc55SAndreas Gohr * Cleans any unwanted stuff from table names 148083afc55SAndreas Gohr * 149083afc55SAndreas Gohr * @param string $table 150083afc55SAndreas Gohr * @return string 151083afc55SAndreas Gohr */ 152d6d97f60SAnna Dabrowska public static function cleanTableName($table) 153d6d97f60SAnna Dabrowska { 1542af472dcSAndreas Gohr $table = strtolower($table); 155083afc55SAndreas Gohr $table = preg_replace('/[^a-z0-9_]+/', '', $table); 156083afc55SAndreas Gohr $table = preg_replace('/^[0-9_]+/', '', $table); 157083afc55SAndreas Gohr $table = trim($table); 158083afc55SAndreas Gohr return $table; 159083afc55SAndreas Gohr } 160083afc55SAndreas Gohr 161127d6bacSMichael Große 162127d6bacSMichael Große /** 163097f4a53SAndreas Gohr * Gets a list of all available schemas 164097f4a53SAndreas Gohr * 1657c080d69SAndreas Gohr * @return \string[] 166097f4a53SAndreas Gohr */ 1675b808f9fSAnna Dabrowska public static function getAll() 168d6d97f60SAnna Dabrowska { 169097f4a53SAndreas Gohr /** @var \helper_plugin_struct_db $helper */ 170097f4a53SAndreas Gohr $helper = plugin_load('helper', 'struct_db'); 1717cbcfbdbSAndreas Gohr $db = $helper->getDB(false); 172097f4a53SAndreas Gohr if (!$db) return array(); 173097f4a53SAndreas Gohr 174*79b29326SAnna Dabrowska $tables = $db->queryAll("SELECT DISTINCT tbl FROM schemas ORDER BY tbl"); 175097f4a53SAndreas Gohr 176097f4a53SAndreas Gohr $result = array(); 177097f4a53SAndreas Gohr foreach ($tables as $row) { 178097f4a53SAndreas Gohr $result[] = $row['tbl']; 179097f4a53SAndreas Gohr } 180097f4a53SAndreas Gohr return $result; 181097f4a53SAndreas Gohr } 182097f4a53SAndreas Gohr 183097f4a53SAndreas Gohr /** 184d5a1a6dcSAndreas Gohr * Delete all data associated with this schema 185d5a1a6dcSAndreas Gohr * 186d5a1a6dcSAndreas Gohr * This is really all data ever! Be careful! 187d5a1a6dcSAndreas Gohr */ 188d6d97f60SAnna Dabrowska public function delete() 189d6d97f60SAnna Dabrowska { 190d5a1a6dcSAndreas Gohr if (!$this->id) throw new StructException('can not delete unsaved schema'); 191d5a1a6dcSAndreas Gohr 192d5a1a6dcSAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 193d5a1a6dcSAndreas Gohr 194d5a1a6dcSAndreas Gohr $sql = "DROP TABLE ?"; 195d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, 'data_' . $this->table); 196d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, 'multi_' . $this->table); 197d5a1a6dcSAndreas Gohr 198d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schema_assignments WHERE tbl = ?"; 199d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, $this->table); 200d5a1a6dcSAndreas Gohr 201d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schema_assignments_patterns WHERE tbl = ?"; 202d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, $this->table); 203d5a1a6dcSAndreas Gohr 204d5a1a6dcSAndreas Gohr $sql = "SELECT T.id 205d5a1a6dcSAndreas Gohr FROM types T, schema_cols SC, schemas S 206d5a1a6dcSAndreas Gohr WHERE T.id = SC.tid 207d5a1a6dcSAndreas Gohr AND SC.sid = S.id 208d5a1a6dcSAndreas Gohr AND S.tbl = ?"; 209d5a1a6dcSAndreas Gohr $sql = "DELETE FROM types WHERE id IN ($sql)"; 210d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, $this->table); 211d5a1a6dcSAndreas Gohr 212d5a1a6dcSAndreas Gohr $sql = "SELECT id 213d5a1a6dcSAndreas Gohr FROM schemas 214d5a1a6dcSAndreas Gohr WHERE tbl = ?"; 215d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schema_cols WHERE sid IN ($sql)"; 216d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, $this->table); 217d5a1a6dcSAndreas Gohr 218d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schemas WHERE tbl = ?"; 219d5a1a6dcSAndreas Gohr $this->sqlite->query($sql, $this->table); 220d5a1a6dcSAndreas Gohr 221d5a1a6dcSAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 222d5a1a6dcSAndreas Gohr $this->sqlite->query('VACUUM'); 223f9f13d8cSAndreas Gohr 224f9f13d8cSAndreas Gohr // a deleted schema should not be used anymore, but let's make sure it's somewhat sane anyway 225f9f13d8cSAndreas Gohr $this->id = 0; 226f9f13d8cSAndreas Gohr $this->columns = array(); 227f9f13d8cSAndreas Gohr $this->maxsort = 0; 228f9f13d8cSAndreas Gohr $this->ts = 0; 229d5a1a6dcSAndreas Gohr } 230d5a1a6dcSAndreas Gohr 23179c83e06SMichael Große 23279c83e06SMichael Große /** 23379c83e06SMichael Große * Clear all data of a schema, but retain the schema itself 23479c83e06SMichael Große */ 235d6d97f60SAnna Dabrowska public function clear() 236d6d97f60SAnna Dabrowska { 23779c83e06SMichael Große if (!$this->id) throw new StructException('can not clear data of unsaved schema'); 23879c83e06SMichael Große 23979c83e06SMichael Große $this->sqlite->query('BEGIN TRANSACTION'); 24079c83e06SMichael Große $sql = 'DELETE FROM ?'; 24179c83e06SMichael Große $this->sqlite->query($sql, 'data_' . $this->table); 24279c83e06SMichael Große $this->sqlite->query($sql, 'multi_' . $this->table); 24379c83e06SMichael Große $this->sqlite->query('COMMIT TRANSACTION'); 24479c83e06SMichael Große $this->sqlite->query('VACUUM'); 24579c83e06SMichael Große } 24679c83e06SMichael Große 247d5a1a6dcSAndreas Gohr /** 2481c502704SAndreas Gohr * @return int 2491c502704SAndreas Gohr */ 250d6d97f60SAnna Dabrowska public function getId() 251d6d97f60SAnna Dabrowska { 2521c502704SAndreas Gohr return $this->id; 2531c502704SAndreas Gohr } 2541c502704SAndreas Gohr 2551c502704SAndreas Gohr /** 256587e314dSAndreas Gohr * @return int returns the timestamp this Schema was created at 257f411d872SAndreas Gohr */ 258d6d97f60SAnna Dabrowska public function getTimeStamp() 259d6d97f60SAnna Dabrowska { 260f411d872SAndreas Gohr return $this->ts; 261f411d872SAndreas Gohr } 262f411d872SAndreas Gohr 263f411d872SAndreas Gohr /** 264fa7b96aaSMichael Grosse * @return string 265fa7b96aaSMichael Grosse */ 266d6d97f60SAnna Dabrowska public function getUser() 267d6d97f60SAnna Dabrowska { 268fa7b96aaSMichael Grosse return $this->user; 269fa7b96aaSMichael Grosse } 270fa7b96aaSMichael Grosse 271d6d97f60SAnna Dabrowska public function getConfig() 272d6d97f60SAnna Dabrowska { 273127d6bacSMichael Große return $this->config; 274e2c90eebSAndreas Gohr } 275e2c90eebSAndreas Gohr 276fa7b96aaSMichael Grosse /** 277f800af69SMichael Große * Returns the translated label for this schema 278f800af69SMichael Große * 279f800af69SMichael Große * Uses the current language as determined by $conf['lang']. Falls back to english 280f800af69SMichael Große * and then to the Schema label 281f800af69SMichael Große * 282f800af69SMichael Große * @return string 283f800af69SMichael Große */ 284d6d97f60SAnna Dabrowska public function getTranslatedLabel() 285d6d97f60SAnna Dabrowska { 286f800af69SMichael Große return $this->getTranslatedKey('label', $this->table); 287f800af69SMichael Große } 288f800af69SMichael Große 289f800af69SMichael Große /** 2906ebbbb8eSAndreas Gohr * Checks if the current user may edit data in this schema 2916ebbbb8eSAndreas Gohr * 2926ebbbb8eSAndreas Gohr * @return bool 2936ebbbb8eSAndreas Gohr */ 294d6d97f60SAnna Dabrowska public function isEditable() 295d6d97f60SAnna Dabrowska { 2966ebbbb8eSAndreas Gohr global $USERINFO; 297ecf2cba2SAndreas Gohr global $INPUT; 298127d6bacSMichael Große if ($this->config['allowed editors'] === '') return true; 299ecf2cba2SAndreas Gohr if ($INPUT->server->str('REMOTE_USER') === '') return false; 3006ebbbb8eSAndreas Gohr if (auth_isadmin()) return true; 301ecf2cba2SAndreas Gohr return auth_isMember($this->config['allowed editors'], $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 3026ebbbb8eSAndreas Gohr } 3036ebbbb8eSAndreas Gohr 3046ebbbb8eSAndreas Gohr /** 3056c9d1a10SAnna Dabrowska * 3066c9d1a10SAnna Dabrowska * @return bool 3076c9d1a10SAnna Dabrowska */ 3086c9d1a10SAnna Dabrowska public function isInternal() 3096c9d1a10SAnna Dabrowska { 3106c9d1a10SAnna Dabrowska return (bool) $this->config['internal']; 3116c9d1a10SAnna Dabrowska } 3126c9d1a10SAnna Dabrowska 3136c9d1a10SAnna Dabrowska /** 314ce206ec7SAndreas Gohr * Returns a list of columns in this schema 315ce206ec7SAndreas Gohr * 316ce206ec7SAndreas Gohr * @param bool $withDisabled if false, disabled columns will not be returned 317ce206ec7SAndreas Gohr * @return Column[] 3181c502704SAndreas Gohr */ 319d6d97f60SAnna Dabrowska public function getColumns($withDisabled = true) 320d6d97f60SAnna Dabrowska { 321ce206ec7SAndreas Gohr if (!$withDisabled) { 322ce206ec7SAndreas Gohr return array_filter( 323ce206ec7SAndreas Gohr $this->columns, 324ce206ec7SAndreas Gohr function (Column $col) { 325ce206ec7SAndreas Gohr return $col->isEnabled(); 326ce206ec7SAndreas Gohr } 327ce206ec7SAndreas Gohr ); 328ce206ec7SAndreas Gohr } 329ce206ec7SAndreas Gohr 3301c502704SAndreas Gohr return $this->columns; 3311c502704SAndreas Gohr } 3321c502704SAndreas Gohr 333ae697e1fSAndreas Gohr /** 3345742aea9SAndreas Gohr * Find a column in the schema by its label 3355742aea9SAndreas Gohr * 3365742aea9SAndreas Gohr * Only enabled columns are returned! 3375742aea9SAndreas Gohr * 3385742aea9SAndreas Gohr * @param $name 3395742aea9SAndreas Gohr * @return bool|Column 3405742aea9SAndreas Gohr */ 341d6d97f60SAnna Dabrowska public function findColumn($name) 342d6d97f60SAnna Dabrowska { 3435742aea9SAndreas Gohr foreach ($this->columns as $col) { 344a91bbca2SAndreas Gohr if ($col->isEnabled() && PhpString::strtolower($col->getLabel()) == PhpString::strtolower($name)) { 3455742aea9SAndreas Gohr return $col; 3465742aea9SAndreas Gohr } 3475742aea9SAndreas Gohr } 3485742aea9SAndreas Gohr return false; 3495742aea9SAndreas Gohr } 3505742aea9SAndreas Gohr 3515742aea9SAndreas Gohr /** 352ae697e1fSAndreas Gohr * @return string 353ae697e1fSAndreas Gohr */ 354d6d97f60SAnna Dabrowska public function getTable() 355d6d97f60SAnna Dabrowska { 356ae697e1fSAndreas Gohr return $this->table; 357ae697e1fSAndreas Gohr } 3581c502704SAndreas Gohr 359ae697e1fSAndreas Gohr /** 360ae697e1fSAndreas Gohr * @return int the highest sort number used in this schema 361ae697e1fSAndreas Gohr */ 362d6d97f60SAnna Dabrowska public function getMaxsort() 363d6d97f60SAnna Dabrowska { 364ae697e1fSAndreas Gohr return $this->maxsort; 365ae697e1fSAndreas Gohr } 3661c502704SAndreas Gohr 367d486d6d7SAndreas Gohr /** 368d486d6d7SAndreas Gohr * @return string the JSON representing this schema 369d486d6d7SAndreas Gohr */ 370d6d97f60SAnna Dabrowska public function toJSON() 371d6d97f60SAnna Dabrowska { 372d486d6d7SAndreas Gohr $data = array( 373d486d6d7SAndreas Gohr 'structversion' => $this->structversion, 374d486d6d7SAndreas Gohr 'schema' => $this->getTable(), 375d486d6d7SAndreas Gohr 'id' => $this->getId(), 37645313413SMichael Grosse 'user' => $this->getUser(), 377127d6bacSMichael Große 'config' => $this->getConfig(), 378d486d6d7SAndreas Gohr 'columns' => array() 379d486d6d7SAndreas Gohr ); 380d486d6d7SAndreas Gohr 381d486d6d7SAndreas Gohr foreach ($this->columns as $column) { 382d486d6d7SAndreas Gohr $data['columns'][] = array( 383d486d6d7SAndreas Gohr 'colref' => $column->getColref(), 384d486d6d7SAndreas Gohr 'ismulti' => $column->isMulti(), 385d486d6d7SAndreas Gohr 'isenabled' => $column->isEnabled(), 386d486d6d7SAndreas Gohr 'sort' => $column->getSort(), 387d486d6d7SAndreas Gohr 'label' => $column->getLabel(), 388d486d6d7SAndreas Gohr 'class' => $column->getType()->getClass(), 389d486d6d7SAndreas Gohr 'config' => $column->getType()->getConfig(), 390d486d6d7SAndreas Gohr ); 391d486d6d7SAndreas Gohr } 392d486d6d7SAndreas Gohr 393d486d6d7SAndreas Gohr return json_encode($data, JSON_PRETTY_PRINT); 394d486d6d7SAndreas Gohr } 395083afc55SAndreas Gohr} 396