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