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