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