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 scripts.
24 */
25class CScript extends CApiService {
26
27	protected $tableName = 'scripts';
28	protected $tableAlias = 's';
29	protected $sortColumns = ['scriptid', 'name'];
30
31	/**
32	 * This property, if filled out, will contain all hostrgroup ids
33	 * that requested scripts did inherit from.
34	 * Keyed by scriptid.
35	 *
36	 * @var array|HostGroup[]
37	 */
38	protected $parent_host_groups = [];
39
40	/**
41	 * @param array $options
42	 *
43	 * @throws APIException if the input is invalid.
44	 *
45	 * @return array|int
46	 */
47	public function get(array $options) {
48		$script_fields = ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description',
49			'confirmation', 'type', 'execute_on'
50		];
51		$group_fields = ['groupid', 'name', 'flags', 'internal'];
52		$host_fields = ['hostid', 'host', 'name', 'description', 'status', 'proxy_hostid', 'inventory_mode', 'flags',
53			'available', 'snmp_available', 'jmx_available', 'ipmi_available', 'error', 'snmp_error', 'jmx_error',
54			'ipmi_error', 'errors_from', 'snmp_errors_from', 'jmx_errors_from', 'ipmi_errors_from', 'disable_until',
55			'snmp_disable_until', 'jmx_disable_until', 'ipmi_disable_until', 'ipmi_authtype', 'ipmi_privilege',
56			'ipmi_username', 'ipmi_password', 'maintenanceid', 'maintenance_status', 'maintenance_type',
57			'maintenance_from', 'tls_connect', 'tls_accept', 'tls_issuer', 'tls_subject', 'tls_psk_identity', 'tls_psk'
58		];
59
60		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
61			// filter
62			'scriptids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
63			'hostids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
64			'groupids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
65			'usrgrpids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
66			'filter' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
67				'scriptid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
68				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
69				'command' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
70				'host_access' =>			['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])],
71				'usrgrpid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
72				'groupid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
73				'confirmation' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
74				'type' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI])],
75				'execute_on' =>				['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [ZBX_SCRIPT_EXECUTE_ON_AGENT, ZBX_SCRIPT_EXECUTE_ON_SERVER, ZBX_SCRIPT_EXECUTE_ON_PROXY])]
76			]],
77			'search' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
78				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
79				'command' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
80				'description' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
81				'confirmation' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
82			]],
83			'searchByAny' =>			['type' => API_BOOLEAN, 'default' => false],
84			'startSearch' =>			['type' => API_FLAG, 'default' => false],
85			'excludeSearch' =>			['type' => API_FLAG, 'default' => false],
86			'searchWildcardsEnabled' =>	['type' => API_BOOLEAN, 'default' => false],
87			// output
88			'output' =>					['type' => API_OUTPUT, 'in' => implode(',', $script_fields), 'default' => API_OUTPUT_EXTEND],
89			'selectGroups' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $group_fields), 'default' => null],
90			'selectHosts' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $host_fields), 'default' => null],
91			'countOutput' =>			['type' => API_FLAG, 'default' => false],
92			// sort and limit
93			'sortfield' =>				['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
94			'sortorder' =>				['type' => API_SORTORDER, 'default' => []],
95			'limit' =>					['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
96			// flags
97			'editable' =>				['type' => API_BOOLEAN, 'default' => false],
98			'preservekeys' =>			['type' => API_BOOLEAN, 'default' => false]
99		]];
100		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
101			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
102		}
103
104		$sql_parts = [
105			'select'	=> ['scripts' => 's.scriptid'],
106			'from'		=> ['scripts' => 'scripts s'],
107			'where'		=> [],
108			'order'		=> []
109		];
110
111		// editable + permission check
112		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
113			if ($options['editable']) {
114				return $options['countOutput'] ? 0 : [];
115			}
116
117			$user_groups = getUserGroupsByUserId(self::$userData['userid']);
118
119			$sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $user_groups).')';
120			$sql_parts['where'][] = '(s.groupid IS NULL OR EXISTS ('.
121				'SELECT NULL'.
122				' FROM rights r'.
123				' WHERE s.groupid=r.id'.
124					' AND '.dbConditionInt('r.groupid', $user_groups).
125				' GROUP BY r.id'.
126				' HAVING MIN(r.permission)>'.PERM_DENY.
127			'))';
128		}
129
130		$host_groups = null;
131		$host_groups_by_hostids = null;
132		$host_groups_by_groupids = null;
133
134		// Hostids and groupids selection API calls must be made separately because we must intersect enriched groupids.
135		if ($options['hostids'] !== null) {
136			$host_groups_by_hostids = enrichParentGroups(API::HostGroup()->get([
137				'output' => ['groupid', 'name'],
138				'hostids' => $options['hostids'],
139				'preservekeys' => true
140			]));
141		}
142		if ($options['groupids'] !== null) {
143			$host_groups_by_groupids = enrichParentGroups(API::HostGroup()->get([
144				'output' => ['groupid', 'name'],
145				'groupids' => $options['groupids'],
146				'preservekeys' => true
147			]));
148		}
149
150		if ($host_groups_by_groupids !== null && $host_groups_by_hostids !== null) {
151			$host_groups = array_intersect_key($host_groups_by_hostids, $host_groups_by_groupids);
152		}
153		elseif ($host_groups_by_hostids !== null) {
154			$host_groups = $host_groups_by_hostids;
155		}
156		elseif ($host_groups_by_groupids !== null) {
157			$host_groups = $host_groups_by_groupids;
158		}
159
160		if ($host_groups !== null) {
161			$sql_parts['where'][] = '('.dbConditionInt('s.groupid', array_keys($host_groups)).' OR s.groupid IS NULL)';
162			$this->parent_host_groups = $host_groups;
163		}
164
165		// usrgrpids
166		if ($options['usrgrpids'] !== null) {
167			$sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $options['usrgrpids']).')';
168		}
169
170		// scriptids
171		if ($options['scriptids'] !== null) {
172			$sql_parts['where'][] = dbConditionInt('s.scriptid', $options['scriptids']);
173		}
174
175		// search
176		if ($options['search'] !== null) {
177			zbx_db_search('scripts s', $options, $sql_parts);
178		}
179
180		// filter
181		if ($options['filter'] !== null) {
182			$this->dbFilter('scripts s', $options, $sql_parts);
183		}
184
185		$db_scripts = [];
186
187		$sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
188		$sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
189
190		$result = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']);
191
192		while ($db_script = DBfetch($result)) {
193			if ($options['countOutput']) {
194				return $db_script['rowscount'];
195			}
196
197			$db_scripts[$db_script['scriptid']] = $db_script;
198		}
199
200		if ($db_scripts) {
201			$db_scripts = $this->addRelatedObjects($options, $db_scripts);
202			$db_scripts = $this->unsetExtraFields($db_scripts, ['scriptid', 'groupid', 'host_access'],
203				$options['output']
204			);
205
206			if (!$options['preservekeys']) {
207				$db_scripts = array_values($db_scripts);
208			}
209		}
210
211		return $db_scripts;
212	}
213
214	/**
215	 * @param array $scripts
216	 *
217	 * @return array
218	 */
219	public function create(array $scripts) {
220		$this->validateCreate($scripts);
221
222		$scriptids = DB::insert('scripts', $scripts);
223
224		foreach ($scripts as $index => &$script) {
225			$script['scriptid'] = $scriptids[$index];
226		}
227		unset($script);
228
229		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_SCRIPT, $scripts);
230
231		return ['scriptids' => $scriptids];
232	}
233
234	/**
235	 * @param array $scripts
236	 *
237	 * @throws APIException if the input is invalid
238	 */
239	protected function validateCreate(array &$scripts) {
240		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
241			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
242		}
243
244		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
245			'name' =>			['type' => API_SCRIPT_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('scripts', 'name')],
246			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI])],
247			'execute_on' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_EXECUTE_ON_AGENT, ZBX_SCRIPT_EXECUTE_ON_SERVER, ZBX_SCRIPT_EXECUTE_ON_PROXY])],
248			'command' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')],
249			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'description')],
250			'usrgrpid' =>		['type' => API_ID],
251			'groupid' =>		['type' => API_ID],
252			'host_access' =>	['type' => API_INT32, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])],
253			'confirmation' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'confirmation')]
254		]];
255		if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) {
256			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
257		}
258
259		$scripts = $this->checkExecutionType($scripts);
260		$this->checkUserGroups($scripts);
261		$this->checkHostGroups($scripts);
262		$this->checkDuplicates($scripts);
263	}
264
265	/**
266	 * @param array $scripts
267	 *
268	 * @return array
269	 */
270	public function update(array $scripts) {
271		$this->validateUpdate($scripts, $db_scripts);
272
273		$upd_scripts = [];
274
275		foreach ($scripts as $script) {
276			$db_script = $db_scripts[$script['scriptid']];
277
278			$upd_script = [];
279
280			// strings
281			foreach (['name', 'command', 'description', 'confirmation'] as $field_name) {
282				if (array_key_exists($field_name, $script) && $script[$field_name] !== $db_script[$field_name]) {
283					$upd_script[$field_name] = $script[$field_name];
284				}
285			}
286			// integers
287			foreach (['type', 'execute_on', 'usrgrpid', 'groupid', 'host_access'] as $field_name) {
288				if (array_key_exists($field_name, $script) && $script[$field_name] != $db_script[$field_name]) {
289					$upd_script[$field_name] = $script[$field_name];
290				}
291			}
292
293			if ($upd_script) {
294				$upd_scripts[] = [
295					'values' => $upd_script,
296					'where' => ['scriptid' => $script['scriptid']]
297				];
298			}
299		}
300
301		if ($upd_scripts) {
302			DB::update('scripts', $upd_scripts);
303		}
304
305		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_SCRIPT, $scripts, $db_scripts);
306
307		return ['scriptids' => zbx_objectValues($scripts, 'scriptid')];
308	}
309
310	/**
311	 * @param array $scripts
312	 * @param array $db_scripts
313	 *
314	 * @throws APIException if the input is invalid
315	 */
316	protected function validateUpdate(array &$scripts, array &$db_scripts = null) {
317		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
318			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
319		}
320
321		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['scriptid'], ['name']], 'fields' => [
322			'scriptid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
323			'name' =>			['type' => API_SCRIPT_NAME, 'length' => DB::getFieldLength('scripts', 'name')],
324			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI])],
325			'execute_on' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_EXECUTE_ON_AGENT, ZBX_SCRIPT_EXECUTE_ON_SERVER, ZBX_SCRIPT_EXECUTE_ON_PROXY])],
326			'command' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')],
327			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'description')],
328			'usrgrpid' =>		['type' => API_ID],
329			'groupid' =>		['type' => API_ID],
330			'host_access' =>	['type' => API_INT32, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])],
331			'confirmation' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'confirmation')]
332		]];
333		if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) {
334			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
335		}
336
337		$db_scripts = DB::select('scripts', [
338			'output' => ['scriptid', 'name', 'type', 'execute_on', 'command', 'description', 'usrgrpid', 'groupid',
339				'host_access', 'confirmation'
340			],
341			'scriptids' => zbx_objectValues($scripts, 'scriptid'),
342			'preservekeys' => true
343		]);
344
345		$new_name_scripts = [];
346
347		foreach ($scripts as $script) {
348			if (!array_key_exists($script['scriptid'], $db_scripts)) {
349				self::exception(ZBX_API_ERROR_PERMISSIONS,
350					_('No permissions to referred object or it does not exist!')
351				);
352			}
353
354			$db_script = $db_scripts[$script['scriptid']];
355
356			if (array_key_exists('name', $script) && trimPath($script['name']) !== trimPath($db_script['name'])) {
357				$new_name_scripts[] = $script;
358			}
359		}
360
361		$scripts = $this->checkExecutionType($scripts);
362		$this->checkUserGroups($scripts);
363		$this->checkHostGroups($scripts);
364		if ($new_name_scripts) {
365			$this->checkDuplicates($new_name_scripts);
366		}
367	}
368
369	/**
370	 * Validate incompatible options ZBX_SCRIPT_TYPE_IPMI and ZBX_SCRIPT_EXECUTE_ON_AGENT.
371	 *
372	 * @param array $scripts
373	 *
374	 * @return array
375	 */
376	private function checkExecutionType(array $scripts) {
377		foreach ($scripts as &$script) {
378			if (array_key_exists('type', $script) && $script['type'] == ZBX_SCRIPT_TYPE_IPMI) {
379				if (!array_key_exists('execute_on', $script)) {
380					$script['execute_on'] = ZBX_SCRIPT_EXECUTE_ON_SERVER;
381				}
382				elseif ($script['execute_on'] == ZBX_SCRIPT_EXECUTE_ON_AGENT) {
383					self::exception(ZBX_API_ERROR_PARAMETERS, _('IPMI scripts can be executed only by server.'));
384				}
385			}
386		}
387		unset($script);
388
389		return $scripts;
390	}
391
392	/**
393	 * Check for valid user groups.
394	 *
395	 * @param array $scripts
396	 * @param array $scripts[]['usrgrpid']  (optional)
397	 *
398	 * @throws APIException  if user group is not exists.
399	 */
400	private function checkUserGroups(array $scripts) {
401		$usrgrpids = [];
402
403		foreach ($scripts as $script) {
404			if (array_key_exists('usrgrpid', $script) && $script['usrgrpid'] != 0) {
405				$usrgrpids[$script['usrgrpid']] = true;
406			}
407		}
408
409		if (!$usrgrpids) {
410			return;
411		}
412
413		$usrgrpids = array_keys($usrgrpids);
414
415		$db_usrgrps = DB::select('usrgrp', [
416			'output' => [],
417			'usrgrpids' => $usrgrpids,
418			'preservekeys' => true
419		]);
420
421		foreach ($usrgrpids as $usrgrpid) {
422			if (!array_key_exists($usrgrpid, $db_usrgrps)) {
423				self::exception(ZBX_API_ERROR_PARAMETERS, _s('User group with ID "%1$s" is not available.', $usrgrpid));
424			}
425		}
426	}
427
428	/**
429	 * Check for valid host groups.
430	 *
431	 * @param array $scripts
432	 * @param array $scripts[]['groupid']  (optional)
433	 *
434	 * @throws APIException  if host group is not exists.
435	 */
436	private function checkHostGroups(array $scripts) {
437		$groupids = [];
438
439		foreach ($scripts as $script) {
440			if (array_key_exists('groupid', $script) && $script['groupid'] != 0) {
441				$groupids[$script['groupid']] = true;
442			}
443		}
444
445		if (!$groupids) {
446			return;
447		}
448
449		$groupids = array_keys($groupids);
450
451		$db_groups = DB::select('hstgrp', [
452			'output' => [],
453			'groupids' => $groupids,
454			'preservekeys' => true
455		]);
456
457		foreach ($groupids as $groupid) {
458			if (!array_key_exists($groupid, $db_groups)) {
459				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host group with ID "%1$s" is not available.', $groupid));
460			}
461		}
462	}
463
464	/**
465	 * Auxiliary function for checkDuplicates().
466	 *
467	 * @param array  $folders
468	 * @param string $name
469	 * @param array  $db_folders
470	 * @param string $db_name
471	 *
472	 * @throws APIException
473	 */
474	private static function checkScriptNames(array $folders, $name, array $db_folders, $db_name) {
475		if (array_slice($folders, 0, count($db_folders)) === $db_folders) {
476			self::exception(ZBX_API_ERROR_PARAMETERS,
477				_s('Script menu path "%1$s" already used in script name "%2$s".', $name, $db_name)
478			);
479		}
480
481		if (array_slice($db_folders, 0, count($folders)) === $folders) {
482			self::exception(ZBX_API_ERROR_PARAMETERS,
483				_s('Script name "%1$s" already used in menu path for script "%2$s".', $name, $db_name)
484			);
485		}
486	}
487
488	/**
489	 * Check for duplicated scripts.
490	 *
491	 * @param array  $scripts
492	 * @param string $scripts['scriptid']
493	 * @param string $scripts['name']
494	 *
495	 * @throws APIException  if global script already exists.
496	 */
497	private function checkDuplicates(array $scripts) {
498		$db_scripts = DB::select('scripts', [
499			'output' => ['scriptid', 'name']
500		]);
501
502		$uniq_names = [];
503
504		foreach ($db_scripts as &$db_script) {
505			$db_script['folders'] = array_map('trim', splitPath($db_script['name']));
506			$uniq_names[implode('/', $db_script['folders'])] = true;
507		}
508		unset($db_script);
509
510		$ok_scripts = [];
511
512		foreach ($scripts as $script) {
513			$script['folders'] = array_map('trim', splitPath($script['name']));
514			$uniq_name = implode('/', $script['folders']);
515
516			if (array_key_exists($uniq_name, $uniq_names)) {
517				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Script "%1$s" already exists.', $script['name']));
518			}
519			$uniq_names[$uniq_name] = true;
520
521			foreach ($ok_scripts as $ok_script) {
522				self::checkScriptNames($script['folders'], $script['name'], $ok_script['folders'], $ok_script['name']);
523			}
524
525			foreach ($db_scripts as $db_script) {
526				if (array_key_exists('scriptid', $script) && bccomp($script['scriptid'], $db_script['scriptid']) == 0) {
527					continue;
528				}
529
530				self::checkScriptNames($script['folders'], $script['name'], $db_script['folders'], $db_script['name']);
531			}
532
533			$ok_scripts[] = $script;
534		}
535	}
536
537	/**
538	 * @param array $scriptids
539	 *
540	 * @return array
541	 */
542	public function delete(array $scriptids) {
543		$this->validateDelete($scriptids, $db_scripts);
544
545		DB::delete('scripts', ['scriptid' => $scriptids]);
546
547		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_SCRIPT, $db_scripts);
548
549		return ['scriptids' => $scriptids];
550	}
551
552	/**
553	 * @param array $scriptids
554	 * @param array $db_scripts
555	 *
556	 * @throws APIException if the input is invalid
557	 */
558	protected function validateDelete(array &$scriptids, array &$db_scripts = null) {
559		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
560			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
561		}
562
563		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
564		if (!CApiInputValidator::validate($api_input_rules, $scriptids, '/', $error)) {
565			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
566		}
567
568		$db_scripts = DB::select('scripts', [
569			'output' => ['scriptid', 'name'],
570			'scriptids' => $scriptids,
571			'preservekeys' => true
572		]);
573
574		foreach ($scriptids as $scriptid) {
575			if (!array_key_exists($scriptid, $db_scripts)) {
576				self::exception(ZBX_API_ERROR_PERMISSIONS,
577					_('No permissions to referred object or it does not exist!')
578				);
579			}
580		}
581
582		// Check if deleted scripts used in actions.
583		$db_actions = DBselect(
584			'SELECT a.name,oc.scriptid'.
585			' FROM opcommand oc,operations o,actions a'.
586			' WHERE oc.operationid=o.operationid'.
587				' AND o.actionid=a.actionid'.
588				' AND '.dbConditionInt('oc.scriptid', $scriptids),
589			1
590		);
591
592		if ($db_action = DBfetch($db_actions)) {
593			self::exception(ZBX_API_ERROR_PARAMETERS,
594				_s('Cannot delete scripts. Script "%1$s" is used in action operation "%2$s".',
595					$db_scripts[$db_action['scriptid']]['name'], $db_action['name']
596				)
597			);
598		}
599	}
600
601	/**
602	 * @param array $data
603	 *
604	 * @return array
605	 */
606	public function execute(array $data) {
607		global $ZBX_SERVER, $ZBX_SERVER_PORT;
608
609		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
610			'hostid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
611			'scriptid' =>	['type' => API_ID, 'flags' => API_REQUIRED]
612		]];
613		if (!CApiInputValidator::validate($api_input_rules, $data, '/', $error)) {
614			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
615		}
616
617		$db_hosts = API::Host()->get([
618			'output' => [],
619			'hostids' => $data['hostid']
620		]);
621		if (!$db_hosts) {
622			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
623		}
624
625		$db_scripts = $this->get([
626			'output' => [],
627			'hostids' => $data['hostid'],
628			'scriptids' => $data['scriptid']
629		]);
630		if (!$db_scripts) {
631			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
632		}
633
634		// execute script
635		$zabbix_server = new CZabbixServer($ZBX_SERVER, $ZBX_SERVER_PORT, ZBX_SCRIPT_TIMEOUT, ZBX_SOCKET_BYTES_LIMIT);
636		$result = $zabbix_server->executeScript($data['scriptid'], $data['hostid'], self::$userData['sessionid']);
637
638		if ($result !== false) {
639			// return the result in a backwards-compatible format
640			return [
641				'response' => 'success',
642				'value' => $result
643			];
644		}
645		else {
646			self::exception(ZBX_API_ERROR_INTERNAL, $zabbix_server->getError());
647		}
648	}
649
650	/**
651	 * Returns all the scripts that are available on each given host.
652	 *
653	 * @param $hostids
654	 *
655	 * @return array
656	 */
657	public function getScriptsByHosts($hostids) {
658		zbx_value2array($hostids);
659
660		$scripts_by_host = [];
661
662		if (!$hostids) {
663			return $scripts_by_host;
664		}
665
666		foreach ($hostids as $hostid) {
667			$scripts_by_host[$hostid] = [];
668		}
669
670		$scripts = $this->get([
671			'output' => API_OUTPUT_EXTEND,
672			'hostids' => $hostids,
673			'sortfield' => 'name',
674			'preservekeys' => true
675		]);
676
677		$scripts = $this->addRelatedGroupsAndHosts([
678			'selectGroups' => null,
679			'selectHosts' => ['hostid']
680		], $scripts, $hostids);
681
682		if ($scripts) {
683			// resolve macros
684			$macros_data = [];
685			foreach ($scripts as $scriptid => $script) {
686				if (!empty($script['confirmation'])) {
687					foreach ($script['hosts'] as $host) {
688						if (isset($scripts_by_host[$host['hostid']])) {
689							$macros_data[$host['hostid']][$scriptid] = $script['confirmation'];
690						}
691					}
692				}
693			}
694			if ($macros_data) {
695				$macros_data = CMacrosResolverHelper::resolve([
696					'config' => 'scriptConfirmation',
697					'data' => $macros_data
698				]);
699			}
700
701			foreach ($scripts as $scriptid => $script) {
702				$hosts = $script['hosts'];
703				unset($script['hosts']);
704				// set script to host
705				foreach ($hosts as $host) {
706					$hostid = $host['hostid'];
707
708					if (isset($scripts_by_host[$hostid])) {
709						$size = count($scripts_by_host[$hostid]);
710						$scripts_by_host[$hostid][$size] = $script;
711
712						// set confirmation text with resolved macros
713						if (isset($macros_data[$hostid][$scriptid]) && $script['confirmation']) {
714							$scripts_by_host[$hostid][$size]['confirmation'] = $macros_data[$hostid][$scriptid];
715						}
716					}
717				}
718			}
719		}
720
721		return $scripts_by_host;
722	}
723
724	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
725		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
726
727		if ($options['selectGroups'] !== null || $options['selectHosts'] !== null) {
728			$sqlParts = $this->addQuerySelect($this->fieldId('groupid'), $sqlParts);
729			$sqlParts = $this->addQuerySelect($this->fieldId('host_access'), $sqlParts);
730		}
731
732		return $sqlParts;
733	}
734
735	/**
736	 * Applies relational subselect onto already fetched result.
737	 *
738	 * @param  array $options
739	 * @param  array $result
740	 *
741	 * @return array $result
742	 */
743	protected function addRelatedObjects(array $options, array $result) {
744		$result = parent::addRelatedObjects($options, $result);
745
746		return $this->addRelatedGroupsAndHosts($options, $result);
747	}
748
749	/**
750	 * Applies relational subselect onto already fetched result.
751	 *
752	 * @param  array $options
753	 * @param  mixed $options['selectGroups']
754	 * @param  mixed $options['selectHosts']
755	 * @param  array $result
756	 * @param  array $hostids                  An additional filter by hostids, which will be added to "hosts" key.
757	 *
758	 * @return array $result
759	 */
760	private function addRelatedGroupsAndHosts(array $options, array $result, array $hostids = null) {
761		$is_groups_select = $options['selectGroups'] !== null && $options['selectGroups'];
762		$is_hosts_select = $options['selectHosts'] !== null && $options['selectHosts'];
763
764		if (!$is_groups_select && !$is_hosts_select) {
765			return $result;
766		}
767
768		$host_groups_with_write_access = [];
769		$has_write_access_level = false;
770
771		$group_search_names = [];
772		foreach ($result as $script) {
773			$has_write_access_level |= ($script['host_access'] == PERM_READ_WRITE);
774
775			// If any script belongs to all host groups.
776			if ($script['groupid'] == 0) {
777				$group_search_names = null;
778			}
779
780			if ($group_search_names !== null) {
781				/*
782				 * If scripts were requested by host or group filters, then we have already requested group names
783				 * for all groups linked to scripts. And then we can request less groups by adding them as search
784				 * condition in hostgroup.get. Otherwise we will need to request all groups, user has access to.
785				 */
786				if (array_key_exists($script['groupid'], $this->parent_host_groups)) {
787					$group_search_names[] = $this->parent_host_groups[$script['groupid']]['name'];
788				}
789			}
790		}
791
792		$select_groups = ['name', 'groupid'];
793		$select_groups = $this->outputExtend($options['selectGroups'], $select_groups);
794
795		$host_groups = API::HostGroup()->get([
796			'output' => $select_groups,
797			'search' => $group_search_names ? ['name' => $group_search_names] : null,
798			'searchByAny' => true,
799			'startSearch' => true,
800			'preservekeys' => true
801		]);
802
803		if ($has_write_access_level && $host_groups) {
804			$host_groups_with_write_access = API::HostGroup()->get([
805				'output' => $select_groups,
806				'groupid' => array_keys($host_groups),
807				'preservekeys' => true,
808				'editable' => true
809			]);
810		}
811		else {
812			$host_groups_with_write_access = $host_groups;
813		}
814
815		$nested = [];
816		foreach ($host_groups as $groupid => $group) {
817			$name = $group['name'];
818
819			while (($pos = strrpos($name, '/')) !== false) {
820				$name = substr($name, 0, $pos);
821				$nested[$name][$groupid] = true;
822			}
823		}
824
825		$hstgrp_branch = [];
826		foreach ($host_groups as $groupid => $group) {
827			$hstgrp_branch[$groupid] = [$groupid => true];
828			if (array_key_exists($group['name'], $nested)) {
829				$hstgrp_branch[$groupid] += $nested[$group['name']];
830			}
831		}
832
833		if ($is_hosts_select) {
834			$sql = 'SELECT hostid,groupid FROM hosts_groups'.
835				' WHERE '.dbConditionInt('groupid', array_keys($host_groups));
836			if ($hostids !== null) {
837				$sql .= ' AND '.dbConditionInt('hostid', $hostids);
838			}
839
840			$db_group_hosts = DBSelect($sql);
841
842			$all_hostids = [];
843			$group_to_hosts = [];
844			while ($row = DBFetch($db_group_hosts)) {
845				if (!array_key_exists($row['groupid'], $group_to_hosts)) {
846					$group_to_hosts[$row['groupid']] = [];
847				}
848
849				$group_to_hosts[$row['groupid']][$row['hostid']] = true;
850				$all_hostids[] = $row['hostid'];
851			}
852
853			$used_hosts = API::Host()->get([
854				'output' => $options['selectHosts'],
855				'hostids' => $all_hostids,
856				'preservekeys' => true
857			]);
858		}
859
860		$host_groups = $this->unsetExtraFields($host_groups, ['name', 'groupid'], $options['selectGroups']);
861		$host_groups_with_write_access = $this->unsetExtraFields(
862			$host_groups_with_write_access, ['name', 'groupid'], $options['selectGroups']
863		);
864
865		foreach ($result as &$script) {
866			if ($script['groupid'] == 0) {
867				$script_groups = ($script['host_access'] == PERM_READ_WRITE)
868					? $host_groups_with_write_access
869					: $host_groups;
870			}
871			else {
872				$script_groups = ($script['host_access'] == PERM_READ_WRITE)
873					? array_intersect_key($host_groups_with_write_access, $hstgrp_branch[$script['groupid']])
874					: array_intersect_key($host_groups, $hstgrp_branch[$script['groupid']]);
875			}
876
877			if ($is_groups_select) {
878				$script['groups'] = array_values($script_groups);
879			}
880
881			if ($is_hosts_select) {
882				$script['hosts'] = [];
883				foreach (array_keys($script_groups) as $script_groupid) {
884					if (array_key_exists($script_groupid, $group_to_hosts)) {
885						$script['hosts'] += array_intersect_key($used_hosts, $group_to_hosts[$script_groupid]);
886					}
887				}
888				$script['hosts'] = array_values($script['hosts']);
889			}
890		}
891		unset($script);
892
893		return $result;
894	}
895}
896