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	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'getscriptsbyhosts' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
30		'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
31		'update' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
32		'delete' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
33		'execute' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_EXECUTE_SCRIPTS]
34	];
35
36	protected $tableName = 'scripts';
37	protected $tableAlias = 's';
38	protected $sortColumns = ['scriptid', 'name'];
39
40	/**
41	 * Fields from "actions" table. Used in get() validation and addRelatedObjects() when selecting action fields.
42	 */
43	private $action_fields = ['actionid', 'name', 'eventsource', 'status', 'esc_period', 'pause_suppressed'];
44
45	/**
46	 * This property, if filled out, will contain all hostrgroup ids
47	 * that requested scripts did inherit from.
48	 * Keyed by scriptid.
49	 *
50	 * @var array|HostGroup[]
51	 */
52	protected $parent_host_groups = [];
53
54	/**
55	 * @param array $options
56	 *
57	 * @throws APIException if the input is invalid.
58	 *
59	 * @return array|int
60	 */
61	public function get(array $options) {
62		$script_fields = ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description',
63			'confirmation', 'type', 'execute_on', 'timeout', 'parameters', 'scope', 'port', 'authtype', 'username',
64			'password', 'publickey', 'privatekey', 'menu_path'
65		];
66		$group_fields = ['groupid', 'name', 'flags', 'internal'];
67		$host_fields = ['hostid', 'host', 'name', 'description', 'status', 'proxy_hostid', 'inventory_mode', 'flags',
68			'ipmi_authtype', 'ipmi_privilege', 'ipmi_username', 'ipmi_password', 'maintenanceid', 'maintenance_status',
69			'maintenance_type', 'maintenance_from', 'tls_connect', 'tls_accept', 'tls_issuer', 'tls_subject'
70		];
71
72		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
73			// filter
74			'scriptids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
75			'hostids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
76			'groupids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
77			'usrgrpids' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
78			'filter' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
79				'scriptid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
80				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
81				'command' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
82				'host_access' =>			['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])],
83				'usrgrpid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
84				'groupid' =>				['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
85				'confirmation' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
86				'type' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI, ZBX_SCRIPT_TYPE_SSH, ZBX_SCRIPT_TYPE_TELNET, ZBX_SCRIPT_TYPE_WEBHOOK])],
87				'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])],
88				'scope' =>					['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])],
89				'menu_path' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
90			]],
91			'search' =>					['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
92				'name' =>					['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
93				'command' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
94				'description' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
95				'confirmation' =>			['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
96				'username' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
97				'menu_path' =>				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
98			]],
99			'searchByAny' =>			['type' => API_BOOLEAN, 'default' => false],
100			'startSearch' =>			['type' => API_FLAG, 'default' => false],
101			'excludeSearch' =>			['type' => API_FLAG, 'default' => false],
102			'searchWildcardsEnabled' =>	['type' => API_BOOLEAN, 'default' => false],
103			// output
104			'output' =>					['type' => API_OUTPUT, 'in' => implode(',', $script_fields), 'default' => API_OUTPUT_EXTEND],
105			'selectGroups' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $group_fields), 'default' => null],
106			'selectHosts' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $host_fields), 'default' => null],
107			'selectActions' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->action_fields), 'default' => null],
108			'countOutput' =>			['type' => API_FLAG, 'default' => false],
109			// sort and limit
110			'sortfield' =>				['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
111			'sortorder' =>				['type' => API_SORTORDER, 'default' => []],
112			'limit' =>					['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
113			// flags
114			'editable' =>				['type' => API_BOOLEAN, 'default' => false],
115			'preservekeys' =>			['type' => API_BOOLEAN, 'default' => false]
116		]];
117		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
118			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
119		}
120
121		$sql_parts = [
122			'select'	=> ['scripts' => 's.scriptid'],
123			'from'		=> ['scripts' => 'scripts s'],
124			'where'		=> [],
125			'order'		=> []
126		];
127
128		// editable + permission check
129		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
130			if ($options['editable']) {
131				return $options['countOutput'] ? 0 : [];
132			}
133
134			$user_groups = getUserGroupsByUserId(self::$userData['userid']);
135
136			$sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $user_groups).')';
137			$sql_parts['where'][] = '(s.groupid IS NULL OR EXISTS ('.
138				'SELECT NULL'.
139				' FROM rights r'.
140				' WHERE s.groupid=r.id'.
141					' AND '.dbConditionInt('r.groupid', $user_groups).
142				' GROUP BY r.id'.
143				' HAVING MIN(r.permission)>'.PERM_DENY.
144			'))';
145		}
146
147		$host_groups = null;
148		$host_groups_by_hostids = null;
149		$host_groups_by_groupids = null;
150
151		// Hostids and groupids selection API calls must be made separately because we must intersect enriched groupids.
152		if ($options['hostids'] !== null) {
153			$host_groups_by_hostids = enrichParentGroups(API::HostGroup()->get([
154				'output' => ['groupid', 'name'],
155				'hostids' => $options['hostids'],
156				'preservekeys' => true
157			]));
158		}
159		if ($options['groupids'] !== null) {
160			$host_groups_by_groupids = enrichParentGroups(API::HostGroup()->get([
161				'output' => ['groupid', 'name'],
162				'groupids' => $options['groupids'],
163				'preservekeys' => true
164			]));
165		}
166
167		if ($host_groups_by_groupids !== null && $host_groups_by_hostids !== null) {
168			$host_groups = array_intersect_key($host_groups_by_hostids, $host_groups_by_groupids);
169		}
170		elseif ($host_groups_by_hostids !== null) {
171			$host_groups = $host_groups_by_hostids;
172		}
173		elseif ($host_groups_by_groupids !== null) {
174			$host_groups = $host_groups_by_groupids;
175		}
176
177		if ($host_groups !== null) {
178			$sql_parts['where'][] = '('.dbConditionInt('s.groupid', array_keys($host_groups)).' OR s.groupid IS NULL)';
179			$this->parent_host_groups = $host_groups;
180		}
181
182		// usrgrpids
183		if ($options['usrgrpids'] !== null) {
184			$sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $options['usrgrpids']).')';
185		}
186
187		// scriptids
188		if ($options['scriptids'] !== null) {
189			$sql_parts['where'][] = dbConditionInt('s.scriptid', $options['scriptids']);
190		}
191
192		// search
193		if ($options['search'] !== null) {
194			zbx_db_search('scripts s', $options, $sql_parts);
195		}
196
197		// filter
198		if ($options['filter'] !== null) {
199			$this->dbFilter('scripts s', $options, $sql_parts);
200		}
201
202		$db_scripts = [];
203
204		$sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
205		$sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
206
207		$result = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']);
208
209		while ($db_script = DBfetch($result)) {
210			if ($options['countOutput']) {
211				return $db_script['rowscount'];
212			}
213
214			$db_scripts[$db_script['scriptid']] = $db_script;
215		}
216
217		if ($db_scripts) {
218			$db_scripts = $this->addRelatedObjects($options, $db_scripts);
219			$db_scripts = $this->unsetExtraFields($db_scripts, ['scriptid', 'groupid', 'host_access'],
220				$options['output']
221			);
222
223			if (!$options['preservekeys']) {
224				$db_scripts = array_values($db_scripts);
225			}
226		}
227
228		return $db_scripts;
229	}
230
231	/**
232	 * @param array $scripts
233	 *
234	 * @return array
235	 */
236	public function create(array $scripts) {
237		$this->validateCreate($scripts);
238
239		$scriptids = DB::insert('scripts', $scripts);
240		$scripts_params = [];
241
242		foreach ($scripts as $index => &$script) {
243			$script['scriptid'] = $scriptids[$index];
244
245			if ($script['type'] == ZBX_SCRIPT_TYPE_WEBHOOK && array_key_exists('parameters', $script)) {
246				foreach ($script['parameters'] as $param) {
247					$scripts_params[] = ['scriptid' => $script['scriptid']] + $param;
248				}
249			}
250		}
251		unset($script);
252
253		if ($scripts_params) {
254			DB::insertBatch('script_param', $scripts_params);
255		}
256
257		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_SCRIPT, $scripts);
258
259		return ['scriptids' => $scriptids];
260	}
261
262	/**
263	 * @param array $scripts
264	 *
265	 * @throws APIException if the input is invalid
266	 */
267	protected function validateCreate(array &$scripts) {
268		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
269			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
270		}
271
272		/*
273		 * Get general validation rules and firstly validate name uniqueness and all the possible fields, so that there
274		 * are no invalid fields for any of the script types. Unfortunaly there is also a drawback, since field types
275		 * validated before we know what rules belong to each script type.
276		 */
277		$api_input_rules = $this->getValidationRules('create', $common_fields);
278
279		if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) {
280			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
281		}
282
283		/*
284		 * Then validate each script separately. Depending on script type, each script may have different set of allowed
285		 * fields. Then in case the type is SSH and authtype is set, validate parameters again.
286		 */
287		$i = 0;
288		$check_names = [];
289
290		foreach ($scripts as $script) {
291			$path = '/'.++$i;
292
293			$type_rules = $this->getTypeValidationRules($script['type'], 'create', $type_fields);
294			$this->getScopeValidationRules($script['scope'], $scope_fields);
295
296			$type_rules['fields'] += $common_fields + $scope_fields;
297
298			if (!CApiInputValidator::validate($type_rules, $script, $path, $error)) {
299				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
300			}
301
302			if (array_key_exists('authtype', $script)) {
303				$ssh_rules = $this->getAuthTypeValidationRules($script['authtype'], 'create');
304				$ssh_rules['fields'] += $common_fields + $type_fields + $scope_fields;
305
306				if (!CApiInputValidator::validate($ssh_rules, $script, $path, $error)) {
307					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
308				}
309			}
310
311			$check_names[$script['name']] = true;
312		}
313
314		$db_script_names = API::getApiService()->select('scripts', [
315			'output' => ['scriptid'],
316			'filter' => ['name' => array_keys($check_names)]
317		]);
318
319		if ($db_script_names) {
320			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Script "%1$s" already exists.', $script['name']));
321		}
322
323		// Finally check User and Host IDs.
324		$this->checkUserGroups($scripts);
325		$this->checkHostGroups($scripts);
326	}
327
328	/**
329	 * @param array $scripts
330	 *
331	 * @return array
332	 */
333	public function update(array $scripts) {
334		$this->validateUpdate($scripts, $db_scripts);
335
336		$upd_scripts = [];
337		$scripts_params = [];
338
339		foreach ($scripts as $script) {
340			$scriptid = $script['scriptid'];
341			$db_script = $db_scripts[$scriptid];
342			$db_type = $db_script['type'];
343			$db_authtype = $db_script['authtype'];
344			$db_scope = $db_script['scope'];
345			$type = array_key_exists('type', $script) ? $script['type'] : $db_type;
346			$authtype = array_key_exists('authtype', $script) ? $script['authtype'] : $db_authtype;
347			$scope = array_key_exists('scope', $script) ? $script['scope'] : $db_scope;
348
349			$upd_script = [];
350
351			// strings
352			foreach (['name', 'command', 'description', 'confirmation', 'timeout', 'menu_path', 'username', 'publickey',
353					'privatekey', 'password'] as $field_name) {
354				if (array_key_exists($field_name, $script) && $script[$field_name] !== $db_script[$field_name]) {
355					$upd_script[$field_name] = $script[$field_name];
356				}
357			}
358
359			// integers
360			foreach (['type', 'execute_on', 'usrgrpid', 'groupid', 'host_access', 'scope', 'port', 'authtype']
361					as $field_name) {
362				if (array_key_exists($field_name, $script) && $script[$field_name] != $db_script[$field_name]) {
363					$upd_script[$field_name] = $script[$field_name];
364				}
365			}
366
367			// No mattter what the old type was, clear and reset all unnecessary fields from any other types.
368			if ($type != $db_type) {
369				switch ($type) {
370					case ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT:
371						$upd_script['port'] = '';
372						$upd_script['authtype'] = DB::getDefault('scripts', 'authtype');
373						$upd_script['username'] = '';
374						$upd_script['password'] = '';
375						$upd_script['publickey'] = '';
376						$upd_script['privatekey'] = '';
377						break;
378
379					case ZBX_SCRIPT_TYPE_IPMI:
380						$upd_script['port'] = '';
381						$upd_script['authtype'] = DB::getDefault('scripts', 'authtype');
382						$upd_script['username'] = '';
383						$upd_script['password'] = '';
384						$upd_script['publickey'] = '';
385						$upd_script['privatekey'] = '';
386						$upd_script['execute_on'] = DB::getDefault('scripts', 'execute_on');
387						break;
388
389					case ZBX_SCRIPT_TYPE_SSH:
390						$upd_script['execute_on'] = DB::getDefault('scripts', 'execute_on');
391						break;
392
393					case ZBX_SCRIPT_TYPE_TELNET:
394						$upd_script['authtype'] = DB::getDefault('scripts', 'authtype');
395						$upd_script['publickey'] = '';
396						$upd_script['privatekey'] = '';
397						$upd_script['execute_on'] = DB::getDefault('scripts', 'execute_on');
398						break;
399
400					case ZBX_SCRIPT_TYPE_WEBHOOK:
401						$upd_script['port'] = '';
402						$upd_script['authtype'] = DB::getDefault('scripts', 'authtype');
403						$upd_script['username'] = '';
404						$upd_script['password'] = '';
405						$upd_script['publickey'] = '';
406						$upd_script['privatekey'] = '';
407						$upd_script['execute_on'] = DB::getDefault('scripts', 'execute_on');
408						break;
409				}
410			}
411			elseif ($type == ZBX_SCRIPT_TYPE_SSH && $authtype != $db_authtype && $authtype == ITEM_AUTHTYPE_PASSWORD) {
412				$upd_script['publickey'] = '';
413				$upd_script['privatekey'] = '';
414			}
415
416			if ($scope != $db_scope && $scope == ZBX_SCRIPT_SCOPE_ACTION) {
417				$upd_script['menu_path'] = '';
418				$upd_script['usrgrpid'] = 0;
419				$upd_script['host_access'] = DB::getDefault('scripts', 'host_access');;
420				$upd_script['confirmation'] = '';
421			}
422
423			if ($type == ZBX_SCRIPT_TYPE_WEBHOOK && array_key_exists('parameters', $script)) {
424				$params = [];
425
426				foreach ($script['parameters'] as $param) {
427					$params[$param['name']] = $param['value'];
428				}
429
430				$scripts_params[$scriptid] = $params;
431				unset($script['parameters']);
432			}
433
434			if ($type != $db_type && $db_type == ZBX_SCRIPT_TYPE_WEBHOOK) {
435				$upd_script['timeout'] = DB::getDefault('scripts', 'timeout');
436				$scripts_params[$scriptid] = [];
437			}
438
439			if ($upd_script) {
440				$upd_scripts[] = [
441					'values' => $upd_script,
442					'where' => ['scriptid' => $scriptid]
443				];
444			}
445		}
446
447		if ($upd_scripts) {
448			DB::update('scripts', $upd_scripts);
449		}
450
451		if ($scripts_params) {
452			$insert_script_param = [];
453			$delete_script_param = [];
454			$update_script_param = [];
455			$db_scripts_params = DB::select('script_param', [
456				'output' => ['script_paramid', 'scriptid', 'name', 'value'],
457				'filter' => ['scriptid' => array_keys($scripts_params)]
458			]);
459
460			foreach ($db_scripts_params as $param) {
461				$scriptid = $param['scriptid'];
462
463				if (!array_key_exists($param['name'], $scripts_params[$scriptid])) {
464					$delete_script_param[] = $param['script_paramid'];
465				}
466				elseif ($scripts_params[$scriptid][$param['name']] !== $param['value']) {
467					$update_script_param[] = [
468						'values' => ['value' => $scripts_params[$scriptid][$param['name']]],
469						'where' => ['script_paramid' => $param['script_paramid']]
470					];
471					unset($scripts_params[$scriptid][$param['name']]);
472				}
473				else {
474					unset($scripts_params[$scriptid][$param['name']]);
475				}
476			}
477
478			$scripts_params = array_filter($scripts_params);
479
480			foreach ($scripts_params as $scriptid => $params) {
481				foreach ($params as $name => $value) {
482					$insert_script_param[] = compact('scriptid', 'name', 'value');
483				}
484			}
485
486			if ($delete_script_param) {
487				DB::delete('script_param', ['script_paramid' => array_keys(array_flip($delete_script_param))]);
488			}
489
490			if ($update_script_param) {
491				DB::update('script_param', $update_script_param);
492			}
493
494			if ($insert_script_param) {
495				DB::insert('script_param', $insert_script_param);
496			}
497		}
498
499		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_SCRIPT, $scripts, $db_scripts);
500
501		return ['scriptids' => zbx_objectValues($scripts, 'scriptid')];
502	}
503
504	/**
505	 * @param array $scripts
506	 * @param array $db_scripts
507	 *
508	 * @throws APIException if the input is invalid
509	 */
510	protected function validateUpdate(array &$scripts, array &$db_scripts = null) {
511		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
512			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
513		}
514
515		/*
516		 * Get general validation rules and firstly validate name uniqueness and all the possible fields, so that there
517		 * are no invalid fields for any of the script types. Unfortunaly there is also a drawback, since field types
518		 * validated before we know what rules belong to each script type.
519		 */
520		$api_input_rules = $this->getValidationRules('update', $common_fields);
521
522		if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) {
523			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
524		}
525
526		// Continue to validate script name.
527		$db_scripts = DB::select('scripts', [
528			'output' => ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description',
529				'confirmation', 'type', 'execute_on', 'timeout', 'scope', 'port', 'authtype', 'username', 'password',
530				'publickey', 'privatekey', 'menu_path'
531			],
532			'scriptids' => zbx_objectValues($scripts, 'scriptid'),
533			'preservekeys' => true
534		]);
535
536		$check_names = [];
537		foreach ($scripts as $script) {
538			if (!array_key_exists($script['scriptid'], $db_scripts)) {
539				self::exception(ZBX_API_ERROR_PERMISSIONS,
540					_('No permissions to referred object or it does not exist!')
541				);
542			}
543
544			if (array_key_exists('name', $script)) {
545				$check_names[$script['name']] = true;
546			}
547		}
548
549		if ($check_names) {
550			$db_script_names = API::getApiService()->select('scripts', [
551				'output' => ['scriptid', 'name'],
552				'filter' => ['name' => array_keys($check_names)]
553			]);
554			$db_script_names = zbx_toHash($db_script_names, 'name');
555
556			foreach ($scripts as $script) {
557				if (array_key_exists('name', $script)
558						&& array_key_exists($script['name'], $db_script_names)
559						&& !idcmp($db_script_names[$script['name']]['scriptid'], $script['scriptid'])) {
560					self::exception(ZBX_API_ERROR_PARAMETERS,
561						_s('Script "%1$s" already exists.', $script['name'])
562					);
563				}
564			}
565		}
566
567		// Validate if scripts belong to actions and scope can be changed.
568		$action_scriptids = [];
569
570		foreach ($scripts as $script) {
571			$db_script = $db_scripts[$script['scriptid']];
572
573			if (array_key_exists('scope', $script) && $script['scope'] != ZBX_SCRIPT_SCOPE_ACTION
574					&& $db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) {
575				$action_scriptids[$script['scriptid']] = true;
576			}
577		}
578
579		if ($action_scriptids) {
580			$actions = API::Action()->get([
581				'output' => ['actionid', 'name'],
582				'scriptids' => array_keys($action_scriptids),
583				'selectOperations' => ['opcommand'],
584				'selectRecoveryOperations' => ['opcommand'],
585				'selectAcknowledgeOperations' => ['opcommand']
586			]);
587
588			if ($actions) {
589				foreach ($scripts as $script) {
590					$db_script = $db_scripts[$script['scriptid']];
591
592					if (array_key_exists('scope', $script) && $script['scope'] != ZBX_SCRIPT_SCOPE_ACTION
593							&& $db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) {
594						foreach ($actions as $action) {
595							if ($action['operations']) {
596								// Find at least one usage of script in any of operations.
597								foreach ($action['operations'] as $operation) {
598									if (array_key_exists('opcommand', $operation)
599											&& bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) {
600
601										self::exception(ZBX_API_ERROR_PARAMETERS,
602											_s('Cannot update script scope. Script "%1$s" is used in action "%2$s".',
603												$db_script['name'], $action['name']
604											)
605										);
606									}
607								}
608							}
609
610							if ($action['recoveryOperations']) {
611								foreach ($action['recoveryOperations'] as $operation) {
612									if (array_key_exists('opcommand', $operation)
613											&& bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) {
614										self::exception(ZBX_API_ERROR_PARAMETERS,
615											_s('Cannot update script scope. Script "%1$s" is used in action "%2$s".',
616												$db_script['name'], $action['name']
617											)
618										);
619									}
620								}
621							}
622
623							if ($action['acknowledgeOperations']) {
624								foreach ($action['acknowledgeOperations'] as $operation) {
625									if (array_key_exists('opcommand', $operation)
626											&& bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) {
627										self::exception(ZBX_API_ERROR_PARAMETERS,
628											_s('Cannot update script scope. Script "%1$s" is used in action "%2$s".',
629												$db_script['name'], $action['name']
630											)
631										);
632									}
633								}
634							}
635						}
636					}
637				}
638			}
639		}
640
641		// Populate common and mandatory fields.
642		$scripts = zbx_toHash($scripts, 'scriptid');
643		$scripts = $this->extendFromObjects($scripts, $db_scripts, ['name', 'type', 'command', 'scope']);
644
645		$i = 0;
646		foreach ($scripts as &$script) {
647			$path = '/'.++$i;
648			$db_script = $db_scripts[$script['scriptid']];
649			$method = 'update';
650
651			if (array_key_exists('type', $script) && $script['type'] != $db_script['type']) {
652				// This means that all other fields are now required just like create method.
653				$method = 'create';
654
655				// Populate username field, if no new name is given and types are similar to previous.
656				if (!array_key_exists('username', $script)
657						&& (($db_script['type'] == ZBX_SCRIPT_TYPE_TELNET && $script['type'] == ZBX_SCRIPT_TYPE_SSH)
658							|| ($db_script['type'] == ZBX_SCRIPT_TYPE_SSH
659									&& $script['type'] == ZBX_SCRIPT_TYPE_TELNET))) {
660					$script['username'] = $db_script['username'];
661				}
662			}
663
664			$type_rules = $this->getTypeValidationRules($script['type'], $method, $type_fields);
665			$this->getScopeValidationRules($script['scope'], $scope_fields);
666
667			$type_rules['fields'] += $common_fields + $scope_fields;
668
669			if (!CApiInputValidator::validate($type_rules, $script, $path, $error)) {
670				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
671			}
672
673			if ($script['type'] == ZBX_SCRIPT_TYPE_SSH) {
674				$method = 'update';
675
676				if (array_key_exists('authtype', $script) && $script['authtype'] != $db_script['authtype']) {
677					$method = 'create';
678				}
679
680				$script = $this->extendFromObjects([$script], [$db_script], ['authtype'])[0];
681
682				$ssh_rules = $this->getAuthTypeValidationRules($script['authtype'], $method);
683				$ssh_rules['fields'] += $common_fields + $type_fields + $scope_fields;
684
685				if (!CApiInputValidator::validate($ssh_rules, $script, $path, $error)) {
686					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
687				}
688			}
689		}
690		unset($script);
691
692		$this->checkUserGroups($scripts);
693		$this->checkHostGroups($scripts);
694	}
695
696	/**
697	 * Get general validation rules.
698	 *
699	 * @param string $method [IN]          API method "create" or "update".
700	 * @param array  $common_fields [OUT]  Returns common fields for all script types.
701	 *
702	 * @return array
703	 */
704	protected function getValidationRules(string $method, &$common_fields = []): array {
705		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'fields' => []];
706
707		$common_fields = [
708			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'name')],
709			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI, ZBX_SCRIPT_TYPE_SSH, ZBX_SCRIPT_TYPE_TELNET, ZBX_SCRIPT_TYPE_WEBHOOK])],
710			'scope' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])],
711			'command' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')],
712			'groupid' =>		['type' => API_ID],
713			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'description')]
714		];
715
716		if ($method === 'create') {
717			$common_fields['scope']['default'] = ZBX_SCRIPT_SCOPE_ACTION;
718			$api_input_rules['uniq'] = [['name']];
719			$common_fields['name']['flags'] |= API_REQUIRED;
720			$common_fields['type']['flags'] = API_REQUIRED;
721			$common_fields['command']['flags'] |= API_REQUIRED;
722		}
723		else {
724			$api_input_rules['uniq'] =  [['scriptid'], ['name']];
725			$common_fields += ['scriptid' => ['type' => API_ID, 'flags' => API_REQUIRED]];
726		}
727
728		/*
729		 * Merge together optional fields that depend on script type. Some of these fields are not required for some
730		 * script types. Set only type for now. Unique parameter names, lengths and other flags are set later.
731		 */
732		$api_input_rules['fields'] += $common_fields + [
733			'execute_on' =>		['type' => API_INT32],
734			'menu_path' =>		['type' => API_STRING_UTF8],
735			'usrgrpid' =>		['type' => API_ID],
736			'host_access' =>	['type' => API_INT32],
737			'confirmation' =>	['type' => API_STRING_UTF8],
738			'port' =>			['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO],
739			'authtype' =>		['type' => API_INT32],
740			'username' =>		['type' => API_STRING_UTF8],
741			'publickey' =>		['type' => API_STRING_UTF8],
742			'privatekey' =>		['type' => API_STRING_UTF8],
743			'password' =>		['type' => API_STRING_UTF8],
744			'timeout' =>		['type' => API_TIME_UNIT],
745			'parameters' =>			['type' => API_OBJECTS, 'fields' => [
746				'name' =>				['type' => API_STRING_UTF8],
747				'value' =>				['type' => API_STRING_UTF8]
748			]]
749		];
750
751		return $api_input_rules;
752	}
753
754	/**
755	 * Get validation rules for script scope.
756	 *
757	 * @param int    $scope  [IN]          Script scope.
758	 * @param array  $common_fields [OUT]  Returns common fields for specific script scope.
759	 *
760	 * @return array
761	 */
762	protected function getScopeValidationRules(int $scope, &$common_fields = []): array {
763		$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
764		$common_fields = [];
765
766		if ($scope == ZBX_SCRIPT_SCOPE_HOST || $scope == ZBX_SCRIPT_SCOPE_EVENT) {
767			$common_fields = [
768				'menu_path' =>		['type' => API_SCRIPT_MENU_PATH, 'length' => DB::getFieldLength('scripts', 'menu_path')],
769				'usrgrpid' =>		['type' => API_ID],
770				'host_access' =>	['type' => API_INT32, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])],
771				'confirmation' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'confirmation')]
772			];
773
774			$api_input_rules['fields'] += $common_fields;
775		}
776
777		return $api_input_rules;
778	}
779
780	/**
781	 * Get validation rules for each script type.
782	 *
783	 * @param int    $type   [IN]          Script type.
784	 * @param string $method [IN]          API method "create" or "update".
785	 * @param array  $common_fields [OUT]  Returns common fields for specific script type.
786	 *
787	 * @return array
788	 */
789	protected function getTypeValidationRules(int $type, string $method, &$common_fields = []): array {
790		$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
791		$common_fields = [];
792
793		switch ($type) {
794			case ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT:
795				$api_input_rules['fields'] += [
796					'execute_on' =>		['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_EXECUTE_ON_AGENT, ZBX_SCRIPT_EXECUTE_ON_SERVER, ZBX_SCRIPT_EXECUTE_ON_PROXY])]
797				];
798				break;
799
800			case ZBX_SCRIPT_TYPE_SSH:
801				$common_fields = [
802					'port' =>			['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO],
803					'authtype' =>		['type' => API_INT32, 'in' => implode(',', [ITEM_AUTHTYPE_PASSWORD, ITEM_AUTHTYPE_PUBLICKEY])],
804					'username' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'username')],
805					'password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'password')]
806				];
807
808				if ($method === 'create') {
809					$common_fields['username']['flags'] |= API_REQUIRED;
810				}
811
812				$api_input_rules['fields'] += $common_fields + [
813					'publickey' =>		['type' => API_STRING_UTF8],
814					'privatekey' =>		['type' => API_STRING_UTF8]
815				];
816				break;
817
818			case ZBX_SCRIPT_TYPE_TELNET:
819				$api_input_rules['fields'] += [
820					'port' =>			['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO],
821					'username' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'username')],
822					'password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'password')]
823				];
824
825				if ($method === 'create') {
826					$api_input_rules['fields']['username']['flags'] |= API_REQUIRED;
827				}
828				break;
829
830			case ZBX_SCRIPT_TYPE_WEBHOOK:
831				$api_input_rules['fields'] += [
832					'timeout' =>		['type' => API_TIME_UNIT, 'in' => '1:'.SEC_PER_MIN],
833					'parameters' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
834						'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('script_param', 'name')],
835						'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('script_param', 'value')]
836					]]
837				];
838				break;
839		}
840
841		return $api_input_rules;
842	}
843
844	/**
845	 * Get validation rules for each script authtype.
846	 *
847	 * @param int    $authtype  Script authtype.
848	 * @param string $method    API method "create" or "update".
849	 *
850	 * @return array
851	 */
852	protected function getAuthTypeValidationRules(int $authtype, string $method): array {
853		$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
854
855		if ($authtype == ITEM_AUTHTYPE_PUBLICKEY) {
856			$api_input_rules['fields'] += [
857				'publickey' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'publickey')],
858				'privatekey' =>		['type' => API_STRING_UTF8,'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'privatekey')]
859			];
860
861			if ($method === 'create') {
862				$api_input_rules['fields']['publickey']['flags'] |= API_REQUIRED;
863				$api_input_rules['fields']['privatekey']['flags'] |= API_REQUIRED;
864			}
865		}
866
867		return $api_input_rules;
868	}
869
870	/**
871	 * Check for valid user groups.
872	 *
873	 * @param array $scripts
874	 * @param array $scripts[]['usrgrpid']  (optional)
875	 *
876	 * @throws APIException  if user group is not exists.
877	 */
878	private function checkUserGroups(array $scripts) {
879		$usrgrpids = [];
880
881		foreach ($scripts as $script) {
882			if (array_key_exists('usrgrpid', $script) && $script['usrgrpid'] != 0) {
883				$usrgrpids[$script['usrgrpid']] = true;
884			}
885		}
886
887		if (!$usrgrpids) {
888			return;
889		}
890
891		$usrgrpids = array_keys($usrgrpids);
892
893		$db_usrgrps = DB::select('usrgrp', [
894			'output' => [],
895			'usrgrpids' => $usrgrpids,
896			'preservekeys' => true
897		]);
898
899		foreach ($usrgrpids as $usrgrpid) {
900			if (!array_key_exists($usrgrpid, $db_usrgrps)) {
901				self::exception(ZBX_API_ERROR_PARAMETERS, _s('User group with ID "%1$s" is not available.', $usrgrpid));
902			}
903		}
904	}
905
906	/**
907	 * Check for valid host groups.
908	 *
909	 * @param array $scripts
910	 * @param array $scripts[]['groupid']  (optional)
911	 *
912	 * @throws APIException  if host group is not exists.
913	 */
914	private function checkHostGroups(array $scripts) {
915		$groupids = [];
916
917		foreach ($scripts as $script) {
918			if (array_key_exists('groupid', $script) && $script['groupid'] != 0) {
919				$groupids[$script['groupid']] = true;
920			}
921		}
922
923		if (!$groupids) {
924			return;
925		}
926
927		$groupids = array_keys($groupids);
928
929		$db_groups = DB::select('hstgrp', [
930			'output' => [],
931			'groupids' => $groupids,
932			'preservekeys' => true
933		]);
934
935		foreach ($groupids as $groupid) {
936			if (!array_key_exists($groupid, $db_groups)) {
937				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host group with ID "%1$s" is not available.', $groupid));
938			}
939		}
940	}
941
942	/**
943	 * @param array $scriptids
944	 *
945	 * @return array
946	 */
947	public function delete(array $scriptids) {
948		$this->validateDelete($scriptids, $db_scripts);
949
950		DB::delete('scripts', ['scriptid' => $scriptids]);
951
952		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_SCRIPT, $db_scripts);
953
954		return ['scriptids' => $scriptids];
955	}
956
957	/**
958	 * @param array $scriptids
959	 * @param array $db_scripts
960	 *
961	 * @throws APIException if the input is invalid
962	 */
963	protected function validateDelete(array &$scriptids, array &$db_scripts = null) {
964		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
965			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
966		}
967
968		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
969		if (!CApiInputValidator::validate($api_input_rules, $scriptids, '/', $error)) {
970			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
971		}
972
973		$db_scripts = DB::select('scripts', [
974			'output' => ['scriptid', 'name'],
975			'scriptids' => $scriptids,
976			'preservekeys' => true
977		]);
978
979		foreach ($scriptids as $scriptid) {
980			if (!array_key_exists($scriptid, $db_scripts)) {
981				self::exception(ZBX_API_ERROR_PERMISSIONS,
982					_('No permissions to referred object or it does not exist!')
983				);
984			}
985		}
986
987		// Check if deleted scripts used in actions.
988		$db_actions = DBselect(
989			'SELECT a.name,oc.scriptid'.
990			' FROM opcommand oc,operations o,actions a'.
991			' WHERE oc.operationid=o.operationid'.
992				' AND o.actionid=a.actionid'.
993				' AND '.dbConditionInt('oc.scriptid', $scriptids),
994			1
995		);
996
997		if ($db_action = DBfetch($db_actions)) {
998			self::exception(ZBX_API_ERROR_PARAMETERS,
999				_s('Cannot delete scripts. Script "%1$s" is used in action operation "%2$s".',
1000					$db_scripts[$db_action['scriptid']]['name'], $db_action['name']
1001				)
1002			);
1003		}
1004	}
1005
1006	/**
1007	 * @param array $data
1008	 *
1009	 * @return array
1010	 */
1011	public function execute(array $data) {
1012		global $ZBX_SERVER, $ZBX_SERVER_PORT;
1013
1014		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
1015			'scriptid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
1016			'hostid' =>		['type' => API_ID],
1017			'eventid' =>	['type' => API_ID]
1018		]];
1019		if (!CApiInputValidator::validate($api_input_rules, $data, '/', $error)) {
1020			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1021		}
1022
1023		if (!array_key_exists('hostid', $data) && !array_key_exists('eventid', $data)) {
1024			self::exception(ZBX_API_ERROR_PARAMETERS,
1025				_s('Invalid parameter "%1$s": %2$s.', '/', _s('the parameter "%1$s" is missing', 'eventid'))
1026			);
1027		}
1028
1029		if (array_key_exists('hostid', $data) && array_key_exists('eventid', $data)) {
1030			self::exception(ZBX_API_ERROR_PARAMETERS,
1031				_s('Invalid parameter "%1$s": %2$s.', '/', _s('unexpected parameter "%1$s"', 'eventid'))
1032			);
1033		}
1034
1035		if (array_key_exists('eventid', $data)) {
1036			$db_events = API::Event()->get([
1037				'output' => [],
1038				'selectHosts' => ['hostid'],
1039				'eventids' => $data['eventid']
1040			]);
1041			if (!$db_events) {
1042				self::exception(ZBX_API_ERROR_PERMISSIONS,
1043					_('No permissions to referred object or it does not exist!')
1044				);
1045			}
1046
1047			$hostids = array_column($db_events[0]['hosts'], 'hostid');
1048			$is_event = true;
1049		}
1050		else {
1051			$hostids = $data['hostid'];
1052			$is_event = false;
1053
1054			$db_hosts = API::Host()->get([
1055				'output' => [],
1056				'hostids' => $hostids
1057			]);
1058			if (!$db_hosts) {
1059				self::exception(ZBX_API_ERROR_PERMISSIONS,
1060					_('No permissions to referred object or it does not exist!')
1061				);
1062			}
1063		}
1064
1065		$db_scripts = $this->get([
1066			'output' => [],
1067			'hostids' => $hostids,
1068			'scriptids' => $data['scriptid']
1069		]);
1070		if (!$db_scripts) {
1071			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
1072		}
1073
1074		// execute script
1075		$zabbix_server = new CZabbixServer($ZBX_SERVER, $ZBX_SERVER_PORT,
1076			timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::CONNECT_TIMEOUT)),
1077			timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::SCRIPT_TIMEOUT)), ZBX_SOCKET_BYTES_LIMIT
1078		);
1079		$result = $zabbix_server->executeScript($data['scriptid'], self::$userData['sessionid'],
1080			$is_event ? null : $data['hostid'],
1081			$is_event ? $data['eventid'] : null
1082		);
1083
1084		if ($result !== false) {
1085			// return the result in a backwards-compatible format
1086			return [
1087				'response' => 'success',
1088				'value' => $result,
1089				'debug' => $zabbix_server->getDebug()
1090			];
1091		}
1092		else {
1093			self::exception(ZBX_API_ERROR_INTERNAL, $zabbix_server->getError());
1094		}
1095	}
1096
1097	/**
1098	 * Returns all the scripts that are available on each given host.
1099	 *
1100	 * @param $hostids
1101	 *
1102	 * @return array
1103	 */
1104	public function getScriptsByHosts($hostids) {
1105		zbx_value2array($hostids);
1106
1107		$scripts_by_host = [];
1108
1109		if (!$hostids) {
1110			return $scripts_by_host;
1111		}
1112
1113		foreach ($hostids as $hostid) {
1114			$scripts_by_host[$hostid] = [];
1115		}
1116
1117		$scripts = $this->get([
1118			'output' => API_OUTPUT_EXTEND,
1119			'hostids' => $hostids,
1120			'sortfield' => 'name',
1121			'preservekeys' => true
1122		]);
1123
1124		$scripts = $this->addRelatedGroupsAndHosts([
1125			'selectGroups' => null,
1126			'selectHosts' => ['hostid']
1127		], $scripts, $hostids);
1128
1129		if ($scripts) {
1130			// resolve macros
1131			$macros_data = [];
1132			foreach ($scripts as $scriptid => $script) {
1133				if (!empty($script['confirmation'])) {
1134					foreach ($script['hosts'] as $host) {
1135						if (isset($scripts_by_host[$host['hostid']])) {
1136							$macros_data[$host['hostid']][$scriptid] = $script['confirmation'];
1137						}
1138					}
1139				}
1140			}
1141			if ($macros_data) {
1142				$macros_data = CMacrosResolverHelper::resolve([
1143					'config' => 'scriptConfirmation',
1144					'data' => $macros_data
1145				]);
1146			}
1147
1148			foreach ($scripts as $scriptid => $script) {
1149				$hosts = $script['hosts'];
1150				unset($script['hosts']);
1151				// set script to host
1152				foreach ($hosts as $host) {
1153					$hostid = $host['hostid'];
1154
1155					if (isset($scripts_by_host[$hostid])) {
1156						$size = count($scripts_by_host[$hostid]);
1157						$scripts_by_host[$hostid][$size] = $script;
1158
1159						// set confirmation text with resolved macros
1160						if (isset($macros_data[$hostid][$scriptid]) && $script['confirmation']) {
1161							$scripts_by_host[$hostid][$size]['confirmation'] = $macros_data[$hostid][$scriptid];
1162						}
1163					}
1164				}
1165			}
1166		}
1167
1168		return $scripts_by_host;
1169	}
1170
1171	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
1172		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
1173
1174		if ($options['selectGroups'] !== null || $options['selectHosts'] !== null) {
1175			$sqlParts = $this->addQuerySelect($this->fieldId('groupid'), $sqlParts);
1176			$sqlParts = $this->addQuerySelect($this->fieldId('host_access'), $sqlParts);
1177		}
1178
1179		return $sqlParts;
1180	}
1181
1182	/**
1183	 * Applies relational subselect onto already fetched result.
1184	 *
1185	 * @param  array $options
1186	 * @param  array $result
1187	 *
1188	 * @return array $result
1189	 */
1190	protected function addRelatedObjects(array $options, array $result) {
1191		$result = parent::addRelatedObjects($options, $result);
1192
1193		// Adding actions.
1194		if ($options['selectActions'] !== null && $options['selectActions'] !== API_OUTPUT_COUNT) {
1195			foreach ($result as $scriptid => &$row) {
1196				$row['actions'] = [];
1197			}
1198
1199			$action_scriptids = [];
1200
1201			if ($this->outputIsRequested('scope', $options['output'])) {
1202				foreach ($result as $scriptid => $row) {
1203					if ($row['scope'] == ZBX_SCRIPT_SCOPE_ACTION) {
1204						$action_scriptids[] = $scriptid;
1205					}
1206				}
1207			}
1208			else {
1209				$db_scripts = API::getApiService()->select('scripts', [
1210					'output' => ['scope'],
1211					'filter' => ['scriptid' => array_keys($result)],
1212					'preservekeys' => true
1213				]);
1214				$db_scripts = $this->extendFromObjects($result, $db_scripts, ['scope']);
1215
1216				foreach ($db_scripts as $scriptid => $db_script) {
1217					if ($db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) {
1218						$action_scriptids[] = $scriptid;
1219					}
1220				}
1221			}
1222
1223			if ($action_scriptids) {
1224				if ($options['selectActions'] === API_OUTPUT_EXTEND) {
1225					$action_fields = array_map(function ($field) { return 'a.'.$field; }, $this->action_fields);
1226					$action_fields = implode(',', $action_fields);
1227				}
1228				elseif (is_array($options['selectActions'])) {
1229					$action_fields = $options['selectActions'];
1230
1231					if (!in_array('actionid', $options['selectActions'])) {
1232						$action_fields[] = 'actionid';
1233					}
1234
1235					$action_fields = array_map(function ($field) { return 'a.'.$field; }, $action_fields);
1236					$action_fields = implode(',', $action_fields);
1237				}
1238
1239				$db_script_actions = DBfetchArray(DBselect(
1240					'SELECT DISTINCT oc.scriptid,'.$action_fields.
1241					' FROM actions a,operations o,opcommand oc'.
1242					' WHERE a.actionid=o.actionid'.
1243						' AND o.operationid=oc.operationid'.
1244						' AND '.dbConditionInt('oc.scriptid', $action_scriptids)
1245				));
1246
1247				foreach ($result as $scriptid => &$row) {
1248					if ($db_script_actions) {
1249						foreach ($db_script_actions as $db_script_action) {
1250							if (bccomp($db_script_action['scriptid'], $scriptid) == 0) {
1251								unset($db_script_action['scriptid']);
1252								$row['actions'][] = $db_script_action;
1253							}
1254						}
1255
1256						$row['actions'] = $this->unsetExtraFields($row['actions'], ['actionid'],
1257							$options['selectActions']
1258						);
1259					}
1260				}
1261				unset($row);
1262			}
1263		}
1264
1265		if ($this->outputIsRequested('parameters', $options['output'])) {
1266			foreach ($result as $scriptid => $script) {
1267				$result[$scriptid]['parameters'] = [];
1268			}
1269
1270			$parameters = DB::select('script_param', [
1271				'output' => ['scriptid', 'name', 'value'],
1272				'filter' => ['scriptid' => array_keys($result)]
1273			]);
1274
1275			foreach ($parameters as $parameter) {
1276				$result[$parameter['scriptid']]['parameters'][] = [
1277					'name' => $parameter['name'],
1278					'value' => $parameter['value']
1279				];
1280			}
1281		}
1282
1283		return $this->addRelatedGroupsAndHosts($options, $result);
1284	}
1285
1286	/**
1287	 * Applies relational subselect onto already fetched result.
1288	 *
1289	 * @param  array $options
1290	 * @param  mixed $options['selectGroups']
1291	 * @param  mixed $options['selectHosts']
1292	 * @param  array $result
1293	 * @param  array $hostids                  An additional filter by hostids, which will be added to "hosts" key.
1294	 *
1295	 * @return array $result
1296	 */
1297	private function addRelatedGroupsAndHosts(array $options, array $result, array $hostids = null) {
1298		$is_groups_select = $options['selectGroups'] !== null && $options['selectGroups'];
1299		$is_hosts_select = $options['selectHosts'] !== null && $options['selectHosts'];
1300
1301		if (!$is_groups_select && !$is_hosts_select) {
1302			return $result;
1303		}
1304
1305		$host_groups_with_write_access = [];
1306		$has_write_access_level = false;
1307
1308		$group_search_names = [];
1309		foreach ($result as $script) {
1310			$has_write_access_level |= ($script['host_access'] == PERM_READ_WRITE);
1311
1312			// If any script belongs to all host groups.
1313			if ($script['groupid'] == 0) {
1314				$group_search_names = null;
1315			}
1316
1317			if ($group_search_names !== null) {
1318				/*
1319				 * If scripts were requested by host or group filters, then we have already requested group names
1320				 * for all groups linked to scripts. And then we can request less groups by adding them as search
1321				 * condition in hostgroup.get. Otherwise we will need to request all groups, user has access to.
1322				 */
1323				if (array_key_exists($script['groupid'], $this->parent_host_groups)) {
1324					$group_search_names[] = $this->parent_host_groups[$script['groupid']]['name'];
1325				}
1326			}
1327		}
1328
1329		$select_groups = ['name', 'groupid'];
1330		$select_groups = $this->outputExtend($options['selectGroups'], $select_groups);
1331
1332		$host_groups = API::HostGroup()->get([
1333			'output' => $select_groups,
1334			'search' => $group_search_names ? ['name' => $group_search_names] : null,
1335			'searchByAny' => true,
1336			'startSearch' => true,
1337			'preservekeys' => true
1338		]);
1339
1340		if ($has_write_access_level && $host_groups) {
1341			$host_groups_with_write_access = API::HostGroup()->get([
1342				'output' => $select_groups,
1343				'groupid' => array_keys($host_groups),
1344				'preservekeys' => true,
1345				'editable' => true
1346			]);
1347		}
1348		else {
1349			$host_groups_with_write_access = $host_groups;
1350		}
1351
1352		$nested = [];
1353		foreach ($host_groups as $groupid => $group) {
1354			$name = $group['name'];
1355
1356			while (($pos = strrpos($name, '/')) !== false) {
1357				$name = substr($name, 0, $pos);
1358				$nested[$name][$groupid] = true;
1359			}
1360		}
1361
1362		$hstgrp_branch = [];
1363		foreach ($host_groups as $groupid => $group) {
1364			$hstgrp_branch[$groupid] = [$groupid => true];
1365			if (array_key_exists($group['name'], $nested)) {
1366				$hstgrp_branch[$groupid] += $nested[$group['name']];
1367			}
1368		}
1369
1370		if ($is_hosts_select) {
1371			$sql = 'SELECT hostid,groupid FROM hosts_groups'.
1372				' WHERE '.dbConditionInt('groupid', array_keys($host_groups));
1373			if ($hostids !== null) {
1374				$sql .= ' AND '.dbConditionInt('hostid', $hostids);
1375			}
1376
1377			$db_group_hosts = DBSelect($sql);
1378
1379			$all_hostids = [];
1380			$group_to_hosts = [];
1381			while ($row = DBFetch($db_group_hosts)) {
1382				if (!array_key_exists($row['groupid'], $group_to_hosts)) {
1383					$group_to_hosts[$row['groupid']] = [];
1384				}
1385
1386				$group_to_hosts[$row['groupid']][$row['hostid']] = true;
1387				$all_hostids[] = $row['hostid'];
1388			}
1389
1390			$used_hosts = API::Host()->get([
1391				'output' => $options['selectHosts'],
1392				'hostids' => $all_hostids,
1393				'preservekeys' => true
1394			]);
1395		}
1396
1397		$host_groups = $this->unsetExtraFields($host_groups, ['name', 'groupid'], $options['selectGroups']);
1398		$host_groups_with_write_access = $this->unsetExtraFields(
1399			$host_groups_with_write_access, ['name', 'groupid'], $options['selectGroups']
1400		);
1401
1402		foreach ($result as &$script) {
1403			if ($script['groupid'] == 0) {
1404				$script_groups = ($script['host_access'] == PERM_READ_WRITE)
1405					? $host_groups_with_write_access
1406					: $host_groups;
1407			}
1408			else {
1409				$script_groups = ($script['host_access'] == PERM_READ_WRITE)
1410					? array_intersect_key($host_groups_with_write_access, $hstgrp_branch[$script['groupid']])
1411					: array_intersect_key($host_groups, $hstgrp_branch[$script['groupid']]);
1412			}
1413
1414			if ($is_groups_select) {
1415				$script['groups'] = array_values($script_groups);
1416			}
1417
1418			if ($is_hosts_select) {
1419				$script['hosts'] = [];
1420				foreach (array_keys($script_groups) as $script_groupid) {
1421					if (array_key_exists($script_groupid, $group_to_hosts)) {
1422						$script['hosts'] += array_intersect_key($used_hosts, $group_to_hosts[$script_groupid]);
1423					}
1424				}
1425				$script['hosts'] = array_values($script['hosts']);
1426			}
1427		}
1428		unset($script);
1429
1430		return $result;
1431	}
1432}
1433