1083afc55SAndreas Gohr<?php 2083afc55SAndreas Gohr 3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\meta; 4d486d6d7SAndreas Gohr 5438a804cSAnna Dabrowskause dokuwiki\plugin\sqlite\SQLiteDB; 6ba766201SAndreas Gohruse dokuwiki\plugin\struct\types\AbstractBaseType; 7a91bbca2SAndreas Gohruse dokuwiki\Utf8\PhpString; 8083afc55SAndreas Gohr 97182938bSAndreas Gohr/** 107182938bSAndreas Gohr * Class Schema 117182938bSAndreas Gohr * 127182938bSAndreas Gohr * Represents the schema of a single data table and all its properties. It defines what can be stored in 137182938bSAndreas Gohr * the represented data table and how those contents are formatted. 147182938bSAndreas Gohr * 157182938bSAndreas Gohr * It can be initialized with a timestamp to access the schema as it looked at that particular point in time. 167182938bSAndreas Gohr * 17ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta 187182938bSAndreas Gohr */ 19d6d97f60SAnna Dabrowskaclass Schema 20d6d97f60SAnna Dabrowska{ 21f800af69SMichael Große use TranslationUtilities; 22f800af69SMichael Große 23438a804cSAnna Dabrowska /** @var SQLiteDB|null */ 24083afc55SAndreas Gohr protected $sqlite; 25083afc55SAndreas Gohr 26083afc55SAndreas Gohr /** @var int The ID of this schema */ 27083afc55SAndreas Gohr protected $id = 0; 28083afc55SAndreas Gohr 29fa7b96aaSMichael Grosse /** @var string the user who last edited this schema */ 30fa7b96aaSMichael Grosse protected $user = ''; 31fa7b96aaSMichael Grosse 32083afc55SAndreas Gohr /** @var string name of the associated table */ 33083afc55SAndreas Gohr protected $table = ''; 34083afc55SAndreas Gohr 351c502704SAndreas Gohr /** @var Column[] all the colums */ 367234bfb1Ssplitbrain protected $columns = []; 37083afc55SAndreas Gohr 38083afc55SAndreas Gohr /** @var int */ 39083afc55SAndreas Gohr protected $maxsort = 0; 40083afc55SAndreas Gohr 41250c83c2SAndreas Gohr /** @var int */ 42250c83c2SAndreas Gohr protected $ts = 0; 43250c83c2SAndreas Gohr 44d486d6d7SAndreas Gohr /** @var string struct version info */ 45d486d6d7SAndreas Gohr protected $structversion = '?'; 46d486d6d7SAndreas Gohr 47127d6bacSMichael Große /** @var array config array with label translations */ 487234bfb1Ssplitbrain protected $config = []; 49e2c90eebSAndreas Gohr 50083afc55SAndreas Gohr /** 51083afc55SAndreas Gohr * Schema constructor 527182938bSAndreas Gohr * 53083afc55SAndreas Gohr * @param string $table The table this schema is for 54083afc55SAndreas Gohr * @param int $ts The timestamp for when this schema was valid, 0 for current 55083afc55SAndreas Gohr */ 56d6d97f60SAnna Dabrowska public function __construct($table, $ts = 0) 57d6d97f60SAnna Dabrowska { 587234bfb1Ssplitbrain $baseconfig = ['allowed editors' => '', 'internal' => false]; 59127d6bacSMichael Große 60083afc55SAndreas Gohr /** @var \helper_plugin_struct_db $helper */ 61083afc55SAndreas Gohr $helper = plugin_load('helper', 'struct_db'); 62d486d6d7SAndreas Gohr $info = $helper->getInfo(); 63d486d6d7SAndreas Gohr $this->structversion = $info['date']; 64083afc55SAndreas Gohr $this->sqlite = $helper->getDB(); 65083afc55SAndreas Gohr $table = self::cleanTableName($table); 66083afc55SAndreas Gohr $this->table = $table; 677234bfb1Ssplitbrain 68250c83c2SAndreas Gohr $this->ts = $ts; 69083afc55SAndreas Gohr 70083afc55SAndreas Gohr // load info about the schema itself 71083afc55SAndreas Gohr if ($ts) { 72083afc55SAndreas Gohr $sql = "SELECT * 73083afc55SAndreas Gohr FROM schemas 74083afc55SAndreas Gohr WHERE tbl = ? 75083afc55SAndreas Gohr AND ts <= ? 76083afc55SAndreas Gohr ORDER BY ts DESC 77083afc55SAndreas Gohr LIMIT 1"; 787234bfb1Ssplitbrain $opt = [$table, $ts]; 79083afc55SAndreas Gohr } else { 80083afc55SAndreas Gohr $sql = "SELECT * 81083afc55SAndreas Gohr FROM schemas 82083afc55SAndreas Gohr WHERE tbl = ? 83083afc55SAndreas Gohr ORDER BY ts DESC 84083afc55SAndreas Gohr LIMIT 1"; 857234bfb1Ssplitbrain $opt = [$table]; 86083afc55SAndreas Gohr } 8779b29326SAnna Dabrowska $schema = $this->sqlite->queryAll($sql, $opt); 887234bfb1Ssplitbrain $config = []; 8979b29326SAnna Dabrowska 9079b29326SAnna Dabrowska if (!empty($schema)) { 914e2abec0SMichael Große $result = array_shift($schema); 92083afc55SAndreas Gohr $this->id = $result['id']; 93fa7b96aaSMichael Grosse $this->user = $result['user']; 94587e314dSAndreas Gohr $this->ts = $result['ts']; 95*5e29103aSannda $config = json_decode($result['config'], true, 512, JSON_THROW_ON_ERROR); 96083afc55SAndreas Gohr } 97127d6bacSMichael Große $this->config = array_merge($baseconfig, $config); 987234bfb1Ssplitbrain $this->initTransConfig(['label']); 99083afc55SAndreas Gohr if (!$this->id) return; 100083afc55SAndreas Gohr 101083afc55SAndreas Gohr // load existing columns 102083afc55SAndreas Gohr $sql = "SELECT SC.*, T.* 103083afc55SAndreas Gohr FROM schema_cols SC, 104083afc55SAndreas Gohr types T 1051c502704SAndreas Gohr WHERE SC.sid = ? 1061c502704SAndreas Gohr AND SC.tid = T.id 107083afc55SAndreas Gohr ORDER BY SC.sort"; 10879b29326SAnna Dabrowska $rows = $this->sqlite->queryAll($sql, [$this->id]); 109083afc55SAndreas Gohr 110636c8abaSAndreas Gohr $typeclasses = Column::allTypes(); 111083afc55SAndreas Gohr foreach ($rows as $row) { 112328db41bSAndreas Gohr if ($row['class'] == 'Integer') { 113328db41bSAndreas Gohr $row['class'] = 'Decimal'; 114328db41bSAndreas Gohr } 115328db41bSAndreas Gohr 116636c8abaSAndreas Gohr $class = $typeclasses[$row['class']]; 11798eaa57dSAndreas Gohr if (!class_exists($class)) { 11898eaa57dSAndreas Gohr // This usually never happens, except during development 11998eaa57dSAndreas Gohr msg('Unknown type "' . hsc($row['class']) . '" falling back to Text', -1); 120ba766201SAndreas Gohr $class = 'dokuwiki\\plugin\\struct\\types\\Text'; 12198eaa57dSAndreas Gohr } 12298eaa57dSAndreas Gohr 123*5e29103aSannda $config = json_decode($row['config'], true, 512, JSON_THROW_ON_ERROR); 124bbf3d6aaSAndreas Gohr /** @var AbstractBaseType $type */ 125bbf3d6aaSAndreas Gohr $type = new $class($config, $row['label'], $row['ismulti'], $row['tid']); 126bbf3d6aaSAndreas Gohr $column = new Column( 1271c502704SAndreas Gohr $row['sort'], 128bbf3d6aaSAndreas Gohr $type, 1291c502704SAndreas Gohr $row['colref'], 13063d51bbfSAndreas Gohr $row['enabled'], 13163d51bbfSAndreas Gohr $table 1321c502704SAndreas Gohr ); 133bbf3d6aaSAndreas Gohr $type->setContext($column); 1341c502704SAndreas Gohr 1357629557eSAndreas Gohr $this->columns[] = $column; 136083afc55SAndreas Gohr if ($row['sort'] > $this->maxsort) $this->maxsort = $row['sort']; 137083afc55SAndreas Gohr } 138083afc55SAndreas Gohr } 139083afc55SAndreas Gohr 140083afc55SAndreas Gohr /** 14167641668SAndreas Gohr * @return string identifer for debugging purposes 14267641668SAndreas Gohr */ 143748e747fSAnna Dabrowska public function __toString() 144d6d97f60SAnna Dabrowska { 1457234bfb1Ssplitbrain return self::class . ' ' . $this->table . ' (' . $this->id . ') '; 14667641668SAndreas Gohr } 14767641668SAndreas Gohr 14867641668SAndreas Gohr /** 149083afc55SAndreas Gohr * Cleans any unwanted stuff from table names 150083afc55SAndreas Gohr * 151083afc55SAndreas Gohr * @param string $table 152083afc55SAndreas Gohr * @return string 153083afc55SAndreas Gohr */ 154d6d97f60SAnna Dabrowska public static function cleanTableName($table) 155d6d97f60SAnna Dabrowska { 1562af472dcSAndreas Gohr $table = strtolower($table); 157083afc55SAndreas Gohr $table = preg_replace('/[^a-z0-9_]+/', '', $table); 158083afc55SAndreas Gohr $table = preg_replace('/^[0-9_]+/', '', $table); 159083afc55SAndreas Gohr $table = trim($table); 160083afc55SAndreas Gohr return $table; 161083afc55SAndreas Gohr } 162083afc55SAndreas Gohr 163127d6bacSMichael Große 164127d6bacSMichael Große /** 165097f4a53SAndreas Gohr * Gets a list of all available schemas 166097f4a53SAndreas Gohr * 1677c080d69SAndreas Gohr * @return \string[] 168097f4a53SAndreas Gohr */ 1695b808f9fSAnna Dabrowska public static function getAll() 170d6d97f60SAnna Dabrowska { 171097f4a53SAndreas Gohr /** @var \helper_plugin_struct_db $helper */ 172097f4a53SAndreas Gohr $helper = plugin_load('helper', 'struct_db'); 1737cbcfbdbSAndreas Gohr $db = $helper->getDB(false); 1747234bfb1Ssplitbrain if (!$db instanceof SQLiteDB) return []; 175097f4a53SAndreas Gohr 17679b29326SAnna Dabrowska $tables = $db->queryAll("SELECT DISTINCT tbl FROM schemas ORDER BY tbl"); 177097f4a53SAndreas Gohr 1787234bfb1Ssplitbrain $result = []; 179097f4a53SAndreas Gohr foreach ($tables as $row) { 180097f4a53SAndreas Gohr $result[] = $row['tbl']; 181097f4a53SAndreas Gohr } 182097f4a53SAndreas Gohr return $result; 183097f4a53SAndreas Gohr } 184097f4a53SAndreas Gohr 185097f4a53SAndreas Gohr /** 186d5a1a6dcSAndreas Gohr * Delete all data associated with this schema 187d5a1a6dcSAndreas Gohr * 188d5a1a6dcSAndreas Gohr * This is really all data ever! Be careful! 189d5a1a6dcSAndreas Gohr */ 190d6d97f60SAnna Dabrowska public function delete() 191d6d97f60SAnna Dabrowska { 192d5a1a6dcSAndreas Gohr if (!$this->id) throw new StructException('can not delete unsaved schema'); 193d5a1a6dcSAndreas Gohr 194d5a1a6dcSAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 195d5a1a6dcSAndreas Gohr 196438a804cSAnna Dabrowska $sql = "DROP TABLE "; 197438a804cSAnna Dabrowska $this->sqlite->query($sql . 'data_' . $this->table); 198438a804cSAnna Dabrowska $this->sqlite->query($sql . 'multi_' . $this->table); 199d5a1a6dcSAndreas Gohr 200438a804cSAnna Dabrowska $sql = "DELETE FROM schema_assignments WHERE tbl = '$this->table'"; 201438a804cSAnna Dabrowska $this->sqlite->query($sql); 202d5a1a6dcSAndreas Gohr 203438a804cSAnna Dabrowska $sql = "DELETE FROM schema_assignments_patterns WHERE tbl = '$this->table'"; 204438a804cSAnna Dabrowska $this->sqlite->query($sql); 205d5a1a6dcSAndreas Gohr 206d5a1a6dcSAndreas Gohr $sql = "SELECT T.id 207d5a1a6dcSAndreas Gohr FROM types T, schema_cols SC, schemas S 208d5a1a6dcSAndreas Gohr WHERE T.id = SC.tid 209d5a1a6dcSAndreas Gohr AND SC.sid = S.id 210d5a1a6dcSAndreas Gohr AND S.tbl = ?"; 211d5a1a6dcSAndreas Gohr $sql = "DELETE FROM types WHERE id IN ($sql)"; 2127234bfb1Ssplitbrain 213438a804cSAnna Dabrowska $this->sqlite->query($sql, [$this->table]); 214d5a1a6dcSAndreas Gohr 215d5a1a6dcSAndreas Gohr $sql = "SELECT id 216d5a1a6dcSAndreas Gohr FROM schemas 217d5a1a6dcSAndreas Gohr WHERE tbl = ?"; 218d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schema_cols WHERE sid IN ($sql)"; 2197234bfb1Ssplitbrain 220438a804cSAnna Dabrowska $this->sqlite->query($sql, [$this->table]); 221d5a1a6dcSAndreas Gohr 222d5a1a6dcSAndreas Gohr $sql = "DELETE FROM schemas WHERE tbl = ?"; 223438a804cSAnna Dabrowska $this->sqlite->query($sql, [$this->table]); 224d5a1a6dcSAndreas Gohr 225d5a1a6dcSAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 226d5a1a6dcSAndreas Gohr $this->sqlite->query('VACUUM'); 227f9f13d8cSAndreas Gohr 228f9f13d8cSAndreas Gohr // a deleted schema should not be used anymore, but let's make sure it's somewhat sane anyway 229f9f13d8cSAndreas Gohr $this->id = 0; 2307234bfb1Ssplitbrain $this->columns = []; 231f9f13d8cSAndreas Gohr $this->maxsort = 0; 232f9f13d8cSAndreas Gohr $this->ts = 0; 233d5a1a6dcSAndreas Gohr } 234d5a1a6dcSAndreas Gohr 23579c83e06SMichael Große 23679c83e06SMichael Große /** 23779c83e06SMichael Große * Clear all data of a schema, but retain the schema itself 23879c83e06SMichael Große */ 239d6d97f60SAnna Dabrowska public function clear() 240d6d97f60SAnna Dabrowska { 24179c83e06SMichael Große if (!$this->id) throw new StructException('can not clear data of unsaved schema'); 24279c83e06SMichael Große 24379c83e06SMichael Große $this->sqlite->query('BEGIN TRANSACTION'); 244438a804cSAnna Dabrowska $sql = 'DELETE FROM '; 245438a804cSAnna Dabrowska $this->sqlite->query($sql . 'data_' . $this->table); 246438a804cSAnna Dabrowska $this->sqlite->query($sql . 'multi_' . $this->table); 24779c83e06SMichael Große $this->sqlite->query('COMMIT TRANSACTION'); 24879c83e06SMichael Große $this->sqlite->query('VACUUM'); 24979c83e06SMichael Große } 25079c83e06SMichael Große 251d5a1a6dcSAndreas Gohr /** 2521c502704SAndreas Gohr * @return int 2531c502704SAndreas Gohr */ 254d6d97f60SAnna Dabrowska public function getId() 255d6d97f60SAnna Dabrowska { 2561c502704SAndreas Gohr return $this->id; 2571c502704SAndreas Gohr } 2581c502704SAndreas Gohr 2591c502704SAndreas Gohr /** 260587e314dSAndreas Gohr * @return int returns the timestamp this Schema was created at 261f411d872SAndreas Gohr */ 262d6d97f60SAnna Dabrowska public function getTimeStamp() 263d6d97f60SAnna Dabrowska { 264f411d872SAndreas Gohr return $this->ts; 265f411d872SAndreas Gohr } 266f411d872SAndreas Gohr 267f411d872SAndreas Gohr /** 268fa7b96aaSMichael Grosse * @return string 269fa7b96aaSMichael Grosse */ 270d6d97f60SAnna Dabrowska public function getUser() 271d6d97f60SAnna Dabrowska { 272fa7b96aaSMichael Grosse return $this->user; 273fa7b96aaSMichael Grosse } 274fa7b96aaSMichael Grosse 275d6d97f60SAnna Dabrowska public function getConfig() 276d6d97f60SAnna Dabrowska { 277127d6bacSMichael Große return $this->config; 278e2c90eebSAndreas Gohr } 279e2c90eebSAndreas Gohr 280fa7b96aaSMichael Grosse /** 281f800af69SMichael Große * Returns the translated label for this schema 282f800af69SMichael Große * 283f800af69SMichael Große * Uses the current language as determined by $conf['lang']. Falls back to english 284f800af69SMichael Große * and then to the Schema label 285f800af69SMichael Große * 286f800af69SMichael Große * @return string 287f800af69SMichael Große */ 288d6d97f60SAnna Dabrowska public function getTranslatedLabel() 289d6d97f60SAnna Dabrowska { 290f800af69SMichael Große return $this->getTranslatedKey('label', $this->table); 291f800af69SMichael Große } 292f800af69SMichael Große 293f800af69SMichael Große /** 2946ebbbb8eSAndreas Gohr * Checks if the current user may edit data in this schema 2956ebbbb8eSAndreas Gohr * 2966ebbbb8eSAndreas Gohr * @return bool 2976ebbbb8eSAndreas Gohr */ 298d6d97f60SAnna Dabrowska public function isEditable() 299d6d97f60SAnna Dabrowska { 3006ebbbb8eSAndreas Gohr global $USERINFO; 301ecf2cba2SAndreas Gohr global $INPUT; 302127d6bacSMichael Große if ($this->config['allowed editors'] === '') return true; 303ecf2cba2SAndreas Gohr if ($INPUT->server->str('REMOTE_USER') === '') return false; 3046ebbbb8eSAndreas Gohr if (auth_isadmin()) return true; 305ecf2cba2SAndreas Gohr return auth_isMember($this->config['allowed editors'], $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']); 3066ebbbb8eSAndreas Gohr } 3076ebbbb8eSAndreas Gohr 3086ebbbb8eSAndreas Gohr /** 3096c9d1a10SAnna Dabrowska * 3106c9d1a10SAnna Dabrowska * @return bool 3116c9d1a10SAnna Dabrowska */ 3126c9d1a10SAnna Dabrowska public function isInternal() 3136c9d1a10SAnna Dabrowska { 3146c9d1a10SAnna Dabrowska return (bool)$this->config['internal']; 3156c9d1a10SAnna Dabrowska } 3166c9d1a10SAnna Dabrowska 3176c9d1a10SAnna Dabrowska /** 318ce206ec7SAndreas Gohr * Returns a list of columns in this schema 319ce206ec7SAndreas Gohr * 320ce206ec7SAndreas Gohr * @param bool $withDisabled if false, disabled columns will not be returned 321ce206ec7SAndreas Gohr * @return Column[] 3221c502704SAndreas Gohr */ 323d6d97f60SAnna Dabrowska public function getColumns($withDisabled = true) 324d6d97f60SAnna Dabrowska { 325ce206ec7SAndreas Gohr if (!$withDisabled) { 326ce206ec7SAndreas Gohr return array_filter( 327ce206ec7SAndreas Gohr $this->columns, 328*5e29103aSannda static fn(Column $col) => $col->isEnabled() 329ce206ec7SAndreas Gohr ); 330ce206ec7SAndreas Gohr } 331ce206ec7SAndreas Gohr 3321c502704SAndreas Gohr return $this->columns; 3331c502704SAndreas Gohr } 3341c502704SAndreas Gohr 335ae697e1fSAndreas Gohr /** 3365742aea9SAndreas Gohr * Find a column in the schema by its label 3375742aea9SAndreas Gohr * 3385742aea9SAndreas Gohr * Only enabled columns are returned! 3395742aea9SAndreas Gohr * 3405742aea9SAndreas Gohr * @param $name 3415742aea9SAndreas Gohr * @return bool|Column 3425742aea9SAndreas Gohr */ 343d6d97f60SAnna Dabrowska public function findColumn($name) 344d6d97f60SAnna Dabrowska { 3455742aea9SAndreas Gohr foreach ($this->columns as $col) { 3467234bfb1Ssplitbrain if ($col->isEnabled() && PhpString::strtolower($col->getLabel()) === PhpString::strtolower($name)) { 3475742aea9SAndreas Gohr return $col; 3485742aea9SAndreas Gohr } 3495742aea9SAndreas Gohr } 3505742aea9SAndreas Gohr return false; 3515742aea9SAndreas Gohr } 3525742aea9SAndreas Gohr 3535742aea9SAndreas Gohr /** 354ae697e1fSAndreas Gohr * @return string 355ae697e1fSAndreas Gohr */ 356d6d97f60SAnna Dabrowska public function getTable() 357d6d97f60SAnna Dabrowska { 358ae697e1fSAndreas Gohr return $this->table; 359ae697e1fSAndreas Gohr } 3601c502704SAndreas Gohr 361ae697e1fSAndreas Gohr /** 362ae697e1fSAndreas Gohr * @return int the highest sort number used in this schema 363ae697e1fSAndreas Gohr */ 364d6d97f60SAnna Dabrowska public function getMaxsort() 365d6d97f60SAnna Dabrowska { 366ae697e1fSAndreas Gohr return $this->maxsort; 367ae697e1fSAndreas Gohr } 3681c502704SAndreas Gohr 369d486d6d7SAndreas Gohr /** 370d486d6d7SAndreas Gohr * @return string the JSON representing this schema 371d486d6d7SAndreas Gohr */ 372d6d97f60SAnna Dabrowska public function toJSON() 373d6d97f60SAnna Dabrowska { 3747fe2cdf2SAndreas Gohr $data = [ 3757fe2cdf2SAndreas Gohr 'structversion' => $this->structversion, 3767fe2cdf2SAndreas Gohr 'schema' => $this->getTable(), 3777fe2cdf2SAndreas Gohr 'id' => $this->getId(), 3787fe2cdf2SAndreas Gohr 'user' => $this->getUser(), 3797fe2cdf2SAndreas Gohr 'config' => $this->getConfig(), 3807fe2cdf2SAndreas Gohr 'columns' => [] 3817fe2cdf2SAndreas Gohr ]; 382d486d6d7SAndreas Gohr 383d486d6d7SAndreas Gohr foreach ($this->columns as $column) { 3847fe2cdf2SAndreas Gohr $data['columns'][] = [ 3857fe2cdf2SAndreas Gohr 'colref' => $column->getColref(), 3867fe2cdf2SAndreas Gohr 'ismulti' => $column->isMulti(), 3877fe2cdf2SAndreas Gohr 'isenabled' => $column->isEnabled(), 3887fe2cdf2SAndreas Gohr 'sort' => $column->getSort(), 3897fe2cdf2SAndreas Gohr 'label' => $column->getLabel(), 3907fe2cdf2SAndreas Gohr 'class' => $column->getType()->getClass(), 3917fe2cdf2SAndreas Gohr 'config' => $column->getType()->getConfig() 3927fe2cdf2SAndreas Gohr ]; 393d486d6d7SAndreas Gohr } 394d486d6d7SAndreas Gohr 395d486d6d7SAndreas Gohr return json_encode($data, JSON_PRETTY_PRINT); 396d486d6d7SAndreas Gohr } 397083afc55SAndreas Gohr} 398