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 string comma separated list of allowed editors */ 54 protected $editors = ''; 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 /** @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 if(!$this->sqlite) return; 70 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 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 = $result['chksum']; 99 $this->islookup = $result['islookup']; 100 $this->ts = $result['ts']; 101 $this->editors = $result['editors']; 102 } else { 103 $this->islookup = $islookup; 104 } 105 $this->sqlite->res_close($res); 106 if(!$this->id) return; 107 108 // load existing columns 109 $sql = "SELECT SC.*, T.* 110 FROM schema_cols SC, 111 types T 112 WHERE SC.sid = ? 113 AND SC.tid = T.id 114 ORDER BY SC.sort"; 115 $res = $this->sqlite->query($sql, $this->id); 116 $rows = $this->sqlite->res2arr($res); 117 $this->sqlite->res_close($res); 118 119 foreach($rows as $row) { 120 if($row['class'] == 'Integer') { 121 $row['class'] = 'Decimal'; 122 } 123 124 $class = 'dokuwiki\\plugin\\struct\\types\\' . $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 function __toString() { 152 return __CLASS__.' '.$this->table.' ('.$this->id.') '.($this->islookup ? 'LOOKUP' : 'DATA'); 153 } 154 155 /** 156 * Cleans any unwanted stuff from table names 157 * 158 * @param string $table 159 * @return string 160 */ 161 static public function cleanTableName($table) { 162 $table = strtolower($table); 163 $table = preg_replace('/[^a-z0-9_]+/', '', $table); 164 $table = preg_replace('/^[0-9_]+/', '', $table); 165 $table = trim($table); 166 return $table; 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(); 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 * @return string 250 */ 251 public function getChksum() { 252 return $this->chksum; 253 } 254 255 /** 256 * @return int 257 */ 258 public function getId() { 259 return $this->id; 260 } 261 262 /** 263 * @return int returns the timestamp this Schema was created at 264 */ 265 public function getTimeStamp() { 266 return $this->ts; 267 } 268 269 /** 270 * @return bool is this a lookup schema? 271 */ 272 public function isLookup() { 273 return $this->islookup; 274 } 275 276 /** 277 * @return string 278 */ 279 public function getUser() { 280 return $this->user; 281 } 282 283 public function getEditors() { 284 return $this->editors; 285 } 286 287 /** 288 * Returns a list of columns in this schema 289 * 290 * @param bool $withDisabled if false, disabled columns will not be returned 291 * @return Column[] 292 */ 293 public function getColumns($withDisabled = true) { 294 if(!$withDisabled) { 295 return array_filter( 296 $this->columns, 297 function (Column $col) { 298 return $col->isEnabled(); 299 } 300 ); 301 } 302 303 return $this->columns; 304 } 305 306 /** 307 * Find a column in the schema by its label 308 * 309 * Only enabled columns are returned! 310 * 311 * @param $name 312 * @return bool|Column 313 */ 314 public function findColumn($name) { 315 foreach($this->columns as $col) { 316 if($col->isEnabled() && utf8_strtolower($col->getLabel()) == utf8_strtolower($name)) { 317 return $col; 318 } 319 } 320 return false; 321 } 322 323 /** 324 * @return string 325 */ 326 public function getTable() { 327 return $this->table; 328 } 329 330 /** 331 * @return int the highest sort number used in this schema 332 */ 333 public function getMaxsort() { 334 return $this->maxsort; 335 } 336 337 /** 338 * @return string the JSON representing this schema 339 */ 340 public function toJSON() { 341 $data = array( 342 'structversion' => $this->structversion, 343 'schema' => $this->getTable(), 344 'id' => $this->getId(), 345 'user' => $this->getUser(), 346 'columns' => array() 347 ); 348 349 foreach($this->columns as $column) { 350 $data['columns'][] = array( 351 'colref' => $column->getColref(), 352 'ismulti' => $column->isMulti(), 353 'isenabled' => $column->isEnabled(), 354 'sort' => $column->getSort(), 355 'label' => $column->getLabel(), 356 'class' => $column->getType()->getClass(), 357 'config' => $column->getType()->getConfig(), 358 ); 359 } 360 361 return json_encode($data, JSON_PRETTY_PRINT); 362 } 363} 364