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