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