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