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 = $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 * @return string 257 */ 258 public function getChksum() { 259 return $this->chksum; 260 } 261 262 /** 263 * @return int 264 */ 265 public function getId() { 266 return $this->id; 267 } 268 269 /** 270 * @return int returns the timestamp this Schema was created at 271 */ 272 public function getTimeStamp() { 273 return $this->ts; 274 } 275 276 /** 277 * @return bool is this a lookup schema? 278 */ 279 public function isLookup() { 280 return $this->islookup; 281 } 282 283 /** 284 * @return string 285 */ 286 public function getUser() { 287 return $this->user; 288 } 289 290 public function getConfig() { 291 return $this->config; 292 } 293 294 /** 295 * Returns the translated label for this schema 296 * 297 * Uses the current language as determined by $conf['lang']. Falls back to english 298 * and then to the Schema label 299 * 300 * @return string 301 */ 302 public function getTranslatedLabel() { 303 return $this->getTranslatedKey('label', $this->table); 304 } 305 306 /** 307 * Checks if the current user may edit data in this schema 308 * 309 * @return bool 310 */ 311 public function isEditable() { 312 global $USERINFO; 313 if($this->config['allowed editors'] === '') return true; 314 if(blank($_SERVER['REMOTE_USER'])) return false; 315 if(auth_isadmin()) return true; 316 return auth_isMember($this->config['allowed editors'], $_SERVER['REMOTE_USER'], $USERINFO['grps']); 317 } 318 319 /** 320 * Returns a list of columns in this schema 321 * 322 * @param bool $withDisabled if false, disabled columns will not be returned 323 * @return Column[] 324 */ 325 public function getColumns($withDisabled = true) { 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 foreach($this->columns as $col) { 348 if($col->isEnabled() && utf8_strtolower($col->getLabel()) == utf8_strtolower($name)) { 349 return $col; 350 } 351 } 352 return false; 353 } 354 355 /** 356 * @return string 357 */ 358 public function getTable() { 359 return $this->table; 360 } 361 362 /** 363 * @return int the highest sort number used in this schema 364 */ 365 public function getMaxsort() { 366 return $this->maxsort; 367 } 368 369 /** 370 * @return string the JSON representing this schema 371 */ 372 public function toJSON() { 373 $data = array( 374 'structversion' => $this->structversion, 375 'schema' => $this->getTable(), 376 'id' => $this->getId(), 377 'user' => $this->getUser(), 378 'config' => $this->getConfig(), 379 'columns' => array() 380 ); 381 382 foreach($this->columns as $column) { 383 $data['columns'][] = array( 384 'colref' => $column->getColref(), 385 'ismulti' => $column->isMulti(), 386 'isenabled' => $column->isEnabled(), 387 'sort' => $column->getSort(), 388 'label' => $column->getLabel(), 389 'class' => $column->getType()->getClass(), 390 'config' => $column->getType()->getConfig(), 391 ); 392 } 393 394 return json_encode($data, JSON_PRETTY_PRINT); 395 } 396} 397