1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5/** 6 * Class AccessTable 7 * 8 * Base class for data accessors 9 * 10 * @package dokuwiki\plugin\struct\meta 11 */ 12abstract class AccessTable 13{ 14 15 const DEFAULT_REV = 0; 16 const DEFAULT_LATEST = 1; 17 18 /** @var Schema */ 19 protected $schema; 20 protected $pid; 21 protected $rid; 22 protected $labels = []; 23 protected $ts = 0; 24 /** @var \helper_plugin_sqlite */ 25 protected $sqlite; 26 27 // options on how to retrieve data 28 protected $opt_skipempty = false; 29 30 /** 31 * @var string Name of single-value table 32 */ 33 protected $stable; 34 35 /** 36 * @var string Name of multi-value table 37 */ 38 protected $mtable; 39 40 /** 41 * @var array Column names for the single-value insert/update 42 */ 43 protected $singleCols; 44 45 /** 46 * @var array Input values for the single-value insert/update 47 */ 48 protected $singleValues; 49 50 /** 51 * @var array Input values for the multi-value inserts/updates 52 */ 53 protected $multiValues; 54 55 public static function getPageAccess($tablename, $pid, $ts = 0) 56 { 57 $schema = new Schema($tablename, $ts); 58 return new AccessTableData($schema, $pid, $ts, 0); 59 } 60 61 public static function getSerialAccess($tablename, $pid, $rid = 0) 62 { 63 $schema = new Schema($tablename, 0); 64 return new AccessTableSerial($schema, $pid, 0, $rid); 65 } 66 67 public static function getLookupAccess($tablename, $rid = 0) 68 { 69 $schema = new Schema($tablename, 0); 70 return new AccessTableLookup($schema, '', 0, $rid); 71 } 72 73 /** 74 * Factory method returning the appropriate data accessor (page, lookup or serial) 75 * 76 * @deprecated 77 * @param Schema $schema schema to load 78 * @param string $pid Page id to access 79 * @param int $ts Time at which the data should be read or written 80 * @param int $rid Row id, 0 for page type data, otherwise autoincrement 81 * @return AccessTableData|AccessTableLookup 82 */ 83 public static function bySchema(Schema $schema, $pid, $ts = 0, $rid = 0) 84 { 85 if (self::isTypePage($pid, $ts)) { 86 return new AccessTableData($schema, $pid, $ts, $rid); 87 } 88 return new AccessTableLookup($schema, $pid, $ts, $rid); 89 } 90 91 /** 92 * Factory Method to access data 93 * 94 * @deprecated Use specific methods since we can no longer 95 * guarantee instantiating the required descendant class 96 * @param string $tablename schema to load 97 * @param string $pid Page id to access 98 * @param int $ts Time at which the data should be read or written 99 * @param int $rid Row id, 0 for page type data, otherwise autoincrement 100 * @return AccessTableData|AccessTableLookup 101 */ 102 public static function byTableName($tablename, $pid, $ts = 0, $rid = 0) 103 { 104 // force loading the latest schema for anything other than page data, 105 // for which we might actually need the history 106 if (!self::isTypePage($pid, $ts)) { 107 $schema = new Schema($tablename, time()); 108 } else { 109 $schema = new Schema($tablename, $ts); 110 } 111 return self::bySchema($schema, $pid, $ts, $rid); 112 } 113 114 /** 115 * AccessTable constructor 116 * 117 * @param Schema $schema The schema valid at $ts 118 * @param string $pid Page id 119 * @param int $ts Time at which the data should be read or written, 0 for now 120 * @param int $rid Row id: 0 for pages, autoincremented for other types 121 */ 122 public function __construct($schema, $pid, $ts = 0, $rid = 0) 123 { 124 /** @var \helper_plugin_struct_db $helper */ 125 $helper = plugin_load('helper', 'struct_db'); 126 $this->sqlite = $helper->getDB(); 127 128 if (!$schema->getId()) { 129 throw new StructException('Schema does not exist. Only data of existing schemas can be accessed'); 130 } 131 132 $this->schema = $schema; 133 $this->pid = $pid; 134 $this->rid = $rid; 135 $this->setTimestamp($ts); 136 foreach ($this->schema->getColumns() as $col) { 137 $this->labels[$col->getColref()] = $col->getType()->getLabel(); 138 } 139 } 140 141 /** 142 * gives access to the schema 143 * 144 * @return Schema 145 */ 146 public function getSchema() 147 { 148 return $this->schema; 149 } 150 151 /** 152 * The current pid 153 * 154 * @return string 155 */ 156 public function getPid() 157 { 158 return $this->pid; 159 } 160 161 /** 162 * The current rid 163 * 164 * @return int 165 */ 166 public function getRid() 167 { 168 return $this->rid; 169 } 170 171 /** 172 * Should remove the current data, by either deleting or ovewriting it 173 * 174 * @return bool if the delete succeeded 175 */ 176 abstract public function clearData(); 177 178 /** 179 * Save the data to the database. 180 * 181 * We differentiate between single-value-column and multi-value-column by the value to the respective column-name, 182 * i.e. depending on if that is a string or an array, respectively. 183 * 184 * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields 185 * @return bool success of saving the data to the database 186 */ 187 public function saveData($data) 188 { 189 if (!$this->validateTypeData($data)) { 190 return false; 191 } 192 193 $this->stable = 'data_' . $this->schema->getTable(); 194 $this->mtable = 'multi_' . $this->schema->getTable(); 195 196 $colrefs = array_flip($this->labels); 197 198 foreach ($data as $colname => $value) { 199 if (!isset($colrefs[$colname])) { 200 throw new StructException("Unknown column %s in schema.", hsc($colname)); 201 } 202 203 $this->singleCols[] = 'col' . $colrefs[$colname]; 204 if (is_array($value)) { 205 foreach ($value as $index => $multivalue) { 206 $this->multiValues[] = [$colrefs[$colname], $index + 1, $multivalue]; 207 } 208 // copy first value to the single column 209 if (isset($value[0])) { 210 $this->singleValues[] = $value[0]; 211 } else { 212 $this->singleValues[] = null; 213 } 214 } else { 215 $this->singleValues[] = $value; 216 } 217 } 218 219 $this->sqlite->query('BEGIN TRANSACTION'); 220 221 $ok = $this->beforeSave(); 222 223 // insert single values 224 $ok = $ok && $this->sqlite->query( 225 $this->getSingleSql(), 226 array_merge($this->getSingleNoninputValues(), $this->singleValues) 227 ); 228 229 $ok = $ok && $this->afterSingleSave(); 230 231 // insert multi values 232 if ($ok && $this->multiValues) { 233 $multisql = $this->getMultiSql(); 234 $multiNoninputValues = $this->getMultiNoninputValues(); 235 foreach ($this->multiValues as $value) { 236 $ok = $ok && $this->sqlite->query( 237 $multisql, 238 array_merge($multiNoninputValues, $value) 239 ); 240 } 241 } 242 243 if (!$ok) { 244 $this->sqlite->query('ROLLBACK TRANSACTION'); 245 return false; 246 } 247 $this->sqlite->query('COMMIT TRANSACTION'); 248 return true; 249 } 250 251 /** 252 * Check whether all required data is present 253 * 254 * @param array $data 255 * @return bool 256 */ 257 abstract protected function validateTypeData($data); 258 259 /** 260 * Names of non-input columns to be inserted into SQL query 261 * 262 * @return array 263 */ 264 abstract protected function getSingleNoninputCols(); 265 266 /** 267 * Values for non-input columns to be inserted into SQL query 268 * for single-value tables 269 * 270 * @return array 271 */ 272 abstract protected function getSingleNoninputValues(); 273 274 /** 275 * String template for single-value table 276 * 277 * @return string 278 */ 279 protected function getSingleSql() 280 { 281 $cols = array_merge($this->getSingleNoninputCols(), $this->singleCols); 282 $cols = join(',', $cols); 283 $vals = array_merge($this->getSingleNoninputValues(), $this->singleValues); 284 285 return "INSERT INTO $this->stable ($cols) VALUES (" . trim(str_repeat('?,', count($vals)), ',') . ');'; 286 } 287 288 /** 289 * Optional operations to be executed before saving data 290 * 291 * @return bool False if any of the operations failed and transaction should be rolled back 292 */ 293 protected function beforeSave() 294 { 295 return true; 296 } 297 298 /** 299 * Optional operations to be executed after saving data to single-value table, 300 * before saving multivalues 301 * 302 * @return bool False if anything goes wrong and transaction should be rolled back 303 */ 304 protected function afterSingleSave() 305 { 306 return true; 307 } 308 309 /** 310 * String template for multi-value table 311 * 312 * @return string 313 */ 314 abstract protected function getMultiSql(); 315 316 /** 317 * Values for non-input columns to be inserted into SQL query 318 * for multi-value tables 319 * @return array 320 */ 321 abstract protected function getMultiNoninputValues(); 322 323 324 /** 325 * Should empty or invisible (inpage) fields be returned? 326 * 327 * Defaults to false 328 * 329 * @param null|bool $set new value, null to read only 330 * @return bool current value (after set) 331 */ 332 public function optionSkipEmpty($set = null) 333 { 334 if (!is_null($set)) { 335 $this->opt_skipempty = $set; 336 } 337 return $this->opt_skipempty; 338 } 339 340 /** 341 * Get the value of a single column 342 * 343 * @param Column $column 344 * @return Value|null 345 */ 346 public function getDataColumn($column) 347 { 348 $data = $this->getData(); 349 foreach ($data as $value) { 350 if ($value->getColumn() == $column) { 351 return $value; 352 } 353 } 354 return null; 355 } 356 357 /** 358 * returns the data saved for the page 359 * 360 * @return Value[] a list of values saved for the current page 361 */ 362 public function getData() 363 { 364 $data = $this->getDataFromDB(); 365 $data = $this->consolidateData($data, false); 366 return $data; 367 } 368 369 /** 370 * returns the data saved for the page as associative array 371 * 372 * The array returned is in the same format as used in @see saveData() 373 * 374 * It always returns raw Values! 375 * 376 * @return array 377 */ 378 public function getDataArray() 379 { 380 $data = $this->getDataFromDB(); 381 $data = $this->consolidateData($data, true); 382 return $data; 383 } 384 385 /** 386 * Return the data in pseudo syntax 387 */ 388 public function getDataPseudoSyntax() 389 { 390 $result = ''; 391 $data = $this->getData(); 392 393 foreach ($data as $value) { 394 $key = $value->getColumn()->getFullQualifiedLabel(); 395 $value = $value->getDisplayValue(); 396 if (is_array($value)) $value = join(', ', $value); 397 $result .= sprintf("% -20s : %s\n", $key, $value); 398 } 399 return $result; 400 } 401 402 /** 403 * retrieve the data saved for the page from the database. Usually there is no need to call this function. 404 * Call @see SchemaData::getData instead. 405 */ 406 protected function getDataFromDB() 407 { 408 $idColumn = self::isTypePage($this->pid, $this->ts) ? 'pid' : 'rid'; 409 list($sql, $opt) = $this->buildGetDataSQL($idColumn); 410 411 $res = $this->sqlite->query($sql, $opt); 412 $data = $this->sqlite->res2arr($res); 413 $this->sqlite->res_close($res); 414 return $data; 415 } 416 417 /** 418 * Creates a proper result array from the database data 419 * 420 * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB 421 * @param bool $asarray return data as associative array (true) or as array of Values (false) 422 * @return array|Value[] 423 */ 424 protected function consolidateData($DBdata, $asarray = false) 425 { 426 $data = array(); 427 428 $sep = Search::CONCAT_SEPARATOR; 429 430 foreach ($this->schema->getColumns(false) as $col) { 431 // if no data saved yet, return empty strings 432 if ($DBdata) { 433 $val = $DBdata[0]['out' . $col->getColref()]; 434 } else { 435 $val = ''; 436 } 437 438 // multi val data is concatenated 439 if ($col->isMulti()) { 440 $val = explode($sep, $val); 441 $val = array_filter($val); 442 } 443 444 $value = new Value($col, $val); 445 446 if ($this->opt_skipempty && $value->isEmpty()) continue; 447 if ($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption? 448 449 // for arrays, we return the raw value only 450 if ($asarray) { 451 $data[$col->getLabel()] = $value->getRawValue(); 452 } else { 453 $data[$col->getLabel()] = $value; 454 } 455 } 456 457 return $data; 458 } 459 460 /** 461 * Builds the SQL statement to select the data for this page and schema 462 * 463 * @return array Two fields: the SQL string and the parameters array 464 */ 465 protected function buildGetDataSQL($idColumn = 'pid') 466 { 467 $sep = Search::CONCAT_SEPARATOR; 468 $stable = 'data_' . $this->schema->getTable(); 469 $mtable = 'multi_' . $this->schema->getTable(); 470 471 $QB = new QueryBuilder(); 472 $QB->addTable($stable, 'DATA'); 473 $QB->addSelectColumn('DATA', $idColumn, strtoupper($idColumn)); 474 $QB->addGroupByStatement("DATA.$idColumn"); 475 476 foreach ($this->schema->getColumns(false) as $col) { 477 $colref = $col->getColref(); 478 $colname = 'col' . $colref; 479 $outname = 'out' . $colref; 480 481 if ($col->getType()->isMulti()) { 482 $tn = 'M' . $colref; 483 $QB->addLeftJoin( 484 'DATA', 485 $mtable, 486 $tn, 487 "DATA.$idColumn = $tn.$idColumn AND DATA.rev = $tn.rev AND $tn.colref = $colref" 488 ); 489 $col->getType()->select($QB, $tn, 'value', $outname); 490 $sel = $QB->getSelectStatement($outname); 491 $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname); 492 } else { 493 $col->getType()->select($QB, 'DATA', $colname, $outname); 494 $QB->addGroupByStatement($outname); 495 } 496 } 497 498 $pl = $QB->addValue($this->{$idColumn}); 499 $QB->filters()->whereAnd("DATA.$idColumn = $pl"); 500 $pl = $QB->addValue($this->getLastRevisionTimestamp()); 501 $QB->filters()->whereAnd("DATA.rev = $pl"); 502 503 return $QB->getSQL(); 504 } 505 506 /** 507 * @param int $ts 508 */ 509 public function setTimestamp($ts) 510 { 511 if ($ts && $ts < $this->schema->getTimeStamp()) { 512 throw new StructException('Given timestamp is not valid for current Schema'); 513 } 514 515 $this->ts = $ts; 516 } 517 518 /** 519 * Returns the timestamp from the current data 520 * @return int 521 */ 522 public function getTimestamp() 523 { 524 return $this->ts; 525 } 526 527 /** 528 * Return the last time an edit happened for this table for the currently set 529 * time and pid. Used in @see buildGetDataSQL() 530 * 531 * @return int 532 */ 533 abstract protected function getLastRevisionTimestamp(); 534 535 /** 536 * Check if the given data validates against the current types. 537 * 538 * @param array $data 539 * @return AccessDataValidator 540 */ 541 public function getValidator($data) 542 { 543 return new AccessDataValidator($this, $data); 544 } 545 546 /** 547 * Returns true if data is of type "page" 548 * 549 * @param string $pid 550 * @param int $rev 551 * @param int $rid 552 * @return bool 553 */ 554 public static function isTypePage($pid, $rev) 555 { 556 return $rev > 0; 557 } 558 559 /** 560 * Returns true if data is of type "lookup" 561 * 562 * @param string $pid 563 * @param int $rev 564 * @param int $rid 565 * @return bool 566 */ 567 public static function isTypeLookup($pid, $rev) 568 { 569 return $pid === ''; 570 } 571 572 /** 573 * Returns true if data is of type "serial" 574 * 575 * @param string $pid 576 * @param int $rev 577 * @param int $rid 578 * @return bool 579 */ 580 public static function isTypeSerial($pid, $rev) 581 { 582 return $pid !== '' && $rev === 0; 583 } 584} 585