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
22/**
23 * Class containing methods for operations with item general.
24 *
25 * @package API
26 */
27abstract class CItemGeneral extends CApiService {
28
29	const ERROR_EXISTS_TEMPLATE = 'existsTemplate';
30	const ERROR_EXISTS = 'exists';
31	const ERROR_NO_INTERFACE = 'noInterface';
32	const ERROR_INVALID_KEY = 'invalidKey';
33
34	protected $fieldRules;
35
36	/**
37	 * @abstract
38	 *
39	 * @param array $options
40	 *
41	 * @return array
42	 */
43	abstract public function get($options = []);
44
45	public function __construct() {
46		parent::__construct();
47
48		// template - if templated item, value is taken from template item, cannot be changed on host
49		// system - values should not be updated
50		// host - value should be null for template items
51		$this->fieldRules = [
52			'type'					=> ['template' => 1],
53			'snmp_community'		=> [],
54			'snmp_oid'				=> ['template' => 1],
55			'hostid'				=> [],
56			'name'					=> ['template' => 1],
57			'description'			=> [],
58			'key_'					=> ['template' => 1],
59			'delay'					=> [],
60			'history'				=> [],
61			'trends'				=> [],
62			'status'				=> [],
63			'value_type'			=> ['template' => 1],
64			'trapper_hosts'			=> [],
65			'units'					=> ['template' => 1],
66			'multiplier'			=> ['template' => 1],
67			'delta'					=> ['template' => 1],
68			'snmpv3_contextname'	=> [],
69			'snmpv3_securityname'	=> [],
70			'snmpv3_securitylevel'	=> [],
71			'snmpv3_authprotocol'	=> [],
72			'snmpv3_authpassphrase'	=> [],
73			'snmpv3_privprotocol'	=> [],
74			'snmpv3_privpassphrase'	=> [],
75			'formula'				=> ['template' => 1],
76			'error'					=> ['system' => 1],
77			'lastlogsize'			=> ['system' => 1],
78			'logtimefmt'			=> [],
79			'templateid'			=> ['system' => 1],
80			'valuemapid'			=> ['template' => 1],
81			'delay_flex'			=> [],
82			'params'				=> [],
83			'ipmi_sensor'			=> ['template' => 1],
84			'data_type'				=> ['template' => 1],
85			'authtype'				=> [],
86			'username'				=> [],
87			'password'				=> [],
88			'publickey'				=> [],
89			'privatekey'			=> [],
90			'mtime'					=> ['system' => 1],
91			'flags'					=> [],
92			'filter'				=> [],
93			'interfaceid'			=> ['host' => 1],
94			'port'					=> [],
95			'inventory_link'		=> [],
96			'lifetime'				=> []
97		];
98
99		$this->errorMessages = array_merge($this->errorMessages, [
100			self::ERROR_NO_INTERFACE => _('Cannot find host interface on "%1$s" for item key "%2$s".')
101		]);
102	}
103
104	/**
105	 * Check items data.
106	 *
107	 * Any system field passed to the function will be unset.
108	 *
109	 * @throw APIException
110	 *
111	 * @param array $items passed by reference
112	 * @param bool  $update
113	 *
114	 * @return void
115	 */
116	protected function checkInput(array &$items, $update = false) {
117		if ($update) {
118			$itemDbFields = ['itemid' => null];
119
120			$dbItemsFields = ['itemid', 'templateid'];
121			foreach ($this->fieldRules as $field => $rule) {
122				if (!isset($rule['system'])) {
123					$dbItemsFields[] = $field;
124				}
125			}
126
127			$dbItems = $this->get([
128				'output' => $dbItemsFields,
129				'itemids' => zbx_objectValues($items, 'itemid'),
130				'editable' => true,
131				'preservekeys' => true
132			]);
133
134			$dbHosts = API::Host()->get([
135				'output' => ['hostid', 'status', 'name'],
136				'hostids' => zbx_objectValues($dbItems, 'hostid'),
137				'templated_hosts' => true,
138				'editable' => true,
139				'selectApplications' => ['applicationid', 'flags'],
140				'preservekeys' => true
141			]);
142		}
143		else {
144			$itemDbFields = [
145				'name' => null,
146				'key_' => null,
147				'hostid' => null,
148				'type' => null,
149				'value_type' => null,
150				'delay' => '0',
151				'delay_flex' => ''
152			];
153
154			$dbHosts = API::Host()->get([
155				'output' => ['hostid', 'status', 'name'],
156				'hostids' => zbx_objectValues($items, 'hostid'),
157				'templated_hosts' => true,
158				'editable' => true,
159				'selectApplications' => ['applicationid', 'flags'],
160				'preservekeys' => true
161			]);
162		}
163
164		// interfaces
165		$interfaces = API::HostInterface()->get([
166			'output' => ['interfaceid', 'hostid', 'type'],
167			'hostids' => zbx_objectValues($dbHosts, 'hostid'),
168			'nopermissions' => true,
169			'preservekeys' => true
170		]);
171
172		if ($update) {
173			$updateDiscoveredValidator = new CUpdateDiscoveredValidator([
174				'allowed' => ['itemid', 'status'],
175				'messageAllowedField' => _('Cannot update "%2$s" for a discovered item "%1$s".')
176			]);
177			foreach ($items as $item) {
178				// check permissions
179				if (!isset($dbItems[$item['itemid']])) {
180					self::exception(ZBX_API_ERROR_PARAMETERS,
181						_('No permissions to referred object or it does not exist!'));
182				}
183
184				$dbItem = $dbItems[$item['itemid']];
185
186				$itemName = isset($item['name']) ? $item['name'] : $dbItem['name'];
187
188				// discovered fields, except status, cannot be updated
189				$updateDiscoveredValidator->setObjectName($itemName);
190				$this->checkPartialValidator($item, $updateDiscoveredValidator, $dbItem);
191			}
192
193			$items = $this->extendObjects($this->tableName(), $items, ['name', 'flags']);
194		}
195
196		$item_key_parser = new CItemKey();
197
198		foreach ($items as $inum => &$item) {
199			$item = $this->clearValues($item);
200
201			$fullItem = $items[$inum];
202
203			if (!check_db_fields($itemDbFields, $item)) {
204				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
205			}
206
207			if ($update) {
208				check_db_fields($dbItems[$item['itemid']], $fullItem);
209
210				$this->checkNoParameters(
211					$item,
212					['templateid', 'state'],
213					_('Cannot update "%1$s" for item "%2$s".'),
214					$item['name']
215				);
216
217				// apply rules
218				foreach ($this->fieldRules as $field => $rules) {
219					if ((0 != $fullItem['templateid'] && isset($rules['template'])) || isset($rules['system'])) {
220						unset($item[$field]);
221
222						// For templated item and fields that should not be modified, use the value from DB.
223						if (array_key_exists($field, $dbItems[$item['itemid']])
224								&& array_key_exists($field, $fullItem)) {
225							$fullItem[$field] = $dbItems[$item['itemid']][$field];
226						}
227					}
228				}
229
230				if (!isset($item['key_'])) {
231					$item['key_'] = $fullItem['key_'];
232				}
233				if (!isset($item['hostid'])) {
234					$item['hostid'] = $fullItem['hostid'];
235				}
236
237				// if a templated item is being assigned to an interface with a different type, ignore it
238				$itemInterfaceType = itemTypeInterface($dbItems[$item['itemid']]['type']);
239				if ($fullItem['templateid'] && isset($item['interfaceid']) && isset($interfaces[$item['interfaceid']])
240						&& $itemInterfaceType !== INTERFACE_TYPE_ANY && $interfaces[$item['interfaceid']]['type'] != $itemInterfaceType) {
241
242					unset($item['interfaceid']);
243				}
244			}
245			else {
246				if (!isset($dbHosts[$item['hostid']])) {
247					self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!'));
248				}
249
250				check_db_fields($itemDbFields, $fullItem);
251
252				$this->checkNoParameters(
253					$item,
254					['templateid', 'state'],
255					_('Cannot set "%1$s" for item "%2$s".'),
256					$item['name']
257				);
258			}
259
260			$host = $dbHosts[$fullItem['hostid']];
261
262			if ($fullItem['type'] == ITEM_TYPE_ZABBIX_ACTIVE) {
263				$item['delay_flex'] = '';
264				$fullItem['delay_flex'] = '';
265			}
266			if ($fullItem['value_type'] == ITEM_VALUE_TYPE_STR) {
267				$item['delta'] = 0;
268			}
269			if ($fullItem['value_type'] != ITEM_VALUE_TYPE_UINT64) {
270				$item['data_type'] = 0;
271			}
272
273			// For non-numeric types, whichever value was entered in trends field, is overwritten to zero.
274			if ($fullItem['value_type'] == ITEM_VALUE_TYPE_STR || $fullItem['value_type'] == ITEM_VALUE_TYPE_LOG
275					|| $fullItem['value_type'] == ITEM_VALUE_TYPE_TEXT) {
276				$item['trends'] = 0;
277			}
278
279			// check if the item requires an interface
280			$itemInterfaceType = itemTypeInterface($fullItem['type']);
281			if ($itemInterfaceType !== false && $host['status'] != HOST_STATUS_TEMPLATE) {
282				if (!$fullItem['interfaceid']) {
283					self::exception(ZBX_API_ERROR_PARAMETERS, _('No interface found.'));
284				}
285				elseif (!isset($interfaces[$fullItem['interfaceid']]) || bccomp($interfaces[$fullItem['interfaceid']]['hostid'], $fullItem['hostid']) != 0) {
286					self::exception(ZBX_API_ERROR_PARAMETERS, _('Item uses host interface from non-parent host.'));
287				}
288				elseif ($itemInterfaceType !== INTERFACE_TYPE_ANY && $interfaces[$fullItem['interfaceid']]['type'] != $itemInterfaceType) {
289					self::exception(ZBX_API_ERROR_PARAMETERS, _('Item uses incorrect interface type.'));
290				}
291			}
292			// no interface required, just set it to null
293			else {
294				$item['interfaceid'] = 0;
295			}
296
297			// item key
298			if ($fullItem['type'] == ITEM_TYPE_DB_MONITOR) {
299				if (!isset($fullItem['flags']) || $fullItem['flags'] != ZBX_FLAG_DISCOVERY_RULE) {
300					if (strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_DB_MONITOR) == 0) {
301						self::exception(ZBX_API_ERROR_PARAMETERS,
302							_('Check the key, please. Default example was passed.')
303						);
304					}
305				}
306				elseif ($fullItem['flags'] == ZBX_FLAG_DISCOVERY_RULE) {
307					if (strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_DB_MONITOR_DISCOVERY) == 0) {
308						self::exception(ZBX_API_ERROR_PARAMETERS,
309							_('Check the key, please. Default example was passed.')
310						);
311					}
312				}
313			}
314			elseif (($fullItem['type'] == ITEM_TYPE_SSH && strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_SSH) == 0)
315					|| ($fullItem['type'] == ITEM_TYPE_TELNET && strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_TELNET) == 0)
316					|| ($fullItem['type'] == ITEM_TYPE_JMX && strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_JMX) == 0)) {
317				self::exception(ZBX_API_ERROR_PARAMETERS, _('Check the key, please. Default example was passed.'));
318			}
319
320			// key
321			if ($item_key_parser->parse($fullItem['key_']) != CParser::PARSE_SUCCESS) {
322				self::exception(ZBX_API_ERROR_PARAMETERS,
323					_params($this->getErrorMsg(self::ERROR_INVALID_KEY), [
324						$fullItem['key_'], $fullItem['name'], $host['name'], $item_key_parser->getError()
325					])
326				);
327			}
328
329			// parameters
330			if ($fullItem['type'] == ITEM_TYPE_AGGREGATE) {
331				$params_num = $item_key_parser->getParamsNum();
332
333				if (!str_in_array($item_key_parser->getKey(), ['grpmax', 'grpmin', 'grpsum', 'grpavg'])
334						|| $params_num > 4 || $params_num < 3
335						|| ($params_num == 3 && $item_key_parser->getParam(2) !== 'last')
336						|| !str_in_array($item_key_parser->getParam(2), ['last', 'min', 'max', 'avg', 'sum', 'count'])) {
337					self::exception(ZBX_API_ERROR_PARAMETERS,
338						_s('Key "%1$s" does not match <grpmax|grpmin|grpsum|grpavg>["Host group(s)", "Item key",'.
339							' "<last|min|max|avg|sum|count>", "parameter"].', $item_key_parser->getKey()));
340				}
341			}
342
343			// type of information
344			if ($fullItem['type'] == ITEM_TYPE_AGGREGATE && $fullItem['value_type'] != ITEM_VALUE_TYPE_UINT64
345					&& $fullItem['value_type'] != ITEM_VALUE_TYPE_FLOAT) {
346					self::exception(ZBX_API_ERROR_PARAMETERS,
347						_('Type of information must be "Numeric (unsigned)" or "Numeric (float)" for aggregate items.'));
348			}
349
350			// update interval
351			if ($fullItem['type'] != ITEM_TYPE_TRAPPER && $fullItem['type'] != ITEM_TYPE_SNMPTRAP) {
352				// delay must be between 0 and 86400, if delay is 0, delay_flex interval must be set.
353				if ($fullItem['delay'] < 0 || $fullItem['delay'] > SEC_PER_DAY
354					|| ($fullItem['delay'] == 0 && $fullItem['delay_flex'] === '')) {
355					self::exception(ZBX_API_ERROR_PARAMETERS,
356						_('Item will not be refreshed. Please enter a correct update interval.')
357					);
358				}
359
360				// Don't parse empty strings, they will not be valid.
361				if ($fullItem['delay_flex'] !== '') {
362					// Validate item delay_flex string. First check syntax with parser, then validate time ranges.
363					$item_delay_flex_parser = new CItemDelayFlexParser($fullItem['delay_flex']);
364
365					if ($item_delay_flex_parser->isValid()) {
366						$delay_flex_validator = new CItemDelayFlexValidator();
367
368						if ($delay_flex_validator->validate($item_delay_flex_parser->getIntervals())) {
369							// Some valid intervals exist at this point.
370							$flexible_intervals = $item_delay_flex_parser->getFlexibleIntervals();
371
372							// If there are no flexible intervals, skip the next check calculation.
373							if (!$flexible_intervals) {
374								continue;
375							}
376
377							$nextCheck = calculateItemNextCheck(0, $fullItem['delay'],
378								$item_delay_flex_parser->getFlexibleIntervals($flexible_intervals),
379								time()
380							);
381
382							if ($nextCheck == ZBX_JAN_2038) {
383								self::exception(ZBX_API_ERROR_PARAMETERS,
384									_('Item will not be refreshed. Please enter a correct update interval.')
385								);
386							}
387						}
388						else {
389							self::exception(ZBX_API_ERROR_PARAMETERS, $delay_flex_validator->getError());
390						}
391					}
392					else {
393						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid interval "%1$s": %2$s.',
394							$fullItem['delay_flex'],
395							$item_delay_flex_parser->getError())
396						);
397					}
398				}
399			}
400
401			// ssh, telnet
402			if ($fullItem['type'] == ITEM_TYPE_SSH || $fullItem['type'] == ITEM_TYPE_TELNET) {
403				if (zbx_empty($fullItem['username'])) {
404					self::exception(ZBX_API_ERROR_PARAMETERS, _('No authentication user name specified.'));
405				}
406
407				if ($fullItem['type'] == ITEM_TYPE_SSH && $fullItem['authtype'] == ITEM_AUTHTYPE_PUBLICKEY) {
408					if (zbx_empty($fullItem['publickey'])) {
409						self::exception(ZBX_API_ERROR_PARAMETERS, _('No public key file specified.'));
410					}
411					if (zbx_empty($fullItem['privatekey'])) {
412						self::exception(ZBX_API_ERROR_PARAMETERS, _('No private key file specified.'));
413					}
414				}
415			}
416
417			// snmp trap
418			if ($fullItem['type'] == ITEM_TYPE_SNMPTRAP
419					&& $fullItem['key_'] !== 'snmptrap.fallback' && $item_key_parser->getKey() !== 'snmptrap') {
420				self::exception(ZBX_API_ERROR_PARAMETERS, _('SNMP trap key is invalid.'));
421			}
422
423			// snmp oid
424			if ((in_array($fullItem['type'], [ITEM_TYPE_SNMPV1, ITEM_TYPE_SNMPV2C, ITEM_TYPE_SNMPV3]))
425					&& zbx_empty($fullItem['snmp_oid'])) {
426				self::exception(ZBX_API_ERROR_PARAMETERS, _('No SNMP OID specified.'));
427			}
428
429			// snmp community
430			if (in_array($fullItem['type'], [ITEM_TYPE_SNMPV1, ITEM_TYPE_SNMPV2C])
431					&& zbx_empty($fullItem['snmp_community'])) {
432				self::exception(ZBX_API_ERROR_PARAMETERS, _('No SNMP community specified.'));
433			}
434
435			// snmp port
436			if (isset($fullItem['port']) && !zbx_empty($fullItem['port']) && !validatePortNumberOrMacro($fullItem['port'])) {
437				self::exception(ZBX_API_ERROR_PARAMETERS,
438					_s('Item "%1$s:%2$s" has invalid port: "%3$s".', $fullItem['name'], $fullItem['key_'], $fullItem['port']));
439			}
440
441			if (isset($fullItem['snmpv3_securitylevel']) && $fullItem['snmpv3_securitylevel'] != ITEM_SNMPV3_SECURITYLEVEL_NOAUTHNOPRIV) {
442				// snmpv3 authprotocol
443				if (str_in_array($fullItem['snmpv3_securitylevel'], [ITEM_SNMPV3_SECURITYLEVEL_AUTHNOPRIV, ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV])) {
444					if (isset($fullItem['snmpv3_authprotocol']) && (zbx_empty($fullItem['snmpv3_authprotocol'])
445							|| !str_in_array($fullItem['snmpv3_authprotocol'],
446								[ITEM_AUTHPROTOCOL_MD5, ITEM_AUTHPROTOCOL_SHA]))) {
447						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect authentication protocol for item "%1$s".', $fullItem['name']));
448					}
449				}
450
451				// snmpv3 privprotocol
452				if ($fullItem['snmpv3_securitylevel'] == ITEM_SNMPV3_SECURITYLEVEL_AUTHPRIV) {
453					if (isset($fullItem['snmpv3_privprotocol']) && (zbx_empty($fullItem['snmpv3_privprotocol'])
454							|| !str_in_array($fullItem['snmpv3_privprotocol'],
455								[ITEM_PRIVPROTOCOL_DES, ITEM_PRIVPROTOCOL_AES]))) {
456						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect privacy protocol for item "%1$s".', $fullItem['name']));
457					}
458				}
459			}
460
461			if (isset($item['applications']) && $item['applications']) {
462				/*
463				 * 'flags' is available for update and item prototypes.
464				 * Don't allow discovered or any other application types for item prototypes in 'applications' option.
465				 */
466				if (array_key_exists('flags', $fullItem) && $fullItem['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) {
467					foreach ($host['applications'] as $num => $application) {
468						if ($application['flags'] != ZBX_FLAG_DISCOVERY_NORMAL) {
469							unset($host['applications'][$num]);
470						}
471					}
472				}
473
474				// check that the given applications belong to the item's host
475				$dbApplicationIds = zbx_objectValues($host['applications'], 'applicationid');
476				foreach ($item['applications'] as $appId) {
477					if (!in_array($appId, $dbApplicationIds)) {
478						$error = _s('Application with ID "%1$s" is not available on "%2$s".', $appId, $host['name']);
479						self::exception(ZBX_API_ERROR_PARAMETERS, $error);
480					}
481				}
482			}
483
484			$this->checkSpecificFields($fullItem);
485		}
486		unset($item);
487
488		$this->checkExistingItems($items);
489	}
490
491	protected function checkSpecificFields(array $item) {
492		return true;
493	}
494
495	protected function clearValues(array $item) {
496		if (isset($item['port']) && $item['port'] != '') {
497			$item['port'] = ltrim($item['port'], '0');
498			if ($item['port'] == '') {
499				$item['port'] = 0;
500			}
501		}
502
503		if (isset($item['lifetime']) && $item['lifetime'] != '') {
504			$item['lifetime'] = ltrim($item['lifetime'], '0');
505			if ($item['lifetime'] == '') {
506				$item['lifetime'] = 0;
507			}
508		}
509
510		return $item;
511	}
512
513	protected function errorInheritFlags($flag, $key, $host) {
514		switch ($flag) {
515			case ZBX_FLAG_DISCOVERY_NORMAL:
516				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item.', $key, $host));
517				break;
518			case ZBX_FLAG_DISCOVERY_RULE:
519				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as a discovery rule.', $key, $host));
520				break;
521			case ZBX_FLAG_DISCOVERY_PROTOTYPE:
522				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item prototype.', $key, $host));
523				break;
524			case ZBX_FLAG_DISCOVERY_CREATED:
525				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item created from item prototype.', $key, $host));
526				break;
527			default:
528				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as unknown item element.', $key, $host));
529		}
530	}
531
532	/**
533	 * Returns the interface that best matches the given item.
534	 *
535	 * @param array $itemType   An item
536	 * @param array $interfaces An array of interfaces to choose from
537	 *
538	 * @return array|boolean    The best matching interface;
539	 *							an empty array of no matching interface was found;
540	 *							false, if the item does not need an interface
541	 */
542	public static function findInterfaceForItem(array $item, array $interfaces) {
543		$typeInterface = [];
544		foreach ($interfaces as $interface) {
545			if ($interface['main'] == 1) {
546				$typeInterface[$interface['type']] = $interface;
547			}
548		}
549
550		// find item interface type
551		$type = itemTypeInterface($item['type']);
552
553		$matchingInterface = [];
554
555		// the item can use any interface
556		if ($type == INTERFACE_TYPE_ANY) {
557			$interfaceTypes = [
558				INTERFACE_TYPE_AGENT,
559				INTERFACE_TYPE_SNMP,
560				INTERFACE_TYPE_JMX,
561				INTERFACE_TYPE_IPMI
562			];
563			foreach ($interfaceTypes as $itype) {
564				if (isset($typeInterface[$itype])) {
565					$matchingInterface = $typeInterface[$itype];
566					break;
567				}
568			}
569		}
570		// the item uses a specific type of interface
571		elseif ($type !== false) {
572			$matchingInterface = (isset($typeInterface[$type])) ? $typeInterface[$type] : [];
573		}
574		// the item does not need an interface
575		else {
576			$matchingInterface = false;
577		}
578
579		return $matchingInterface;
580	}
581
582	public function isReadable($ids) {
583		if (!is_array($ids)) {
584			return false;
585		}
586		if (empty($ids)) {
587			return true;
588		}
589
590		$ids = array_unique($ids);
591
592		$count = $this->get([
593			'itemids' => $ids,
594			'countOutput' => true
595		]);
596
597		return (count($ids) == $count);
598	}
599
600	public function isWritable($ids) {
601		if (!is_array($ids)) {
602			return false;
603		}
604		if (empty($ids)) {
605			return true;
606		}
607
608		$ids = array_unique($ids);
609
610		$count = $this->get([
611			'itemids' => $ids,
612			'editable' => true,
613			'countOutput' => true
614		]);
615
616		return (count($ids) == $count);
617	}
618
619	/**
620	 * Checks whether the given items are referenced by any graphs and tries to
621	 * unset these references, if they are no longer used.
622	 *
623	 * @throws APIException if at least one of the item can't be deleted
624	 *
625	 * @param array $itemids   An array of item IDs
626	 */
627	protected function checkGraphReference(array $itemids) {
628		$this->checkUseInGraphAxis($itemids, true);
629		$this->checkUseInGraphAxis($itemids);
630	}
631
632	/**
633	 * Checks if any of the given items are used as min/max Y values in a graph.
634	 *
635	 * if there are graphs, that have an y*_itemid column set, but the
636	 * y*_type column is not set to GRAPH_YAXIS_TYPE_ITEM_VALUE, the y*_itemid
637	 * column will be set to NULL.
638	 *
639	 * If the $checkMax parameter is set to true, the items will be checked against
640	 * max Y values, otherwise, they will be checked against min Y values.
641	 *
642	 * @throws APIException if any of the given items are used as min/max Y values in a graph.
643	 *
644	 * @param array $itemids   An array of items IDs
645	 * @param bool  $check_max
646	 */
647	protected function checkUseInGraphAxis(array $itemids, $check_max = false) {
648		$field_name_itemid = $check_max ? 'ymax_itemid' : 'ymin_itemid';
649		$field_name_type = $check_max ? 'ymax_type' : 'ymin_type';
650		$error = $check_max
651			? 'Could not delete these items because some of them are used as MAX values for graphs.'
652			: 'Could not delete these items because some of them are used as MIN values for graphs.';
653
654		$result = DBselect(
655			'SELECT g.graphid,g.'.$field_name_type.
656			' FROM graphs g'.
657			' WHERE '.dbConditionInt('g.'.$field_name_itemid, $itemids)
658		);
659
660		$update_graphs = [];
661
662		while ($row = DBfetch($result)) {
663			// check if Y type is actually set to GRAPH_YAXIS_TYPE_ITEM_VALUE
664			if ($row[$field_name_type] == GRAPH_YAXIS_TYPE_ITEM_VALUE) {
665				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
666			}
667			else {
668				$update_graphs[] = [
669					'values' => [$field_name_itemid => 0],
670					'where' => ['graphid' => $row['graphid']]
671				];
672			}
673		}
674		unset($graph);
675
676		// if there are graphs, that have an y*_itemid column set, but the
677		// y*_type column is not set to GRAPH_YAXIS_TYPE_ITEM_VALUE, set y*_itemid to NULL.
678		// Otherwise we won't be able to delete them.
679		if ($update_graphs) {
680			DB::update('graphs', $update_graphs);
681		}
682	}
683
684	/**
685	 * Updates the children of the item on the given hosts and propagates the inheritance to the child hosts.
686	 *
687	 * @abstract
688	 *
689	 * @param array $items          an array of items to inherit
690	 * @param array|null $hostids   an array of hosts to inherit to; if set to null, the children will be updated on all
691	 *                              child hosts
692	 *
693	 * @return bool
694	 */
695	abstract protected function inherit(array $items, array $hostids = null);
696
697	/**
698	 * Prepares and returns an array of child items, inherited from items $itemsToInherit on the given hosts.
699	 *
700	 * @param array      $itemsToInherit
701	 * @param array|null $hostIds
702	 *
703	 * @return array an array of unsaved child items
704	 */
705	protected function prepareInheritedItems(array $itemsToInherit, array $hostIds = null) {
706		// fetch all child hosts
707		$chdHosts = API::Host()->get([
708			'output' => ['hostid', 'host', 'status'],
709			'selectParentTemplates' => ['templateid'],
710			'selectInterfaces' => API_OUTPUT_EXTEND,
711			'templateids' => zbx_objectValues($itemsToInherit, 'hostid'),
712			'hostids' => $hostIds,
713			'preservekeys' => true,
714			'nopermissions' => true,
715			'templated_hosts' => true
716		]);
717		if (empty($chdHosts)) {
718			return [];
719		}
720
721		$newItems = [];
722		foreach ($chdHosts as $hostId => $host) {
723			$templateids = zbx_toHash($host['parentTemplates'], 'templateid');
724
725			// skip items not from parent templates of current host
726			$parentItems = [];
727			foreach ($itemsToInherit as $inum => $parentItem) {
728				if (isset($templateids[$parentItem['hostid']])) {
729					$parentItems[$inum] = $parentItem;
730				}
731			}
732
733			// check existing items to decide insert or update
734			$exItems = API::Item()->get([
735				'output' => ['itemid', 'type', 'key_', 'flags', 'templateid'],
736				'hostids' => $hostId,
737				'preservekeys' => true,
738				'nopermissions' => true,
739				'filter' => ['flags' => null]
740			]);
741
742			$exItemsKeys = zbx_toHash($exItems, 'key_');
743			$exItemsTpl = zbx_toHash($exItems, 'templateid');
744
745			$itemids_with_application_prototypes = [];
746
747			foreach ($parentItems as $parentItem) {
748				if (isset($parentItem['applicationPrototypes']) && is_array($parentItem['applicationPrototypes'])
749						&& !array_key_exists('ruleid', $parentItem)) {
750					$itemids_with_application_prototypes[$parentItem['itemid']] = true;
751				}
752			}
753
754			if ($itemids_with_application_prototypes) {
755				$discovery_rules = DBfetchArray(DBselect(
756					'SELECT id.itemid,id.parent_itemid'.
757					' FROM item_discovery id'.
758					' WHERE '.dbConditionInt('id.itemid', array_keys($itemids_with_application_prototypes))
759				));
760				$discovery_rules = zbx_toHash($discovery_rules, 'itemid');
761			}
762
763			foreach ($parentItems as $parentItem) {
764				$exItem = null;
765
766				// check if an item of a different type with the same key exists
767				if (isset($exItemsKeys[$parentItem['key_']])) {
768					$exItem = $exItemsKeys[$parentItem['key_']];
769					if ($exItem['flags'] != $parentItem['flags']) {
770						$this->errorInheritFlags($exItem['flags'], $exItem['key_'], $host['host']);
771					}
772				}
773
774				// update by templateid
775				if (isset($exItemsTpl[$parentItem['itemid']])) {
776					$exItem = $exItemsTpl[$parentItem['itemid']];
777
778					if (isset($exItemsKeys[$parentItem['key_']])
779						&& !idcmp($exItemsKeys[$parentItem['key_']]['templateid'], $parentItem['itemid'])) {
780						self::exception(
781							ZBX_API_ERROR_PARAMETERS,
782							_params($this->getErrorMsg(self::ERROR_EXISTS), [$parentItem['key_'], $host['host']])
783						);
784					}
785				}
786
787				// update by key
788				if (isset($exItemsKeys[$parentItem['key_']])) {
789					$exItem = $exItemsKeys[$parentItem['key_']];
790
791					if ($exItem['templateid'] > 0 && !idcmp($exItem['templateid'], $parentItem['itemid'])) {
792
793						self::exception(
794							ZBX_API_ERROR_PARAMETERS,
795							_params($this->getErrorMsg(self::ERROR_EXISTS_TEMPLATE), [
796								$parentItem['key_'],
797								$host['host']
798							])
799						);
800					}
801				}
802
803				if ($host['status'] == HOST_STATUS_TEMPLATE || !isset($parentItem['type'])) {
804					unset($parentItem['interfaceid']);
805				}
806				elseif ((isset($parentItem['type']) && isset($exItem) && $parentItem['type'] != $exItem['type']) || !isset($exItem)) {
807					$interface = self::findInterfaceForItem($parentItem, $host['interfaces']);
808
809					if (!empty($interface)) {
810						$parentItem['interfaceid'] = $interface['interfaceid'];
811					}
812					elseif ($interface !== false) {
813						self::exception(
814							ZBX_API_ERROR_PARAMETERS,
815							_params($this->getErrorMsg(self::ERROR_NO_INTERFACE), [
816								$host['host'],
817								$parentItem['key_']
818							])
819						);
820					}
821				}
822				else {
823					unset($parentItem['interfaceid']);
824				}
825
826				// copying item
827				$newItem = $parentItem;
828				$newItem['hostid'] = $host['hostid'];
829				$newItem['templateid'] = $parentItem['itemid'];
830
831				// setting item application
832				if (isset($parentItem['applications'])) {
833					$newItem['applications'] = get_same_applications_for_host($parentItem['applications'], $host['hostid']);
834				}
835
836				if ($parentItem['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE
837						&& array_key_exists('applicationPrototypes', $parentItem)) {
838
839					// Get discovery rule ID for current item prototype, if it is not yet set.
840					if (array_key_exists('ruleid', $parentItem)) {
841						$discovery_ruleid = $parentItem['ruleid'];
842					}
843					else {
844						$discovery_ruleid = $discovery_rules[$parentItem['itemid']]['parent_itemid'];
845					}
846
847					$newItem['applicationPrototypes'] = [];
848
849					$db_application_prototypes = DBfetchArray(DBselect(
850						'SELECT ap.application_prototypeid,ap.name'.
851						' FROM application_prototype ap'.
852						' WHERE ap.itemid='.zbx_dbstr($discovery_ruleid).
853							' AND '.dbConditionString('ap.name',
854								zbx_objectValues($parentItem['applicationPrototypes'], 'name')
855							)
856					));
857
858					$db_application_prototypes = zbx_toHash($db_application_prototypes, 'name');
859
860					foreach ($parentItem['applicationPrototypes'] as $application_prototype) {
861						$db_application_prototype = $db_application_prototypes[$application_prototype['name']];
862
863						$newItem['applicationPrototypes'][] = [
864							'name' => $application_prototype['name'],
865							'templateid' => $db_application_prototype['application_prototypeid']
866						];
867					}
868				}
869
870				if ($exItem) {
871					$newItem['itemid'] = $exItem['itemid'];
872				}
873				else {
874					unset($newItem['itemid']);
875				}
876				$newItems[] = $newItem;
877			}
878		}
879
880		return $newItems;
881	}
882
883	/**
884	 * Check if any item from list already exists.
885	 * If items have item ids it will check for existing item with different itemid.
886	 *
887	 * @throw APIException
888	 *
889	 * @param array $items
890	 */
891	protected function checkExistingItems(array $items) {
892		$itemKeysByHostId = [];
893		$itemIds = [];
894		foreach ($items as $item) {
895			if (!isset($itemKeysByHostId[$item['hostid']])) {
896				$itemKeysByHostId[$item['hostid']] = [];
897			}
898			$itemKeysByHostId[$item['hostid']][] = $item['key_'];
899
900			if (isset($item['itemid'])) {
901				$itemIds[] = $item['itemid'];
902			}
903		}
904
905		$sqlWhere = [];
906		foreach ($itemKeysByHostId as $hostId => $keys) {
907			$sqlWhere[] = '(i.hostid='.zbx_dbstr($hostId).' AND '.dbConditionString('i.key_', $keys).')';
908		}
909
910		if ($sqlWhere) {
911			$sql = 'SELECT i.key_,h.host'.
912					' FROM items i,hosts h'.
913					' WHERE i.hostid=h.hostid AND ('.implode(' OR ', $sqlWhere).')';
914
915			// if we update existing items we need to exclude them from result.
916			if ($itemIds) {
917				$sql .= ' AND '.dbConditionInt('i.itemid', $itemIds, true);
918			}
919			$dbItems = DBselect($sql, 1);
920			while ($dbItem = DBfetch($dbItems)) {
921				self::exception(ZBX_API_ERROR_PARAMETERS,
922					_s('Item with key "%1$s" already exists on "%2$s".', $dbItem['key_'], $dbItem['host']));
923			}
924		}
925	}
926
927	protected function addRelatedObjects(array $options, array $result) {
928		$result = parent::addRelatedObjects($options, $result);
929
930		// adding hosts
931		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
932			$relationMap = $this->createRelationMap($result, 'itemid', 'hostid');
933			$hosts = API::Host()->get([
934				'hostids' => $relationMap->getRelatedIds(),
935				'templated_hosts' => true,
936				'output' => $options['selectHosts'],
937				'nopermissions' => true,
938				'preservekeys' => true
939			]);
940			$result = $relationMap->mapMany($result, $hosts, 'hosts');
941		}
942
943		return $result;
944	}
945}
946