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 http tests.
24 */
25class CHttpTest extends CApiService {
26
27	protected $tableName = 'httptest';
28	protected $tableAlias = 'ht';
29	protected $sortColumns = ['httptestid', 'name'];
30
31	/**
32	 * Get data about web scenarios.
33	 *
34	 * @param array $options
35	 *
36	 * @return array
37	 */
38	public function get($options = []) {
39		$result = [];
40
41		$sqlParts = [
42			'select'	=> ['httptests' => 'ht.httptestid'],
43			'from'		=> ['httptest' => 'httptest ht'],
44			'where'		=> [],
45			'group'		=> [],
46			'order'		=> [],
47			'limit'		=> null
48		];
49
50		$defOptions = [
51			'httptestids'    => null,
52			'applicationids' => null,
53			'hostids'        => null,
54			'groupids'       => null,
55			'templateids'    => null,
56			'editable'       => false,
57			'inherited'      => null,
58			'templated'      => null,
59			'monitored'      => null,
60			'nopermissions'  => null,
61			// filter
62			'filter'         => null,
63			'search'         => null,
64			'searchByAny'    => null,
65			'startSearch'    => false,
66			'excludeSearch'  => false,
67			// output
68			'output'         => API_OUTPUT_EXTEND,
69			'expandName'     => null,
70			'expandStepName' => null,
71			'selectHosts'    => null,
72			'selectSteps'    => null,
73			'countOutput'    => false,
74			'groupCount'     => false,
75			'preservekeys'   => false,
76			'sortfield'      => '',
77			'sortorder'      => '',
78			'limit'          => null
79		];
80		$options = zbx_array_merge($defOptions, $options);
81
82		// editable + PERMISSION CHECK
83		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
84			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
85			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
86
87			$sqlParts['where'][] = 'EXISTS ('.
88					'SELECT NULL'.
89					' FROM hosts_groups hgg'.
90						' JOIN rights r'.
91							' ON r.id=hgg.groupid'.
92								' AND '.dbConditionInt('r.groupid', $userGroups).
93					' WHERE ht.hostid=hgg.hostid'.
94					' GROUP BY hgg.hostid'.
95					' HAVING MIN(r.permission)>'.PERM_DENY.
96						' AND MAX(r.permission)>='.zbx_dbstr($permission).
97					')';
98		}
99
100		// httptestids
101		if (!is_null($options['httptestids'])) {
102			zbx_value2array($options['httptestids']);
103
104			$sqlParts['where']['httptestid'] = dbConditionInt('ht.httptestid', $options['httptestids']);
105		}
106
107		// templateids
108		if (!is_null($options['templateids'])) {
109			zbx_value2array($options['templateids']);
110
111			if (!is_null($options['hostids'])) {
112				zbx_value2array($options['hostids']);
113				$options['hostids'] = array_merge($options['hostids'], $options['templateids']);
114			}
115			else {
116				$options['hostids'] = $options['templateids'];
117			}
118		}
119		// hostids
120		if (!is_null($options['hostids'])) {
121			zbx_value2array($options['hostids']);
122
123			$sqlParts['where']['hostid'] = dbConditionInt('ht.hostid', $options['hostids']);
124
125			if ($options['groupCount']) {
126				$sqlParts['group']['hostid'] = 'ht.hostid';
127			}
128		}
129
130		// groupids
131		if (!is_null($options['groupids'])) {
132			zbx_value2array($options['groupids']);
133
134			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
135			$sqlParts['where'][] = dbConditionInt('hg.groupid', $options['groupids']);
136			$sqlParts['where'][] = 'hg.hostid=ht.hostid';
137
138			if ($options['groupCount']) {
139				$sqlParts['group']['hg'] = 'hg.groupid';
140			}
141		}
142
143		// applicationids
144		if (!is_null($options['applicationids'])) {
145			zbx_value2array($options['applicationids']);
146
147			$sqlParts['where'][] = dbConditionId('ht.applicationid', $options['applicationids']);
148		}
149
150		// inherited
151		if (isset($options['inherited'])) {
152			$sqlParts['where'][] = $options['inherited'] ? 'ht.templateid IS NOT NULL' : 'ht.templateid IS NULL';
153		}
154
155		// templated
156		if (isset($options['templated'])) {
157			$sqlParts['from']['hosts'] = 'hosts h';
158			$sqlParts['where']['ha'] = 'h.hostid=ht.hostid';
159			if ($options['templated']) {
160				$sqlParts['where'][] = 'h.status='.HOST_STATUS_TEMPLATE;
161			}
162			else {
163				$sqlParts['where'][] = 'h.status<>'.HOST_STATUS_TEMPLATE;
164			}
165		}
166
167		// monitored
168		if (!is_null($options['monitored'])) {
169			$sqlParts['from']['hosts'] = 'hosts h';
170			$sqlParts['where']['hht'] = 'h.hostid=ht.hostid';
171
172			if ($options['monitored']) {
173				$sqlParts['where'][] = 'h.status='.HOST_STATUS_MONITORED;
174				$sqlParts['where'][] = 'ht.status='.ITEM_STATUS_ACTIVE;
175			}
176			else {
177				$sqlParts['where'][] = '(h.status<>'.HOST_STATUS_MONITORED.' OR ht.status<>'.ITEM_STATUS_ACTIVE.')';
178			}
179		}
180
181		// search
182		if (is_array($options['search'])) {
183			zbx_db_search('httptest ht', $options, $sqlParts);
184		}
185
186		// filter
187		if (is_array($options['filter'])) {
188			if (array_key_exists('delay', $options['filter']) && $options['filter']['delay'] !== null) {
189				$options['filter']['delay'] = getTimeUnitFilters($options['filter']['delay']);
190			}
191
192			$this->dbFilter('httptest ht', $options, $sqlParts);
193		}
194
195		// limit
196		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
197			$sqlParts['limit'] = $options['limit'];
198		}
199
200		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
201		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
202		$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
203		while ($httpTest = DBfetch($res)) {
204			if ($options['countOutput']) {
205				if ($options['groupCount']) {
206					$result[] = $httpTest;
207				}
208				else {
209					$result = $httpTest['rowscount'];
210				}
211			}
212			else {
213				$result[$httpTest['httptestid']] = $httpTest;
214			}
215		}
216
217		if ($options['countOutput']) {
218			return $result;
219		}
220
221		if ($result) {
222			$result = $this->addRelatedObjects($options, $result);
223
224			// expandName
225			$nameRequested = (is_array($options['output']) && in_array('name', $options['output']))
226				|| $options['output'] == API_OUTPUT_EXTEND;
227			$expandName = $options['expandName'] !== null && $nameRequested;
228
229			// expandStepName
230			$stepNameRequested = $options['selectSteps'] == API_OUTPUT_EXTEND
231				|| (is_array($options['selectSteps']) && in_array('name', $options['selectSteps']));
232			$expandStepName = $options['expandStepName'] !== null && $stepNameRequested;
233
234			if ($expandName || $expandStepName) {
235				$result = resolveHttpTestMacros($result, $expandName, $expandStepName);
236			}
237
238			$result = $this->unsetExtraFields($result, ['hostid'], $options['output']);
239		}
240
241		// removing keys (hash -> array)
242		if (!$options['preservekeys']) {
243			$result = zbx_cleanHashes($result);
244		}
245
246		return $result;
247	}
248
249	/**
250	 * Create web scenario.
251	 *
252	 * @param $httptests
253	 *
254	 * @return array
255	 */
256	public function create($httptests) {
257		$this->validateCreate($httptests);
258
259		$httptests = Manager::HttpTest()->persist($httptests);
260
261		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_SCENARIO, $httptests);
262
263		return ['httptestids' => zbx_objectValues($httptests, 'httptestid')];
264	}
265
266	/**
267	 * @param array $httpTests
268	 *
269	 * @throws APIException if the input is invalid.
270	 */
271	protected function validateCreate(array &$httptests) {
272		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid', 'name']], 'fields' => [
273			'hostid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
274			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest', 'name')],
275			'applicationid' =>		['type' => API_ID],
276			'delay' =>				['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_DAY],
277			'retries' =>			['type' => API_INT32, 'in' => '1:10'],
278			'agent' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'agent')],
279			'http_proxy' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_proxy')],
280			'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
281				'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'name')],
282				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'value')]
283			]],
284			'headers' =>			['type' => API_OBJECTS, 'fields' => [
285				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'name')],
286				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'value')]
287			]],
288			'status' =>				['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STATUS_ACTIVE, HTTPTEST_STATUS_DISABLED])],
289			'authentication' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_AUTH_NONE, HTTPTEST_AUTH_BASIC, HTTPTEST_AUTH_NTLM, HTTPTEST_AUTH_KERBEROS])],
290			'http_user' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_user')],
291			'http_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_password')],
292			'verify_peer' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_PEER_OFF, HTTPTEST_VERIFY_PEER_ON])],
293			'verify_host' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_HOST_OFF, HTTPTEST_VERIFY_HOST_ON])],
294			'ssl_cert_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_cert_file')],
295			'ssl_key_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_file')],
296			'ssl_key_password' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_password')],
297			'steps' =>				['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['name'], ['no']], 'fields' => [
298				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'name')],
299				'no' =>					['type' => API_INT32, 'flags' => API_REQUIRED],
300				'url' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'url')],
301				'query_fields' =>		['type' => API_OBJECTS, 'fields' => [
302					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
303					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
304				]],
305				'posts' =>				['type' => API_HTTP_POST, 'length' => DB::getFieldLength('httpstep', 'posts'), 'name-length' => DB::getFieldLength('httpstep_field', 'name'), 'value-length' => DB::getFieldLength('httpstep_field', 'value')],
306				'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
307					'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'name')],
308					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
309				]],
310				'headers' =>			['type' => API_OBJECTS, 'fields' => [
311					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
312					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'value')]
313				]],
314				'follow_redirects' =>	['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_FOLLOW_REDIRECTS_OFF, HTTPTEST_STEP_FOLLOW_REDIRECTS_ON])],
315				'retrieve_mode' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_RETRIEVE_MODE_CONTENT, HTTPTEST_STEP_RETRIEVE_MODE_HEADERS, HTTPTEST_STEP_RETRIEVE_MODE_BOTH])],
316				'timeout' =>			['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_HOUR],
317				'required' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'required')],
318				'status_codes' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'status_codes')]
319			]]
320		]];
321		if (!CApiInputValidator::validate($api_input_rules, $httptests, '/', $error)) {
322			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
323		}
324
325		$names_by_hostid = [];
326
327		foreach ($httptests as $httptest) {
328			$names_by_hostid[$httptest['hostid']][] = $httptest['name'];
329		}
330
331		$this->checkHostPermissions(array_keys($names_by_hostid));
332		$this->checkDuplicates($names_by_hostid);
333		$this->checkApplications($httptests, __FUNCTION__);
334		$this->validateAuthParameters($httptests, __FUNCTION__);
335		$this->validateSslParameters($httptests, __FUNCTION__);
336		$this->validateSteps($httptests, __FUNCTION__);
337	}
338
339	/**
340	 * @param $httptests
341	 *
342	 * @return array
343	 */
344	public function update($httptests) {
345		$this->validateUpdate($httptests, $db_httptests);
346
347		Manager::HttpTest()->persist($httptests);
348
349		foreach ($db_httptests as &$db_httptest) {
350			unset($db_httptest['headers'], $db_httptest['variables'], $db_httptest['steps']);
351		}
352		unset($db_httptest);
353
354		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_SCENARIO, $httptests, $db_httptests);
355
356		return ['httptestids' => zbx_objectValues($httptests, 'httptestid')];
357	}
358
359	/**
360	 * @param array $httptests
361	 * @param array $db_httptests
362	 *
363	 * @throws APIException if the input is invalid.
364	 */
365	protected function validateUpdate(array &$httptests, array &$db_httptests = null) {
366		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['httptestid']], 'fields' => [
367			'httptestid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
368			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest', 'name')],
369			'applicationid' =>		['type' => API_ID],
370			'delay' =>				['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_DAY],
371			'retries' =>			['type' => API_INT32, 'in' => '1:10'],
372			'agent' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'agent')],
373			'http_proxy' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_proxy')],
374			'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
375				'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'name')],
376				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'value')]
377			]],
378			'headers' =>			['type' => API_OBJECTS, 'fields' => [
379				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'name')],
380				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'value')]
381			]],
382			'status' =>				['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STATUS_ACTIVE, HTTPTEST_STATUS_DISABLED])],
383			'authentication' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_AUTH_NONE, HTTPTEST_AUTH_BASIC, HTTPTEST_AUTH_NTLM, HTTPTEST_AUTH_KERBEROS])],
384			'http_user' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_user')],
385			'http_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_password')],
386			'verify_peer' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_PEER_OFF, HTTPTEST_VERIFY_PEER_ON])],
387			'verify_host' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_HOST_OFF, HTTPTEST_VERIFY_HOST_ON])],
388			'ssl_cert_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_cert_file')],
389			'ssl_key_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_file')],
390			'ssl_key_password' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_password')],
391			'steps' =>				['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['httpstepid'], ['name'], ['no']], 'fields' => [
392				'httpstepid' =>			['type' => API_ID],
393				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'name')],
394				'no' =>					['type' => API_INT32],
395				'url' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'url')],
396				'query_fields' =>		['type' => API_OBJECTS, 'fields' => [
397					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
398					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
399				]],
400				'posts' =>				['type' => API_HTTP_POST, 'length' => DB::getFieldLength('httpstep', 'posts'), 'name-length' => DB::getFieldLength('httpstep_field', 'name'), 'value-length' => DB::getFieldLength('httpstep_field', 'value')],
401				'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
402					'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'name')],
403					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
404				]],
405				'headers' =>			['type' => API_OBJECTS, 'fields' => [
406					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
407					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'value')]
408				]],
409				'follow_redirects' =>	['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_FOLLOW_REDIRECTS_OFF, HTTPTEST_STEP_FOLLOW_REDIRECTS_ON])],
410				'retrieve_mode' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_RETRIEVE_MODE_CONTENT, HTTPTEST_STEP_RETRIEVE_MODE_HEADERS, HTTPTEST_STEP_RETRIEVE_MODE_BOTH])],
411				'timeout' =>			['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_HOUR],
412				'required' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'required')],
413				'status_codes' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'status_codes')]
414			]]
415		]];
416		if (!CApiInputValidator::validate($api_input_rules, $httptests, '/', $error)) {
417			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
418		}
419
420		// permissions
421		$db_httptests = $this->get([
422			'output' => ['httptestid', 'hostid', 'name', 'applicationid', 'delay', 'retries', 'agent', 'http_proxy',
423				'status', 'authentication', 'http_user', 'http_password', 'verify_peer', 'verify_host',
424				'ssl_cert_file', 'ssl_key_file', 'ssl_key_password', 'templateid'
425			],
426			'selectSteps' => ['httpstepid', 'name', 'no', 'url', 'timeout', 'posts', 'required',
427				'status_codes', 'follow_redirects', 'retrieve_mode', 'post_type'
428			],
429			'httptestids' => zbx_objectValues($httptests, 'httptestid'),
430			'editable' => true,
431			'preservekeys' => true
432		]);
433
434		foreach ($db_httptests as &$db_httptest) {
435			$db_httptest['headers'] = [];
436			$db_httptest['variables'] = [];
437			$db_httptest['steps'] = zbx_toHash($db_httptest['steps'], 'httpstepid');
438		}
439		unset($db_httptest);
440
441		$names_by_hostid = [];
442
443		foreach ($httptests as $httptest) {
444			if (!array_key_exists($httptest['httptestid'], $db_httptests)) {
445				self::exception(ZBX_API_ERROR_PERMISSIONS,
446					_('No permissions to referred object or it does not exist!')
447				);
448			}
449
450			$db_httptest = $db_httptests[$httptest['httptestid']];
451
452			if (array_key_exists('name', $httptest)) {
453				if ($db_httptest['templateid'] != 0) {
454					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
455						'Cannot update a templated web scenario "%1$s": %2$s.', $httptest['name'],
456						_s('unexpected parameter "%1$s"', 'name')
457					));
458				}
459
460				if ($httptest['name'] !== $db_httptest['name']) {
461					$names_by_hostid[$db_httptest['hostid']][] = $httptest['name'];
462				}
463			}
464		}
465
466		$httptests = $this->extendObjectsByKey($httptests, $db_httptests, 'httptestid', ['hostid', 'name']);
467
468		// uniqueness
469		foreach ($httptests as &$httptest) {
470			$db_httptest = $db_httptests[$httptest['httptestid']];
471
472			if (array_key_exists('steps', $httptest)) {
473				// unexpected patameters for templated web scenario steps
474				if ($db_httptest['templateid'] != 0) {
475					foreach ($httptest['steps'] as $httpstep) {
476						foreach (['name', 'no'] as $field_name) {
477							if (array_key_exists($field_name, $httpstep)) {
478								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
479									'Cannot update step for a templated web scenario "%1$s": %2$s.', $httptest['name'],
480									_s('unexpected parameter "%1$s"', $field_name)
481								));
482							}
483						}
484					}
485				}
486
487				$httptest['steps'] =
488					$this->extendObjectsByKey($httptest['steps'], $db_httptest['steps'], 'httpstepid', ['name']);
489			}
490		}
491		unset($httptest);
492
493		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['hostid', 'name']], 'fields' => [
494			'steps' =>	['type' => API_OBJECTS, 'uniq' => [['name']]]
495		]];
496		if (!CApiInputValidator::validateUniqueness($api_input_rules, $httptests, '/', $error)) {
497			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
498		}
499
500		// validation
501		if ($names_by_hostid) {
502			$this->checkDuplicates($names_by_hostid);
503		}
504		$this->checkApplications($httptests, __FUNCTION__, $db_httptests);
505		$this->validateAuthParameters($httptests, __FUNCTION__, $db_httptests);
506		$this->validateSslParameters($httptests, __FUNCTION__, $db_httptests);
507		$this->validateSteps($httptests, __FUNCTION__, $db_httptests);
508
509		return $httptests;
510	}
511
512	/**
513	 * Delete web scenario.
514	 *
515	 * @param array $httptestids
516	 * @param bool  $nopermissions
517	 *
518	 * @return array
519	 */
520	public function delete(array $httptestids, $nopermissions = false) {
521		// TODO: remove $nopermissions hack
522
523		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
524		if (!CApiInputValidator::validate($api_input_rules, $httptestids, '/', $error)) {
525			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
526		}
527
528		$db_httptests = $this->get([
529			'output' => ['httptestid', 'name', 'templateid'],
530			'httptestids' => $httptestids,
531			'editable' => true,
532			'preservekeys' => true
533		]);
534
535		if (!$nopermissions) {
536			foreach ($httptestids as $httptestid) {
537				if (!array_key_exists($httptestid, $db_httptests)) {
538					self::exception(ZBX_API_ERROR_PERMISSIONS,
539						_('No permissions to referred object or it does not exist!')
540					);
541				}
542
543				$db_httptest = $db_httptests[$httptestid];
544
545				if ($db_httptest['templateid'] != 0) {
546					self::exception(ZBX_API_ERROR_PARAMETERS,
547						_s('Cannot delete templated web scenario "%1$s".', $db_httptest['name'])
548					);
549				}
550			}
551		}
552
553		$parent_httptestids = $httptestids;
554		$child_httptestids = [];
555		do {
556			$parent_httptestids = array_keys(DB::select('httptest', [
557				'output' => [],
558				'filter' => ['templateid' => $parent_httptestids],
559				'preservekeys' => true
560			]));
561
562			$child_httptestids = array_merge($child_httptestids, $parent_httptestids);
563		}
564		while ($parent_httptestids);
565
566		$del_httptestids = array_merge($httptestids, $child_httptestids);
567		$del_itemids = [];
568
569		$db_httptestitems = DBselect(
570			'SELECT hti.itemid'.
571			' FROM httptestitem hti'.
572			' WHERE '.dbConditionInt('hti.httptestid', $del_httptestids)
573		);
574		while ($db_httptestitem = DBfetch($db_httptestitems)) {
575			$del_itemids[] = $db_httptestitem['itemid'];
576		}
577
578		$db_httpstepitems = DBselect(
579			'SELECT hsi.itemid'.
580			' FROM httpstepitem hsi,httpstep hs'.
581			' WHERE hsi.httpstepid=hs.httpstepid'.
582				' AND '.dbConditionInt('hs.httptestid', $del_httptestids)
583		);
584		while ($db_httpstepitem = DBfetch($db_httpstepitems)) {
585			$del_itemids[] = $db_httpstepitem['itemid'];
586		}
587
588		if ($del_itemids) {
589			CItemManager::delete($del_itemids);
590		}
591
592		DB::delete('httptest', ['httptestid' => $del_httptestids]);
593
594		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_SCENARIO, $db_httptests);
595
596		return ['httptestids' => $httptestids];
597	}
598
599	/**
600	 * Checks if the current user has access to the given hosts and templates.
601	 *
602	 * @param array $hostids  an array of host or template IDs
603	 *
604	 * @throws APIException if the user doesn't have write permissions for the given hosts.
605	 */
606	private function checkHostPermissions(array $hostids) {
607		if ($hostids) {
608			$count = API::Host()->get([
609				'countOutput' => true,
610				'hostids' => $hostids,
611				'editable' => true
612			]);
613
614			if ($count == count($hostids)) {
615				return;
616			}
617
618			$count += API::Template()->get([
619				'countOutput' => true,
620				'templateids' => $hostids,
621				'editable' => true
622			]);
623
624			if ($count != count($hostids)) {
625				self::exception(ZBX_API_ERROR_PERMISSIONS,
626					_('No permissions to referred object or it does not exist!')
627				);
628			}
629		}
630	}
631
632	/**
633	 * Check for duplicated web scenarios.
634	 *
635	 * @param array $names_by_hostid
636	 *
637	 * @throws APIException  if web scenario already exists.
638	 */
639	private function checkDuplicates(array $names_by_hostid) {
640		$sql_where = [];
641		foreach ($names_by_hostid as $hostid => $names) {
642			$sql_where[] = '(ht.hostid='.$hostid.' AND '.dbConditionString('ht.name', $names).')';
643		}
644
645		$db_httptests = DBfetchArray(
646			DBselect('SELECT ht.name FROM httptest ht WHERE '.implode(' OR ', $sql_where), 1)
647		);
648
649		if ($db_httptests) {
650			self::exception(ZBX_API_ERROR_PARAMETERS,
651				_s('Web scenario "%1$s" already exists.', $db_httptests[0]['name'])
652			);
653		}
654	}
655
656	/**
657	 * Check that application belongs to web scenario host.
658	 *
659	 * @param array  $httptests
660	 * @param string $method
661	 * @param array  $db_httptests
662	 *
663	 * @throws APIException  if application does not exists or belongs to another host.
664	 */
665	private function checkApplications(array $httptests, $method, array $db_httptests = null) {
666		$applicationids = [];
667
668		foreach ($httptests as $index => $httptest) {
669			if (array_key_exists('applicationid', $httptest) && $httptest['applicationid'] != 0
670					&& ($method === 'validateCreate'
671						|| $httptest['applicationid'] != $db_httptests[$httptest['httptestid']]['applicationid'])) {
672				$applicationids[$httptest['applicationid']] = true;
673			}
674		}
675
676		if (!$applicationids) {
677			return;
678		}
679
680		$db_applications = DB::select('applications', [
681			'output' => ['applicationid', 'hostid', 'name', 'flags'],
682			'applicationids' => array_keys($applicationids),
683			'preservekeys' => true
684		]);
685
686		foreach ($httptests as $index => $httptest) {
687			if (array_key_exists('applicationid', $httptest) && $httptest['applicationid'] != 0
688					&& ($method === 'validateCreate'
689						|| $httptest['applicationid'] != $db_httptests[$httptest['httptestid']]['applicationid'])) {
690				if (!array_key_exists($httptest['applicationid'], $db_applications)) {
691					self::exception(ZBX_API_ERROR_PARAMETERS,
692						_s('Application with applicationid "%1$s" does not exist.', $httptest['applicationid'])
693					);
694				}
695
696				$db_application = $db_applications[$httptest['applicationid']];
697
698				if ($db_application['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
699					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
700						'Cannot add a discovered application "%1$s" to a web scenario.', $db_application['name']
701					));
702				}
703
704				$hostid = ($method === 'validateCreate')
705					? $httptest['hostid']
706					: $db_httptests[$httptest['httptestid']]['hostid'];
707
708				if (bccomp($db_application['hostid'], $hostid) != 0) {
709					self::exception(ZBX_API_ERROR_PARAMETERS,
710						_('The web scenario application belongs to a different host than the web scenario host.')
711					);
712				}
713			}
714		}
715	}
716
717	/**
718	 * @param array  $httptests
719	 * @param string $method
720	 * @param array  $db_httptests
721	 *
722	 * @throws APIException
723	 */
724	protected function validateSteps(array &$httptests, $method, array $db_httptests = null) {
725		if ($method === 'validateUpdate') {
726			foreach ($httptests as $httptest) {
727				if (!array_key_exists('steps', $httptest)) {
728					continue;
729				}
730
731				$db_httptest = $db_httptests[$httptest['httptestid']];
732
733				if ($db_httptest['templateid'] != 0) {
734					if (count($httptest['steps']) != count($db_httptest['steps'])) {
735						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect templated web scenario step count.'));
736					}
737
738					foreach ($httptest['steps'] as $httpstep) {
739						if (!array_key_exists('httpstepid', $httpstep)) {
740							self::exception(ZBX_API_ERROR_PARAMETERS, _s(
741								'Cannot update step for a templated web scenario "%1$s": %2$s.', $httptest['name'],
742								_s('the parameter "%1$s" is missing', 'httpstepid')
743							));
744						}
745						elseif (!array_key_exists($httpstep['httpstepid'], $db_httptest['steps'])) {
746							self::exception(ZBX_API_ERROR_PARAMETERS,
747								_('No permissions to referred object or it does not exist!')
748							);
749						}
750					}
751				}
752			}
753		}
754
755		$this->checkStatusCodes($httptests);
756		$this->validateRetrieveMode($httptests, $method, $db_httptests);
757	}
758
759	/**
760	 * Validate http response code range.
761	 * Range can be empty string or list of comma separated numeric strings or user macros.
762	 *
763	 * Examples: '100-199, 301, 404, 500-550, {$MACRO}-200, {$MACRO}-{$MACRO}'
764	 *
765	 * @param array $httptests
766	 *
767	 * @throws APIException if the status code range is invalid.
768	 */
769	private function checkStatusCodes(array $httptests) {
770		$ranges_parser = new CRangesParser(['usermacros' => true]);
771
772		foreach ($httptests as $httptest) {
773			if (!array_key_exists('steps', $httptest)) {
774				continue;
775			}
776
777			foreach ($httptest['steps'] as $httpstep) {
778				if (!array_key_exists('status_codes', $httpstep) || $httpstep['status_codes'] === '') {
779					continue;
780				}
781
782				if ($ranges_parser->parse($httpstep['status_codes']) != CParser::PARSE_SUCCESS) {
783					self::exception(ZBX_API_ERROR_PARAMETERS,
784						_s('Invalid response code "%1$s".', $httpstep['status_codes'])
785					);
786				}
787			}
788		}
789	}
790
791	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
792		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
793
794		if (!$options['countOutput']) {
795			// make sure we request the hostid to be able to expand macros
796			if ($options['expandName'] !== null || $options['expandStepName'] !== null || $options['selectHosts'] !== null) {
797				$sqlParts = $this->addQuerySelect($this->fieldId('hostid'), $sqlParts);
798			}
799		}
800
801		return $sqlParts;
802	}
803
804	protected function addRelatedObjects(array $options, array $result) {
805		$result = parent::addRelatedObjects($options, $result);
806
807		$httpTestIds = array_keys($result);
808
809		// adding headers and variables
810		$fields = [
811			ZBX_HTTPFIELD_HEADER => 'headers',
812			ZBX_HTTPFIELD_VARIABLE => 'variables'
813		];
814		foreach ($fields as $type => $field) {
815			if (!$this->outputIsRequested($field, $options['output'])) {
816				unset($fields[$type]);
817			}
818		}
819
820		if ($fields) {
821			$db_httpfields = DB::select('httptest_field', [
822				'output' => ['httptestid', 'name', 'value', 'type'],
823				'filter' => [
824					'httptestid' => $httpTestIds,
825					'type' => array_keys($fields)
826				],
827				'sortfield' => ['httptest_fieldid']
828			]);
829
830			foreach ($result as &$httptest) {
831				foreach ($fields as $field) {
832					$httptest[$field] = [];
833				}
834			}
835			unset($httptest);
836
837			foreach ($db_httpfields as $db_httpfield) {
838				$result[$db_httpfield['httptestid']][$fields[$db_httpfield['type']]][] = [
839					'name' => $db_httpfield['name'],
840					'value' => $db_httpfield['value']
841				];
842			}
843		}
844
845		// adding hosts
846		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
847			$relationMap = $this->createRelationMap($result, 'httptestid', 'hostid');
848			$hosts = API::Host()->get([
849				'output' => $options['selectHosts'],
850				'hostid' => $relationMap->getRelatedIds(),
851				'nopermissions' => true,
852				'templated_hosts' => true,
853				'preservekeys' => true
854			]);
855			$result = $relationMap->mapMany($result, $hosts, 'hosts');
856		}
857
858		// adding steps
859		if ($options['selectSteps'] !== null) {
860			if ($options['selectSteps'] != API_OUTPUT_COUNT) {
861				$fields = [
862					ZBX_HTTPFIELD_HEADER => 'headers',
863					ZBX_HTTPFIELD_VARIABLE => 'variables',
864					ZBX_HTTPFIELD_QUERY_FIELD => 'query_fields',
865					ZBX_HTTPFIELD_POST_FIELD => 'posts'
866				];
867				foreach ($fields as $type => $field) {
868					if (!$this->outputIsRequested($field, $options['selectSteps'])) {
869						unset($fields[$type]);
870					}
871				}
872
873				$db_httpsteps = API::getApiService()->select('httpstep', [
874					'output' => $this->outputExtend($options['selectSteps'], ['httptestid', 'httpstepid', 'post_type']),
875					'filter' => ['httptestid' => $httpTestIds],
876					'preservekeys' => true
877				]);
878				$relationMap = $this->createRelationMap($db_httpsteps, 'httptestid', 'httpstepid');
879
880				if ($fields) {
881					foreach ($db_httpsteps as &$db_httpstep) {
882						foreach ($fields as $type => $field) {
883							if ($type != ZBX_HTTPFIELD_POST_FIELD || $db_httpstep['post_type'] == ZBX_POSTTYPE_FORM) {
884								$db_httpstep[$field] = [];
885							}
886						}
887					}
888					unset($db_httpstep);
889
890					$db_httpstep_fields = DB::select('httpstep_field', [
891						'output' => ['httpstepid', 'name', 'value', 'type'],
892						'filter' => [
893							'httpstepid' => array_keys($db_httpsteps),
894							'type' => array_keys($fields)
895						],
896						'sortfield' => ['httpstep_fieldid']
897					]);
898
899					foreach ($db_httpstep_fields as $db_httpstep_field) {
900						$db_httpstep = &$db_httpsteps[$db_httpstep_field['httpstepid']];
901
902						if ($db_httpstep_field['type'] != ZBX_HTTPFIELD_POST_FIELD
903								|| $db_httpstep['post_type'] == ZBX_POSTTYPE_FORM) {
904							$db_httpstep[$fields[$db_httpstep_field['type']]][] = [
905								'name' => $db_httpstep_field['name'],
906								'value' => $db_httpstep_field['value']
907							];
908						}
909					}
910					unset($db_httpstep);
911				}
912
913				$db_httpsteps = $this->unsetExtraFields($db_httpsteps, ['httptestid', 'httpstepid', 'post_type'],
914					$options['selectSteps']
915				);
916				$result = $relationMap->mapMany($result, $db_httpsteps, 'steps');
917			}
918			else {
919				$dbHttpSteps = DBselect(
920					'SELECT hs.httptestid,COUNT(hs.httpstepid) AS stepscnt'.
921						' FROM httpstep hs'.
922						' WHERE '.dbConditionInt('hs.httptestid', $httpTestIds).
923						' GROUP BY hs.httptestid'
924				);
925				while ($dbHttpStep = DBfetch($dbHttpSteps)) {
926					$result[$dbHttpStep['httptestid']]['steps'] = $dbHttpStep['stepscnt'];
927				}
928			}
929		}
930
931		return $result;
932	}
933
934	/**
935	 * @param array  $httptests
936	 * @param string $method
937	 * @param array  $db_httptests
938	 *
939	 * @throws APIException  if auth parameters are invalid.
940	 */
941	private function validateAuthParameters(array &$httptests, $method, array $db_httptests = null) {
942		foreach ($httptests as &$httptest) {
943			if (array_key_exists('authentication', $httptest) || array_key_exists('http_user', $httptest)
944					|| array_key_exists('http_password', $httptest)) {
945				$httptest += [
946					'authentication' => ($method === 'validateUpdate')
947						? $db_httptests[$httptest['httptestid']]['authentication']
948						: HTTPTEST_AUTH_NONE
949				];
950
951				if ($httptest['authentication'] == HTTPTEST_AUTH_NONE) {
952					foreach (['http_user', 'http_password'] as $field_name) {
953						$httptest += [$field_name => ''];
954
955						if ($httptest[$field_name] !== '') {
956							self::exception(ZBX_API_ERROR_PARAMETERS,
957								_s('Incorrect value for field "%1$s": %2$s.', $field_name, _('should be empty'))
958							);
959						}
960					}
961				}
962			}
963		}
964		unset($httptest);
965	}
966
967	/**
968	 * @param array  $httptests
969	 * @param string $method
970	 * @param array  $db_httptests
971	 *
972	 * @throws APIException if SSL cert is present but SSL key is not.
973	 */
974	private function validateSslParameters(array &$httptests, $method, array $db_httptests = null) {
975		foreach ($httptests as &$httptest) {
976			if (array_key_exists('ssl_key_password', $httptest)
977					|| array_key_exists('ssl_key_file', $httptest)
978					|| array_key_exists('ssl_cert_file', $httptest)) {
979				if ($method === 'validateCreate') {
980					$httptest += [
981						'ssl_key_password' => '',
982						'ssl_key_file' => '',
983						'ssl_cert_file' => ''
984					];
985				}
986				else {
987					$db_httptest = $db_httptests[$httptest['httptestid']];
988					$httptest += [
989						'ssl_key_password' => $db_httptest['ssl_key_password'],
990						'ssl_key_file' => $db_httptest['ssl_key_file'],
991						'ssl_cert_file' => $db_httptest['ssl_cert_file']
992					];
993				}
994
995				if ($httptest['ssl_key_password'] != '' && $httptest['ssl_key_file'] == '') {
996					self::exception(ZBX_API_ERROR_PARAMETERS,
997						_s('Empty SSL key file for web scenario "%1$s".', $httptest['name'])
998					);
999				}
1000
1001				if ($httptest['ssl_key_file'] != '' && $httptest['ssl_cert_file'] == '') {
1002					self::exception(ZBX_API_ERROR_PARAMETERS,
1003						_s('Empty SSL certificate file for web scenario "%1$s".', $httptest['name'])
1004					);
1005				}
1006			}
1007		}
1008		unset($httptest);
1009	}
1010
1011	/**
1012	 * @param array  $httptests
1013	 * @param string $method
1014	 * @param array  $db_httptests
1015	 *
1016	 * @throws APIException if parameters is invalid.
1017	 */
1018	private function validateRetrieveMode(array &$httptests, $method, array $db_httptests = null) {
1019		foreach ($httptests as &$httptest) {
1020			if (!array_key_exists('steps', $httptest)) {
1021				continue;
1022			}
1023
1024			foreach ($httptest['steps'] as &$httpstep) {
1025				if (array_key_exists('retrieve_mode', $httpstep)
1026						|| array_key_exists('posts', $httpstep)
1027						|| array_key_exists('required', $httpstep)) {
1028
1029					if ($method === 'validateCreate' || !array_key_exists('httpstepid', $httpstep)) {
1030						$httpstep += [
1031							'retrieve_mode' => HTTPTEST_STEP_RETRIEVE_MODE_CONTENT,
1032							'posts' => '',
1033							'required' => ''
1034						];
1035					}
1036					else {
1037						$db_httptest = $db_httptests[$httptest['httptestid']];
1038						$db_httpstep = $db_httptest['steps'][$httpstep['httpstepid']];
1039						$httpstep += [
1040							'retrieve_mode' => $db_httpstep['retrieve_mode'],
1041							'required' => $db_httpstep['required'],
1042							'posts' => ($db_httpstep['retrieve_mode'] != HTTPTEST_STEP_RETRIEVE_MODE_HEADERS)
1043								? $db_httpstep['posts']
1044								: ''
1045						];
1046					}
1047
1048					if ($httpstep['retrieve_mode'] == HTTPTEST_STEP_RETRIEVE_MODE_HEADERS) {
1049						if ($httpstep['posts'] !== '' && $httpstep['posts'] !== []) {
1050							$field_name = $httpstep['required'] !== '' ? 'required' : 'posts';
1051
1052							self::exception(ZBX_API_ERROR_PARAMETERS,
1053								_s('Incorrect value for field "%1$s": %2$s.', 'posts', _('should be empty'))
1054							);
1055						}
1056					}
1057				}
1058			}
1059			unset($httpstep);
1060		}
1061		unset($httptest);
1062	}
1063}
1064