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