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