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		$httptests = $this->convertHttpPairs($httptests);
258		$this->validateCreate($httptests);
259
260		$httptests = Manager::HttpTest()->persist($httptests);
261
262		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_SCENARIO, $httptests);
263
264		return ['httptestids' => zbx_objectValues($httptests, 'httptestid')];
265	}
266
267	/**
268	 * @param array $httpTests
269	 *
270	 * @throws APIException if the input is invalid.
271	 */
272	protected function validateCreate(array &$httptests) {
273		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['hostid', 'name']], 'fields' => [
274			'hostid' =>				['type' => API_ID, 'flags' => API_REQUIRED],
275			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest', 'name')],
276			'applicationid' =>		['type' => API_ID],
277			'delay' =>				['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_DAY],
278			'retries' =>			['type' => API_INT32, 'in' => '1:10'],
279			'agent' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'agent')],
280			'http_proxy' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_proxy')],
281			'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
282				'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'name')],
283				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'value')]
284			]],
285			'headers' =>			['type' => API_OBJECTS, 'fields' => [
286				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'name')],
287				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'value')]
288			]],
289			'status' =>				['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STATUS_ACTIVE, HTTPTEST_STATUS_DISABLED])],
290			'authentication' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_AUTH_NONE, HTTPTEST_AUTH_BASIC, HTTPTEST_AUTH_NTLM])],
291			'http_user' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_user')],
292			'http_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_password')],
293			'verify_peer' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_PEER_OFF, HTTPTEST_VERIFY_PEER_ON])],
294			'verify_host' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_HOST_OFF, HTTPTEST_VERIFY_HOST_ON])],
295			'ssl_cert_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_cert_file')],
296			'ssl_key_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_file')],
297			'ssl_key_password' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_password')],
298			'steps' =>				['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'uniq' => [['name'], ['no']], 'fields' => [
299				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'name')],
300				'no' =>					['type' => API_INT32, 'flags' => API_REQUIRED],
301				'url' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'url')],
302				'query_fields' =>		['type' => API_OBJECTS, 'fields' => [
303					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
304					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
305				]],
306				'posts' =>				['type' => API_HTTP_POST, 'length' => DB::getFieldLength('httpstep', 'posts'), 'name-length' => DB::getFieldLength('httpstep_field', 'name'), 'value-length' => DB::getFieldLength('httpstep_field', 'value')],
307				'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
308					'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'name')],
309					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
310				]],
311				'headers' =>			['type' => API_OBJECTS, 'fields' => [
312					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
313					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'value')]
314				]],
315				'follow_redirects' =>	['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_FOLLOW_REDIRECTS_OFF, HTTPTEST_STEP_FOLLOW_REDIRECTS_ON])],
316				'retrieve_mode' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_RETRIEVE_MODE_CONTENT, HTTPTEST_STEP_RETRIEVE_MODE_HEADERS])],
317				'timeout' =>			['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_HOUR],
318				'required' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'required')],
319				'status_codes' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'status_codes')]
320			]]
321		]];
322		if (!CApiInputValidator::validate($api_input_rules, $httptests, '/', $error)) {
323			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
324		}
325
326		$names_by_hostid = [];
327
328		foreach ($httptests as $httptest) {
329			$names_by_hostid[$httptest['hostid']][] = $httptest['name'];
330		}
331
332		$this->checkHostPermissions(array_keys($names_by_hostid));
333		$this->checkDuplicates($names_by_hostid);
334		$this->checkApplications($httptests, __FUNCTION__);
335		$this->validateAuthParameters($httptests, __FUNCTION__);
336		$this->validateSslParameters($httptests, __FUNCTION__);
337		$this->validateSteps($httptests, __FUNCTION__);
338	}
339
340	/**
341	 * @param $httptests
342	 *
343	 * @return array
344	 */
345	public function update($httptests) {
346		$httptests = $this->convertHttpPairs($httptests);
347		$this->validateUpdate($httptests, $db_httptests);
348
349		Manager::HttpTest()->persist($httptests);
350
351		foreach ($db_httptests as &$db_httptest) {
352			unset($db_httptest['headers'], $db_httptest['variables'], $db_httptest['steps']);
353		}
354		unset($db_httptest);
355
356		$this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_SCENARIO, $httptests, $db_httptests);
357
358		return ['httptestids' => zbx_objectValues($httptests, 'httptestid')];
359	}
360
361	/**
362	 * @param array $httptests
363	 * @param array $db_httptests
364	 *
365	 * @throws APIException if the input is invalid.
366	 */
367	protected function validateUpdate(array &$httptests, array &$db_httptests = null) {
368		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['httptestid']], 'fields' => [
369			'httptestid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
370			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest', 'name')],
371			'applicationid' =>		['type' => API_ID],
372			'delay' =>				['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_DAY],
373			'retries' =>			['type' => API_INT32, 'in' => '1:10'],
374			'agent' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'agent')],
375			'http_proxy' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_proxy')],
376			'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
377				'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'name')],
378				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httptest_field', 'value')]
379			]],
380			'headers' =>			['type' => API_OBJECTS, 'fields' => [
381				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'name')],
382				'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httptest_field', 'value')]
383			]],
384			'status' =>				['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STATUS_ACTIVE, HTTPTEST_STATUS_DISABLED])],
385			'authentication' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_AUTH_NONE, HTTPTEST_AUTH_BASIC, HTTPTEST_AUTH_NTLM])],
386			'http_user' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_user')],
387			'http_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'http_password')],
388			'verify_peer' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_PEER_OFF, HTTPTEST_VERIFY_PEER_ON])],
389			'verify_host' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_VERIFY_HOST_OFF, HTTPTEST_VERIFY_HOST_ON])],
390			'ssl_cert_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_cert_file')],
391			'ssl_key_file' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_file')],
392			'ssl_key_password' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httptest', 'ssl_key_password')],
393			'steps' =>				['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['httpstepid'], ['name'], ['no']], 'fields' => [
394				'httpstepid' =>			['type' => API_ID],
395				'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'name')],
396				'no' =>					['type' => API_INT32],
397				'url' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep', 'url')],
398				'query_fields' =>		['type' => API_OBJECTS, 'fields' => [
399					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
400					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
401				]],
402				'posts' =>				['type' => API_HTTP_POST, 'length' => DB::getFieldLength('httpstep', 'posts'), 'name-length' => DB::getFieldLength('httpstep_field', 'name'), 'value-length' => DB::getFieldLength('httpstep_field', 'value')],
403				'variables' =>			['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [
404					'name' =>				['type' => API_VARIABLE_NAME, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'name')],
405					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('httpstep_field', 'value')]
406				]],
407				'headers' =>			['type' => API_OBJECTS, 'fields' => [
408					'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'name')],
409					'value' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('httpstep_field', 'value')]
410				]],
411				'follow_redirects' =>	['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_FOLLOW_REDIRECTS_OFF, HTTPTEST_STEP_FOLLOW_REDIRECTS_ON])],
412				'retrieve_mode' =>		['type' => API_INT32, 'in' => implode(',', [HTTPTEST_STEP_RETRIEVE_MODE_CONTENT, HTTPTEST_STEP_RETRIEVE_MODE_HEADERS])],
413				'timeout' =>			['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO, 'in' => '1:'.SEC_PER_HOUR],
414				'required' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'required')],
415				'status_codes' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('httpstep', 'status_codes')]
416			]]
417		]];
418		if (!CApiInputValidator::validate($api_input_rules, $httptests, '/', $error)) {
419			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
420		}
421
422		// permissions
423		$db_httptests = $this->get([
424			'output' => ['httptestid', 'hostid', 'name', 'applicationid', 'delay', 'retries', 'agent', 'http_proxy',
425				'status', 'authentication', 'http_user', 'http_password', 'verify_peer', 'verify_host',
426				'ssl_cert_file', 'ssl_key_file', 'ssl_key_password', 'templateid'
427			],
428			'selectSteps' => ['httpstepid', 'name', 'no', 'url', 'timeout', 'posts', 'required',
429				'status_codes', 'follow_redirects', 'retrieve_mode', 'post_type'
430			],
431			'httptestids' => zbx_objectValues($httptests, 'httptestid'),
432			'editable' => true,
433			'preservekeys' => true
434		]);
435
436		foreach ($db_httptests as &$db_httptest) {
437			$db_httptest['headers'] = [];
438			$db_httptest['variables'] = [];
439			$db_httptest['steps'] = zbx_toHash($db_httptest['steps'], 'httpstepid');
440		}
441		unset($db_httptest);
442
443		$names_by_hostid = [];
444
445		foreach ($httptests as $httptest) {
446			if (!array_key_exists($httptest['httptestid'], $db_httptests)) {
447				self::exception(ZBX_API_ERROR_PERMISSIONS,
448					_('No permissions to referred object or it does not exist!')
449				);
450			}
451
452			$db_httptest = $db_httptests[$httptest['httptestid']];
453
454			if (array_key_exists('name', $httptest)) {
455				if ($db_httptest['templateid'] != 0) {
456					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
457						'Cannot update a templated web scenario "%1$s": %2$s.', $httptest['name'],
458						_s('unexpected parameter "%1$s"', 'name')
459					));
460				}
461
462				if ($httptest['name'] !== $db_httptest['name']) {
463					$names_by_hostid[$db_httptest['hostid']][] = $httptest['name'];
464				}
465			}
466		}
467
468		$httptests = $this->extendObjectsByKey($httptests, $db_httptests, 'httptestid', ['hostid', 'name']);
469
470		// uniqueness
471		foreach ($httptests as &$httptest) {
472			$db_httptest = $db_httptests[$httptest['httptestid']];
473
474			if (array_key_exists('steps', $httptest)) {
475				// unexpected patameters for templated web scenario steps
476				if ($db_httptest['templateid'] != 0) {
477					foreach ($httptest['steps'] as $httpstep) {
478						foreach (['name', 'no'] as $field_name) {
479							if (array_key_exists($field_name, $httpstep)) {
480								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
481									'Cannot update step for a templated web scenario "%1$s": %2$s.', $httptest['name'],
482									_s('unexpected parameter "%1$s"', $field_name)
483								));
484							}
485						}
486					}
487				}
488
489				$httptest['steps'] =
490					$this->extendObjectsByKey($httptest['steps'], $db_httptest['steps'], 'httpstepid', ['name']);
491			}
492		}
493		unset($httptest);
494
495		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['hostid', 'name']], 'fields' => [
496			'steps' =>	['type' => API_OBJECTS, 'uniq' => [['name']]]
497		]];
498		if (!CApiInputValidator::validateUniqueness($api_input_rules, $httptests, '/', $error)) {
499			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
500		}
501
502		// validation
503		if ($names_by_hostid) {
504			$this->checkDuplicates($names_by_hostid);
505		}
506		$this->checkApplications($httptests, __FUNCTION__, $db_httptests);
507		$this->validateAuthParameters($httptests, __FUNCTION__, $db_httptests);
508		$this->validateSslParameters($httptests, __FUNCTION__, $db_httptests);
509		$this->validateSteps($httptests, __FUNCTION__, $db_httptests);
510
511		return $httptests;
512	}
513
514	/**
515	 * Delete web scenario.
516	 *
517	 * @param array $httptestids
518	 * @param bool  $nopermissions
519	 *
520	 * @return array
521	 */
522	public function delete(array $httptestids, $nopermissions = false) {
523		// TODO: remove $nopermissions hack
524
525		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
526		if (!CApiInputValidator::validate($api_input_rules, $httptestids, '/', $error)) {
527			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
528		}
529
530		$db_httptests = $this->get([
531			'output' => ['httptestid', 'name', 'templateid'],
532			'httptestids' => $httptestids,
533			'editable' => true,
534			'preservekeys' => true
535		]);
536
537		if (!$nopermissions) {
538			foreach ($httptestids as $httptestid) {
539				if (!array_key_exists($httptestid, $db_httptests)) {
540					self::exception(ZBX_API_ERROR_PERMISSIONS,
541						_('No permissions to referred object or it does not exist!')
542					);
543				}
544
545				$db_httptest = $db_httptests[$httptestid];
546
547				if ($db_httptest['templateid'] != 0) {
548					self::exception(ZBX_API_ERROR_PARAMETERS,
549						_s('Cannot delete templated web scenario "%1$s".', $db_httptest['name'])
550					);
551				}
552			}
553		}
554
555		$parent_httptestids = $httptestids;
556		$child_httptestids = [];
557		do {
558			$parent_httptestids = array_keys(DB::select('httptest', [
559				'output' => [],
560				'filter' => ['templateid' => $parent_httptestids],
561				'preservekeys' => true
562			]));
563
564			$child_httptestids = array_merge($child_httptestids, $parent_httptestids);
565		}
566		while ($parent_httptestids);
567
568		$del_httptestids = array_merge($httptestids, $child_httptestids);
569		$del_itemids = [];
570
571		$db_httptestitems = DBselect(
572			'SELECT hti.itemid'.
573			' FROM httptestitem hti'.
574			' WHERE '.dbConditionInt('hti.httptestid', $del_httptestids)
575		);
576		while ($db_httptestitem = DBfetch($db_httptestitems)) {
577			$del_itemids[] = $db_httptestitem['itemid'];
578		}
579
580		$db_httpstepitems = DBselect(
581			'SELECT hsi.itemid'.
582			' FROM httpstepitem hsi,httpstep hs'.
583			' WHERE hsi.httpstepid=hs.httpstepid'.
584				' AND '.dbConditionInt('hs.httptestid', $del_httptestids)
585		);
586		while ($db_httpstepitem = DBfetch($db_httpstepitems)) {
587			$del_itemids[] = $db_httpstepitem['itemid'];
588		}
589
590		if ($del_itemids) {
591			CItemManager::delete($del_itemids);
592		}
593
594		DB::delete('httptest', ['httptestid' => $del_httptestids]);
595
596		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_SCENARIO, $db_httptests);
597
598		return ['httptestids' => $httptestids];
599	}
600
601	/**
602	 * Checks if the current user has access to the given hosts and templates.
603	 *
604	 * @param array $hostids  an array of host or template IDs
605	 *
606	 * @throws APIException if the user doesn't have write permissions for the given hosts.
607	 */
608	private function checkHostPermissions(array $hostids) {
609		if ($hostids) {
610			$count = API::Host()->get([
611				'countOutput' => true,
612				'hostids' => $hostids,
613				'editable' => true
614			]);
615
616			if ($count == count($hostids)) {
617				return;
618			}
619
620			$count += API::Template()->get([
621				'countOutput' => true,
622				'templateids' => $hostids,
623				'editable' => true
624			]);
625
626			if ($count != count($hostids)) {
627				self::exception(ZBX_API_ERROR_PERMISSIONS,
628					_('No permissions to referred object or it does not exist!')
629				);
630			}
631		}
632	}
633
634	/**
635	 * Check for duplicated web scenarios.
636	 *
637	 * @param array $names_by_hostid
638	 *
639	 * @throws APIException  if web scenario already exists.
640	 */
641	private function checkDuplicates(array $names_by_hostid) {
642		$sql_where = [];
643		foreach ($names_by_hostid as $hostid => $names) {
644			$sql_where[] = '(ht.hostid='.$hostid.' AND '.dbConditionString('ht.name', $names).')';
645		}
646
647		$db_httptests = DBfetchArray(
648			DBselect('SELECT ht.name FROM httptest ht WHERE '.implode(' OR ', $sql_where), 1)
649		);
650
651		if ($db_httptests) {
652			self::exception(ZBX_API_ERROR_PARAMETERS,
653				_s('Web scenario "%1$s" already exists.', $db_httptests[0]['name'])
654			);
655		}
656	}
657
658	/**
659	 * Check that application belongs to web scenario host.
660	 *
661	 * @param array  $httptests
662	 * @param string $method
663	 * @param array  $db_httptests
664	 *
665	 * @throws APIException  if application does not exists or belongs to another host.
666	 */
667	private function checkApplications(array $httptests, $method, array $db_httptests = null) {
668		$applicationids = [];
669
670		foreach ($httptests as $index => $httptest) {
671			if (array_key_exists('applicationid', $httptest) && $httptest['applicationid'] != 0
672					&& ($method === 'validateCreate'
673						|| $httptest['applicationid'] != $db_httptests[$httptest['httptestid']]['applicationid'])) {
674				$applicationids[$httptest['applicationid']] = true;
675			}
676		}
677
678		if (!$applicationids) {
679			return;
680		}
681
682		$db_applications = DB::select('applications', [
683			'output' => ['applicationid', 'hostid', 'name', 'flags'],
684			'applicationids' => array_keys($applicationids),
685			'preservekeys' => true
686		]);
687
688		foreach ($httptests as $index => $httptest) {
689			if (array_key_exists('applicationid', $httptest) && $httptest['applicationid'] != 0
690					&& ($method === 'validateCreate'
691						|| $httptest['applicationid'] != $db_httptests[$httptest['httptestid']]['applicationid'])) {
692				if (!array_key_exists($httptest['applicationid'], $db_applications)) {
693					self::exception(ZBX_API_ERROR_PARAMETERS,
694						_s('Application with applicationid "%1$s" does not exist.', $httptest['applicationid'])
695					);
696				}
697
698				$db_application = $db_applications[$httptest['applicationid']];
699
700				if ($db_application['flags'] == ZBX_FLAG_DISCOVERY_CREATED) {
701					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
702						'Cannot add a discovered application "%1$s" to a web scenario.', $db_application['name']
703					));
704				}
705
706				$hostid = ($method === 'validateCreate')
707					? $httptest['hostid']
708					: $db_httptests[$httptest['httptestid']]['hostid'];
709
710				if (bccomp($db_application['hostid'], $hostid) != 0) {
711					self::exception(ZBX_API_ERROR_PARAMETERS,
712						_('The web scenario application belongs to a different host than the web scenario host.')
713					);
714				}
715			}
716		}
717	}
718
719	/**
720	 * @param array  $httptests
721	 * @param string $method
722	 * @param array  $db_httptests
723	 *
724	 * @throws APIException
725	 */
726	protected function validateSteps(array &$httptests, $method, array $db_httptests = null) {
727		if ($method === 'validateUpdate') {
728			foreach ($httptests as $httptest) {
729				if (!array_key_exists('steps', $httptest)) {
730					continue;
731				}
732
733				$db_httptest = $db_httptests[$httptest['httptestid']];
734
735				if ($db_httptest['templateid'] != 0) {
736					if (count($httptest['steps']) != count($db_httptest['steps'])) {
737						self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect templated web scenario step count.'));
738					}
739
740					foreach ($httptest['steps'] as $httpstep) {
741						if (!array_key_exists('httpstepid', $httpstep)) {
742							self::exception(ZBX_API_ERROR_PARAMETERS, _s(
743								'Cannot update step for a templated web scenario "%1$s": %2$s.', $httptest['name'],
744								_s('the parameter "%1$s" is missing', 'httpstepid')
745							));
746						}
747						elseif (!array_key_exists($httpstep['httpstepid'], $db_httptest['steps'])) {
748							self::exception(ZBX_API_ERROR_PARAMETERS,
749								_('No permissions to referred object or it does not exist!')
750							);
751						}
752					}
753				}
754			}
755		}
756
757		$this->checkStatusCodes($httptests);
758		$this->validateRetrieveMode($httptests, $method, $db_httptests);
759	}
760
761	/**
762	 * Validate http response code range.
763	 * Range can be empty string or list of comma separated numeric strings or user macros.
764	 *
765	 * Examples: '100-199, 301, 404, 500-550, {$MACRO}-200, {$MACRO}-{$MACRO}'
766	 *
767	 * @param array $httptests
768	 *
769	 * @throws APIException if the status code range is invalid.
770	 */
771	private function checkStatusCodes(array $httptests) {
772		$ranges_parser = new CRangesParser(['usermacros' => true]);
773
774		foreach ($httptests as $httptest) {
775			if (!array_key_exists('steps', $httptest)) {
776				continue;
777			}
778
779			foreach ($httptest['steps'] as $httpstep) {
780				if (!array_key_exists('status_codes', $httpstep) || $httpstep['status_codes'] === '') {
781					continue;
782				}
783
784				if ($ranges_parser->parse($httpstep['status_codes']) != CParser::PARSE_SUCCESS) {
785					self::exception(ZBX_API_ERROR_PARAMETERS,
786						_s('Invalid response code "%1$s".', $httpstep['status_codes'])
787					);
788				}
789			}
790		}
791	}
792
793	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
794		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
795
796		if (!$options['countOutput']) {
797			// make sure we request the hostid to be able to expand macros
798			if ($options['expandName'] !== null || $options['expandStepName'] !== null || $options['selectHosts'] !== null) {
799				$sqlParts = $this->addQuerySelect($this->fieldId('hostid'), $sqlParts);
800			}
801		}
802
803		return $sqlParts;
804	}
805
806	protected function addRelatedObjects(array $options, array $result) {
807		$result = parent::addRelatedObjects($options, $result);
808
809		$httpTestIds = array_keys($result);
810
811		// adding headers and variables
812		$fields = [
813			ZBX_HTTPFIELD_HEADER => 'headers',
814			ZBX_HTTPFIELD_VARIABLE => 'variables'
815		];
816		foreach ($fields as $type => $field) {
817			if (!$this->outputIsRequested($field, $options['output'])) {
818				unset($fields[$type]);
819			}
820		}
821
822		if ($fields) {
823			$db_httpfields = DB::select('httptest_field', [
824				'output' => ['httptestid', 'name', 'value', 'type'],
825				'filter' => [
826					'httptestid' => $httpTestIds,
827					'type' => array_keys($fields)
828				],
829				'sortfield' => ['httptest_fieldid']
830			]);
831
832			foreach ($result as &$httptest) {
833				foreach ($fields as $field) {
834					$httptest[$field] = [];
835				}
836			}
837			unset($httptest);
838
839			foreach ($db_httpfields as $db_httpfield) {
840				$result[$db_httpfield['httptestid']][$fields[$db_httpfield['type']]][] = [
841					'name' => $db_httpfield['name'],
842					'value' => $db_httpfield['value']
843				];
844			}
845		}
846
847		// adding hosts
848		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
849			$relationMap = $this->createRelationMap($result, 'httptestid', 'hostid');
850			$hosts = API::Host()->get([
851				'output' => $options['selectHosts'],
852				'hostid' => $relationMap->getRelatedIds(),
853				'nopermissions' => true,
854				'templated_hosts' => true,
855				'preservekeys' => true
856			]);
857			$result = $relationMap->mapMany($result, $hosts, 'hosts');
858		}
859
860		// adding steps
861		if ($options['selectSteps'] !== null) {
862			if ($options['selectSteps'] != API_OUTPUT_COUNT) {
863				$fields = [
864					ZBX_HTTPFIELD_HEADER => 'headers',
865					ZBX_HTTPFIELD_VARIABLE => 'variables',
866					ZBX_HTTPFIELD_QUERY_FIELD => 'query_fields',
867					ZBX_HTTPFIELD_POST_FIELD => 'posts'
868				];
869				foreach ($fields as $type => $field) {
870					if (!$this->outputIsRequested($field, $options['selectSteps'])) {
871						unset($fields[$type]);
872					}
873				}
874
875				$db_httpsteps = API::getApiService()->select('httpstep', [
876					'output' => $this->outputExtend($options['selectSteps'], ['httptestid', 'httpstepid', 'post_type']),
877					'filter' => ['httptestid' => $httpTestIds],
878					'preservekeys' => true
879				]);
880				$relationMap = $this->createRelationMap($db_httpsteps, 'httptestid', 'httpstepid');
881
882				if ($fields) {
883					foreach ($db_httpsteps as &$db_httpstep) {
884						foreach ($fields as $type => $field) {
885							if ($type != ZBX_HTTPFIELD_POST_FIELD || $db_httpstep['post_type'] == ZBX_POSTTYPE_FORM) {
886								$db_httpstep[$field] = [];
887							}
888						}
889					}
890					unset($db_httpstep);
891
892					$db_httpstep_fields = DB::select('httpstep_field', [
893						'output' => ['httpstepid', 'name', 'value', 'type'],
894						'filter' => [
895							'httpstepid' => array_keys($db_httpsteps),
896							'type' => array_keys($fields)
897						],
898						'sortfield' => ['httpstep_fieldid']
899					]);
900
901					foreach ($db_httpstep_fields as $db_httpstep_field) {
902						$db_httpstep = &$db_httpsteps[$db_httpstep_field['httpstepid']];
903
904						if ($db_httpstep_field['type'] != ZBX_HTTPFIELD_POST_FIELD
905								|| $db_httpstep['post_type'] == ZBX_POSTTYPE_FORM) {
906							$db_httpstep[$fields[$db_httpstep_field['type']]][] = [
907								'name' => $db_httpstep_field['name'],
908								'value' => $db_httpstep_field['value']
909							];
910						}
911					}
912					unset($db_httpstep);
913				}
914
915				$db_httpsteps = $this->unsetExtraFields($db_httpsteps, ['httptestid', 'httpstepid', 'post_type'],
916					$options['selectSteps']
917				);
918				$result = $relationMap->mapMany($result, $db_httpsteps, 'steps');
919			}
920			else {
921				$dbHttpSteps = DBselect(
922					'SELECT hs.httptestid,COUNT(hs.httpstepid) AS stepscnt'.
923						' FROM httpstep hs'.
924						' WHERE '.dbConditionInt('hs.httptestid', $httpTestIds).
925						' GROUP BY hs.httptestid'
926				);
927				while ($dbHttpStep = DBfetch($dbHttpSteps)) {
928					$result[$dbHttpStep['httptestid']]['steps'] = $dbHttpStep['stepscnt'];
929				}
930			}
931		}
932
933		return $result;
934	}
935
936	/**
937	 * @param array  $httptests
938	 * @param string $method
939	 * @param array  $db_httptests
940	 *
941	 * @throws APIException  if auth parameters are invalid.
942	 */
943	private function validateAuthParameters(array &$httptests, $method, array $db_httptests = null) {
944		foreach ($httptests as &$httptest) {
945			if (array_key_exists('authentication', $httptest) || array_key_exists('http_user', $httptest)
946					|| array_key_exists('http_password', $httptest)) {
947				$httptest += [
948					'authentication' => ($method === 'validateUpdate')
949						? $db_httptests[$httptest['httptestid']]['authentication']
950						: HTTPTEST_AUTH_NONE
951				];
952
953				if ($httptest['authentication'] == HTTPTEST_AUTH_NONE) {
954					foreach (['http_user', 'http_password'] as $field_name) {
955						$httptest += [$field_name => ''];
956
957						if ($httptest[$field_name] !== '') {
958							self::exception(ZBX_API_ERROR_PARAMETERS,
959								_s('Incorrect value for field "%1$s": %2$s.', $field_name, _('should be empty'))
960							);
961						}
962					}
963				}
964			}
965		}
966		unset($httptest);
967	}
968
969	/**
970	 * @param array  $httptests
971	 * @param string $method
972	 * @param array  $db_httptests
973	 *
974	 * @throws APIException if SSL cert is present but SSL key is not.
975	 */
976	private function validateSslParameters(array &$httptests, $method, array $db_httptests = null) {
977		foreach ($httptests as &$httptest) {
978			if (array_key_exists('ssl_key_password', $httptest)
979					|| array_key_exists('ssl_key_file', $httptest)
980					|| array_key_exists('ssl_cert_file', $httptest)) {
981				if ($method === 'validateCreate') {
982					$httptest += [
983						'ssl_key_password' => '',
984						'ssl_key_file' => '',
985						'ssl_cert_file' => ''
986					];
987				}
988				else {
989					$db_httptest = $db_httptests[$httptest['httptestid']];
990					$httptest += [
991						'ssl_key_password' => $db_httptest['ssl_key_password'],
992						'ssl_key_file' => $db_httptest['ssl_key_file'],
993						'ssl_cert_file' => $db_httptest['ssl_cert_file']
994					];
995				}
996
997				if ($httptest['ssl_key_password'] != '' && $httptest['ssl_key_file'] == '') {
998					self::exception(ZBX_API_ERROR_PARAMETERS,
999						_s('Empty SSL key file for web scenario "%1$s".', $httptest['name'])
1000					);
1001				}
1002
1003				if ($httptest['ssl_key_file'] != '' && $httptest['ssl_cert_file'] == '') {
1004					self::exception(ZBX_API_ERROR_PARAMETERS,
1005						_s('Empty SSL certificate file for web scenario "%1$s".', $httptest['name'])
1006					);
1007				}
1008			}
1009		}
1010		unset($httptest);
1011	}
1012
1013	/**
1014	 * @param array  $httptests
1015	 * @param string $method
1016	 * @param array  $db_httptests
1017	 *
1018	 * @throws APIException if parameters is invalid.
1019	 */
1020	private function validateRetrieveMode(array &$httptests, $method, array $db_httptests = null) {
1021		foreach ($httptests as &$httptest) {
1022			if (!array_key_exists('steps', $httptest)) {
1023				continue;
1024			}
1025
1026			foreach ($httptest['steps'] as &$httpstep) {
1027				if (array_key_exists('retrieve_mode', $httpstep)
1028						|| array_key_exists('posts', $httpstep)
1029						|| array_key_exists('required', $httpstep)) {
1030
1031					if ($method === 'validateCreate' || !array_key_exists('httpstepid', $httpstep)) {
1032						$httpstep += [
1033							'retrieve_mode' => HTTPTEST_STEP_RETRIEVE_MODE_CONTENT,
1034							'posts' => '',
1035							'required' => ''
1036						];
1037					}
1038					else {
1039						$db_httptest = $db_httptests[$httptest['httptestid']];
1040						$db_httpstep = $db_httptest['steps'][$httpstep['httpstepid']];
1041						$httpstep += ['retrieve_mode' => $db_httpstep['retrieve_mode']];
1042						$httpstep += [
1043							'posts' => ($httpstep['retrieve_mode'] == HTTPTEST_STEP_RETRIEVE_MODE_CONTENT)
1044								? $db_httpstep['posts']
1045								: '',
1046							'required' => ($httpstep['retrieve_mode'] == HTTPTEST_STEP_RETRIEVE_MODE_CONTENT)
1047								? $db_httpstep['required']
1048								: ''
1049						];
1050					}
1051
1052					if ($httpstep['retrieve_mode'] == HTTPTEST_STEP_RETRIEVE_MODE_HEADERS) {
1053						if (($httpstep['posts'] !== '' && $httpstep['posts'] !== []) || $httpstep['required'] !== '') {
1054							$field_name = $httpstep['required'] !== '' ? 'required' : 'posts';
1055
1056							self::exception(ZBX_API_ERROR_PARAMETERS,
1057								_s('Incorrect value for field "%1$s": %2$s.', $field_name, _('should be empty'))
1058							);
1059						}
1060					}
1061				}
1062			}
1063			unset($httpstep);
1064		}
1065		unset($httptest);
1066	}
1067
1068	/**
1069	 * Convert string to HTTP pair array.
1070	 *
1071	 * @param string $data
1072	 * @param string $delimiter
1073	 *
1074	 * @return mixed
1075	 */
1076	private function convertHTTPPairString($data, $delimiter) {
1077		/* converts to pair array */
1078		$pairs = array_values(array_filter(explode("\n", str_replace("\r", "\n", $data))));
1079		foreach ($pairs as &$pair) {
1080			$pair = explode($delimiter, $pair, 2);
1081			$pair = [
1082				'name' => $pair[0],
1083				'value' => array_key_exists(1, $pair) ? $pair[1] : ''
1084			];
1085		}
1086		unset($pair);
1087
1088		return $pairs;
1089	}
1090
1091	/**
1092	 * Convert headers and variables from string to HTTP pair array.
1093	 * @deprecated conversion will be removed in future
1094	 *
1095	 * @param array  $httptests
1096	 *
1097	 * @return array
1098	 */
1099	private function convertHttpPairs($httptests) {
1100		reset($httptests);
1101
1102		if (!is_int(key($httptests))) {
1103			$httptests = [$httptests];
1104		}
1105
1106		$fields = [
1107			'headers' => ':',
1108			'variables' => '='
1109		];
1110
1111		foreach ($httptests as &$httptest) {
1112			foreach ($fields as $field => $delimiter) {
1113				if (is_array($httptest) && array_key_exists($field, $httptest) && is_string($httptest[$field])) {
1114					$this->deprecated('using string format for field "'.$field.'" is deprecated.');
1115					$httptest[$field] = $this->convertHTTPPairString($httptest[$field], $delimiter);
1116				}
1117			}
1118
1119			if (array_key_exists('steps', $httptest) && is_array($httptest['steps'])) {
1120				foreach ($httptest['steps'] as &$step) {
1121					foreach ($fields as $field => $delimiter) {
1122						if (is_array($step) && array_key_exists($field, $step) && is_string($step[$field])) {
1123							$this->deprecated('using string format for field "'.$field.'" is deprecated.');
1124							$step[$field] = $this->convertHTTPPairString($step[$field], $delimiter);
1125						}
1126					}
1127				}
1128				unset($step);
1129			}
1130		}
1131		unset($httptest);
1132
1133		return $httptests;
1134	}
1135}
1136