1<?php 2/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Repository; 5 6use DateTime; 7use Icinga\Application\Logger; 8use Icinga\Data\Filter\Filter; 9use Icinga\Data\Filter\FilterExpression; 10use Icinga\Data\Selectable; 11use Icinga\Exception\ProgrammingError; 12use Icinga\Exception\QueryException; 13use Icinga\Exception\StatementException; 14use Icinga\Util\ASN1; 15use Icinga\Util\StringHelper; 16use InvalidArgumentException; 17 18/** 19 * Abstract base class for concrete repository implementations 20 * 21 * To utilize this class and its features, the following is required: 22 * <ul> 23 * <li>Concrete implementations need to initialize Repository::$queryColumns</li> 24 * <li>The datasource passed to a repository must implement the Selectable interface</li> 25 * <li>The datasource must yield an instance of Queryable when its select() method is called</li> 26 * </ul> 27 */ 28abstract class Repository implements Selectable 29{ 30 /** 31 * The format to use when converting values of type date_time 32 */ 33 const DATETIME_FORMAT = 'd/m/Y g:i A'; 34 35 /** 36 * The name of this repository 37 * 38 * @var string 39 */ 40 protected $name; 41 42 /** 43 * The datasource being used 44 * 45 * @var Selectable 46 */ 47 protected $ds; 48 49 /** 50 * The base table name this repository is responsible for 51 * 52 * This will be automatically set to the first key of $queryColumns if not explicitly set. 53 * 54 * @var string 55 */ 56 protected $baseTable; 57 58 /** 59 * The virtual tables being provided 60 * 61 * This may be initialized by concrete repository implementations with an array 62 * where a key is the name of a virtual table and its value the real table name. 63 * 64 * @var array 65 */ 66 protected $virtualTables; 67 68 /** 69 * The query columns being provided 70 * 71 * This must be initialized by concrete repository implementations, in the following format 72 * <code> 73 * array( 74 * 'baseTable' => array( 75 * 'column1', 76 * 'alias1' => 'column2', 77 * 'alias2' => 'column3' 78 * ) 79 * ) 80 * </code> 81 * 82 * @var array 83 */ 84 protected $queryColumns; 85 86 /** 87 * The columns (or aliases) which are not permitted to be queried 88 * 89 * Blacklisted query columns can still occur in a filter expression or sort rule. 90 * 91 * @var array An array of strings 92 */ 93 protected $blacklistedQueryColumns; 94 95 /** 96 * Whether the blacklisted query columns are in the legacy format 97 * 98 * @var bool 99 */ 100 protected $legacyBlacklistedQueryColumns; 101 102 /** 103 * The filter columns being provided 104 * 105 * This may be intialized by concrete repository implementations, in the following format 106 * <code> 107 * array( 108 * 'alias_or_column_name', 109 * 'label_to_show_in_the_filter_editor' => 'alias_or_column_name' 110 * ) 111 * </code> 112 * 113 * @var array 114 */ 115 protected $filterColumns; 116 117 /** 118 * Whether the provided filter columns are in the legacy format 119 * 120 * @var bool 121 */ 122 protected $legacyFilterColumns; 123 124 /** 125 * The search columns (or aliases) being provided 126 * 127 * @var array An array of strings 128 */ 129 protected $searchColumns; 130 131 /** 132 * Whether the provided search columns are in the legacy format 133 * 134 * @var bool 135 */ 136 protected $legacySearchColumns; 137 138 /** 139 * The sort rules to be applied on a query 140 * 141 * This may be initialized by concrete repository implementations, in the following format 142 * <code> 143 * array( 144 * 'alias_or_column_name' => array( 145 * 'order' => 'asc' 146 * ), 147 * 'alias_or_column_name' => array( 148 * 'columns' => array( 149 * 'once_more_the_alias_or_column_name_as_in_the_parent_key', 150 * 'an_additional_alias_or_column_name_with_a_specific_direction asc' 151 * ), 152 * 'order' => 'desc' 153 * ), 154 * 'alias_or_column_name' => array( 155 * 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column') 156 * // Ascendant sort by default 157 * ) 158 * ) 159 * </code> 160 * Note that it's mandatory to supply the alias name in case there is one. 161 * 162 * @var array 163 */ 164 protected $sortRules; 165 166 /** 167 * Whether the provided sort rules are in the legacy format 168 * 169 * @var bool 170 */ 171 protected $legacySortRules; 172 173 /** 174 * The value conversion rules to apply on a query or statement 175 * 176 * This may be initialized by concrete repository implementations and describes for which aliases or column 177 * names what type of conversion is available. For entries, where the key is the alias/column and the value 178 * is the type identifier, the repository attempts to find a conversion method for the alias/column first and, 179 * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the 180 * repository only attempts to find a conversion method for the alias/column. The name of a conversion method 181 * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and 182 * groupname will be translated to retrieveGroupname) 183 * 184 * @var array 185 */ 186 protected $conversionRules; 187 188 /** 189 * An array to map table names to aliases 190 * 191 * @var array 192 */ 193 protected $aliasTableMap; 194 195 /** 196 * A flattened array to map query columns to aliases 197 * 198 * @var array 199 */ 200 protected $aliasColumnMap; 201 202 /** 203 * An array to map table names to query columns 204 * 205 * @var array 206 */ 207 protected $columnTableMap; 208 209 /** 210 * A flattened array to map aliases to query columns 211 * 212 * @var array 213 */ 214 protected $columnAliasMap; 215 216 /** 217 * Create a new repository object 218 * 219 * @param Selectable|null $ds The datasource to use. 220 * Only pass null if you have overridden {@link getDataSource()}! 221 */ 222 public function __construct(Selectable $ds = null) 223 { 224 $this->ds = $ds; 225 $this->aliasTableMap = array(); 226 $this->aliasColumnMap = array(); 227 $this->columnTableMap = array(); 228 $this->columnAliasMap = array(); 229 230 $this->init(); 231 } 232 233 /** 234 * Initialize this repository 235 * 236 * Supposed to be overwritten by concrete repository implementations. 237 */ 238 protected function init() 239 { 240 } 241 242 /** 243 * Set this repository's name 244 * 245 * @param string $name 246 * 247 * @return $this 248 */ 249 public function setName($name) 250 { 251 $this->name = $name; 252 return $this; 253 } 254 255 /** 256 * Return this repository's name 257 * 258 * In case no name has been explicitly set yet, the class name is returned. 259 * 260 * @return string 261 */ 262 public function getName() 263 { 264 return $this->name ?: __CLASS__; 265 } 266 267 /** 268 * Return the datasource being used for the given table 269 * 270 * @param string $table 271 * 272 * @return Selectable 273 * 274 * @throws ProgrammingError In case no datasource is available 275 */ 276 public function getDataSource($table = null) 277 { 278 if ($this->ds === null) { 279 throw new ProgrammingError( 280 'No data source available. It is required to either pass it' 281 . ' at initialization time or by overriding this method.' 282 ); 283 } 284 285 return $this->ds; 286 } 287 288 /** 289 * Return the base table name this repository is responsible for 290 * 291 * @return string 292 * 293 * @throws ProgrammingError In case no base table name has been set and 294 * $this->queryColumns does not provide one either 295 */ 296 public function getBaseTable() 297 { 298 if ($this->baseTable === null) { 299 $queryColumns = $this->getQueryColumns(); 300 reset($queryColumns); 301 $this->baseTable = key($queryColumns); 302 if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) { 303 throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable); 304 } 305 } 306 307 return $this->baseTable; 308 } 309 310 /** 311 * Return the virtual tables being provided 312 * 313 * Calls $this->initializeVirtualTables() in case $this->virtualTables is null. 314 * 315 * @return array 316 */ 317 public function getVirtualTables() 318 { 319 if ($this->virtualTables === null) { 320 $this->virtualTables = $this->initializeVirtualTables(); 321 } 322 323 return $this->virtualTables; 324 } 325 326 /** 327 * Overwrite this in your repository implementation in case you need to initialize the virtual tables lazily 328 * 329 * @return array 330 */ 331 protected function initializeVirtualTables() 332 { 333 return array(); 334 } 335 336 /** 337 * Return the query columns being provided 338 * 339 * Calls $this->initializeQueryColumns() in case $this->queryColumns is null. 340 * 341 * @return array 342 */ 343 public function getQueryColumns() 344 { 345 if ($this->queryColumns === null) { 346 $this->queryColumns = $this->initializeQueryColumns(); 347 } 348 349 return $this->queryColumns; 350 } 351 352 /** 353 * Overwrite this in your repository implementation in case you need to initialize the query columns lazily 354 * 355 * @return array 356 */ 357 protected function initializeQueryColumns() 358 { 359 return array(); 360 } 361 362 /** 363 * Return the columns (or aliases) which are not permitted to be queried 364 * 365 * Calls $this->initializeBlacklistedQueryColumns() in case $this->blacklistedQueryColumns is null. 366 * 367 * @param string $table 368 * 369 * @return array 370 */ 371 public function getBlacklistedQueryColumns($table = null) 372 { 373 if ($this->blacklistedQueryColumns === null) { 374 $this->legacyBlacklistedQueryColumns = false; 375 376 $blacklistedQueryColumns = $this->initializeBlacklistedQueryColumns($table); 377 if (is_int(key($blacklistedQueryColumns))) { 378 $this->blacklistedQueryColumns[$table] = $blacklistedQueryColumns; 379 } else { 380 $this->blacklistedQueryColumns = $blacklistedQueryColumns; 381 } 382 } elseif ($this->legacyBlacklistedQueryColumns === null) { 383 $this->legacyBlacklistedQueryColumns = is_int(key($this->blacklistedQueryColumns)); 384 } 385 386 if ($this->legacyBlacklistedQueryColumns) { 387 return $this->blacklistedQueryColumns; 388 } elseif (! isset($this->blacklistedQueryColumns[$table])) { 389 $this->blacklistedQueryColumns[$table] = $this->initializeBlacklistedQueryColumns($table); 390 } 391 392 return $this->blacklistedQueryColumns[$table]; 393 } 394 395 /** 396 * Overwrite this in your repository implementation in case you need to initialize the 397 * blacklisted query columns lazily or dependent on a query's current base table 398 * 399 * @param string $table 400 * 401 * @return array 402 */ 403 protected function initializeBlacklistedQueryColumns() 404 { 405 // $table is not part of the signature due to PHP strict standards 406 return array(); 407 } 408 409 /** 410 * Return the filter columns being provided 411 * 412 * Calls $this->initializeFilterColumns() in case $this->filterColumns is null. 413 * 414 * @param string $table 415 * 416 * @return array 417 */ 418 public function getFilterColumns($table = null) 419 { 420 if ($this->filterColumns === null) { 421 $this->legacyFilterColumns = false; 422 423 $filterColumns = $this->initializeFilterColumns($table); 424 $foundTables = array_intersect_key($this->getQueryColumns(), $filterColumns); 425 if (empty($foundTables)) { 426 $this->filterColumns[$table] = $filterColumns; 427 } else { 428 $this->filterColumns = $filterColumns; 429 } 430 } elseif ($this->legacyFilterColumns === null) { 431 $foundTables = array_intersect_key($this->getQueryColumns(), $this->filterColumns); 432 $this->legacyFilterColumns = empty($foundTables); 433 } 434 435 if ($this->legacyFilterColumns) { 436 return $this->filterColumns; 437 } elseif (! isset($this->filterColumns[$table])) { 438 $this->filterColumns[$table] = $this->initializeFilterColumns($table); 439 } 440 441 return $this->filterColumns[$table]; 442 } 443 444 /** 445 * Overwrite this in your repository implementation in case you need to initialize 446 * the filter columns lazily or dependent on a query's current base table 447 * 448 * @param string $table 449 * 450 * @return array 451 */ 452 protected function initializeFilterColumns() 453 { 454 // $table is not part of the signature due to PHP strict standards 455 return array(); 456 } 457 458 /** 459 * Return the search columns being provided 460 * 461 * Calls $this->initializeSearchColumns() in case $this->searchColumns is null. 462 * 463 * @param string $table 464 * 465 * @return array 466 */ 467 public function getSearchColumns($table = null) 468 { 469 if ($this->searchColumns === null) { 470 $this->legacySearchColumns = false; 471 472 $searchColumns = $this->initializeSearchColumns($table); 473 if (is_int(key($searchColumns))) { 474 $this->searchColumns[$table] = $searchColumns; 475 } else { 476 $this->searchColumns = $searchColumns; 477 } 478 } elseif ($this->legacySearchColumns === null) { 479 $this->legacySearchColumns = is_int(key($this->searchColumns)); 480 } 481 482 if ($this->legacySearchColumns) { 483 return $this->searchColumns; 484 } elseif (! isset($this->searchColumns[$table])) { 485 $this->searchColumns[$table] = $this->initializeSearchColumns($table); 486 } 487 488 return $this->searchColumns[$table]; 489 } 490 491 /** 492 * Overwrite this in your repository implementation in case you need to initialize 493 * the search columns lazily or dependent on a query's current base table 494 * 495 * @param string $table 496 * 497 * @return array 498 */ 499 protected function initializeSearchColumns() 500 { 501 // $table is not part of the signature due to PHP strict standards 502 return array(); 503 } 504 505 /** 506 * Return the sort rules to be applied on a query 507 * 508 * Calls $this->initializeSortRules() in case $this->sortRules is null. 509 * 510 * @param string $table 511 * 512 * @return array 513 */ 514 public function getSortRules($table = null) 515 { 516 if ($this->sortRules === null) { 517 $this->legacySortRules = false; 518 519 $sortRules = $this->initializeSortRules($table); 520 $foundTables = array_intersect_key($this->getQueryColumns(), $sortRules); 521 if (empty($foundTables)) { 522 $this->sortRules[$table] = $sortRules; 523 } else { 524 $this->sortRules = $sortRules; 525 } 526 } elseif ($this->legacySortRules === null) { 527 $foundTables = array_intersect_key($this->getQueryColumns(), $this->sortRules); 528 $this->legacySortRules = empty($foundTables); 529 } 530 531 if ($this->legacySortRules) { 532 return $this->sortRules; 533 } elseif (! isset($this->sortRules[$table])) { 534 $this->sortRules[$table] = $this->initializeSortRules($table); 535 } 536 537 return $this->sortRules[$table]; 538 } 539 540 /** 541 * Overwrite this in your repository implementation in case you need to initialize 542 * the sort rules lazily or dependent on a query's current base table 543 * 544 * @param string $table 545 * 546 * @return array 547 */ 548 protected function initializeSortRules() 549 { 550 // $table is not part of the signature due to PHP strict standards 551 return array(); 552 } 553 554 /** 555 * Return the value conversion rules to apply on a query 556 * 557 * Calls $this->initializeConversionRules() in case $this->conversionRules is null. 558 * 559 * @return array 560 */ 561 public function getConversionRules() 562 { 563 if ($this->conversionRules === null) { 564 $this->conversionRules = $this->initializeConversionRules(); 565 } 566 567 return $this->conversionRules; 568 } 569 570 /** 571 * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily 572 * 573 * @return array 574 */ 575 protected function initializeConversionRules() 576 { 577 return array(); 578 } 579 580 /** 581 * Return an array to map table names to aliases 582 * 583 * @return array 584 */ 585 protected function getAliasTableMap() 586 { 587 if (empty($this->aliasTableMap)) { 588 $this->initializeAliasMaps(); 589 } 590 591 return $this->aliasTableMap; 592 } 593 594 /** 595 * Return a flattened array to map query columns to aliases 596 * 597 * @return array 598 */ 599 protected function getAliasColumnMap() 600 { 601 if (empty($this->aliasColumnMap)) { 602 $this->initializeAliasMaps(); 603 } 604 605 return $this->aliasColumnMap; 606 } 607 608 /** 609 * Return an array to map table names to query columns 610 * 611 * @return array 612 */ 613 protected function getColumnTableMap() 614 { 615 if (empty($this->columnTableMap)) { 616 $this->initializeAliasMaps(); 617 } 618 619 return $this->columnTableMap; 620 } 621 622 /** 623 * Return a flattened array to map aliases to query columns 624 * 625 * @return array 626 */ 627 protected function getColumnAliasMap() 628 { 629 if (empty($this->columnAliasMap)) { 630 $this->initializeAliasMaps(); 631 } 632 633 return $this->columnAliasMap; 634 } 635 636 /** 637 * Initialize $this->aliasTableMap and $this->aliasColumnMap 638 * 639 * @throws ProgrammingError In case $this->queryColumns does not provide any column information 640 */ 641 protected function initializeAliasMaps() 642 { 643 $queryColumns = $this->getQueryColumns(); 644 if (empty($queryColumns)) { 645 throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first'); 646 } 647 648 foreach ($queryColumns as $table => $columns) { 649 foreach ($columns as $alias => $column) { 650 if (! is_string($alias)) { 651 $key = $column; 652 } else { 653 $key = $alias; 654 $column = preg_replace('~\n\s*~', ' ', $column); 655 } 656 657 if (array_key_exists($key, $this->aliasTableMap)) { 658 if ($this->aliasTableMap[$key] !== null) { 659 $existingTable = $this->aliasTableMap[$key]; 660 $existingColumn = $this->aliasColumnMap[$key]; 661 $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable; 662 $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn; 663 $this->aliasTableMap[$key] = null; 664 $this->aliasColumnMap[$key] = null; 665 } 666 667 $this->aliasTableMap[$table . '.' . $key] = $table; 668 $this->aliasColumnMap[$table . '.' . $key] = $column; 669 } else { 670 $this->aliasTableMap[$key] = $table; 671 $this->aliasColumnMap[$key] = $column; 672 } 673 674 if (array_key_exists($column, $this->columnTableMap)) { 675 if ($this->columnTableMap[$column] !== null) { 676 $existingTable = $this->columnTableMap[$column]; 677 $existingAlias = $this->columnAliasMap[$column]; 678 $this->columnTableMap[$existingTable . '.' . $column] = $existingTable; 679 $this->columnAliasMap[$existingTable . '.' . $column] = $existingAlias; 680 $this->columnTableMap[$column] = null; 681 $this->columnAliasMap[$column] = null; 682 } 683 684 $this->columnTableMap[$table . '.' . $column] = $table; 685 $this->columnAliasMap[$table . '.' . $column] = $key; 686 } else { 687 $this->columnTableMap[$column] = $table; 688 $this->columnAliasMap[$column] = $key; 689 } 690 } 691 } 692 } 693 694 /** 695 * Return a new query for the given columns 696 * 697 * @param array $columns The desired columns, if null all columns will be queried 698 * 699 * @return RepositoryQuery 700 */ 701 public function select(array $columns = null) 702 { 703 $query = new RepositoryQuery($this); 704 $query->from($this->getBaseTable(), $columns); 705 return $query; 706 } 707 708 /** 709 * Return whether this repository is capable of converting values for the given table and optional column 710 * 711 * @param string $table 712 * @param string $column 713 * 714 * @return bool 715 */ 716 public function providesValueConversion($table, $column = null) 717 { 718 $conversionRules = $this->getConversionRules(); 719 if (empty($conversionRules)) { 720 return false; 721 } 722 723 if (! isset($conversionRules[$table])) { 724 return false; 725 } elseif ($column === null) { 726 return true; 727 } 728 729 $alias = $this->reassembleQueryColumnAlias($table, $column) ?: $column; 730 return array_key_exists($alias, $conversionRules[$table]) || in_array($alias, $conversionRules[$table]); 731 } 732 733 /** 734 * Convert a value supposed to be transmitted to the data source 735 * 736 * @param string $table The table where to persist the value 737 * @param string $name The alias or column name 738 * @param mixed $value The value to convert 739 * @param RepositoryQuery $query An optional query to pass as context 740 * (Directly passed through to $this->getConverter) 741 * 742 * @return mixed If conversion was possible, the converted value, 743 * otherwise the unchanged value 744 */ 745 public function persistColumn($table, $name, $value, RepositoryQuery $query = null) 746 { 747 $converter = $this->getConverter($table, $name, 'persist', $query); 748 if ($converter !== null) { 749 $value = $this->$converter($value, $name, $table, $query); 750 } 751 752 return $value; 753 } 754 755 /** 756 * Convert a value which was fetched from the data source 757 * 758 * @param string $table The table the value has been fetched from 759 * @param string $name The alias or column name 760 * @param mixed $value The value to convert 761 * @param RepositoryQuery $query An optional query to pass as context 762 * (Directly passed through to $this->getConverter) 763 * 764 * @return mixed If conversion was possible, the converted value, 765 * otherwise the unchanged value 766 */ 767 public function retrieveColumn($table, $name, $value, RepositoryQuery $query = null) 768 { 769 $converter = $this->getConverter($table, $name, 'retrieve', $query); 770 if ($converter !== null) { 771 $value = $this->$converter($value, $name, $table, $query); 772 } 773 774 return $value; 775 } 776 777 /** 778 * Return the name of the conversion method for the given alias or column name and context 779 * 780 * @param string $table The datasource's table 781 * @param string $name The alias or column name for which to return a conversion method 782 * @param string $context The context of the conversion: persist or retrieve 783 * @param RepositoryQuery $query An optional query to pass as context 784 * (unused by the base implementation) 785 * 786 * @return string 787 * 788 * @throws ProgrammingError In case a conversion rule is found but not any conversion method 789 */ 790 protected function getConverter($table, $name, $context, RepositoryQuery $query = null) 791 { 792 $conversionRules = $this->getConversionRules(); 793 if (! isset($conversionRules[$table])) { 794 return; 795 } 796 797 $tableRules = $conversionRules[$table]; 798 if (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { 799 $alias = $name; 800 } 801 802 // Check for a conversion method for the alias/column first 803 if (array_key_exists($alias, $tableRules) || in_array($alias, $tableRules)) { 804 $methodName = $context . join('', array_map('ucfirst', explode('_', $alias))); 805 if (method_exists($this, $methodName)) { 806 return $methodName; 807 } 808 } 809 810 // The conversion method for the type is just a fallback, but it is required to exist if defined 811 if (isset($tableRules[$alias])) { 812 $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$alias]))); 813 if (! method_exists($this, $context . $identifier)) { 814 // Do not throw an error in case at least one conversion method exists 815 if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) { 816 throw new ProgrammingError( 817 'Cannot find any conversion method for type "%s"' 818 . '. Add a proper conversion method or remove the type definition', 819 $tableRules[$alias] 820 ); 821 } 822 823 Logger::debug( 824 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".', 825 $context . $identifier, 826 $tableRules[$alias], 827 $this->getName() 828 ); 829 } else { 830 return $context . $identifier; 831 } 832 } 833 } 834 835 /** 836 * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT 837 * 838 * @param mixed $value 839 * 840 * @return string 841 */ 842 protected function persistDateTime($value) 843 { 844 if (is_numeric($value)) { 845 $value = date(static::DATETIME_FORMAT, $value); 846 } elseif ($value instanceof DateTime) { 847 $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone 848 } elseif ($value !== null) { 849 throw new ProgrammingError( 850 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object', 851 $value 852 ); 853 } 854 855 return $value; 856 } 857 858 /** 859 * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp 860 * 861 * @param string $value 862 * 863 * @return int 864 */ 865 protected function retrieveDateTime($value) 866 { 867 if (is_numeric($value)) { 868 $value = (int) $value; 869 } elseif (is_string($value)) { 870 $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value); 871 if ($dateTime === false) { 872 Logger::debug( 873 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"', 874 $value, 875 static::DATETIME_FORMAT, 876 $this->getName() 877 ); 878 $value = null; 879 } else { 880 $value = $dateTime->getTimestamp(); 881 } 882 } elseif ($value !== null) { 883 throw new ProgrammingError( 884 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string', 885 $value 886 ); 887 } 888 889 return $value; 890 } 891 892 /** 893 * Convert the given array to an comma separated string 894 * 895 * @param array|string $value 896 * 897 * @return string 898 */ 899 protected function persistCommaSeparatedString($value) 900 { 901 if (is_array($value)) { 902 $value = join(',', array_map('trim', $value)); 903 } elseif ($value !== null && !is_string($value)) { 904 throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value); 905 } 906 907 return $value; 908 } 909 910 /** 911 * Convert the given comma separated string to an array 912 * 913 * @param string $value 914 * 915 * @return array 916 */ 917 protected function retrieveCommaSeparatedString($value) 918 { 919 if ($value && is_string($value)) { 920 $value = StringHelper::trimSplit($value); 921 } elseif ($value !== null) { 922 throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value); 923 } 924 925 return $value; 926 } 927 928 /** 929 * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation 930 * 931 * @param string|null $value 932 * 933 * @return int 934 * 935 * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 936 */ 937 protected function retrieveGeneralizedTime($value) 938 { 939 if ($value === null) { 940 return $value; 941 } 942 943 try { 944 return ASN1::parseGeneralizedTime($value)->getTimeStamp(); 945 } catch (InvalidArgumentException $e) { 946 Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage())); 947 } 948 } 949 950 /** 951 * Validate that the requested table exists and resolve it's real name if necessary 952 * 953 * @param string $table The table to validate 954 * @param RepositoryQuery $query An optional query to pass as context 955 * (unused by the base implementation) 956 * 957 * @return string The table's name, may differ from the given one 958 * 959 * @throws ProgrammingError In case the given table does not exist 960 */ 961 public function requireTable($table, RepositoryQuery $query = null) 962 { 963 $queryColumns = $this->getQueryColumns(); 964 if (! isset($queryColumns[$table])) { 965 throw new ProgrammingError('Table "%s" not found', $table); 966 } 967 968 $virtualTables = $this->getVirtualTables(); 969 if (isset($virtualTables[$table])) { 970 $table = $virtualTables[$table]; 971 } 972 973 return $table; 974 } 975 976 /** 977 * Recurse the given filter, require each column for the given table and convert all values 978 * 979 * @param string $table The table being filtered 980 * @param Filter $filter The filter to recurse 981 * @param RepositoryQuery $query An optional query to pass as context 982 * (Directly passed through to $this->requireFilterColumn) 983 * @param bool $clone Whether to clone $filter first 984 * 985 * @return Filter The udpated filter 986 */ 987 public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true) 988 { 989 if ($clone) { 990 $filter = clone $filter; 991 } 992 993 if ($filter->isExpression()) { 994 $column = $filter->getColumn(); 995 $filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter)); 996 $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query)); 997 } elseif ($filter->isChain()) { 998 foreach ($filter->filters() as $chainOrExpression) { 999 $this->requireFilter($table, $chainOrExpression, $query, false); 1000 } 1001 } 1002 1003 return $filter; 1004 } 1005 1006 /** 1007 * Return this repository's query columns of the given table mapped to their respective aliases 1008 * 1009 * @param string $table 1010 * 1011 * @return array 1012 * 1013 * @throws ProgrammingError In case $table does not exist 1014 */ 1015 public function requireAllQueryColumns($table) 1016 { 1017 $queryColumns = $this->getQueryColumns(); 1018 if (! array_key_exists($table, $queryColumns)) { 1019 throw new ProgrammingError('Table name "%s" not found', $table); 1020 } 1021 1022 $blacklist = $this->getBlacklistedQueryColumns($table); 1023 $columns = array(); 1024 foreach ($queryColumns[$table] as $alias => $column) { 1025 $name = is_string($alias) ? $alias : $column; 1026 if (! in_array($name, $blacklist)) { 1027 $columns[$alias] = $this->resolveQueryColumnAlias($table, $name); 1028 } 1029 } 1030 1031 return $columns; 1032 } 1033 1034 /** 1035 * Return the query column name for the given alias or null in case the alias does not exist 1036 * 1037 * @param string $table 1038 * @param string $alias 1039 * 1040 * @return string|null 1041 */ 1042 public function resolveQueryColumnAlias($table, $alias) 1043 { 1044 $aliasColumnMap = $this->getAliasColumnMap(); 1045 if (isset($aliasColumnMap[$alias])) { 1046 return $aliasColumnMap[$alias]; 1047 } 1048 1049 $prefixedAlias = $table . '.' . $alias; 1050 if (isset($aliasColumnMap[$prefixedAlias])) { 1051 return $aliasColumnMap[$prefixedAlias]; 1052 } 1053 } 1054 1055 /** 1056 * Return the alias for the given query column name or null in case the query column name does not exist 1057 * 1058 * @param string $table 1059 * @param string $column 1060 * 1061 * @return string|null 1062 */ 1063 public function reassembleQueryColumnAlias($table, $column) 1064 { 1065 $columnAliasMap = $this->getColumnAliasMap(); 1066 if (isset($columnAliasMap[$column])) { 1067 return $columnAliasMap[$column]; 1068 } 1069 1070 $prefixedColumn = $table . '.' . $column; 1071 if (isset($columnAliasMap[$prefixedColumn])) { 1072 return $columnAliasMap[$prefixedColumn]; 1073 } 1074 } 1075 1076 /** 1077 * Return whether the given alias or query column name is available in the given table 1078 * 1079 * @param string $table 1080 * @param string $alias 1081 * 1082 * @return bool 1083 */ 1084 public function validateQueryColumnAssociation($table, $alias) 1085 { 1086 $aliasTableMap = $this->getAliasTableMap(); 1087 if (isset($aliasTableMap[$alias])) { 1088 return $aliasTableMap[$alias] === $table; 1089 } 1090 1091 $prefixedAlias = $table . '.' . $alias; 1092 if (isset($aliasTableMap[$prefixedAlias])) { 1093 return true; 1094 } 1095 1096 $columnTableMap = $this->getColumnTableMap(); 1097 if (isset($columnTableMap[$alias])) { 1098 return $columnTableMap[$alias] === $table; 1099 } 1100 1101 return isset($columnTableMap[$prefixedAlias]); 1102 } 1103 1104 /** 1105 * Return whether the given column name or alias is a valid query column 1106 * 1107 * @param string $table The table where to look for the column or alias 1108 * @param string $name The column name or alias to check 1109 * 1110 * @return bool 1111 */ 1112 public function hasQueryColumn($table, $name) 1113 { 1114 if ($this->resolveQueryColumnAlias($table, $name) !== null) { 1115 $alias = $name; 1116 } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { 1117 return false; 1118 } 1119 1120 return !in_array($alias, $this->getBlacklistedQueryColumns($table)) 1121 && $this->validateQueryColumnAssociation($table, $name); 1122 } 1123 1124 /** 1125 * Validate that the given column is a valid query target and return it or the actual name if it's an alias 1126 * 1127 * @param string $table The table where to look for the column or alias 1128 * @param string $name The name or alias of the column to validate 1129 * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) 1130 * 1131 * @return string The given column's name 1132 * 1133 * @throws QueryException In case the given column is not a valid query column 1134 */ 1135 public function requireQueryColumn($table, $name, RepositoryQuery $query = null) 1136 { 1137 if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { 1138 $alias = $name; 1139 } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { 1140 $column = $name; 1141 } else { 1142 throw new QueryException(t('Query column "%s" not found'), $name); 1143 } 1144 1145 if (in_array($alias, $this->getBlacklistedQueryColumns($table))) { 1146 throw new QueryException(t('Column "%s" cannot be queried'), $name); 1147 } 1148 1149 if (! $this->validateQueryColumnAssociation($table, $alias)) { 1150 throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table); 1151 } 1152 1153 return $column; 1154 } 1155 1156 /** 1157 * Return whether the given column name or alias is a valid filter column 1158 * 1159 * @param string $table The table where to look for the column or alias 1160 * @param string $name The column name or alias to check 1161 * 1162 * @return bool 1163 */ 1164 public function hasFilterColumn($table, $name) 1165 { 1166 return ($this->resolveQueryColumnAlias($table, $name) !== null 1167 || $this->reassembleQueryColumnAlias($table, $name) !== null) 1168 && $this->validateQueryColumnAssociation($table, $name); 1169 } 1170 1171 /** 1172 * Validate that the given column is a valid filter target and return it or the actual name if it's an alias 1173 * 1174 * @param string $table The table where to look for the column or alias 1175 * @param string $name The name or alias of the column to validate 1176 * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) 1177 * @param FilterExpression $filter An optional filter to pass as context (unused by the base implementation) 1178 * 1179 * @return string The given column's name 1180 * 1181 * @throws QueryException In case the given column is not a valid filter column 1182 */ 1183 public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null) 1184 { 1185 if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { 1186 $alias = $name; 1187 } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { 1188 $column = $name; 1189 } else { 1190 throw new QueryException(t('Filter column "%s" not found'), $name); 1191 } 1192 1193 if (! $this->validateQueryColumnAssociation($table, $alias)) { 1194 throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table); 1195 } 1196 1197 return $column; 1198 } 1199 1200 /** 1201 * Return whether the given column name or alias of the given table is a valid statement column 1202 * 1203 * @param string $table The table where to look for the column or alias 1204 * @param string $name The column name or alias to check 1205 * 1206 * @return bool 1207 */ 1208 public function hasStatementColumn($table, $name) 1209 { 1210 return $this->hasQueryColumn($table, $name); 1211 } 1212 1213 /** 1214 * Validate that the given column is a valid statement column and return it or the actual name if it's an alias 1215 * 1216 * @param string $table The table for which to require the column 1217 * @param string $name The name or alias of the column to validate 1218 * 1219 * @return string The given column's name 1220 * 1221 * @throws StatementException In case the given column is not a statement column 1222 */ 1223 public function requireStatementColumn($table, $name) 1224 { 1225 if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { 1226 $alias = $name; 1227 } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { 1228 $column = $name; 1229 } else { 1230 throw new StatementException('Statement column "%s" not found', $name); 1231 } 1232 1233 if (in_array($alias, $this->getBlacklistedQueryColumns($table))) { 1234 throw new StatementException('Column "%s" cannot be referenced in a statement', $name); 1235 } 1236 1237 if (! $this->validateQueryColumnAssociation($table, $alias)) { 1238 throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table); 1239 } 1240 1241 return $column; 1242 } 1243 1244 /** 1245 * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values 1246 * 1247 * @param string $table 1248 * @param array $data 1249 * 1250 * @return array 1251 */ 1252 public function requireStatementColumns($table, array $data) 1253 { 1254 $resolved = array(); 1255 foreach ($data as $alias => $value) { 1256 $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value); 1257 } 1258 1259 return $resolved; 1260 } 1261} 1262