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