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