1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22class CApiService { 23 24 public static $userData; 25 26 /** 27 * The name of the table. 28 * 29 * @var string 30 */ 31 protected $tableName; 32 33 /** 34 * The alias of the table. 35 * 36 * @var string 37 */ 38 protected $tableAlias = 't'; 39 40 /** 41 * The name of the field used as a private key. 42 * 43 * @var string 44 */ 45 protected $pk; 46 47 /** 48 * An array of field that can be used for sorting. 49 * 50 * @var array 51 */ 52 protected $sortColumns = []; 53 54 /** 55 * An array of allowed get() options that are supported by all APIs. 56 * 57 * @var array 58 */ 59 protected $globalGetOptions = []; 60 61 /** 62 * An array containing all of the allowed get() options for the current API. 63 * 64 * @var array 65 */ 66 protected $getOptions = []; 67 68 /** 69 * An array containing all of the error strings. 70 * 71 * @var array 72 */ 73 protected $errorMessages = []; 74 75 public function __construct() { 76 // set the PK of the table 77 $this->pk = $this->pk($this->tableName()); 78 79 $this->globalGetOptions = [ 80 // filter 81 'filter' => null, 82 'search' => null, 83 'searchByAny' => null, 84 'startSearch' => null, 85 'excludeSearch' => null, 86 'searchWildcardsEnabled'=> null, 87 // output 88 'output' => API_OUTPUT_EXTEND, 89 'countOutput' => null, 90 'groupCount' => null, 91 'preservekeys' => null, 92 'limit' => null 93 ]; 94 $this->getOptions = $this->globalGetOptions; 95 } 96 97 /** 98 * Returns the name of the database table that contains the objects. 99 * 100 * @return string 101 */ 102 public function tableName() { 103 return $this->tableName; 104 } 105 106 /** 107 * Returns the alias of the database table that contains the objects. 108 * 109 * @return string 110 */ 111 protected function tableAlias() { 112 return $this->tableAlias; 113 } 114 115 /** 116 * Returns the table name with the table alias. If the $tableName and $tableAlias 117 * parameters are not given, the name and the alias of the current table will be used. 118 * 119 * @param string $tableName 120 * @param string $tableAlias 121 * 122 * @return string 123 */ 124 protected function tableId($tableName = null, $tableAlias = null) { 125 $tableName = $tableName ? $tableName : $this->tableName(); 126 $tableAlias = $tableAlias ? $tableAlias : $this->tableAlias(); 127 128 return $tableName.' '.$tableAlias; 129 } 130 131 /** 132 * Prepends the table alias to the given field name. If no $tableAlias is given, 133 * the alias of the current table will be used. 134 * 135 * @param string $fieldName 136 * @param string $tableAlias 137 * 138 * @return string 139 */ 140 protected function fieldId($fieldName, $tableAlias = null) { 141 $tableAlias = $tableAlias ? $tableAlias : $this->tableAlias(); 142 143 return $tableAlias.'.'.$fieldName; 144 } 145 146 /** 147 * Returns the name of the field that's used as a private key. If the $tableName is not given, 148 * the PK field of the given table will be returned. 149 * 150 * @param string $tableName; 151 * 152 * @return string 153 */ 154 public function pk($tableName = null) { 155 if ($tableName) { 156 $schema = $this->getTableSchema($tableName); 157 158 if (strpos($schema['key'], ',') !== false) { 159 throw new Exception('Composite private keys are not supported in this API version.'); 160 } 161 162 return $schema['key']; 163 } 164 165 return $this->pk; 166 } 167 168 /** 169 * Returns the name of the option that refers the PK column. If the $tableName parameter 170 * is not given, the Pk option of the current table will be returned. 171 * 172 * @param string $tableName 173 * 174 * @return string 175 */ 176 public function pkOption($tableName = null) { 177 return $this->pk($tableName).'s'; 178 } 179 180 /** 181 * Returns an array that describes the schema of the database table. If no $tableName 182 * is given, the schema of the current table will be returned. 183 * 184 * @param $tableName; 185 * 186 * @return array 187 */ 188 protected function getTableSchema($tableName = null) { 189 $tableName = $tableName ? $tableName : $this->tableName(); 190 191 return DB::getSchema($tableName); 192 } 193 194 /** 195 * Returns true if the table has the given field. If no $tableName is given, 196 * the current table will be used. 197 * 198 * @param string $fieldName 199 * @param string $tableName 200 * 201 * @return boolean 202 */ 203 protected function hasField($fieldName, $tableName = null) { 204 $schema = $this->getTableSchema($tableName); 205 206 return isset($schema['fields'][$fieldName]); 207 } 208 209 /** 210 * Returns a translated error message. 211 * 212 * @param $id 213 * 214 * @return string 215 */ 216 protected function getErrorMsg($id) { 217 return $this->errorMessages[$id]; 218 } 219 220 /** 221 * Adds the given fields to the "output" option if it's not already present. 222 * 223 * @param string $output 224 * @param array $fields either a single field name, or an array of fields 225 * 226 * @return mixed 227 */ 228 protected function outputExtend($output, array $fields) { 229 if ($output === null) { 230 return $fields; 231 } 232 // if output is set to extend, it already contains that field; return it as is 233 elseif ($output === API_OUTPUT_EXTEND) { 234 return $output; 235 } 236 237 // if output is an array, add the additional fields 238 return array_keys(array_flip(array_merge($output, $fields))); 239 } 240 241 /** 242 * Returns true if the given field is requested in the output parameter. 243 * 244 * @param $field 245 * @param $output 246 * 247 * @return bool 248 */ 249 protected function outputIsRequested($field, $output) { 250 switch ($output) { 251 // if all fields are requested, just return true 252 case API_OUTPUT_EXTEND: 253 return true; 254 255 // return false if nothing or an object count is requested 256 case API_OUTPUT_COUNT: 257 case null: 258 return false; 259 260 // if an array of fields is passed, check if the field is present in the array 261 default: 262 return in_array($field, $output); 263 } 264 } 265 266 /** 267 * Unsets fields $fields from the given objects if they are not requested in $output. 268 * 269 * @param array $objects 270 * @param array $fields 271 * @param string|array $output desired output 272 * 273 * @return array 274 */ 275 protected function unsetExtraFields(array $objects, array $fields, $output) { 276 // find the fields that have not been requested 277 $extraFields = []; 278 foreach ($fields as $field) { 279 if (!$this->outputIsRequested($field, $output)) { 280 $extraFields[] = $field; 281 } 282 } 283 284 // unset these fields 285 if ($extraFields) { 286 foreach ($objects as &$object) { 287 foreach ($extraFields as $field) { 288 unset($object[$field]); 289 } 290 } 291 unset($object); 292 } 293 294 return $objects; 295 } 296 297 /** 298 * Creates a relation map for the given objects. 299 * 300 * If the $table parameter is set, the relations will be loaded from a database table, otherwise the map will be 301 * built from two base object properties. 302 * 303 * @param array $objects a hash of base objects 304 * @param string $baseField the base object ID field 305 * @param string $foreignField the related objects ID field 306 * @param string $table table to load the relation from 307 * 308 * @return CRelationMap 309 */ 310 protected function createRelationMap(array $objects, $baseField, $foreignField, $table = null) { 311 $relationMap = new CRelationMap(); 312 313 // create the map from a database table 314 if ($table) { 315 $res = DBselect(API::getApiService()->createSelectQuery($table, [ 316 'output' => [$baseField, $foreignField], 317 'filter' => [$baseField => array_keys($objects)] 318 ])); 319 while ($relation = DBfetch($res)) { 320 $relationMap->addRelation($relation[$baseField], $relation[$foreignField]); 321 } 322 } 323 324 // create a map from the base objects 325 else { 326 foreach ($objects as $object) { 327 $relationMap->addRelation($object[$baseField], $object[$foreignField]); 328 } 329 } 330 331 return $relationMap; 332 } 333 334 /** 335 * Constructs an SQL SELECT query for a specific table from the given API options, executes it and returns 336 * the result. 337 * 338 * TODO: add global 'countOutput' support 339 * 340 * @param string $tableName 341 * @param array $options 342 * 343 * @return array 344 */ 345 protected function select($tableName, array $options) { 346 $limit = isset($options['limit']) ? $options['limit'] : null; 347 348 $sql = $this->createSelectQuery($tableName, $options); 349 350 $objects = DBfetchArray(DBSelect($sql, $limit)); 351 352 if (isset($options['preservekeys'])) { 353 $rs = []; 354 foreach ($objects as $object) { 355 $rs[$object[$this->pk($tableName)]] = $object; 356 } 357 358 return $rs; 359 } 360 else { 361 return $objects; 362 } 363 } 364 365 /** 366 * Creates an SQL SELECT query from the given options. 367 * 368 * @param string $tableName 369 * @param array $options 370 * 371 * @return array 372 */ 373 protected function createSelectQuery($tableName, array $options) { 374 $sqlParts = $this->createSelectQueryParts($tableName, $this->tableAlias(), $options); 375 376 return $this->createSelectQueryFromParts($sqlParts); 377 } 378 379 /** 380 * Builds an SQL parts array from the given options. 381 * 382 * @param string $tableName 383 * @param string $tableAlias 384 * @param array $options 385 * 386 * @return array The resulting SQL parts array 387 */ 388 protected function createSelectQueryParts($tableName, $tableAlias, array $options) { 389 // extend default options 390 $options = zbx_array_merge($this->globalGetOptions, $options); 391 392 $sqlParts = [ 393 'select' => [$this->fieldId($this->pk($tableName), $tableAlias)], 394 'from' => [$this->tableId($tableName, $tableAlias)], 395 'where' => [], 396 'group' => [], 397 'order' => [], 398 'limit' => null 399 ]; 400 401 // add filter options 402 $sqlParts = $this->applyQueryFilterOptions($tableName, $tableAlias, $options, $sqlParts); 403 404 // add output options 405 $sqlParts = $this->applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts); 406 407 // add sort options 408 $sqlParts = $this->applyQuerySortOptions($tableName, $tableAlias, $options, $sqlParts); 409 410 return $sqlParts; 411 } 412 413 /** 414 * Creates a SELECT SQL query from the given SQL parts array. 415 * 416 * @param array $sqlParts An SQL parts array 417 * 418 * @return string The resulting SQL query 419 */ 420 protected function createSelectQueryFromParts(array $sqlParts) { 421 // build query 422 $sqlSelect = implode(',', array_unique($sqlParts['select'])); 423 $sqlFrom = implode(',', array_unique($sqlParts['from'])); 424 $sqlWhere = empty($sqlParts['where']) ? '' : ' WHERE '.implode(' AND ', array_unique($sqlParts['where'])); 425 $sqlGroup = empty($sqlParts['group']) ? '' : ' GROUP BY '.implode(',', array_unique($sqlParts['group'])); 426 $sqlOrder = empty($sqlParts['order']) ? '' : ' ORDER BY '.implode(',', array_unique($sqlParts['order'])); 427 428 return 'SELECT '.zbx_db_distinct($sqlParts).' '.$sqlSelect. 429 ' FROM '.$sqlFrom. 430 $sqlWhere. 431 $sqlGroup. 432 $sqlOrder; 433 } 434 435 /** 436 * Modifies the SQL parts to implement all of the output related options. 437 * 438 * @param string $tableName 439 * @param string $tableAlias 440 * @param array $options 441 * @param array $sqlParts 442 * 443 * @return array The resulting SQL parts array 444 */ 445 protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) { 446 $pkFieldId = $this->fieldId($this->pk($tableName), $tableAlias); 447 448 // count 449 if (isset($options['countOutput']) && !$this->requiresPostSqlFiltering($options)) { 450 $sqlParts['select'] = ['COUNT(DISTINCT '.$pkFieldId.') AS rowscount']; 451 452 // select columns used by group count 453 if (isset($options['groupCount'])) { 454 foreach ($sqlParts['group'] as $fields) { 455 $sqlParts['select'][] = $fields; 456 } 457 } 458 } 459 // custom output 460 elseif (is_array($options['output'])) { 461 // the pk field must always be included for the API to work properly 462 $sqlParts['select'] = [$pkFieldId]; 463 foreach ($options['output'] as $field) { 464 if ($this->hasField($field, $tableName)) { 465 $sqlParts['select'][] = $this->fieldId($field, $tableAlias); 466 } 467 } 468 469 $sqlParts['select'] = array_unique($sqlParts['select']); 470 } 471 // extended output 472 elseif ($options['output'] == API_OUTPUT_EXTEND) { 473 // TODO: API_OUTPUT_EXTEND must return ONLY the fields from the base table 474 $sqlParts = $this->addQuerySelect($this->fieldId('*', $tableAlias), $sqlParts); 475 } 476 477 return $sqlParts; 478 } 479 480 /** 481 * Modifies the SQL parts to implement all of the filter related options. 482 * 483 * @param string $tableName 484 * @param string $tableAlias 485 * @param array $options 486 * @param array $sqlParts 487 * 488 * @return array The resulting SQL parts array 489 */ 490 protected function applyQueryFilterOptions($tableName, $tableAlias, array $options, array $sqlParts) { 491 $pkOption = $this->pkOption($tableName); 492 $tableId = $this->tableId($tableName, $tableAlias); 493 494 // pks 495 if (isset($options[$pkOption])) { 496 zbx_value2array($options[$pkOption]); 497 $sqlParts['where'][] = dbConditionString($this->fieldId($this->pk($tableName), $tableAlias), $options[$pkOption]); 498 } 499 500 // filters 501 if (is_array($options['filter'])) { 502 $this->dbFilter($tableId, $options, $sqlParts); 503 } 504 505 // search 506 if (is_array($options['search'])) { 507 zbx_db_search($tableId, $options, $sqlParts); 508 } 509 510 return $sqlParts; 511 } 512 513 /** 514 * Modifies the SQL parts to implement all of the sorting related options. 515 * Sorting is currently only supported for CApiService::get() methods. 516 * 517 * @param string $tableName 518 * @param string $tableAlias 519 * @param array $options 520 * @param array $sqlParts 521 * 522 * @return array 523 */ 524 protected function applyQuerySortOptions($tableName, $tableAlias, array $options, array $sqlParts) { 525 if ($this->sortColumns && !zbx_empty($options['sortfield'])) { 526 $options['sortfield'] = is_array($options['sortfield']) 527 ? array_unique($options['sortfield']) 528 : [$options['sortfield']]; 529 530 foreach ($options['sortfield'] as $i => $sortfield) { 531 // validate sortfield 532 if (!str_in_array($sortfield, $this->sortColumns)) { 533 throw new APIException(ZBX_API_ERROR_INTERNAL, _s('Sorting by field "%1$s" not allowed.', $sortfield)); 534 } 535 536 // add sort field to order 537 $sortorder = ''; 538 if (is_array($options['sortorder'])) { 539 if (!empty($options['sortorder'][$i])) { 540 $sortorder = ($options['sortorder'][$i] == ZBX_SORT_DOWN) ? ' '.ZBX_SORT_DOWN : ''; 541 } 542 } 543 else { 544 $sortorder = ($options['sortorder'] == ZBX_SORT_DOWN) ? ' '.ZBX_SORT_DOWN : ''; 545 } 546 547 $sqlParts = $this->applyQuerySortField($sortfield, $sortorder, $tableAlias, $sqlParts); 548 } 549 } 550 551 return $sqlParts; 552 } 553 554 /** 555 * Adds a specific property from the 'sortfield' parameter to the $sqlParts array. 556 * 557 * @param string $sortfield 558 * @param string $sortorder 559 * @param string $alias 560 * @param array $sqlParts 561 * 562 * @return array 563 */ 564 protected function applyQuerySortField($sortfield, $sortorder, $alias, array $sqlParts) { 565 // add sort field to select if distinct is used 566 if (count($sqlParts['from']) > 1 567 && !str_in_array($alias.'.'.$sortfield, $sqlParts['select']) 568 && !str_in_array($alias.'.*', $sqlParts['select'])) { 569 570 $sqlParts['select'][$sortfield] = $alias.'.'.$sortfield; 571 } 572 573 $sqlParts['order'][$alias.'.'.$sortfield] = $alias.'.'.$sortfield.$sortorder; 574 575 return $sqlParts; 576 } 577 578 /** 579 * Adds the given field to the SELECT part of the $sqlParts array if it's not already present. 580 * If $sqlParts['select'] not present it is created and field appended. 581 * 582 * @param string $fieldId 583 * @param array $sqlParts 584 * 585 * @return array 586 */ 587 protected function addQuerySelect($fieldId, array $sqlParts) { 588 if (!isset($sqlParts['select'])) { 589 return ['select' => [$fieldId]]; 590 } 591 592 list($tableAlias, $field) = explode('.', $fieldId); 593 594 if (!in_array($fieldId, $sqlParts['select']) && !in_array($this->fieldId('*', $tableAlias), $sqlParts['select'])) { 595 // if we want to select all of the columns, other columns from this table can be removed 596 if ($field == '*') { 597 foreach ($sqlParts['select'] as $key => $selectFieldId) { 598 list($selectTableAlias,) = explode('.', $selectFieldId); 599 600 if ($selectTableAlias == $tableAlias) { 601 unset($sqlParts['select'][$key]); 602 } 603 } 604 } 605 606 $sqlParts['select'][] = $fieldId; 607 } 608 609 return $sqlParts; 610 } 611 612 /** 613 * Adds the given field to the ORDER BY part of the $sqlParts array. 614 * 615 * @param string $fieldId 616 * @param array $sqlParts 617 * @param string $sortorder sort direction, ZBX_SORT_UP or ZBX_SORT_DOWN 618 * 619 * @return array 620 */ 621 protected function addQueryOrder($fieldId, array $sqlParts, $sortorder = null) { 622 // some databases require the sortable column to be present in the SELECT part of the query 623 $sqlParts = $this->addQuerySelect($fieldId, $sqlParts); 624 625 $sqlParts['order'][$fieldId] = $fieldId.($sortorder ? ' '.$sortorder : ''); 626 627 return $sqlParts; 628 } 629 630 /** 631 * Adds the related objects requested by "select*" options to the resulting object set. 632 * 633 * @param array $options 634 * @param array $result an object hash with PKs as keys 635 636 * @return array mixed 637 */ 638 protected function addRelatedObjects(array $options, array $result) { 639 // must be implemented in each API separately 640 641 return $result; 642 } 643 644 /** 645 * Deletes the object with the given IDs with respect to relative objects. 646 * 647 * The method must be extended to handle relative objects. 648 * 649 * @param array $ids 650 */ 651 protected function deleteByIds(array $ids) { 652 DB::delete($this->tableName(), [ 653 $this->pk() => $ids 654 ]); 655 } 656 657 /** 658 * Fetches the fields given in $fields from the database and extends the objects with the loaded data. 659 * 660 * @param string $tableName 661 * @param array $objects 662 * @param array $fields 663 * 664 * @return array 665 */ 666 protected function extendObjects($tableName, array $objects, array $fields) { 667 if ($objects) { 668 $dbObjects = API::getApiService()->select($tableName, [ 669 'output' => $fields, 670 $this->pkOption($tableName) => zbx_objectValues($objects, $this->pk($tableName)), 671 'preservekeys' => true 672 ]); 673 674 foreach ($objects as &$object) { 675 $pk = $object[$this->pk($tableName)]; 676 if (isset($dbObjects[$pk])) { 677 check_db_fields($dbObjects[$pk], $object); 678 } 679 } 680 unset($object); 681 } 682 683 return $objects; 684 } 685 686 /** 687 * An extendObjects() wrapper for singular objects. 688 * 689 * @see extendObjects() 690 * 691 * @param string $tableName 692 * @param array $object 693 * @param array $fields 694 * 695 * @return mixed 696 */ 697 protected function extendObject($tableName, array $object, array $fields) { 698 $objects = $this->extendObjects($tableName, [$object], $fields); 699 700 return reset($objects); 701 } 702 703 /** 704 * For each object in $objects the method copies fields listed in $fields that are not present in the target 705 * object from from the source object. 706 * 707 * Matching objects in both arrays must have the same keys. 708 * 709 * @param array $objects 710 * @param array $sourceObjects 711 * 712 * @return array 713 */ 714 protected function extendFromObjects(array $objects, array $sourceObjects, array $fields) { 715 $fields = array_flip($fields); 716 717 foreach ($objects as $key => &$object) { 718 if (isset($sourceObjects[$key])) { 719 $object += array_intersect_key($sourceObjects[$key], $fields); 720 } 721 } 722 unset($object); 723 724 return $objects; 725 } 726 727 /** 728 * For each object in $objects the method copies fields listed in $fields that are not present in the target 729 * object from the source object. 730 * 731 * @param array $objects 732 * @param array $source 733 * @param string $field_name 734 * @param array $fields 735 * 736 * @return array 737 */ 738 protected function extendObjectsByKey(array $objects, array $source, $field_name, array $fields) { 739 $fields = array_flip($fields); 740 741 foreach ($objects as &$object) { 742 if (array_key_exists($object[$field_name], $source)) { 743 $object += array_intersect_key($source[$object[$field_name]], $fields); 744 } 745 } 746 unset($object); 747 748 return $objects; 749 } 750 751 /** 752 * Checks that each object has a valid ID. 753 * 754 * @param array $objects 755 * @param $idField name of the field that contains the id 756 * @param $messageRequired error message if no ID is given 757 * @param $messageEmpty error message if the ID is empty 758 * @param $messageInvalid error message if the ID is invalid 759 */ 760 protected function checkObjectIds(array $objects, $idField, $messageRequired, $messageEmpty, $messageInvalid) { 761 $idValidator = new CIdValidator([ 762 'messageEmpty' => $messageEmpty, 763 'messageInvalid' => $messageInvalid 764 ]); 765 foreach ($objects as $object) { 766 if (!isset($object[$idField])) { 767 self::exception(ZBX_API_ERROR_PARAMETERS, _params($messageRequired, [$idField])); 768 } 769 770 $this->checkValidator($object[$idField], $idValidator); 771 } 772 } 773 774 /** 775 * Checks if the object has any fields, that are not defined in the schema or in $extraFields. 776 * 777 * @param string $tableName 778 * @param array $object 779 * @param string $error 780 * @param array $extraFields an array of field names, that are not present in the schema, but may be 781 * used in requests 782 * 783 * @throws APIException 784 */ 785 protected function checkUnsupportedFields($tableName, array $object, $error, array $extraFields = []) { 786 $extraFields = array_flip($extraFields); 787 788 foreach ($object as $field => $value) { 789 if (!DB::hasField($tableName, $field) && !isset($extraFields[$field])) { 790 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 791 } 792 } 793 } 794 795 /** 796 * Checks if an object contains any of the given parameters. 797 * 798 * Example: 799 * checkNoParameters($item, array('templateid', 'state'), _('Cannot set "%1$s" for item "%2$s".'), $item['name']); 800 * If any of the parameters 'templateid' or 'state' are present in the object, it will be placed in "%1$s" 801 * and $item['name'] will be placed in "%2$s". 802 * 803 * @throws APIException if any of the parameters are present in the object 804 * 805 * @param array $object 806 * @param array $params array of parameters to check 807 * @param string $error 808 * @param string $objectName 809 */ 810 protected function checkNoParameters(array $object, array $params, $error, $objectName) { 811 foreach ($params as $param) { 812 if (array_key_exists($param, $object)) { 813 $error = _params($error, [$param, $objectName]); 814 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 815 } 816 } 817 } 818 819 /** 820 * Throws an API exception. 821 * 822 * @static 823 * 824 * @param int $code 825 * @param string $error 826 */ 827 protected static function exception($code = ZBX_API_ERROR_INTERNAL, $error = '') { 828 throw new APIException($code, $error); 829 } 830 831 /** 832 * Triggers a deprecated notice. Should be called when a deprecated parameter or method is used. 833 * The notice will not be displayed in the result returned by an API method. 834 * 835 * @param string $error error text 836 */ 837 protected function deprecated($error) { 838 trigger_error($error, E_USER_NOTICE); 839 } 840 841 /** 842 * Apply filter conditions to sql built query. 843 * 844 * @param string $table 845 * @param array $options 846 * @param array $sqlParts 847 * 848 * @return bool 849 */ 850 protected function dbFilter($table, $options, &$sqlParts) { 851 list($table, $tableShort) = explode(' ', $table); 852 853 $tableSchema = DB::getSchema($table); 854 855 $filter = []; 856 foreach ($options['filter'] as $field => $value) { 857 // skip missing fields and text fields (not supported by Oracle) 858 // skip empty values 859 if (!isset($tableSchema['fields'][$field]) || $tableSchema['fields'][$field]['type'] == DB::FIELD_TYPE_TEXT 860 || zbx_empty($value)) { 861 continue; 862 } 863 864 zbx_value2array($value); 865 866 $fieldName = $this->fieldId($field, $tableShort); 867 $filter[$field] = DB::isNumericFieldType($tableSchema['fields'][$field]['type']) 868 ? dbConditionInt($fieldName, $value) 869 : dbConditionString($fieldName, $value); 870 } 871 872 if ($filter) { 873 if (isset($sqlParts['where']['filter'])) { 874 $filter[] = $sqlParts['where']['filter']; 875 } 876 877 if (is_null($options['searchByAny']) || $options['searchByAny'] === false || count($filter) == 1) { 878 $sqlParts['where']['filter'] = implode(' AND ', $filter); 879 } 880 else { 881 $sqlParts['where']['filter'] = '('.implode(' OR ', $filter).')'; 882 } 883 884 return true; 885 } 886 887 return false; 888 } 889 890 /** 891 * Converts a deprecated parameter to a new one in the $params array. If both parameter are used, 892 * the new parameter will override the deprecated one. 893 * If a deprecated parameter is used, a notice will be triggered in the frontend. 894 * 895 * @param array $params 896 * @param string $deprecatedParam 897 * @param string $newParam 898 * 899 * @return array 900 */ 901 protected function convertDeprecatedParam(array $params, $deprecatedParam, $newParam) { 902 if (isset($params[$deprecatedParam])) { 903 self::deprecated('Parameter "'.$deprecatedParam.'" is deprecated.'); 904 905 // if the new parameter is not used, use the deprecated one instead 906 if (!isset($params[$newParam])) { 907 $params[$newParam] = $params[$deprecatedParam]; 908 } 909 910 // unset the deprecated parameter 911 unset($params[$deprecatedParam]); 912 } 913 914 return $params; 915 } 916 917 /** 918 * Check if a set of parameters contains a deprecated parameter or a a parameter with a deprecated value. 919 * If $value is not set, the method will trigger a deprecated notice if $params contains the $paramName key. 920 * If $value is set, the method will trigger a notice if the value of the parameter is equal to the deprecated value 921 * or the parameter is an array and contains a deprecated value. 922 * 923 * @param array $params 924 * @param string $paramName 925 * @param string $value 926 * 927 * @return void 928 */ 929 protected function checkDeprecatedParam(array $params, $paramName, $value = null) { 930 if (isset($params[$paramName])) { 931 if ($value === null) { 932 self::deprecated('Parameter "'.$paramName.'" is deprecated.'); 933 } 934 elseif (is_array($params[$paramName]) && in_array($value, $params[$paramName]) || $params[$paramName] == $value) { 935 self::deprecated('Value "'.$value.'" for parameter "'.$paramName.'" is deprecated.'); 936 } 937 } 938 } 939 940 /** 941 * Runs the given validator and throws an exception if it fails. 942 * 943 * @param $value 944 * @param CValidator $validator 945 */ 946 protected function checkValidator($value, CValidator $validator) { 947 if (!$validator->validate($value)) { 948 self::exception(ZBX_API_ERROR_INTERNAL, $validator->getError()); 949 } 950 } 951 952 /** 953 * Runs the given partial validator and throws an exception if it fails. 954 * 955 * @param array $array 956 * @param CPartialValidatorInterface $validator 957 * @param array $fullArray 958 */ 959 protected function checkPartialValidator(array $array, CPartialValidatorInterface $validator, $fullArray = []) { 960 if (!$validator->validatePartial($array, $fullArray)) { 961 self::exception(ZBX_API_ERROR_INTERNAL, $validator->getError()); 962 } 963 } 964 965 /** 966 * Adds a deprecated property to an array of resulting objects if it's requested in $output. The value for the 967 * deprecated property will be taken from the new one. 968 * 969 * @param array $objects 970 * @param string $deprecatedProperty 971 * @param string $newProperty 972 * @param string|array $output 973 * 974 * @return array 975 */ 976 protected function handleDeprecatedOutput(array $objects, $deprecatedProperty, $newProperty, $output) { 977 if ($this->outputIsRequested($deprecatedProperty, $output)) { 978 foreach ($objects as &$object) { 979 $object[$deprecatedProperty] = $object[$newProperty]; 980 } 981 unset($object); 982 } 983 984 return $objects; 985 } 986 987 /** 988 * Fetch data from DB. 989 * If post SQL filtering is necessary, several queries will be executed. SQL limit is calculated so that minimum 990 * amount of queries would be executed and minimum amount of unnecessary data retrieved. 991 * 992 * @param string $query SQL query 993 * @param array $options API call parameters 994 * 995 * @return array 996 */ 997 protected function customFetch($query, array $options) { 998 if ($this->requiresPostSqlFiltering($options)) { 999 $offset = 0; 1000 1001 // we think that taking twice as necessary elements in first query is fair guess, this cast to int as well 1002 $limit = $options['limit'] ? 2 * $options['limit'] : null; 1003 1004 // we use $minLimit for setting minimum limit twice as big for each consecutive query to not run in lots 1005 // of queries for some cases 1006 $minLimit = $limit; 1007 $allElements = []; 1008 1009 do { 1010 // fetch group of elements 1011 $elements = DBfetchArray(DBselect($query, $limit, $offset)); 1012 1013 // we have potentially more elements 1014 $hasMore = ($limit && count($elements) === $limit); 1015 1016 $elements = $this->applyPostSqlFiltering($elements, $options); 1017 1018 // truncate element set after post SQL filtering, if enough elements or more retrieved via SQL query 1019 if ($options['limit'] && count($allElements) + count($elements) >= $options['limit']) { 1020 $allElements += array_slice($elements, 0, $options['limit'] - count($allElements), true); 1021 break; 1022 } 1023 1024 $allElements += $elements; 1025 1026 // calculate $limit and $offset for next query 1027 if ($limit) { 1028 $offset += $limit; 1029 $minLimit *= 2; 1030 1031 // take care of division by zero 1032 $elemCount = count($elements) ? count($elements) : 1; 1033 1034 // we take $limit as $minLimit or reasonable estimate to get all necessary data in two queries 1035 // with high probability 1036 $limit = max($minLimit, round($limit / $elemCount * ($options['limit'] - count($allElements)) * 2)); 1037 } 1038 } while ($hasMore); 1039 1040 return $allElements; 1041 } 1042 else { 1043 return DBfetchArray(DBselect($query, $options['limit'])); 1044 } 1045 } 1046 1047 /** 1048 * Checks if post SQL filtering necessary. 1049 * 1050 * @param array $options API call parameters 1051 * 1052 * @return bool true if filtering necessary false otherwise 1053 */ 1054 protected function requiresPostSqlFiltering(array $options) { 1055 // must be implemented in each API separately 1056 1057 return false; 1058 } 1059 1060 /** 1061 * Removes elements which could not be removed within SQL query. 1062 * 1063 * @param array $elements list of elements on whom perform filtering 1064 * @param array $options API call parameters 1065 * 1066 * @return array input array $elements with some elements removed 1067 */ 1068 protected function applyPostSqlFiltering(array $elements, array $options) { 1069 // must be implemented in each API separately 1070 1071 return $elements; 1072 } 1073} 1074