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 * @fixme we need a flag here to disable per-save db transactions if this is executed in bulk 185 * @param array $data typelabel => value for single fields or typelabel => array(value, value, ...) for multi fields 186 * @return bool success of saving the data to the database 187 */ 188 public function saveData($data) 189 { 190 if (!$this->validateTypeData($data)) { 191 return false; 192 } 193 194 $this->stable = 'data_' . $this->schema->getTable(); 195 $this->mtable = 'multi_' . $this->schema->getTable(); 196 197 $colrefs = array_flip($this->labels); 198 199 foreach ($data as $colname => $value) { 200 if (!isset($colrefs[$colname])) { 201 throw new StructException("Unknown column %s in schema.", hsc($colname)); 202 } 203 204 $this->singleCols[] = 'col' . $colrefs[$colname]; 205 if (is_array($value)) { 206 foreach ($value as $index => $multivalue) { 207 $this->multiValues[] = [$colrefs[$colname], $index + 1, $multivalue]; 208 } 209 // copy first value to the single column 210 if (isset($value[0])) { 211 $this->singleValues[] = $value[0]; 212 } else { 213 $this->singleValues[] = null; 214 } 215 } else { 216 $this->singleValues[] = $value; 217 } 218 } 219 220 $this->sqlite->query('BEGIN TRANSACTION'); 221 222 $ok = $this->beforeSave(); 223 224 // insert single values 225 $ok = $ok && $this->sqlite->query( 226 $this->getSingleSql(), 227 array_merge($this->getSingleNoninputValues(), $this->singleValues) 228 ); 229 230 $ok = $ok && $this->afterSingleSave(); 231 232 // insert multi values 233 if ($ok && $this->multiValues) { 234 $multisql = $this->getMultiSql(); 235 $multiNoninputValues = $this->getMultiNoninputValues(); 236 foreach ($this->multiValues as $value) { 237 $ok = $ok && $this->sqlite->query( 238 $multisql, 239 array_merge($multiNoninputValues, $value) 240 ); 241 } 242 } 243 244 if (!$ok) { 245 $this->sqlite->query('ROLLBACK TRANSACTION'); 246 return false; 247 } 248 $this->sqlite->query('COMMIT TRANSACTION'); 249 return true; 250 } 251 252 /** 253 * Check whether all required data is present 254 * 255 * @param array $data 256 * @return bool 257 */ 258 abstract protected function validateTypeData($data); 259 260 /** 261 * Names of non-input columns to be inserted into SQL query 262 * 263 * @return array 264 */ 265 abstract protected function getSingleNoninputCols(); 266 267 /** 268 * Values for non-input columns to be inserted into SQL query 269 * for single-value tables 270 * 271 * @return array 272 */ 273 abstract protected function getSingleNoninputValues(); 274 275 /** 276 * String template for single-value table 277 * 278 * @return string 279 */ 280 protected function getSingleSql() 281 { 282 $cols = array_merge($this->getSingleNoninputCols(), $this->singleCols); 283 $cols = join(',', $cols); 284 $vals = array_merge($this->getSingleNoninputValues(), $this->singleValues); 285 286 return "INSERT INTO $this->stable ($cols) VALUES (" . trim(str_repeat('?,', count($vals)), ',') . ');'; 287 } 288 289 /** 290 * Optional operations to be executed before saving data 291 * 292 * @return bool False if any of the operations failed and transaction should be rolled back 293 */ 294 protected function beforeSave() 295 { 296 return true; 297 } 298 299 /** 300 * Optional operations to be executed after saving data to single-value table, 301 * before saving multivalues 302 * 303 * @return bool False if anything goes wrong and transaction should be rolled back 304 */ 305 protected function afterSingleSave() 306 { 307 return true; 308 } 309 310 /** 311 * String template for multi-value table 312 * 313 * @return string 314 */ 315 abstract protected function getMultiSql(); 316 317 /** 318 * Values for non-input columns to be inserted into SQL query 319 * for multi-value tables 320 * @return array 321 */ 322 abstract protected function getMultiNoninputValues(); 323 324 325 /** 326 * Should empty or invisible (inpage) fields be returned? 327 * 328 * Defaults to false 329 * 330 * @param null|bool $set new value, null to read only 331 * @return bool current value (after set) 332 */ 333 public function optionSkipEmpty($set = null) 334 { 335 if (!is_null($set)) { 336 $this->opt_skipempty = $set; 337 } 338 return $this->opt_skipempty; 339 } 340 341 /** 342 * Get the value of a single column 343 * 344 * @param Column $column 345 * @return Value|null 346 */ 347 public function getDataColumn($column) 348 { 349 $data = $this->getData(); 350 foreach ($data as $value) { 351 if ($value->getColumn() == $column) { 352 return $value; 353 } 354 } 355 return null; 356 } 357 358 /** 359 * returns the data saved for the page 360 * 361 * @return Value[] a list of values saved for the current page 362 */ 363 public function getData() 364 { 365 $data = $this->getDataFromDB(); 366 $data = $this->consolidateData($data, false); 367 return $data; 368 } 369 370 /** 371 * returns the data saved for the page as associative array 372 * 373 * The array returned is in the same format as used in @see saveData() 374 * 375 * It always returns raw Values! 376 * 377 * @return array 378 */ 379 public function getDataArray() 380 { 381 $data = $this->getDataFromDB(); 382 $data = $this->consolidateData($data, true); 383 return $data; 384 } 385 386 /** 387 * Return the data in pseudo syntax 388 */ 389 public function getDataPseudoSyntax() 390 { 391 $result = ''; 392 $data = $this->getData(); 393 394 foreach ($data as $value) { 395 $key = $value->getColumn()->getFullQualifiedLabel(); 396 $value = $value->getDisplayValue(); 397 if (is_array($value)) $value = join(', ', $value); 398 $result .= sprintf("% -20s : %s\n", $key, $value); 399 } 400 return $result; 401 } 402 403 /** 404 * retrieve the data saved for the page from the database. Usually there is no need to call this function. 405 * Call @see SchemaData::getData instead. 406 */ 407 protected function getDataFromDB() 408 { 409 $idColumn = self::isTypePage($this->pid, $this->ts) ? 'pid' : 'rid'; 410 list($sql, $opt) = $this->buildGetDataSQL($idColumn); 411 412 $res = $this->sqlite->query($sql, $opt); 413 $data = $this->sqlite->res2arr($res); 414 $this->sqlite->res_close($res); 415 return $data; 416 } 417 418 /** 419 * Creates a proper result array from the database data 420 * 421 * @param array $DBdata the data as it is retrieved from the database, i.e. by SchemaData::getDataFromDB 422 * @param bool $asarray return data as associative array (true) or as array of Values (false) 423 * @return array|Value[] 424 */ 425 protected function consolidateData($DBdata, $asarray = false) 426 { 427 $data = array(); 428 429 $sep = Search::CONCAT_SEPARATOR; 430 431 foreach ($this->schema->getColumns(false) as $col) { 432 // if no data saved yet, return empty strings 433 if ($DBdata) { 434 $val = $DBdata[0]['out' . $col->getColref()]; 435 } else { 436 $val = ''; 437 } 438 439 // multi val data is concatenated 440 if ($col->isMulti()) { 441 $val = explode($sep, $val); 442 $val = array_filter($val); 443 } 444 445 $value = new Value($col, $val); 446 447 if ($this->opt_skipempty && $value->isEmpty()) continue; 448 if ($this->opt_skipempty && !$col->isVisibleInPage()) continue; //FIXME is this a correct assumption? 449 450 // for arrays, we return the raw value only 451 if ($asarray) { 452 $data[$col->getLabel()] = $value->getRawValue(); 453 } else { 454 $data[$col->getLabel()] = $value; 455 } 456 } 457 458 return $data; 459 } 460 461 /** 462 * Builds the SQL statement to select the data for this page and schema 463 * 464 * @return array Two fields: the SQL string and the parameters array 465 */ 466 protected function buildGetDataSQL($idColumn = 'pid') 467 { 468 $sep = Search::CONCAT_SEPARATOR; 469 $stable = 'data_' . $this->schema->getTable(); 470 $mtable = 'multi_' . $this->schema->getTable(); 471 472 $QB = new QueryBuilder(); 473 $QB->addTable($stable, 'DATA'); 474 $QB->addSelectColumn('DATA', $idColumn, strtoupper($idColumn)); 475 $QB->addGroupByStatement("DATA.$idColumn"); 476 477 foreach ($this->schema->getColumns(false) as $col) { 478 $colref = $col->getColref(); 479 $colname = 'col' . $colref; 480 $outname = 'out' . $colref; 481 482 if ($col->getType()->isMulti()) { 483 $tn = 'M' . $colref; 484 $QB->addLeftJoin( 485 'DATA', 486 $mtable, 487 $tn, 488 "DATA.$idColumn = $tn.$idColumn AND DATA.rev = $tn.rev AND $tn.colref = $colref" 489 ); 490 $col->getType()->select($QB, $tn, 'value', $outname); 491 $sel = $QB->getSelectStatement($outname); 492 $QB->addSelectStatement("GROUP_CONCAT($sel, '$sep')", $outname); 493 } else { 494 $col->getType()->select($QB, 'DATA', $colname, $outname); 495 $QB->addGroupByStatement($outname); 496 } 497 } 498 499 $pl = $QB->addValue($this->{$idColumn}); 500 $QB->filters()->whereAnd("DATA.$idColumn = $pl"); 501 $pl = $QB->addValue($this->getLastRevisionTimestamp()); 502 $QB->filters()->whereAnd("DATA.rev = $pl"); 503 504 return $QB->getSQL(); 505 } 506 507 /** 508 * @param int $ts 509 */ 510 public function setTimestamp($ts) 511 { 512 if ($ts && $ts < $this->schema->getTimeStamp()) { 513 throw new StructException('Given timestamp is not valid for current Schema'); 514 } 515 516 $this->ts = $ts; 517 } 518 519 /** 520 * Returns the timestamp from the current data 521 * @return int 522 */ 523 public function getTimestamp() 524 { 525 return $this->ts; 526 } 527 528 /** 529 * Return the last time an edit happened for this table for the currently set 530 * time and pid. Used in @see buildGetDataSQL() 531 * 532 * @return int 533 */ 534 abstract protected function getLastRevisionTimestamp(); 535 536 /** 537 * Check if the given data validates against the current types. 538 * 539 * @param array $data 540 * @return AccessDataValidator 541 */ 542 public function getValidator($data) 543 { 544 return new AccessDataValidator($this, $data); 545 } 546 547 /** 548 * Returns true if data is of type "page" 549 * 550 * @param string $pid 551 * @param int $rev 552 * @param int $rid 553 * @return bool 554 */ 555 public static function isTypePage($pid, $rev) 556 { 557 return $rev > 0; 558 } 559 560 /** 561 * Returns true if data is of type "lookup" 562 * 563 * @param string $pid 564 * @param int $rev 565 * @param int $rid 566 * @return bool 567 */ 568 public static function isTypeLookup($pid, $rev) 569 { 570 return $pid === ''; 571 } 572 573 /** 574 * Returns true if data is of type "serial" 575 * 576 * @param string $pid 577 * @param int $rev 578 * @param int $rid 579 * @return bool 580 */ 581 public static function isTypeSerial($pid, $rev) 582 { 583 return $pid !== '' && $rev === 0; 584 } 585} 586