1<?php
2
3# This file is a part of RackTables, a datacenter and server room management
4# framework. See accompanying file "COPYING" for the full copyright and
5# licensing information.
6
7/*
8*
9*  This file is a library of computational functions for RackTables.
10*
11*/
12
13defineIfNotDefined ('TAGNAME_REGEXP', '/^[\p{L}0-9-]( ?([._~+%-] ?)?[\p{L}0-9:])*(%|\+)?$/u');
14defineIfNotDefined ('AUTOTAGNAME_REGEXP', '/^\$[\p{L}0-9-]( ?([._~+%-] ?)?[\p{L}0-9:])*(%|\+)?$/u');
15
16$loclist[0] = 'front';
17$loclist[1] = 'interior';
18$loclist[2] = 'rear';
19$loclist['front'] = 0;
20$loclist['interior'] = 1;
21$loclist['rear'] = 2;
22$template[0] = array (TRUE, TRUE, TRUE);
23$template[1] = array (TRUE, TRUE, FALSE);
24$template[2] = array (FALSE, TRUE, TRUE);
25$template[3] = array (TRUE, FALSE, FALSE);
26$template[4] = array (FALSE, TRUE, FALSE);
27$template[5] = array (FALSE, FALSE, TRUE);
28$templateWidth[0] = 3;
29$templateWidth[1] = 2;
30$templateWidth[2] = 2;
31$templateWidth[3] = 1;
32$templateWidth[4] = 1;
33$templateWidth[5] = 1;
34
35$message_buffering = FALSE;
36
37define ('CHAP_OBJTYPE', 1);
38define ('RE_L2_IFCFG', '/^[0-9A-F]{2}(:[0-9A-F]{2}){5}$/'); // most ifconfigs
39define ('RE_L2_IFCFG_SUNOS', '/^[0-9A-F]{1,2}(:[0-9A-F]{1,2}){5}$/'); // SunOS ifconfig
40define ('RE_L2_CISCO', '/^[0-9A-F]{4}(\.[0-9A-F]{4}){2}$/');
41define ('RE_L2_HP', '/^[0-9A-F]{6}-[0-9A-F]{6}$/');
42define ('RE_L2_HUAWEI', '/^[0-9A-F]{4}(-[0-9A-F]{4}){2}$/');
43define ('RE_L2_SOLID', '/^[0-9A-F]{12}$/');
44define ('RE_L2_IPCFG', '/^[0-9A-F]{2}(-[0-9A-F]{2}){5}$/');
45define ('RE_L2_WWN_COLON', '/^[0-9A-F]{2}(:[0-9A-F]{2}){7}$/');
46define ('RE_L2_WWN_HYPHEN', '/^[0-9A-F]{2}(-[0-9A-F]{2}){7}$/');
47define ('RE_L2_WWN_SOLID', '/^[0-9A-F]{16}$/');
48define ('RE_L2_IPOIB_COLON', '/^[0-9A-F]{2}(:[0-9A-F]{2}){19}$/');
49define ('RE_L2_IPOIB_HYPHEN', '/^[0-9A-F]{2}(-[0-9A-F]{2}){19}$/');
50define ('RE_L2_IPOIB_SOLID', '/^[0-9A-F]{40}$/');
51define ('RE_IP4_ADDR', '#^[0-9]{1,3}(\.[0-9]{1,3}){3}$#');
52define ('RE_IP4_NET', '#^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$#');
53define ('RE_STATIC_URI', '#^(?:[[:alnum:]]+[[:alnum:]_.-]*/)+[[:alnum:]\._-]+\.([[:alpha:]]+)$#');
54define ('E_8021Q_NOERROR', 0);
55define ('E_8021Q_VERSION_CONFLICT', 101);
56define ('E_8021Q_PULL_REMOTE_ERROR', 102);
57define ('E_8021Q_PUSH_REMOTE_ERROR', 103);
58define ('E_8021Q_SYNC_DISABLED', 104);
59define ('VLAN_MIN_ID', 1);
60define ('VLAN_MAX_ID', 4094);
61define ('VLAN_DFL_ID', 1);
62define ('TAB_REMEMBER_TIMEOUT', 300);
63
64// Entity type by page number mapping is 1:1 atm, but may change later.
65$etype_by_pageno = array
66(
67	'ipv4net' => 'ipv4net',
68	'ipv6net' => 'ipv6net',
69	'ipv4rspool' => 'ipv4rspool',
70	'ipv4vs' => 'ipv4vs',
71	'ipvs' => 'ipvs',
72	'object' => 'object',
73	'rack' => 'rack',
74	'row' => 'row',
75	'location' => 'location',
76	'user' => 'user',
77	'file' => 'file',
78	'vst' => 'vst',
79);
80$pageno_by_etype = array_flip ($etype_by_pageno);
81
82// Rack thumbnail image width summands: "front", "interior" and "rear" elements w/o surrounding border.
83$rtwidth = array
84(
85	0 => 9,
86	1 => 21,
87	2 => 9
88);
89
90$location_obj_types = array
91(
92	1560,
93	1561,
94	1562
95);
96
97// 802.1Q deploy queue titles
98$dqtitle = array
99(
100	'sync_aging' => 'Normal, aging',
101	'resync_aging' => 'Failed, aging',
102	'sync_ready' => 'Normal, ready for sync',
103	'resync_ready' => 'Failed, ready for retry',
104	'disabled' => 'Sync disabled',
105	'done' => 'Up to date',
106);
107
108$wdm_packs = array
109(
110	'1000cwdm80' => array
111	(
112		'title' => '1000Base-CWDM80 (8 channels)',
113		'iif_ids' => array (3, 4),
114		'oif_ids' => array (1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216),
115	),
116	'1000dwdm80' => array // ITU channels 20~61
117	(
118		'title' => '1000Base-DWDM80 (42 channels)',
119		'iif_ids' => array (3, 4),
120		'oif_ids' => array
121		(
122			1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226,
123			1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236,
124			1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246,
125			1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256,
126			1257, 1258
127		),
128	),
129	'10000dwdm80' => array // same channels for 10GE
130	(
131		'title' => '10GBase-ZR-DWDM80 (42 channels)',
132		'iif_ids' => array (9, 6, 5, 8, 7),
133		'oif_ids' => array
134		(
135			1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268,
136			1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278,
137			1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288,
138			1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298,
139			1299, 1300
140		),
141	),
142	'10000dwdm40' => array
143	(
144		'title' => '10GBase-ER-DWDM40 (42 channels)',
145		'iif_ids' => array (9, 6, 5, 8, 7),
146		'oif_ids' => array
147		(
148			1425, 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, 1434,
149			1435, 1436, 1437, 1438, 1439, 1440, 1441, 1442, 1443, 1444,
150			1445, 1446, 1447, 1448, 1449, 1450, 1451, 1452, 1453, 1454,
151			1455, 1456, 1457, 1458, 1459, 1460, 1461, 1462, 1463, 1464,
152			1465, 1466
153		),
154	),
155);
156
157// Default input for renderExpirations(), can be overridden in local plugins.
158$expirations = array();
159$expirations[21] = array
160(
161	array ('from' => -365, 'to' => 0, 'class' => 'has_problems_', 'title' => 'has expired within last year'),
162	array ('from' => 0, 'to' => 30, 'class' => 'row_', 'title' => 'expires within 30 days'),
163	array ('from' => 30, 'to' => 60, 'class' => 'row_', 'title' => 'expires within 60 days'),
164	array ('from' => 60, 'to' => 90, 'class' => 'row_', 'title' => 'expires within 90 days'),
165);
166$expirations[22] = $expirations[21];
167$expirations[24] = $expirations[21];
168
169$natv4_proto = array ('TCP' => 'TCP', 'UDP' => 'UDP', 'ALL' => 'ALL');
170
171$log_messages = array(); // messages waiting for displaying
172
173function defineIfNotDefined ($constant, $value, $case_insensitive = FALSE)
174{
175	if (defined ($constant) === FALSE)
176		define ($constant, $value, $case_insensitive);
177}
178
179// For backward compatibility only, remove later.
180function assertUIntArg ($argname, $allow_zero = FALSE)
181{
182	return $allow_zero ? assertUnsignedIntArg ($argname) : assertNaturalNumArg ($argname);
183}
184
185// Tell whether the argument is a decimal integer (or, alternatively, a numeric
186// string with a decimal integer).
187function isInteger ($arg)
188{
189	// In PHP 7.0.0 and later is_numeric() rejects a string that contains
190	// a hexadecimal number, help PHP 5 achieve the same result here.
191	return is_numeric ($arg) &&
192		(! is_string ($arg) || FALSE === mb_strstr ($arg, '0x')) &&
193		is_int (0 + $arg);
194}
195
196function isUnsignedInteger ($arg)
197{
198	return isInteger ($arg) && $arg >= 0;
199}
200
201function isNaturalNumber ($arg)
202{
203	return isInteger ($arg) && $arg >= 1;
204}
205
206function isHTMLColor ($color)
207{
208	return 1 == preg_match ('/^[0-9A-F]{6}$/i', $color);
209}
210
211function isValidVLANID ($x)
212{
213	return isInteger ($x) && $x >= VLAN_MIN_ID && $x <= VLAN_MAX_ID;
214}
215
216function assertUnsignedIntArg ($argname)
217{
218	if (! isset ($_REQUEST[$argname]))
219		throw new InvalidRequestArgException ($argname, '', 'parameter is missing');
220	if (! isUnsignedInteger ($_REQUEST[$argname]))
221		throw new InvalidRequestArgException ($argname, $_REQUEST[$argname], 'parameter is not an unsigned integer (or 0)');
222	return $_REQUEST[$argname];
223}
224
225function assertNaturalNumArg ($argname)
226{
227	if (! isset ($_REQUEST[$argname]))
228		throw new InvalidRequestArgException ($argname, '', 'parameter is missing');
229	if (! isNaturalNumber ($_REQUEST[$argname]))
230		throw new InvalidRequestArgException ($argname, $_REQUEST[$argname], 'parameter is not a natural number');
231	return $_REQUEST[$argname];
232}
233
234# Make sure the arg is a parsable date, return its UNIX timestamp equivalent
235# (or empty string for empty input, when allowed).
236#
237# FIXME: This function should be removed for the reasons below:
238# 1. Its naming is wrong as it would accept any argument value that is valid
239#    per DATETIME_FORMAT configuration variable, which by default is set to
240#    date only but can include time if necessary.
241# 2. It converts the target value from a string to a UNIX timestamp, which is
242#    not the semantics of similar functions.
243# 3. It is not used anywhere in the code anymore.
244function assertDateArg ($argname, $ok_if_empty = FALSE)
245{
246	if ('' == $arg = assertStringArg ($argname, $ok_if_empty))
247		return '';
248	try
249	{
250		return timestampFromDatetimestr ($arg);
251	}
252	catch (InvalidArgException $e)
253	{
254		throw $e->newIRAE ($argname);
255	}
256}
257
258// This function assures that specified argument was passed
259// and is a non-empty string.
260function assertStringArg ($argname, $ok_if_empty = FALSE)
261{
262	global $sic;
263	if (!isset ($_REQUEST[$argname]))
264		throw new InvalidRequestArgException($argname, '', 'parameter is missing');
265	if (!is_string ($_REQUEST[$argname]))
266		throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
267	if (! $ok_if_empty && $_REQUEST[$argname] == '')
268		throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
269	return $sic[$argname];
270}
271
272function assertBoolArg ($argname, $ok_if_empty = FALSE)
273{
274	if (!isset ($_REQUEST[$argname]))
275		throw new InvalidRequestArgException($argname, '', 'parameter is missing');
276	if (! is_string ($_REQUEST[$argname]) || $_REQUEST[$argname] != 'on')
277		throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
278	if (! $ok_if_empty && $_REQUEST[$argname] == '')
279		throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
280	return $_REQUEST[$argname] == TRUE;
281}
282
283// function returns binary IP address, or throws an exception
284function assertIPArg ($argname)
285{
286	try
287	{
288		return ip_parse (assertStringArg ($argname));
289	}
290	catch (InvalidRequestArgException $irae)
291	{
292		throw $irae;
293	}
294	catch (InvalidArgException $e)
295	{
296		throw $e->newIRAE ($argname);
297	}
298}
299
300// function returns binary IPv4 address, or throws an exception
301function assertIPv4Arg ($argname)
302{
303	try
304	{
305		return ip4_parse (assertStringArg ($argname));
306	}
307	catch (InvalidRequestArgException $irae)
308	{
309		throw $irae;
310	}
311	catch (InvalidArgException $e)
312	{
313		throw $e->newIRAE ($argname);
314	}
315}
316
317// function returns binary IPv6 address, or throws an exception
318function assertIPv6Arg ($argname)
319{
320	try
321	{
322		return ip6_parse (assertStringArg ($argname));
323	}
324	catch (InvalidRequestArgException $irae)
325	{
326		throw $irae;
327	}
328	catch (InvalidArgException $e)
329	{
330		throw $e->newIRAE ($argname);
331	}
332}
333
334function assertPCREArg ($argname)
335{
336	$arg = assertStringArg ($argname, TRUE); // empty pattern is Ok
337	if (FALSE === @preg_match ($arg, 'test'))
338		throw new InvalidRequestArgException($argname, $arg, 'PCRE validation failed');
339	return $arg;
340}
341
342function isPCRE ($arg)
343{
344	return isset ($arg) && FALSE !== @preg_match ($arg, 'test');
345}
346
347function genericAssertion ($argname, $argtype)
348{
349	global $sic;
350	switch ($argtype)
351	{
352	case 'string':
353		return assertStringArg ($argname);
354	case 'string0':
355		return assertStringArg ($argname, TRUE);
356	case 'natural0':
357		if ('' == assertStringArg ($argname, TRUE))
358			return '';
359		// fall through
360		// old style, for backward compatibility
361	case 'uint':
362	case 'natural':
363		return assertNaturalNumArg ($argname);
364	case 'unsigned0':
365		if ('' == assertStringArg ($argname, TRUE))
366			return '';
367		// fall through
368		// old style, for backward compatibility
369	case 'uint0':
370	case 'unsigned':
371		return assertUnsignedIntArg ($argname);
372	case 'decimal0':
373		if ('' == assertStringArg ($argname, TRUE))
374			return '';
375		// fall through
376	case 'decimal':
377		if (! preg_match ('/^\d+(\.\d+)?$/', assertStringArg ($argname)))
378			throw new InvalidRequestArgException ($argname, $sic[$argname], 'format error');
379		return $sic[$argname];
380	case 'inet':
381		return assertIPArg ($argname);
382	case 'inet4':
383		return assertIPv4Arg ($argname);
384	case 'inet6':
385		return assertIPv6Arg ($argname);
386	case 'l2address0':
387		if ('' == assertStringArg ($argname, TRUE))
388			return '';
389		// fall through
390	case 'l2address':
391		assertStringArg ($argname);
392		try
393		{
394			l2addressForDatabase ($sic[$argname]);
395		}
396		catch (InvalidArgException $iae)
397		{
398			throw $iae->newIRAE ($argname);
399		}
400		return $sic[$argname];
401	case 'tag':
402		if (!validTagName (assertStringArg ($argname)))
403			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Invalid tag name');
404		return $sic[$argname];
405	case 'pcre':
406		return assertPCREArg ($argname);
407	case 'json':
408		if (NULL === ($ret = json_decode (assertStringArg ($argname), TRUE)))
409			throw new InvalidRequestArgException ($argname, '(omitted)', 'Invalid JSON code received from client');
410		return $ret;
411	case 'array':
412		if (! array_key_exists ($argname, $_REQUEST))
413			throw new InvalidRequestArgException ($argname, '(missing argument)');
414		if (! is_array ($_REQUEST[$argname]))
415			throw new InvalidRequestArgException ($argname, '(omitted)', 'argument is not an array');
416		return $_REQUEST[$argname];
417	case 'array0':
418		if (! array_key_exists ($argname, $_REQUEST))
419			return array();
420		if (! is_array ($_REQUEST[$argname]))
421			throw new InvalidRequestArgException ($argname, '(omitted)', 'argument is not an array');
422		return $_REQUEST[$argname];
423	case 'datetime0':
424		if ('' == assertStringArg ($argname, TRUE))
425			return '';
426		// fall through
427	case 'datetime':
428		$argvalue = assertStringArg ($argname);
429		try
430		{
431			timestampFromDatetimestr ($argvalue); // discard the result on success
432		}
433		catch (InvalidArgException $iae)
434		{
435			throw $iae->newIRAE ($argname);
436		}
437		return $argvalue;
438	case 'dateonly0':
439		if ('' == assertStringArg ($argname, TRUE))
440			return '';
441		// fall through
442	case 'dateonly':
443		$argvalue = assertStringArg ($argname);
444		try
445		{
446			SQLDateFromDateStr ($argvalue); // discard the result on success
447		}
448		catch (InvalidArgException $iae)
449		{
450			throw $iae->newIRAE ($argname);
451		}
452		return $argvalue;
453	case 'enum/attr_type':
454		assertStringArg ($argname);
455		if (!in_array ($sic[$argname], array ('uint', 'float', 'string', 'dict','date')))
456			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
457		return $sic[$argname];
458	case 'enum/vlan_type':
459		assertStringArg ($argname);
460		// "Alien" type is not valid until the logic is fixed to implement it in full.
461		if (!in_array ($sic[$argname], array ('ondemand', 'compulsory')))
462			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
463		return $sic[$argname];
464	case 'enum/wdmstd':
465		assertStringArg ($argname);
466		global $wdm_packs;
467		if (! array_key_exists ($sic[$argname], $wdm_packs))
468			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
469		return $sic[$argname];
470	case 'enum/ipproto':
471		assertStringArg ($argname);
472		global $vs_proto;
473		if (!array_key_exists ($sic[$argname], $vs_proto))
474			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
475		return $sic[$argname];
476	case 'enum/natv4proto':
477		assertStringArg ($argname);
478		global $natv4_proto;
479		if (! array_key_exists ($sic[$argname], $natv4_proto))
480			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
481		return $sic[$argname];
482	case 'enum/alloc_type':
483		assertStringArg ($argname);
484		if (!in_array ($sic[$argname], array ('regular', 'shared', 'virtual', 'router', 'sharedrouter', 'point2point')))
485			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
486		return $sic[$argname];
487	case 'enum/dqcode':
488		assertStringArg ($argname);
489		global $dqtitle;
490		if (! array_key_exists ($sic[$argname], $dqtitle))
491			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
492		return $sic[$argname];
493	case 'enum/yesno':
494		if (! in_array ($sic[$argname], array ('yes', 'no')))
495			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
496		return $sic[$argname];
497	case 'iif':
498		assertNaturalNumArg ($argname);
499		if (!array_key_exists ($sic[$argname], getPortIIFOptions()))
500			throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
501		return $sic[$argname];
502	// 'vlan' -- any valid VLAN ID except the default
503	// 'vlan1' -- any valid VLAN ID including the default
504	case 'vlan':
505	case 'vlan1':
506		assertNaturalNumArg ($argname);
507		if (! isValidVLANID ($sic[$argname]))
508			throw new InvalidRequestArgException ($argname, $sic[$argname], 'not a valid VLAN ID');
509		if ($argtype == 'vlan' && $sic[$argname] == VLAN_DFL_ID)
510			throw new InvalidRequestArgException ($argname, $sic[$argname], 'default VLAN not allowed');
511		return $sic[$argname];
512	case 'uint-vlan':
513	case 'uint-vlan1':
514		$argvalue = assertStringArg ($argname);
515		try
516		{
517			list ($vdom_id, $vlan_id) = decodeVLANCK ($argvalue);
518		}
519		catch (InvalidArgException $iae)
520		{
521			throw $iae->newIRAE ($argname);
522		}
523		if ($argtype == 'uint-vlan' && $vlan_id == VLAN_DFL_ID)
524			throw new InvalidRequestArgException ($argname, $argvalue, 'default VLAN not allowed');
525		return $argvalue;
526	case 'rackcode/expr':
527		if ('' == assertStringArg ($argname, TRUE))
528			return array();
529		if (! $expr = compileExpression ($sic[$argname]))
530			throw new InvalidRequestArgException ($argname, $sic[$argname], 'not a valid RackCode expression');
531		return $expr;
532	case 'htmlcolor0':
533		if ('' == assertStringArg ($argname, TRUE))
534			return '';
535		// fall through
536	case 'htmlcolor':
537		$argvalue = assertStringArg ($argname);
538		if (! isHTMLColor ($argvalue))
539			throw new InvalidRequestArgException ($argname, $argvalue, 'not an HTML color');
540		return $argvalue;
541	default:
542		throw new InvalidArgException ('argtype', $argtype); // comes not from user's input
543	}
544}
545
546// return HTML form checkbox value (TRUE of FALSE) by name of its input control
547function isCheckSet ($input_name, $mode = 'bool')
548{
549	$value = isset ($_REQUEST[$input_name]) && $_REQUEST[$input_name] == 'on';
550	switch ($mode)
551	{
552		case 'bool' : return $value;
553		case 'yesno': return $value ? 'yes' : 'no';
554		default: throw new InvalidArgException ('mode', $mode);
555	}
556}
557
558// Validate and return "bypass" value for the current context, if one is
559// defined for it, or NULL otherwise.
560// There is at least one bit of code that depends on the NULL return value
561// (although it does not explicitly check for it), it is the "interface" case
562// in index.php, which makes an unconditional call to here. Changing this
563// function to throw an exception instead will require changing at least
564// that code too.
565function getBypassValue()
566{
567	global $page, $pageno;
568	if (!array_key_exists ('bypass', $page[$pageno]))
569		return NULL;
570	if (!array_key_exists ('bypass_type', $page[$pageno]))
571		throw new RackTablesError ("Internal structure error at node '${pageno}' (bypass_type is not set)", RackTablesError::INTERNAL);
572	return genericAssertion ($page[$pageno]['bypass'], $page[$pageno]['bypass_type']);
573}
574
575// fills $args array with the bypass values of specified $pageno that are provided in $_REQUEST
576function fillBypassValues ($pageno, &$args)
577{
578	global $page, $sic;
579	if (isset ($page[$pageno]['bypass']))
580	{
581		$param_name = $page[$pageno]['bypass'];
582		if (! array_key_exists ($param_name, $args) && isset ($sic[$param_name]))
583			$args[$param_name] = $sic[$param_name];
584	}
585	if (isset ($page[$pageno]['bypass_tabs']))
586		foreach ($page[$pageno]['bypass_tabs'] as $param_name)
587			if (! array_key_exists ($param_name, $args) && isset ($sic[$param_name]))
588				$args[$param_name] = $sic[$param_name];
589}
590
591// Objects of some types should be explicitly shown as
592// anonymous (labelless). This function is a single place where the
593// decision about displayed name is made.
594function formatObjectDisplayedName ($name, $objtype_id)
595{
596	return ($name != '') ? $name : sprintf ('[%s]', decodeObjectType ($objtype_id));
597}
598
599// Set the dname attribute within a cell
600function setDisplayedName (&$cell)
601{
602	if ($cell['realm'] == 'object')
603	{
604		$cell['dname'] = formatObjectDisplayedName ($cell['name'], $cell['objtype_id']);
605		// If the object has a container, set its dname as well
606		if ($cell['container_id'])
607			$cell['container_dname'] = formatObjectDisplayedName ($cell['container_name'], $cell['container_objtype_id']);
608	}
609	elseif ($cell['realm'] == 'ipv4vs')
610		if ($cell['proto'] == 'MARK')
611			$cell['dname'] = 'fwmark: ' . implode ('', unpack ('N', substr ($cell['vip_bin'], 0, 4)));
612		else
613			$cell['dname'] = $cell['vip'] . ':' . $cell['vport'] . '/' . $cell['proto'];
614}
615
616// This function finds height of solid rectangle of atoms that are all
617// assigned to the same object. Rectangle base is defined by specified
618// template.
619function rectHeight ($rackData, $startRow, $template_idx)
620{
621	$height = 0;
622	// The first met object_id is used to match all the folowing IDs.
623	$object_id = 0;
624	global $template;
625	do
626	{
627		for ($locidx = 0; $locidx < 3; $locidx++)
628		{
629			// At least one value in template is TRUE, but the following block
630			// can meet 'skipped' atoms. Let's ensure there is at least some result
631			// after processing the first row.
632			if ($template[$template_idx][$locidx])
633			{
634				if
635				(
636					isset ($rackData[$startRow - $height][$locidx]['skipped']) ||
637					isset ($rackData[$startRow - $height][$locidx]['rowspan']) ||
638					isset ($rackData[$startRow - $height][$locidx]['colspan']) ||
639					$rackData[$startRow - $height][$locidx]['state'] != 'T'
640				)
641					break 2;
642				if ($object_id == 0)
643					$object_id = $rackData[$startRow - $height][$locidx]['object_id'];
644				if ($object_id != $rackData[$startRow - $height][$locidx]['object_id'])
645					break 2;
646			}
647		}
648		// If the first row can't offer anything, bail out.
649		if ($height == 0 && $object_id == 0)
650			break;
651		$height++;
652	}
653	while ($startRow - $height > 0);
654	return $height;
655}
656
657// This function marks atoms to be avoided by rectHeight() and assigns rowspan/colspan
658// attributes.
659function markSpan (&$rackData, $startRow, $maxheight, $template_idx)
660{
661	global $template, $templateWidth;
662	$colspan = 0;
663	for ($height = 0; $height < $maxheight; $height++)
664		for ($locidx = 0; $locidx < 3; $locidx++)
665			if ($template[$template_idx][$locidx])
666			{
667				// Add colspan/rowspan to the first row met and mark the following ones to skip.
668				// Explicitly show even single-cell spanned atoms, because rectHeight()
669				// is expeciting this data for correct calculation.
670				if ($colspan != 0)
671					$rackData[$startRow - $height][$locidx]['skipped'] = TRUE;
672				else
673				{
674					$colspan = $templateWidth[$template_idx];
675					if ($colspan >= 1)
676						$rackData[$startRow - $height][$locidx]['colspan'] = $colspan;
677					if ($maxheight >= 1)
678						$rackData[$startRow - $height][$locidx]['rowspan'] = $maxheight;
679				}
680			}
681}
682
683// This function sets rowspan/solspan/skipped atom attributes for renderRack()
684// by finding _all_ possible rectangles for each rack unit and then selecting
685// the widest of those with the maximal square.
686function markAllSpans (&$rackData)
687{
688	for ($i = $rackData['height']; $i > 0; $i--)
689		while (markBestSpan ($rackData, $i));
690}
691
692// Calculate height of 6 possible span templates (array is presorted by width
693// descending) and mark the best (if any).
694function markBestSpan (&$rackData, $i)
695{
696	global $templateWidth;
697	$height = array();
698	$square = array();
699	foreach ($templateWidth as $j => $width)
700	{
701		$height[$j] = rectHeight ($rackData, $i, $j);
702		$square[$j] = $height[$j] * $width;
703	}
704	// find the widest rectangle of those with maximal height
705	if (0 == $maxsquare = max ($square))
706		return FALSE;
707	$best_template_index = array_search ($maxsquare, $square);
708	// distribute span marks
709	markSpan ($rackData, $i, $height[$best_template_index], $best_template_index);
710	return TRUE;
711}
712
713// OK to mount to 'F' atoms and unmount from the current object's 'T' atoms.
714function applyObjectMountMask (&$rackData, $object_id)
715{
716	for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
717		for ($locidx = 0; $locidx < 3; $locidx++)
718			switch ($rackData[$unit_no][$locidx]['state'])
719			{
720				case 'F':
721					$rackData[$unit_no][$locidx]['enabled'] = TRUE;
722					break;
723				case 'T':
724					$rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
725					break;
726				default:
727					$rackData[$unit_no][$locidx]['enabled'] = FALSE;
728			}
729}
730
731// check permissions for rack modification
732function rackModificationPermitted ($rackData, $op, $with_context=TRUE)
733{
734	if ($with_context && ! permitted (NULL, NULL, $op))
735		return FALSE;
736	$rack_op_annex = array_merge ($rackData['etags'], $rackData['itags'], $rackData['atags']);
737	return permitted (NULL, NULL, $op, $rack_op_annex);
738}
739
740// Design change means transition between 'F' and 'A' and back.
741function applyRackDesignMask (&$rackData)
742{
743	for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
744		for ($locidx = 0; $locidx < 3; $locidx++)
745			switch ($rackData[$unit_no][$locidx]['state'])
746			{
747				case 'F':
748				case 'A':
749					$rackData[$unit_no][$locidx]['enabled'] = TRUE;
750					break;
751				default:
752					$rackData[$unit_no][$locidx]['enabled'] = FALSE;
753			}
754}
755
756// The same for 'F' and 'U'.
757function applyRackProblemMask (&$rackData)
758{
759	for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
760		for ($locidx = 0; $locidx < 3; $locidx++)
761			switch ($rackData[$unit_no][$locidx]['state'])
762			{
763				case 'F':
764				case 'U':
765					$rackData[$unit_no][$locidx]['enabled'] = TRUE;
766					break;
767				default:
768					$rackData[$unit_no][$locidx]['enabled'] = FALSE;
769			}
770}
771
772// This function highlights specified object by amending the
773// 'hl' suffix of the class name.
774function highlightObject (&$rackData, $object_id)
775{
776	// Also highlight parent objects
777	$object = spotEntity ('object', $object_id);
778	$parents = reindexById (getParents ($object, 'object'));
779
780	for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
781		for ($locidx = 0; $locidx < 3; $locidx++)
782		{
783			$atom = &$rackData[$unit_no][$locidx];
784			if
785			(
786				$atom['state'] == 'T' &&
787				($atom['object_id'] == $object_id || isset ($parents[$atom['object_id']]))
788			)
789				$atom['hl'] = 'h' . $atom['hl'];
790		}
791}
792
793// This function marks atoms to selected or not depending on their current state.
794function markupAtomGrid (&$data, $checked_state)
795{
796	for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
797		for ($locidx = 0; $locidx < 3; $locidx++)
798			if ($data[$unit_no][$locidx]['enabled'])
799				$data[$unit_no][$locidx]['checked'] =
800					$data[$unit_no][$locidx]['state'] == $checked_state ? ' checked' : '';
801}
802
803// This function is almost a clone of processGridForm(), except it does not modify
804// the database. This code assumes that a correct filter has already been applied,
805// hence it just sets or unsets checkbox inputs and leaves the atom state intact.
806function mergeGridFormToRack (&$rackData)
807{
808	$rack_id = $rackData['id'];
809	for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
810		for ($locidx = 0; $locidx < 3; $locidx++)
811			if ($rackData[$unit_no][$locidx]['enabled'])
812				$rackData[$unit_no][$locidx]['checked'] =
813					isCheckSet ("atom_${rack_id}_${unit_no}_${locidx}") ? ' checked' : '';
814}
815
816// wrapper around ip4_mask and ip6_mask
817// netmask conversion from length to binary string
818// v4/v6 mode is toggled by $is_ipv6 parameter
819// Throws exception if $prefix_len is invalid
820function ip_mask ($prefix_len, $is_ipv6)
821{
822	return $is_ipv6 ? ip6_mask ($prefix_len) : ip4_mask ($prefix_len);
823}
824
825// netmask conversion from length to binary string
826// Throws exception if $prefix_len is invalid
827function ip4_mask ($prefix_len)
828{
829	static $mask = array
830	(
831		"\x00\x00\x00\x00", // 0
832		"\x80\x00\x00\x00", // 1
833		"\xC0\x00\x00\x00", // 2
834		"\xE0\x00\x00\x00", // 3
835		"\xF0\x00\x00\x00", // 4
836		"\xF8\x00\x00\x00", // 5
837		"\xFC\x00\x00\x00", // 6
838		"\xFE\x00\x00\x00", // 7
839		"\xFF\x00\x00\x00", // 8
840		"\xFF\x80\x00\x00", // 9
841		"\xFF\xC0\x00\x00", // 10
842		"\xFF\xE0\x00\x00", // 11
843		"\xFF\xF0\x00\x00", // 12
844		"\xFF\xF8\x00\x00", // 13
845		"\xFF\xFC\x00\x00", // 14
846		"\xFF\xFE\x00\x00", // 15
847		"\xFF\xFF\x00\x00", // 16
848		"\xFF\xFF\x80\x00", // 17
849		"\xFF\xFF\xC0\x00", // 18
850		"\xFF\xFF\xE0\x00", // 19
851		"\xFF\xFF\xF0\x00", // 20
852		"\xFF\xFF\xF8\x00", // 21
853		"\xFF\xFF\xFC\x00", // 22
854		"\xFF\xFF\xFE\x00", // 23
855		"\xFF\xFF\xFF\x00", // 24
856		"\xFF\xFF\xFF\x80", // 25
857		"\xFF\xFF\xFF\xC0", // 26
858		"\xFF\xFF\xFF\xE0", // 27
859		"\xFF\xFF\xFF\xF0", // 28
860		"\xFF\xFF\xFF\xF8", // 29
861		"\xFF\xFF\xFF\xFC", // 30
862		"\xFF\xFF\xFF\xFE", // 31
863		"\xFF\xFF\xFF\xFF", // 32
864	);
865
866	if ($prefix_len >= 0 && $prefix_len <= 32)
867		return $mask[$prefix_len];
868	throw new InvalidArgException ('prefix_len', $prefix_len);
869}
870
871// netmask conversion from length to binary string
872// Throws exception if $prefix_len is invalid
873function ip6_mask ($prefix_len)
874{
875	static $mask = array
876	(
877		"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 0
878		"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 1
879		"\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 2
880		"\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 3
881		"\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 4
882		"\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 5
883		"\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 6
884		"\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 7
885		"\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 8
886		"\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 9
887		"\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 10
888		"\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 11
889		"\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 12
890		"\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 13
891		"\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 14
892		"\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 15
893		"\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 16
894		"\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 17
895		"\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 18
896		"\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 19
897		"\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 20
898		"\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 21
899		"\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 22
900		"\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 23
901		"\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 24
902		"\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 25
903		"\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 26
904		"\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 27
905		"\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 28
906		"\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 29
907		"\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 30
908		"\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 31
909		"\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 32
910		"\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 33
911		"\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 34
912		"\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 35
913		"\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 36
914		"\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 37
915		"\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 38
916		"\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 39
917		"\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 40
918		"\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 41
919		"\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 42
920		"\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 43
921		"\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 44
922		"\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 45
923		"\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 46
924		"\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 47
925		"\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 48
926		"\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 49
927		"\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 50
928		"\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 51
929		"\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 52
930		"\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 53
931		"\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 54
932		"\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 55
933		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00", // 56
934		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00\x00", // 57
935		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00\x00", // 58
936		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00\x00", // 59
937		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00\x00", // 60
938		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00\x00", // 61
939		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00\x00", // 62
940		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00\x00", // 63
941		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00", // 64
942		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00\x00", // 65
943		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00\x00", // 66
944		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00\x00", // 67
945		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00\x00", // 68
946		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00\x00", // 69
947		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00\x00", // 70
948		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00\x00", // 71
949		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00\x00", // 72
950		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00\x00", // 73
951		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00\x00", // 74
952		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00\x00", // 75
953		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00\x00", // 76
954		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00\x00", // 77
955		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00\x00", // 78
956		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00\x00", // 79
957		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00\x00", // 80
958		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00\x00", // 81
959		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00\x00", // 82
960		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00\x00", // 83
961		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00\x00", // 84
962		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00\x00", // 85
963		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00\x00", // 86
964		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00\x00", // 87
965		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\x00", // 88
966		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00\x00", // 89
967		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00\x00", // 90
968		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00\x00", // 91
969		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00\x00", // 92
970		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00\x00", // 93
971		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00\x00", // 94
972		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00\x00", // 95
973		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00", // 96
974		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00\x00", // 97
975		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00\x00", // 98
976		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00\x00", // 99
977		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00\x00", // 100
978		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00\x00", // 101
979		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00\x00", // 102
980		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x00", // 103
981		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00", // 104
982		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00\x00", // 105
983		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00\x00", // 106
984		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00\x00", // 107
985		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00\x00", // 108
986		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x00", // 109
987		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x00", // 110
988		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00", // 111
989		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00", // 112
990		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x00", // 113
991		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0\x00", // 114
992		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x00", // 115
993		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x00", // 116
994		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00", // 117
995		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00", // 118
996		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00", // 119
997		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", // 120
998		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80", // 121
999		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC0", // 122
1000		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0", // 123
1001		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0", // 124
1002		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8", // 125
1003		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC", // 126
1004		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE", // 127
1005		"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", // 128
1006	);
1007
1008	if ($prefix_len >= 0 && $prefix_len <= 128)
1009		return $mask[$prefix_len];
1010	throw new InvalidArgException ('prefix_len', $prefix_len);
1011}
1012
1013// Return a uniformly (010203040506 or 0102030405060708 or likewise) formatted
1014// L2 address for a string in one of the recognized formats, an empty string for
1015// an empty string or raise an exception on invalid input.
1016function l2addressForDatabase ($string)
1017{
1018	$string = strtoupper (trim ($string));
1019	$ret = '';
1020	switch (TRUE)
1021	{
1022		case $string == '':
1023		case preg_match (RE_L2_SOLID, $string):
1024		case preg_match (RE_L2_WWN_SOLID, $string):
1025		case preg_match (RE_L2_IPOIB_SOLID, $string):
1026			$ret = $string;
1027			break;
1028		case preg_match (RE_L2_IFCFG, $string):
1029		case preg_match (RE_L2_WWN_COLON, $string):
1030		case preg_match (RE_L2_IPOIB_COLON, $string):
1031			$ret = str_replace (':', '', $string);
1032			break;
1033		// SunOS notation regexp will also match other ifconfigs notations hence should
1034		// be matched and conditioned after the others, not before.
1035		case preg_match (RE_L2_IFCFG_SUNOS, $string):
1036			foreach (explode (':', $string) as $byte)
1037				$ret .= (strlen ($byte) == 1 ? '0' : '') . $byte;
1038			break;
1039		case preg_match (RE_L2_CISCO, $string):
1040			$ret = str_replace ('.', '', $string);
1041			break;
1042		case preg_match (RE_L2_HP, $string):
1043		case preg_match (RE_L2_HUAWEI, $string):
1044			$ret = str_replace ('-', '', $string);
1045			break;
1046		case preg_match (RE_L2_IPCFG, $string):
1047		case preg_match (RE_L2_WWN_HYPHEN, $string):
1048		case preg_match (RE_L2_IPOIB_HYPHEN, $string):
1049			$ret = str_replace ('-', '', $string);
1050			break;
1051		default:
1052			throw new InvalidArgException ('string', $string, 'malformed MAC/WWN/IPoIB address');
1053	}
1054	// Some switches return this invalid address through SNMP. Disregard the malformed data
1055	// and return an empty string, which will translate to NULL when (if) the address goes
1056	// into the database. This suppresses unnecessary violations of the L2 address constraint.
1057	if ($ret === '000000000000')
1058		$ret = '';
1059	return $ret;
1060}
1061
1062// The input to this function is valid iff it is an output of l2addressForDatabase() or a NULL.
1063function l2addressFromDatabase ($string)
1064{
1065	// $string normally comes from the database in uppercase, test it with
1066	// the regexps, which are now uppercase.
1067	switch (TRUE)
1068	{
1069		case $string === NULL:
1070		case $string === '':
1071			return '';
1072		case preg_match (RE_L2_SOLID, $string):
1073		case preg_match (RE_L2_WWN_SOLID, $string):
1074		case preg_match (RE_L2_IPOIB_SOLID, $string):
1075			return implode (':', str_split ($string, 2));
1076		default:
1077			throw new InvalidArgException ('string', $string, 'invalid format');
1078	}
1079}
1080
1081function HTMLColorForDatabase ($string)
1082{
1083	$ret = 0;
1084	// The HTTP request coming through the opspec declaration will indicate an undefined
1085	// color value with an empty string, PHP code in addition to that is likely to use
1086	// NULL, which comes from the database.
1087	if ($string === NULL || $string === '')
1088		return NULL;
1089	if (! isHTMLColor ($string) || 1 != sscanf (mb_strtoupper ($string), '%06X', $ret))
1090		throw new InvalidArgException ('string', $string, 'not an HTML color');
1091	return $ret;
1092}
1093
1094// No code currently depends on this function, it is here for completeness.
1095function HTMLColorFromDatabase ($u)
1096{
1097	if ($u === NULL)
1098		return NULL;
1099	if (! isUnsignedInteger ($u))
1100		throw new InvalidArgException ('u', $u, 'not an unsigned integer');
1101	if ($u > 0xFFFFFF)
1102		throw new InvalidArgException ('u', $u, 'value out of range');
1103	return sprintf ('%06X', $u);
1104}
1105
1106// DEPRECATED, remove in 0.21.0
1107function getPrevIDforRack ($row_id, $rack_id)
1108{
1109	$n = getRackNeighbors ($row_id, $rack_id);
1110	return $n['prev'];
1111}
1112
1113// DEPRECATED, remove in 0.21.0
1114function getNextIDforRack ($row_id, $rack_id)
1115{
1116	$n = getRackNeighbors ($row_id, $rack_id);
1117	return $n['next'];
1118}
1119
1120function getRackNeighbors ($row_id, $rack_id)
1121{
1122	$ret = array ('prev' => NULL, 'next' => NULL);
1123	$ids = selectRackOrder ($row_id);
1124	$index = array_search ($rack_id, $ids);
1125	if ($index !== FALSE && $index > 0)
1126		$ret['prev'] = $ids[$index - 1];
1127	if ($index !== FALSE && $index + 1 < count ($ids))
1128		$ret['next'] = $ids[$index + 1];
1129	return $ret;
1130}
1131
1132// Return a list of rack IDs that are P or less positions
1133// far from the given rack in its row.
1134function getProximateRacks ($rack_id, $proximity = 0)
1135{
1136	$ret = array ($rack_id);
1137	if ($proximity > 0)
1138	{
1139		$rack = spotEntity ('rack', $rack_id);
1140		$rackList = selectRackOrder ($rack['row_id']);
1141		$cur_item = array_search ($rack_id, $rackList);
1142		if (FALSE !== $cur_item)
1143		{
1144			if ($todo = min ($cur_item, $proximity))
1145				$ret = array_merge ($ret, array_slice ($rackList, $cur_item - $todo, $todo));
1146			if ($todo = min (count ($rackList) - 1 - $cur_item, $proximity))
1147				$ret = array_merge ($ret, array_slice ($rackList, $cur_item + 1, $todo));
1148		}
1149	}
1150	return $ret;
1151}
1152
1153function sortTokenize ($a, $b)
1154{
1155	$aold='';
1156	while ($a != $aold)
1157	{
1158		$aold=$a;
1159		$a = preg_replace('/[^a-zA-Z0-9]/',' ',$a);
1160		$a = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$a);
1161		$a = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$a);
1162	}
1163
1164	$bold='';
1165	while ($b != $bold)
1166	{
1167		$bold=$b;
1168		$b = preg_replace('/[^a-zA-Z0-9]/',' ',$b);
1169		$b = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$b);
1170		$b = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$b);
1171	}
1172
1173	$ar = explode(' ', $a);
1174	$br = explode(' ', $b);
1175	$arc = count ($ar);
1176	$brc = count ($br);
1177	for ($i = 0; $i < $arc && $i < $brc; $i++)
1178	{
1179		if (isUnsignedInteger ($ar[$i]) && isUnsignedInteger ($br[$i]))
1180			$ret = numCompare ($ar[$i], $br[$i]);
1181		else
1182			$ret = strcasecmp($ar[$i], $br[$i]);
1183		if ($ret != 0)
1184			return $ret;
1185	}
1186	return numCompare ($arc, $brc);
1187}
1188
1189// This function returns an array of single element of object's FQDN attribute,
1190// if FQDN is set. The next choice is object's common name, if it looks like a
1191// hostname. Otherwise an array of all 'regular' IP addresses of the
1192// object is returned (which may appear 0 and more elements long).
1193function findAllEndpoints ($object_id, $fallback = '')
1194{
1195	foreach (getAttrValues ($object_id) as $record)
1196		if ($record['id'] == 3 && $record['value'] != '') // FQDN
1197			return array ($record['value']);
1198	$regular = array();
1199	foreach (getObjectIPv4AllocationList ($object_id) as $ip_bin => $alloc)
1200		if ($alloc['type'] == 'regular')
1201			$regular[] = ip4_format ($ip_bin);
1202	// FIXME: add IPv6 allocations to this list
1203	if (!count ($regular) && $fallback != '')
1204		return array ($fallback);
1205	return $regular;
1206}
1207
1208// Some records in the dictionary may be written as plain text or as Wiki
1209// link in the following syntax:
1210// 1. word
1211// 2. [[word URL]] // FIXME: this isn't working
1212// 3. [[word word word | URL]]
1213// This function parses the line in $record['value'] and modifies $record:
1214// $record['o_value'] is set to be the first part of link (word word word)
1215// $record['a_value'] is the same, but with %GPASS and %GSKIP macros applied
1216// $record['href'] is set to URL if it is specified in the input value
1217function parseWikiLink (&$record)
1218{
1219	if (! preg_match ('/^\[\[(.+)\]\]$/', $record['value'], $matches))
1220		$record['o_value'] = $record['value'];
1221	else
1222	{
1223		$s = explode ('|', $matches[1]);
1224		if (isset ($s[1]))
1225			$record['href'] = trim ($s[1]);
1226		$record['o_value'] = trim ($s[0]);
1227	}
1228	$record['a_value'] = execGMarker ($record['o_value']);
1229}
1230
1231// FIXME: should this be saved as "P-data"?
1232function execGMarker ($line)
1233{
1234	return preg_replace ('/^.+%GSKIP%/', '',
1235		preg_replace ('/^(.+)%GPASS%/', '\\1 ',
1236			preg_replace ('/%L\d+,\d+(H|V|)%/', '', $line)));
1237}
1238
1239// extract the layout information from the %L...% marker in the dictionary info
1240// This is somewhat similar to the %GPASS %GSKIP
1241function extractLayout (&$record)
1242{
1243	if (preg_match ('/%L(\d+),(\d+)(H|V|)%/', $record['value'], $matches))
1244	{
1245		$record['rows'] = $matches[1];
1246		$record['cols'] = $matches[2];
1247		$record['layout'] = $matches[3];
1248		if ($record['layout'] == '')
1249			$record['layout'] = ($record['cols'] >= 4) ? 'V' : 'H';
1250	}
1251}
1252
1253// rackspace usage for a single rack
1254// (T + W + U) / (height * 3 - A)
1255function getRSUforRack ($data)
1256{
1257	$counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
1258	for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
1259		for ($locidx = 0; $locidx < 3; $locidx++)
1260			$counter[$data[$unit_no][$locidx]['state']]++;
1261	return ($counter['T'] + $counter['W'] + $counter['U']) /
1262		($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
1263}
1264
1265// Same for row.
1266function getRSUforRow ($rowData)
1267{
1268	if (!count ($rowData))
1269		return 0;
1270	$counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
1271	$total_height = 0;
1272	foreach (array_keys ($rowData) as $rack_id)
1273	{
1274		$data = spotEntity ('rack', $rack_id);
1275		amplifyCell ($data);
1276		$total_height += $data['height'];
1277		for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
1278			for ($locidx = 0; $locidx < 3; $locidx++)
1279				$counter[$data[$unit_no][$locidx]['state']]++;
1280	}
1281	return ($counter['T'] + $counter['W'] + $counter['U']) /
1282		($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
1283}
1284
1285function string_insert_hrefs_callback ($m)
1286{
1287	$t_url_href    = 'href="' . rtrim($m[1], '.') . '"';
1288	$s_url_replace = "<a ${t_url_href}>$m[1]</a> [<a ${t_url_href} target=\"_blank\">^</a>]";
1289	return $s_url_replace;
1290}
1291
1292# Detect URLs and email addresses in the string and replace them with href anchors
1293# (adopted from MantisBT, core/string_api.php:string_insert_hrefs).
1294function string_insert_hrefs ($p_string)
1295{
1296	static $s_url_regex = NULL;
1297	static $s_url_replace = NULL;
1298	static $s_email_regex = NULL;
1299	static $s_anchor_regex = '/(<a[^>]*>.*?<\/a>)/is';
1300
1301	if (getConfigVar ('DETECT_URLS') != 'yes')
1302		return $p_string;
1303
1304	# Initialize static variables
1305	if (is_null ($s_url_regex))
1306	{
1307		# URL regex
1308		$t_url_protocol = '(?:[[:alpha:]][-+.[:alnum:]]*):\/\/';
1309
1310		# %2A notation in url's
1311		$t_url_hex = '%[[:digit:]A-Fa-f]{2}';
1312
1313		# valid set of characters that may occur in url scheme. Note: - should be first (A-F != -AF).
1314		$t_url_valid_chars       = '-_.,!~*\';\/?%^\\\\:@&={\|}+$#[:alnum:]\pL';
1315		$t_url_chars             = "(?:${t_url_hex}|[${t_url_valid_chars}\(\)\[\]])";
1316		$t_url_chars2            = "(?:${t_url_hex}|[${t_url_valid_chars}])";
1317		$t_url_chars_in_brackets = "(?:${t_url_hex}|[${t_url_valid_chars}\(\)])";
1318		$t_url_chars_in_parens   = "(?:${t_url_hex}|[${t_url_valid_chars}\[\]])";
1319
1320		$t_url_part1 = "${t_url_chars}";
1321		$t_url_part2 = "(?:\(${t_url_chars_in_parens}*\)|\[${t_url_chars_in_brackets}*\]|${t_url_chars2})";
1322
1323		$s_url_regex = "/(${t_url_protocol}(${t_url_part1}*?${t_url_part2}+))/su";
1324
1325		# URL replacement
1326		$t_url_href    = "href=\"'.rtrim('\\1','.').'\"";
1327		$s_url_replace = "'<a ${t_url_href}>\\1</a> [<a ${t_url_href} target=\"_blank\">^</a>]'";
1328
1329		# e-mail regex
1330		$s_email_regex = substr_replace (email_regex_simple(), '(?:mailto:)?', 1, 0);
1331	}
1332
1333	# Find any URL in a string and replace it by a clickable link
1334	$p_string = preg_replace_callback
1335	(
1336		$s_url_regex,
1337		'string_insert_hrefs_callback',
1338		$p_string
1339	);
1340
1341	# Find any email addresses in the string and replace them with a clickable
1342	# mailto: link, making sure that we skip processing of any existing anchor
1343	# tags, to avoid parts of URLs such as https://user@example.com/ or
1344	# http://user:password@example.com/ to be not treated as an email.
1345	$t_pieces = preg_split ($s_anchor_regex, $p_string, NULL, PREG_SPLIT_DELIM_CAPTURE);
1346	$p_string = '';
1347	foreach ($t_pieces as $piece)
1348		if (preg_match ($s_anchor_regex, $piece))
1349			$p_string .= $piece;
1350		else
1351			$p_string .= preg_replace ($s_email_regex, '<a href="mailto:\0">\0</a>', $piece);
1352
1353	return $p_string;
1354}
1355
1356# Adopted from MantisBT, core/email_api.php:email_regex_simple.
1357function email_regex_simple()
1358{
1359	static $s_email_regex = NULL;
1360
1361	if (is_null ($s_email_regex))
1362	{
1363		$t_recipient = "([a-z0-9!#*+\/=?^_{|}~-]+(?:\.[a-z0-9!#*+\/=?^_{|}~-]+)*)";
1364
1365		# a domain is one or more subdomains
1366		$t_subdomain = "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)";
1367		$t_domain    = "(${t_subdomain}(?:\.${t_subdomain})*)";
1368
1369		$s_email_regex = "/${t_recipient}\@${t_domain}/i";
1370	}
1371	return $s_email_regex;
1372}
1373
1374// Parse AUTOPORTS_CONFIG and return a list of generated pairs (port_type, port_name)
1375// for the requested object.
1376function getAutoPorts ($object)
1377{
1378	return parseAutoPortsConfig (getAutoPortsConfig ($object));
1379}
1380
1381// Extract automatic ports schema from the AUTOPORTS_CONFIG
1382// based on the given object's type. Can be overriden
1383function getAutoPortsConfig ($object)
1384{
1385	$override = callHook ('getAutoPortsConfig_hook', $object);
1386	if (isset ($override))
1387		return $override;
1388
1389	$typemap = explode (';', str_replace (' ', '', getConfigVar ('AUTOPORTS_CONFIG')));
1390	foreach ($typemap as $equation)
1391	{
1392		$tmp = explode ('=', $equation);
1393		if (count ($tmp) != 2)
1394			continue;
1395		$objtype_id = $tmp[0];
1396		if ($objtype_id == $object['objtype_id'])
1397			return $tmp[1];
1398	}
1399}
1400
1401function parseAutoPortsConfig ($schema)
1402{
1403	$ret = array();
1404
1405	foreach (explode ('+', $schema) as $product)
1406	{
1407		$tmp = explode ('*', $product);
1408		if (count ($tmp) > 4 || count ($tmp) < 3)
1409			continue;
1410		# format: <number of ports>*<port_type_id>[*<sprintf_name>*<startnumber>]
1411		$nports = $tmp[0];
1412		$port_type = $tmp[1];
1413		$format = $tmp[2];
1414		$startnum = isset ($tmp[3]) ? $tmp[3] : 0;
1415		for ($i = 0; $i < $nports; $i++)
1416			$ret[] = array ('type' => $port_type, 'name' => @sprintf ($format, $i + $startnum));
1417	}
1418	return $ret;
1419}
1420
1421// Use pre-served trace to traverse the tree, then place given node where it belongs.
1422function pokeNode (&$tree, $trace, $key, $value, $threshold = 0)
1423{
1424	$self = __FUNCTION__;
1425	// This function needs the trace to be followed FIFO-way. The fastest
1426	// way to do so is to use array_push() for putting values into the
1427	// list and array_shift() for getting them out. This exposed up to 11%
1428	// performance gain compared to other patterns of array_push/array_unshift/
1429	// array_reverse/array_pop/array_shift conjunction.
1430	$myid = array_shift ($trace);
1431	if (count ($trace)) // not yet reached the target
1432		$self ($tree[$myid]['kids'], $trace, $key, $value, $threshold);
1433	else // just did
1434	{
1435		if (! $threshold || ($threshold && $tree[$myid]['kidc'] + 1 < $threshold))
1436			$tree[$myid]['kids'][$key] = $value;
1437		// Reset accumulated records once, when the limit is reached, not each time
1438		// after that.
1439		if (++$tree[$myid]['kidc'] == $threshold)
1440			$tree[$myid]['kids'] = array();
1441	}
1442}
1443
1444// Likewise traverse the tree with the trace and return the final node.
1445// This function is not currently used in RackTables main code but it works well.
1446function peekNode ($tree, $trace, $target_id)
1447{
1448	$self = __FUNCTION__;
1449	if (NULL === ($next = array_shift ($trace))) // warm
1450	{
1451		foreach ($tree as $node)
1452			if (array_key_exists ('id', $node) && $node['id'] == $target_id) // hot
1453				return $node;
1454	}
1455	else // cold
1456	{
1457		foreach ($tree as $node)
1458			if (array_key_exists ('id', $node) && $node['id'] == $next) // warmer
1459				return $self ($node['kids'], $trace, $target_id);
1460	}
1461	throw new RackTablesError ('inconsistent tree data', RackTablesError::INTERNAL);
1462}
1463
1464// The structure used in RackTables to represent tags is called a forest of rooted
1465// trees, which is a set of directed graphs each having a node appointed as root
1466// and exactly one path possible from the root node to every other node of the
1467// graph.
1468//
1469// The TagTree database table contains a generic list of graph nodes with each node
1470// having an optional incoming directed edge from any existing node. This table
1471// generally can encode any set of any directed graphs that allow at most one
1472// incoming edge per node. This includes but is not limited to the forest of rooted
1473// trees. However, a number of RackTables functions specifically relies upon
1474// consistent relations between the tags (presented either as a complete forest
1475// structure or just as each node's path from the root), hence an early validation
1476// step is required in the PHP code to implement the constraints in full.
1477//
1478// The function below implements this step. For every node on the input list that
1479// belongs to a forest of rooted trees it sets the 'trace' key to the sequence of
1480// node IDs that leads from tree root up to (but not including) the node. As an
1481// edge case, for each root node it sets this sequence to an empty list. For any
1482// nodes not in the forest (i.e., those that form any graph cycle or descend from
1483// such a cycle) it leaves 'trace' unset.
1484function addTraceToNodes ($nodelist)
1485{
1486	foreach ($nodelist as $nodeid => $node)
1487	{
1488		$trace = array();
1489		$parentid = $node['parent_id'];
1490		while ($parentid != NULL)
1491		{
1492			if (! isset ($nodelist[$parentid]))
1493			{
1494				// bad parent_id
1495				$trace = NULL;
1496				break;
1497			}
1498
1499			// check for cycles every 10 steps
1500			if (0 == (count ($trace) % 10) && in_array ($parentid, $trace))
1501			{
1502				// cycle detected
1503				$trace = NULL;
1504				break;
1505			}
1506			array_unshift ($trace, $parentid);
1507			$parentid = $nodelist[$parentid]['parent_id'];
1508		}
1509		if (isset ($trace))
1510			$nodelist[$nodeid]['trace'] = $trace;
1511	}
1512	return $nodelist;
1513}
1514
1515function treeItemCmp ($a, $b)
1516{
1517	return $a['__tree_index'] - $b['__tree_index'];
1518}
1519
1520function getTagTree()
1521{
1522	return treeFromList (getTagUsage());
1523}
1524
1525// Build a tree from the item list and return it. Input and output data is
1526// indexed by item id (nested items in output are recursively stored in 'kids'
1527// key, which is in turn indexed by id. Functions that are ready to handle
1528// tree collapsion/expansion themselves may request non-zero threshold value
1529// for smaller resulting tree.
1530// FIXME: The 2nd argument to this function seems not to be used any more.
1531// FIXME: The structure this function returns is a forest of rooted trees
1532//        despite the terminology it used to use.
1533function treeFromList ($nodelist, $threshold = 0)
1534{
1535	$tree = array();
1536
1537	// Preserve original ordering in __tree_index.
1538	$ti = 0;
1539	foreach (array_keys ($nodelist) as $key)
1540	{
1541		$nodelist[$key]['__tree_index'] = $ti++;
1542		$nodelist[$key]['kidc'] = 0;
1543		$nodelist[$key]['kids'] = array();
1544	}
1545
1546	$done_ids = array();
1547	do
1548	{
1549		$nextpass = FALSE;
1550		foreach (array_keys ($nodelist) as $nodeid)
1551		{
1552			$node = $nodelist[$nodeid];
1553			// Skip any irrelevant nodes early as they will fail the checks below anyway.
1554			if (! array_key_exists ('trace', $node))
1555				continue;
1556			$parentid = $node['parent_id'];
1557			// Moving a node from the input list to the output tree potentially enables more
1558			// nodes to make it from the list to the same tree as well, hence in this case make
1559			// another full round after the current one.
1560
1561			if ($parentid == NULL) // A root node?
1562			{
1563				$tree[$nodeid] = $node;
1564				unset ($nodelist[$nodeid]);
1565				$done_ids[] = $nodeid;
1566				$nextpass = TRUE;
1567			}
1568			elseif (in_array ($parentid, $done_ids)) // Has a direct parent node already on the tree?
1569			{
1570				// Being here implies the current node's trace is at least one element long.
1571				pokeNode ($tree, $node['trace'], $nodeid, $node, $threshold);
1572				unset ($nodelist[$nodeid]);
1573				$done_ids[] = $nodeid;
1574				$nextpass = TRUE;
1575			}
1576		}
1577	}
1578	while ($nextpass);
1579	sortTree ($tree, 'treeItemCmp'); // sort the resulting tree by the order in original list
1580	return $tree;
1581}
1582
1583// Return those tags that belong to the full list of tags but don't belong
1584// to the forest of rooted trees as found by addTraceToNodes().
1585function getInvalidNodes ($nodelist)
1586{
1587	$ret = array();
1588	foreach ($nodelist as $node_id => $node)
1589		if (! array_key_exists ('trace', $node))
1590			$ret[$node_id] = $node;
1591	return $ret;
1592}
1593
1594// Throw an exception unless it is OK to assign the given parent ID
1595// to the node with the given ID.
1596function assertValidParentId ($nodelist, $node_id, $parent_id)
1597{
1598	if ($parent_id == 0)
1599		return;
1600	if ($parent_id == $node_id)
1601		throw new InvalidArgException ('parent_id', $parent_id, 'must be different from the tag ID');
1602	if (! array_key_exists ($parent_id, $nodelist))
1603		throw new InvalidArgException ('parent_id', $parent_id, 'must refer to an existing tag');
1604	if (! array_key_exists ('trace', $nodelist[$parent_id]))
1605		throw new InvalidArgException ('parent_id', $parent_id, 'would add to an existing graph cycle');
1606	if (in_array ($node_id, $nodelist[$parent_id]['trace']))
1607		throw new InvalidArgException ('parent_id', $parent_id, 'would create a new graph cycle');
1608}
1609
1610// Given an existing node ID filter a list of traced nodes and silently skip
1611// the nodes that are not valid parent node options. Filtering criteria are
1612// effectively the same as in the function above but use a simpler expression.
1613function getParentNodeOptionsExisting ($nodelist, $textfield, $node_id)
1614{
1615	$ret = array (0 => '-- NONE --');
1616	foreach ($nodelist as $key => $each)
1617		if
1618		(
1619			$key != $node_id &&
1620			array_key_exists ('trace', $each) &&
1621			! in_array ($node_id, $each['trace'])
1622		)
1623			$ret[$key] = $each[$textfield];
1624	return $ret;
1625}
1626
1627// Idem, but for a new node, which doesn't yet exist, or a node that is based
1628// on a circular reference. The condition is even simpler in this case.
1629function getParentNodeOptionsNew ($nodelist, $textfield)
1630{
1631	$ret = array (0 => '-- NONE --');
1632	foreach ($nodelist as $key => $each)
1633		if (array_key_exists ('trace', $each))
1634			$ret[$key] = $each[$textfield];
1635	return $ret;
1636}
1637
1638// removes implicit tags from ['etags'] array and fills ['itags'] array
1639// Replaces call sequence "getExplicitTagsOnly, getImplicitTags"
1640function sortEntityTags (&$cell)
1641{
1642	global $taglist;
1643	if (! is_array ($cell['etags']))
1644		throw new InvalidArgException ('cell[etags]', $cell['etags']);
1645	$cell['itags'] = array();
1646	foreach ($cell['etags'] as $tag_id => $taginfo)
1647		foreach ($taglist[$tag_id]['trace'] as $parent_id)
1648		{
1649			$cell['itags'][$parent_id] = $taglist[$parent_id];
1650			unset ($cell['etags'][$parent_id]);
1651		}
1652}
1653
1654// Return the list of missing implicit tags.
1655function getImplicitTags ($oldtags)
1656{
1657	global $taglist;
1658	$tmp = array();
1659	foreach ($oldtags as $taginfo)
1660		$tmp = array_merge ($tmp, $taglist[$taginfo['id']]['trace']);
1661	// The call below already includes a call to array_unique().
1662	return buildTagChainFromIds ($tmp);
1663}
1664
1665// Minimize the chain: exclude all implicit tags and return the result.
1666// This function makes use of an external cache with a miss/hit ratio
1667// about 3/7 (ticket:255).
1668function getExplicitTagsOnly ($chain)
1669{
1670	global $taglist, $tagRelCache;
1671	$ret = array();
1672	foreach (array_keys ($chain) as $keyA) // check each A
1673	{
1674		$tagidA = $chain[$keyA]['id'];
1675		// do not include A in result, if A is seen on the trace of any B!=A
1676		foreach (array_keys ($chain) as $keyB)
1677		{
1678			$tagidB = $chain[$keyB]['id'];
1679			if ($tagidA == $tagidB)
1680				continue;
1681			if (!isset ($tagRelCache[$tagidA][$tagidB]))
1682				$tagRelCache[$tagidA][$tagidB] = in_array ($tagidA, $taglist[$tagidB]['trace']);
1683			if ($tagRelCache[$tagidA][$tagidB] === TRUE) // A is ancestor of B
1684				continue 2; // skip this A
1685		}
1686		$ret[] = $chain[$keyA];
1687	}
1688	return $ret;
1689}
1690
1691// Check, if the given tag is present on the chain (will only work
1692// for regular tags with tag ID set.
1693function tagOnChain ($taginfo, $tagchain)
1694{
1695	if (!isset ($taginfo['id']))
1696		return FALSE;
1697	return NULL !== scanArrayForItem ($tagchain, 'id', $taginfo['id']);
1698}
1699
1700function tagNameOnChain ($tagname, $tagchain)
1701{
1702	return NULL !== scanArrayForItem ($tagchain, 'tag', $tagname);
1703}
1704
1705// Return TRUE, if two tags chains differ (order of tags doesn't matter).
1706// Assume that neither of the lists contains duplicates.
1707// FIXME: a faster than O(x^2) method is possible for this calculation.
1708function tagChainCmp ($chain1, $chain2)
1709{
1710	if (count ($chain1) != count ($chain2))
1711		return TRUE;
1712	foreach ($chain1 as $taginfo1)
1713		if (!tagOnChain ($taginfo1, $chain2))
1714			return TRUE;
1715	return FALSE;
1716}
1717
1718// returns an array of tag ids that have $tagid as its parent (all levels)
1719function getTagDescendents ($tagid)
1720{
1721	global $taglist;
1722	$ret = array();
1723	foreach ($taglist as $id => $taginfo)
1724		if (array_key_exists ('trace', $taginfo) && in_array ($tagid, $taginfo['trace']))
1725			$ret[] = $id;
1726	return $ret;
1727}
1728
1729function redirectIfNecessary ()
1730{
1731	global
1732		$trigger,
1733		$pageno,
1734		$tabno;
1735	startSession();
1736	if
1737	(
1738		! isset ($_REQUEST['tab']) &&
1739		isset ($_SESSION['RTLT'][$pageno]) &&
1740		getConfigVar ('SHOW_LAST_TAB') == 'yes' &&
1741		permitted ($pageno, $_SESSION['RTLT'][$pageno]['tabname']) &&
1742		time() - $_SESSION['RTLT'][$pageno]['time'] <= TAB_REMEMBER_TIMEOUT
1743	)
1744		redirectUser (buildRedirectURL ($pageno, $_SESSION['RTLT'][$pageno]['tabname']));
1745
1746	// Fall back to default when a trigger was OK about a tab when generating the previous page
1747	// but isn't OK anymore when generating the current page and the tab is the requested tab.
1748	if
1749	(
1750		isset ($trigger[$pageno][$tabno]) &&
1751		'' == call_user_func ($trigger[$pageno][$tabno])
1752	)
1753	{
1754		$_SESSION['RTLT'][$pageno]['dont_remember'] = 1;
1755		redirectUser (buildRedirectURL ($pageno, 'default'));
1756	}
1757	if (is_array (@$_SESSION['RTLT'][$pageno]) && isset ($_SESSION['RTLT'][$pageno]['dont_remember']))
1758		unset ($_SESSION['RTLT'][$pageno]['dont_remember']);
1759	// store the last visited tab name
1760	if (isset ($_REQUEST['tab']))
1761		$_SESSION['RTLT'][$pageno] = array ('tabname' => $tabno, 'time' => time());
1762	session_commit(); // There was no redirection, unlock the session data.
1763}
1764
1765function prepareNavigation()
1766{
1767	global $pageno, $tabno;
1768	$pageno = array_fetch ($_REQUEST, 'page', 'index');
1769	$tabno = array_fetch ($_REQUEST, 'tab', 'default');
1770}
1771
1772function fixContext ($target = NULL)
1773{
1774	global
1775		$pageno,
1776		$auto_tags,
1777		$expl_tags,
1778		$impl_tags,
1779		$target_given_tags,
1780		$user_given_tags,
1781		$etype_by_pageno;
1782
1783	if ($target !== NULL)
1784	{
1785		$target_given_tags = $target['etags'];
1786		// Do not reset the autochain as it may have items generated during the authentication step.
1787		// Ignore the "user" realm to keep the displayed user account autotags out of the
1788		// effective security context.
1789		if ($target['realm'] != 'user')
1790			$auto_tags = array_merge ($auto_tags, $target['atags']);
1791	}
1792	elseif (array_key_exists ($pageno, $etype_by_pageno))
1793	{
1794		// Each page listed in the map above requires one natural argument.
1795		$target = spotEntity ($etype_by_pageno[$pageno], getBypassValue());
1796		$target_given_tags = $target['etags'];
1797		if ($target['realm'] != 'user')
1798			$auto_tags = array_merge ($auto_tags, $target['atags']);
1799	}
1800	elseif ($pageno == 'ipaddress' && $net = spotNetworkByIP (getBypassValue()))
1801	{
1802		// IP addresses inherit context tags from their parent networks
1803		$target_given_tags = $net['etags'];
1804		$auto_tags = array_merge ($auto_tags, $net['atags']);
1805	}
1806	// Explicit and implicit chains should be normally empty at this point, so
1807	// overwrite the contents anyway.
1808	$expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1809	$impl_tags = getImplicitTags ($expl_tags);
1810}
1811
1812# Merge e/i/a-tags of the given cell structures into current context, when
1813# these aren't there yet.
1814function spreadContext ($extracell)
1815{
1816	global
1817		$auto_tags,
1818		$expl_tags,
1819		$impl_tags,
1820		$target_given_tags,
1821		$user_given_tags;
1822	foreach ($extracell['atags'] as $taginfo)
1823		if (! tagNameOnChain ($taginfo['tag'], $auto_tags))
1824			$auto_tags[] = $taginfo;
1825	$target_given_tags = mergeTagChains ($target_given_tags, $extracell['etags']);
1826	$expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1827	$impl_tags = getImplicitTags ($expl_tags);
1828}
1829
1830# return a structure suitable for feeding into restoreContext()
1831function getContext()
1832{
1833	global
1834		$auto_tags,
1835		$expl_tags,
1836		$impl_tags,
1837		$target_given_tags;
1838	return array
1839	(
1840		'auto_tags' => $auto_tags,
1841		'expl_tags' => $expl_tags,
1842		'impl_tags' => $impl_tags,
1843		'target_given_tags' => $target_given_tags,
1844	);
1845}
1846
1847function restoreContext ($ctx)
1848{
1849	global
1850		$auto_tags,
1851		$expl_tags,
1852		$impl_tags,
1853		$target_given_tags;
1854	$auto_tags = $ctx['auto_tags'];
1855	$expl_tags = $ctx['expl_tags'];
1856	$impl_tags = $ctx['impl_tags'];
1857	$target_given_tags = $ctx['target_given_tags'];
1858}
1859
1860// Take a list of user-supplied tag IDs to build a list of valid taginfo
1861// records indexed by tag IDs (tag chain).
1862function buildTagChainFromIds ($tagidlist)
1863{
1864	global $taglist;
1865	$ret = array();
1866	foreach (array_unique ($tagidlist) as $tag_id)
1867		if (isset ($taglist[$tag_id]))
1868			$ret[] = $taglist[$tag_id];
1869	return $ret;
1870}
1871
1872function buildTagIdsFromChain ($tagchain)
1873{
1874	$ret = array();
1875	foreach ($tagchain as $taginfo)
1876		$ret[] = $taginfo['id'];
1877	return array_unique ($ret);
1878}
1879
1880// Process a given tag tree and return only meaningful branches. The resulting
1881// (sub)tree will have refcnt leaves on every last branch.
1882function getObjectiveTagTree ($tree, $realm, $preselect)
1883{
1884	$self = __FUNCTION__;
1885	$ret = array();
1886	foreach ($tree as $taginfo)
1887	{
1888		$subsearch = $self ($taginfo['kids'], $realm, $preselect);
1889		// If the current node addresses something, add it to the result
1890		// regardless of how many sub-nodes it features.
1891		if
1892		(
1893			isset ($taginfo['refcnt'][$realm]) ||
1894			count ($subsearch) > 1 ||
1895			in_array ($taginfo['id'], $preselect)
1896		)
1897			$ret[] = array
1898			(
1899				'id' => $taginfo['id'],
1900				'tag' => $taginfo['tag'],
1901				'parent_id' => $taginfo['parent_id'],
1902				'refcnt' => $taginfo['refcnt'],
1903				'color' => $taginfo['color'],
1904				'description' => $taginfo['description'],
1905				'kids' => $subsearch
1906			);
1907		else
1908			$ret = array_merge ($ret, $subsearch);
1909	}
1910	return $ret;
1911}
1912
1913// Preprocess tag tree to get only tags that can effectively reduce given filter result,
1914// then pass shrinked tag tree to getObjectiveTagTree and return its result.
1915// This makes sense only if andor mode is 'and', otherwise function does not modify tree.
1916// 'Given filter' is a pair of $entity_list(filter result) and $preselect(filter data).
1917// 'Effectively' means reduce to non-empty result.
1918function getShrinkedTagTree ($entity_list, $realm, $preselect)
1919{
1920	$tagtree = getTagTree();
1921	if ($preselect['andor'] != 'and' || empty($entity_list) && $preselect['is_empty'])
1922		return getObjectiveTagTree($tagtree, $realm, $preselect['tagidlist']);
1923
1924	$used_tags = array(); //associative, keys - tag ids, values - taginfos
1925	foreach ($entity_list as $entity)
1926	{
1927		foreach ($entity['etags'] as $etag)
1928			if (! array_key_exists($etag['id'], $used_tags))
1929				$used_tags[$etag['id']] = 1;
1930			else
1931				$used_tags[$etag['id']]++;
1932
1933		foreach ($entity['itags'] as $itag)
1934			if (! array_key_exists($itag['id'], $used_tags))
1935				$used_tags[$itag['id']] = 0;
1936	}
1937
1938	$shrinked_tree = shrinkSubtree($tagtree, $used_tags, $preselect, $realm);
1939	return getObjectiveTagTree($shrinked_tree, $realm, $preselect['tagidlist']);
1940}
1941
1942// deletes item from tag subtree unless it exists in $used_tags and not preselected
1943function shrinkSubtree ($tree, $used_tags, $preselect, $realm)
1944{
1945	$self = __FUNCTION__;
1946
1947	foreach ($tree as $i => &$item)
1948	{
1949		$item['kids'] = $self($item['kids'], $used_tags, $preselect, $realm);
1950		$item['kidc'] = count($item['kids']);
1951		if
1952		(
1953			! array_key_exists($item['id'], $used_tags) &&
1954			! in_array($item['id'], $preselect['tagidlist']) &&
1955			! $item['kidc']
1956		)
1957			unset($tree[$i]);
1958		else
1959		{
1960			if (isset ($used_tags[$item['id']]) && $used_tags[$item['id']])
1961				$item['refcnt'][$realm] = $used_tags[$item['id']];
1962			else
1963				unset($item['refcnt'][$realm]);
1964		}
1965	}
1966	return $tree;
1967}
1968
1969// Get taginfo record by tag name, return NULL, if record doesn't exist.
1970function getTagByName ($tag_name)
1971{
1972	global $taglist;
1973	static $cache = NULL;
1974	if (! isset ($cache))
1975	{
1976		$cache = array();
1977		foreach ($taglist as $key => $taginfo)
1978			$cache[$taginfo['tag']] = $taginfo;
1979	}
1980	return array_fetch($cache, $tag_name, NULL);
1981}
1982
1983// Merge two chains, filtering dupes out. Return the resulting superset.
1984function mergeTagChains ($chainA, $chainB)
1985{
1986	// $ret = $chainA;
1987	// Reindex by tag id in any case.
1988	$ret = array();
1989	foreach ($chainA as $tag)
1990		$ret[$tag['id']] = $tag;
1991	foreach ($chainB as $tag)
1992		if (!isset ($ret[$tag['id']]))
1993			$ret[$tag['id']] = $tag;
1994	return $ret;
1995}
1996
1997# Return a list consisting of tag ID of the given tree node and IDs of all
1998# nodes it contains.
1999# This is an earlier and more generic variety of getTagDescendents().
2000function getTagIDListForNode ($treenode)
2001{
2002	$self = __FUNCTION__;
2003	$ret = array ($treenode['id']);
2004	foreach ($treenode['kids'] as $item)
2005		$ret = array_merge ($ret, $self ($item));
2006	return $ret;
2007}
2008
2009function applyCellFilter ($realm, $cellfilter, $parent_id = NULL)
2010{
2011	if ($res = callHook ('applyCellFilter_hook', $realm, $cellfilter, $parent_id))
2012		return $res;
2013	return filterCellList (listCells ($realm, $parent_id), $cellfilter['expression']);
2014}
2015
2016function getCellFilter ()
2017{
2018	global $sic;
2019	global $pageno;
2020	$andor_used = FALSE;
2021	startSession();
2022	// On the form submit "andor" appears on the request and means the user is
2023	// trying to start a new filter or clear the existing one.
2024	if (isset($_REQUEST['andor']))
2025		$andor_used = TRUE;
2026	if ($andor_used || array_key_exists ('clear-cf', $_REQUEST))
2027		unset($_SESSION[$pageno]); // delete saved filter
2028
2029	// otherwise inject saved filter to the $_REQUEST and $sic vars
2030	elseif (isset ($_SESSION[$pageno]['filter']) && is_array ($_SESSION[$pageno]['filter']) && getConfigVar ('STATIC_FILTER') == 'yes')
2031		foreach (array('andor', 'cfe', 'cft[]', 'cfp[]', 'nft[]', 'nfp[]') as $param)
2032		{
2033			$param = str_replace ('[]', '', $param, $is_array);
2034			if (! isset ($_REQUEST[$param]) && isset ($_SESSION[$pageno]['filter'][$param]) && (! $is_array || is_array ($_SESSION[$pageno]['filter'][$param])))
2035			{
2036				$_REQUEST[$param] = $_SESSION[$pageno]['filter'][$param];
2037				if (! $is_array)
2038					$sic[$param] = $_REQUEST[$param];
2039			}
2040		}
2041
2042	$ret = array
2043	(
2044		'tagidlist' => array(),
2045		'tnamelist' => array(),
2046		'pnamelist' => array(),
2047		'negatedlist' => array(),
2048		'andor' => '',
2049		'text' => '',
2050		'extratext' => '',
2051		'expression' => array(),
2052		'urlextra' => '', // Just put text here and let makeHref call urlencode().
2053		'is_empty' => TRUE,
2054	);
2055	switch (TRUE)
2056	{
2057	case (!isset ($_REQUEST['andor'])):
2058		$andor = getConfigVar ('FILTER_DEFAULT_ANDOR');
2059		break;
2060	case ($_REQUEST['andor'] == 'and'):
2061	case ($_REQUEST['andor'] == 'or'):
2062		$_SESSION[$pageno]['filter']['andor'] = $_REQUEST['andor'];
2063		$ret['andor'] = $andor = $_REQUEST['andor'];
2064		break;
2065	default:
2066		showWarning ('Invalid and/or switch value in submitted form');
2067		session_commit();
2068		return NULL;
2069	}
2070	// Both tags and predicates that don't exist, should be
2071	// handled somehow. Discard them silently for now.
2072	global $taglist, $pTable;
2073	foreach (array ('cft', 'cfp', 'nft', 'nfp') as $param)
2074		if (isset ($_REQUEST[$param]) && is_array ($_REQUEST[$param]))
2075		{
2076			$_SESSION[$pageno]['filter'][$param] = $_REQUEST[$param];
2077			foreach ($_REQUEST[$param] as $req_key)
2078			{
2079				if (strpos ($param, 'ft') !== FALSE)
2080				{
2081					// param is a taglist
2082					if (! isset ($taglist[$req_key]))
2083						continue;
2084					$ret['tagidlist'][] = $req_key;
2085					$ret['tnamelist'][] = $taglist[$req_key]['tag'];
2086					$text = '{' . $taglist[$req_key]['tag'] . '}';
2087				}
2088				else
2089				{
2090					// param is a predicate list
2091					if (! isset ($pTable[$req_key]))
2092						continue;
2093					$ret['pnamelist'][] = $req_key;
2094					$text = '[' . $req_key . ']';
2095				}
2096				if (strpos ($param, 'nf') === 0)
2097				{
2098					$text = "not $text";
2099					$ret['negatedlist'][] = $req_key;
2100				}
2101				if (! empty ($ret['text']))
2102				{
2103					$andor_used = TRUE;
2104					$ret['text'] .= " $andor ";
2105				}
2106				$ret['text'] .= $text;
2107				$ret['urlextra'] .= '&' . $param . '[]=' . $req_key;
2108			}
2109		}
2110	// Extra text comes from TEXTAREA and is easily screwed by standard escaping function.
2111	if (isset ($sic['cfe']))
2112	{
2113		$_SESSION[$pageno]['filter']['cfe'] = $sic['cfe'];
2114		$ret['extratext'] = trim ($sic['cfe']);
2115		$ret['urlextra'] .= '&cfe=' . $ret['extratext'];
2116	}
2117	$finaltext = array();
2118	if ($ret['text'] != '')
2119		$finaltext[] = '(' . $ret['text'] . ')';
2120	if ($ret['extratext'] != '')
2121		$finaltext[] = '(' . $ret['extratext'] . ')';
2122	$andor_used = $andor_used || (count($finaltext) > 1);
2123	$finaltext = implode (' ' . $andor . ' ', $finaltext);
2124	if ($finaltext != '')
2125	{
2126		$ret['is_empty'] = FALSE;
2127		$ret['expression'] = compileExpression ($finaltext);
2128		// It's not quite fair enough to put the blame of the whole text onto
2129		// non-empty "extra" portion of it, but it's the only user-generated portion
2130		// of it, thus the most probable cause of parse error.
2131		if ($ret['extratext'] != '')
2132			$ret['extraclass'] = $ret['expression'] ? 'validation-success' : 'validation-error';
2133	}
2134	if (! $andor_used)
2135		$ret['andor'] = getConfigVar ('FILTER_DEFAULT_ANDOR');
2136	else
2137		$ret['urlextra'] .= '&andor=' . $ret['andor'];
2138	session_commit();
2139	return $ret;
2140}
2141
2142function buildRedirectURL ($nextpage = NULL, $nexttab = NULL, $moreArgs = array())
2143{
2144	$params = array();
2145	if ($nextpage !== NULL)
2146		$params['page'] = $nextpage;
2147	if ($nexttab !== NULL)
2148		$params['tab'] = $nexttab;
2149	$params = makePageParams ($params + $moreArgs);
2150	unset ($params['module']); // 'interface' module is the default
2151	return makeHref ($params);
2152}
2153
2154// store the accumulated message list into he $SESSION array to display them later
2155function backupLogMessages()
2156{
2157	global $log_messages;
2158	if (! empty ($log_messages))
2159	{
2160		startSession();
2161		$_SESSION['log'] = $log_messages;
2162		session_commit();
2163	}
2164}
2165
2166function redirectUser ($url)
2167{
2168	backupLogMessages();
2169	header ("Location: " . $url);
2170	die;
2171}
2172
2173function getRackImageWidth ()
2174{
2175	global $rtwidth;
2176	return 3 + $rtwidth[0] + $rtwidth[1] + $rtwidth[2] + 3;
2177}
2178
2179function getRackImageHeight ($units)
2180{
2181	return 3 + 3 + $units * 2;
2182}
2183
2184// Indicate occupation state of each IP address: none, ordinary or problematic.
2185// Returns number of marked up (busy) addresses
2186function markupIPAddrList (&$addrlist)
2187{
2188	$used = 0;
2189	foreach (array_keys ($addrlist) as $ip_bin)
2190	{
2191		$refc = array
2192		(
2193			'shared' => 0,  // virtual
2194			'virtual' => 0, // loopback
2195			'regular' => 0, // connected host
2196			'router' => 0,   // connected gateway
2197			'sharedrouter' => 0, // VRRP gateway
2198			'point2point' => 0,
2199		);
2200		$nallocs = 0;
2201		foreach ($addrlist[$ip_bin]['allocs'] as $a)
2202		{
2203			$refc[$a['type']]++;
2204			$nallocs++;
2205		}
2206		$nreserved = ($addrlist[$ip_bin]['reserved'] == 'yes') ? 1 : 0; // only one reservation is possible ever
2207		if ($nallocs && $nreserved ||                                                     // reserved cannot be allocated
2208                    $nallocs > 1 && $nallocs != $refc['shared'] && $refc['sharedrouter'] == 0 ||  // multiple shared IPs allowed
2209                    $nallocs > 1 && $nallocs != $refc['sharedrouter'] && $refc['shared'] ==0)     // multiple shared routers allowed
2210		{
2211			$addrlist[$ip_bin]['class'] = 'trerror';
2212			++$used;
2213		}
2214		elseif (! isIPAddressEmpty ($addrlist[$ip_bin], array ('name', 'comment', 'inpf', 'outpf'))) // these fields don't trigger the 'busy' status
2215		{
2216			$addrlist[$ip_bin]['class'] = 'trbusy';
2217			++$used;
2218		}
2219		else
2220			$addrlist[$ip_bin]['class'] = '';
2221	}
2222	return $used;
2223}
2224
2225function findNetRouters ($net)
2226{
2227	if (isset ($net['own_addrlist']))
2228		$own_addrlist = $net['own_addrlist'];
2229	else
2230	{
2231		// do not call loadIPAddrList, it is expensive.
2232		// instead, do our own DB scan only for router allocations
2233		$rtrlist = scanIPNet ($net, IPSCAN_DO_ALLOCS | IPSCAN_RTR_ONLY);
2234		$own_addrlist = filterOwnAddrList ($net, $rtrlist);
2235	}
2236	return findRouters ($own_addrlist);
2237}
2238
2239// Scan the given address list (returned by scanIPv4Space/scanIPv6Space) and return a list of all routers found.
2240function findRouters ($addrlist)
2241{
2242	$ret = array();
2243	foreach ($addrlist as $addr)
2244		foreach ($addr['allocs'] as $alloc)
2245			if ($alloc['type'] == 'router' || $alloc['type'] == 'sharedrouter')
2246				$ret[] = array
2247				(
2248					'id' => $alloc['object_id'],
2249					'iface' => $alloc['name'],
2250					'dname' => $alloc['object_name'],
2251					'ip_bin' => $addr['ip_bin'],
2252				);
2253	return $ret;
2254}
2255
2256function numSign ($x)
2257{
2258	if ($x < 0)
2259		return -1;
2260	if ($x > 0)
2261		return 1;
2262	return 0;
2263}
2264
2265function numCompare ($a, $b)
2266{
2267	return numSign ($a - $b);
2268}
2269
2270// compare binary IPs (IPv4 are less than IPv6)
2271// valid return values are: 1, 0, -1
2272function IPCmp ($ip_binA, $ip_binB)
2273{
2274	if (strlen ($ip_binA) !== strlen ($ip_binB))
2275		return numCompare (strlen ($ip_binA), strlen ($ip_binB));
2276	return numSign (strcmp ($ip_binA, $ip_binB));
2277}
2278
2279// Binary compare the first addresses of each pair
2280// If the first addresses of the compared pairs are equal, compare last addresses in reverse order
2281// valid return values are: 1, 0, -1
2282function IPSpaceCmp ($pairA, $pairB)
2283{
2284	$first = IPCmp ($pairA['first'], $pairB['first']);
2285	return $first ? $first : IPCmp ($pairB['last'], $pairA['last']);
2286}
2287
2288// filter netsted ip pairs
2289function reduceIPPairList ($pairlist)
2290{
2291	$ret = array();
2292	$left = $pairlist;
2293	usort ($left, 'IPSpaceCmp');
2294	while ($left)
2295	{
2296		$agg = array_shift ($left);
2297		$ret[] = $agg;
2298		foreach ($left as $id => $pair)
2299			if ($pair['first'] >= $agg['first'] && $pair['last'] <= $agg['last'])
2300				unset ($left[$id]);
2301	}
2302	return $ret;
2303}
2304
2305// Compare networks. When sorting a tree, the records on the list will have
2306// distinct base IP addresses.
2307// valid return values are: 2, 1, 0, -1, -2
2308// -2, 2 have special meaning: $netA includes $netB or vice versa, respecively
2309// "The comparison function must return an integer less than, equal to, or greater
2310// than zero if the first argument is considered to be respectively less than,
2311// equal to, or greater than the second." (c) PHP manual
2312function IPNetworkCmp ($netA, $netB)
2313{
2314	$ret = IPCmp ($netA['ip_bin'], $netB['ip_bin']);
2315	if ($ret == 0)
2316		$ret = $netA['mask'] < $netB['mask'] ? -1 : ($netA['mask'] > $netB['mask'] ? 1 : 0);
2317	if ($ret == -1 && $netA['ip_bin'] === ($netB['ip_bin'] & $netA['mask_bin']))
2318		$ret = -2;
2319	if ($ret == 1 && $netB['ip_bin'] === ($netA['ip_bin'] & $netB['mask_bin']))
2320		$ret = 2;
2321	return $ret;
2322}
2323
2324function IPNetContainsOrEqual ($netA, $netB)
2325{
2326	$res = IPNetworkCmp ($netA, $netB);
2327	return ($res == -2 || $res == 0);
2328}
2329
2330function IPNetContains ($netA, $netB)
2331{
2332	return (-2 == IPNetworkCmp ($netA, $netB));
2333}
2334
2335function IPNetsIntersect ($netA, $netB)
2336{
2337	return ($netA['ip_bin'] & $netB['mask_bin']) === $netB['ip_bin'] ||
2338		($netB['ip_bin'] & $netA['mask_bin']) === $netA['ip_bin'];
2339}
2340
2341function ip_in_range ($ip_bin, $range)
2342{
2343	return ($ip_bin & $range['mask_bin']) === $range['ip_bin'];
2344}
2345
2346// Sort each level of the tree independently using the given compare function.
2347function sortTree (&$tree, $cmpfunc = '')
2348{
2349	$self = __FUNCTION__;
2350	if (! is_callable ($cmpfunc))
2351		throw new InvalidArgException ('cmpfunc', $cmpfunc, 'is not a callable function');
2352	usort ($tree, $cmpfunc);
2353	foreach (array_keys ($tree) as $tagid)
2354		$self ($tree[$tagid]['kids'], $cmpfunc);
2355}
2356
2357function iptree_fill (&$netdata)
2358{
2359	if (! isset ($netdata['kids']) || ! count ($netdata['kids']))
2360		return;
2361
2362	foreach ($netdata['spare_ranges'] as $mask => $list)
2363	{
2364		$spare_mask = $mask;
2365		// align spare IPv6 nets by nibble boundary
2366		if (strlen ($netdata['ip_bin']) == 16 && $mask % 4)
2367			$spare_mask = $mask + 4 - ($mask % 4);
2368		foreach ($list as $ip_bin)
2369			foreach (splitNetworkByMask (constructIPRange ($ip_bin, $mask), $spare_mask) as $spare)
2370				$netdata['kids'][] = $spare + array('kids' => array(), 'kidc' => 0, 'name' => '');
2371	}
2372
2373	if (count ($netdata['kids']) != $netdata['kidc'])
2374	{
2375		$netdata['kidc'] = count ($netdata['kids']);
2376		usort ($netdata['kids'], 'IPNetworkCmp');
2377	}
2378}
2379
2380// returns TRUE if inet_ntop and inet_pton functions exist
2381function is_inet_avail()
2382{
2383	static $ret = NULL;
2384	if (! isset ($ret))
2385		$ret = is_callable ('inet_pton') && is_callable ('inet_ntop');
2386	return $ret;
2387}
2388
2389// returns TRUE if inet_ntop and inet_pton functions exist and support IPv6
2390function is_inet6_avail()
2391{
2392	static $ret = NULL;
2393	if (! isset ($ret))
2394		$ret = is_inet_avail() && defined ('AF_INET6');
2395	return $ret;
2396}
2397
2398function ip_format ($ip_bin)
2399{
2400	switch (strlen ($ip_bin))
2401	{
2402		case 4:  return ip4_format ($ip_bin);
2403		case 16: return ip6_format ($ip_bin);
2404		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2405	}
2406}
2407
2408function ip4_format ($ip_bin)
2409{
2410	if (4 == strlen ($ip_bin))
2411	{
2412		if (is_inet_avail())
2413		{
2414			$ret = @inet_ntop ($ip_bin);
2415			if ($ret !== FALSE)
2416				return $ret;
2417		}
2418		else
2419			return implode ('.', unpack ('C*', $ip_bin));
2420	}
2421	throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2422}
2423
2424function ip6_format ($ip_bin)
2425{
2426	do {
2427		if (16 != strlen ($ip_bin))
2428			break;
2429
2430		if (is_inet6_avail())
2431		{
2432			$ret = @inet_ntop ($ip_bin);
2433			if ($ret !== FALSE)
2434				return $ret;
2435			break;
2436		}
2437
2438		// maybe this is IPv6-to-IPv4 address?
2439		if (substr ($ip_bin, 0, 12) == "\0\0\0\0\0\0\0\0\0\0\xff\xff")
2440			return '::ffff:' . implode ('.', unpack ('C*', substr ($ip_bin, 12, 4)));
2441
2442		$result = array();
2443		$hole_index = NULL;
2444		$max_hole_index = NULL;
2445		$hole_length = 0;
2446		$max_hole_length = 0;
2447
2448		for ($i = 0; $i < 8; $i++)
2449		{
2450			$unpacked = unpack ('n', substr ($ip_bin, $i * 2, 2));
2451			$value = array_shift ($unpacked);
2452			$result[] = dechex ($value & 0xffff);
2453			if ($value != 0)
2454			{
2455				unset ($hole_index);
2456				$hole_length = 0;
2457			}
2458			else
2459			{
2460				if (! isset ($hole_index))
2461					$hole_index = $i;
2462				if (++$hole_length >= $max_hole_length)
2463				{
2464					$max_hole_index = $hole_index;
2465					$max_hole_length = $hole_length;
2466				}
2467			}
2468		}
2469		if (isset ($max_hole_index))
2470		{
2471			array_splice ($result, $max_hole_index, $max_hole_length, array (''));
2472			if ($max_hole_index == 0 && $max_hole_length == 8)
2473				return '::';
2474			elseif ($max_hole_index == 0)
2475				return ':' . implode (':', $result);
2476			elseif ($max_hole_index + $max_hole_length == 8)
2477				return implode (':', $result) . ':';
2478		}
2479		return implode (':', $result);
2480	} while (FALSE);
2481
2482	throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2483}
2484
2485function ip_parse ($ip)
2486{
2487	try
2488	{
2489		if (FALSE !== strpos ($ip, ':'))
2490			return ip6_parse ($ip);
2491		else
2492			return ip4_parse ($ip);
2493	}
2494	catch (InvalidArgException $e)
2495	{
2496		// re-throw with general error message, without specifying an IP family
2497		throw new InvalidArgException ('ip', $ip, "Invalid IP address");
2498	}
2499}
2500
2501function ip4_parse ($ip)
2502{
2503	if (is_inet_avail())
2504	{
2505		if (FALSE !== ($ret = @inet_pton ($ip)) && strlen ($ret) == 4)
2506			return $ret;
2507	}
2508	elseif (FALSE !== ($int = ip2long ($ip)))
2509		return pack ('N', $int);
2510
2511	throw new InvalidArgException ('ip', $ip, "Invalid IPv4 address");
2512}
2513
2514// returns 16-byte string ip_bin
2515// throws exception if unable to parse
2516function ip6_parse ($ip)
2517{
2518	do {
2519		if (is_inet6_avail())
2520		{
2521			if (FALSE !== ($ret = @inet_pton ($ip)) && strlen ($ret) == 16)
2522				return $ret;
2523			break;
2524		}
2525
2526		if (empty ($ip))
2527			break;
2528
2529		$result = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; // 16 bytes
2530		// remove one of double beginning/tailing colons
2531		if (substr ($ip, 0, 2) == '::')
2532			$ip = substr ($ip, 1);
2533		elseif (substr ($ip, -2, 2) == '::')
2534			$ip = substr ($ip, 0, strlen ($ip) - 1);
2535
2536		$tokens = explode (':', $ip);
2537		$last_token = $tokens[count ($tokens) - 1];
2538		$split = explode ('.', $last_token);
2539		if (count ($split) == 4)
2540		{
2541			$hex_tokens = array();
2542			$hex_tokens[] = dechex ($split[0] * 256 + $split[1]);
2543			$hex_tokens[] = dechex ($split[2] * 256 + $split[3]);
2544			array_splice ($tokens, -1, 1, $hex_tokens);
2545		}
2546		if (count ($tokens) > 8)
2547			break;
2548		for ($i = 0; $i < count ($tokens); $i++)
2549		{
2550			if ($tokens[$i] != '')
2551			{
2552				if (! set_word_value ($result, $i, $tokens[$i]))
2553					break;
2554			}
2555			else
2556			{
2557				$k = 8; //index in result string (last word)
2558				for ($j = count ($tokens) - 1; $j > $i; $j--) // $j is an index in $tokens for reverse walk
2559					if ($tokens[$j] == '')
2560						break;
2561					elseif (! set_word_value ($result, --$k, $tokens[$j]))
2562						break;
2563				if ($i != $j)
2564					break; //error, more than 1 '::' range
2565				break;
2566			}
2567		}
2568		if (! isset ($k) && count ($tokens) != 8)
2569			break;
2570		return $result;
2571	} while (FALSE);
2572
2573	throw new InvalidArgException ('ip', $ip, "Invalid IPv6 address");
2574}
2575
2576function ip_get_arpa ($ip_bin)
2577{
2578	switch (strlen ($ip_bin))
2579	{
2580		case 4:  return ip4_get_arpa ($ip_bin);
2581		case 16: return ip6_get_arpa ($ip_bin);
2582		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2583	}
2584}
2585
2586function ip4_get_arpa ($ip_bin)
2587{
2588	$ret = '';
2589	for ($i = 3; $i >= 0; $i--)
2590		$ret .= ord($ip_bin[$i]) . '.';
2591	return $ret . 'in-addr.arpa';
2592}
2593
2594function ip6_get_arpa ($ip_bin)
2595{
2596	$ret = '';
2597	$hex = implode ('', unpack('H32', $ip_bin));
2598	for ($i = 31; $i >= 0; $i--)
2599		$ret .= $hex[$i] . '.';
2600	return $ret . 'ip6.arpa';
2601}
2602
2603function set_word_value (&$haystack, $nword, $hexvalue)
2604{
2605	// check that $hexvalue is like /^[0-9a-fA-F]*$/
2606	for ($i = 0; $i < strlen ($hexvalue); $i++)
2607	{
2608		$char = ord ($hexvalue[$i]);
2609		if (! ($char >= 0x30 && $char <= 0x39 || $char >= 0x41 && $char <= 0x46 || $char >=0x61 && $char <= 0x66))
2610			return FALSE;
2611	}
2612	$haystack = substr_replace ($haystack, pack ('n', hexdec ($hexvalue)), $nword * 2, 2);
2613	return TRUE;
2614}
2615
2616// returns binary IP or FALSE
2617function ip_checkparse ($ip)
2618{
2619	try
2620	{
2621		return ip_parse ($ip);
2622	}
2623	catch (InvalidArgException $e)
2624	{
2625		return FALSE;
2626	}
2627}
2628
2629// returns binary IP or FALSE
2630function ip4_checkparse ($ip)
2631{
2632	try
2633	{
2634		return ip4_parse ($ip);
2635	}
2636	catch (InvalidArgException $e)
2637	{
2638		return FALSE;
2639	}
2640}
2641
2642// returns binary IP or FALSE
2643function ip6_checkparse ($ip)
2644{
2645	try
2646	{
2647		return ip6_parse ($ip);
2648	}
2649	catch (InvalidArgException $e)
2650	{
2651		return FALSE;
2652	}
2653}
2654
2655function ip4_int2bin ($ip_int)
2656{
2657	return pack ('N', $ip_int + 0);
2658}
2659
2660function ip4_bin2int ($ip_bin)
2661{
2662	if (4 != strlen ($ip_bin))
2663		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2664	$ret = array_first (unpack ('N', $ip_bin));
2665	if (PHP_INT_SIZE > 4 && $ret < 0)
2666		$ret = $ret & 0xffffffff;
2667	return $ret;
2668}
2669
2670// Use this function only when you need to export binary ip out of PHP running context (e.g., DB)
2671// !DO NOT perform arithmetic and bitwise operations with the result of this function!
2672function ip4_bin2db ($ip_bin)
2673{
2674	$ip_int = ip4_bin2int ($ip_bin);
2675	return $ip_int >= 0 ? $ip_int : sprintf ('%u', 0x00000000 + $ip_int);
2676}
2677
2678function ip_last ($net)
2679{
2680	return $net['ip_bin'] | ~$net['mask_bin'];
2681}
2682
2683function ip_next ($ip_bin)
2684{
2685	$ret = $ip_bin;
2686	$p = 1;
2687	for ($i = strlen ($ret) - 1; $i >= 0; $i--)
2688	{
2689		$oct = $p + ord ($ret[$i]);
2690		$ret[$i] = chr ($oct & 0xff);
2691		if ($oct <= 255 && $oct >= 0)
2692			break;
2693	}
2694	return $ret;
2695}
2696
2697function ip_prev ($ip_bin)
2698{
2699	$ret = $ip_bin;
2700	$p = -1;
2701	for ($i = strlen ($ret) - 1; $i >= 0; $i--)
2702	{
2703		$oct = $p + ord ($ret[$i]);
2704		$ret[$i] = chr ($oct & 0xff);
2705		if ($oct <= 255 && $oct >= 0)
2706			break;
2707	}
2708	return $ret;
2709}
2710
2711function ip4_range_size ($range)
2712{
2713	return ip4_mask_size ($range['mask']);
2714}
2715
2716function ip4_mask_size ($mask)
2717{
2718	switch (TRUE)
2719	{
2720		case ($mask > 1 && $mask <= 32):
2721			return (0x7fffffff >> ($mask - 1)) + 1;
2722		// constants below are not representable in 32-bit PHP's int type,
2723		// so they are literally hardcoded and returned as strings on 32-bit architecture.
2724		case ($mask == 1):
2725			return 2147483648;
2726		case ($mask == 0):
2727			return 4294967296;
2728		default:
2729			throw new InvalidArgException ('mask', $mask, 'Invalid IPv4 prefix length');
2730	}
2731}
2732
2733// returns array with keys 'ip', 'ip_bin', 'mask', 'mask_bin'
2734function constructIPRange ($ip_bin, $mask = NULL)
2735{
2736	$node = array();
2737	switch (strlen ($ip_bin))
2738	{
2739		case 4: // IPv4
2740			if ($mask === NULL)
2741				$mask = 32;
2742			elseif (! isUnsignedInteger ($mask) || $mask > 32)
2743				throw new InvalidArgException ('mask', $mask, "Invalid v4 prefix length");
2744			$node['mask_bin'] = ip4_mask ($mask);
2745			$node['mask'] = $mask;
2746			$node['ip_bin'] = $ip_bin & $node['mask_bin'];
2747			$node['ip'] = ip4_format ($node['ip_bin']);
2748			break;
2749		case 16: // IPv6
2750			if ($mask === NULL)
2751				$mask = 128;
2752			elseif (! isUnsignedInteger ($mask) || $mask > 128)
2753				throw new InvalidArgException ('mask', $mask, "Invalid v6 prefix length");
2754			$node['mask_bin'] = ip6_mask ($mask);
2755			$node['mask'] = $mask;
2756			$node['ip_bin'] = $ip_bin & $node['mask_bin'];
2757			$node['ip'] = ip6_format ($node['ip_bin']);
2758			break;
2759		default:
2760			throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2761	}
2762	return $node;
2763}
2764
2765// Return minimal IP address structure
2766function constructIPAddress ($ip_bin)
2767{
2768	// common v4/v6 part
2769	$ret = array
2770	(
2771		'ip' => ip_format ($ip_bin),
2772		'ip_bin' => $ip_bin,
2773		'name' => '',
2774		'comment' => '',
2775		'reserved' => 'no',
2776		'allocs' => array(),
2777		'vslist' => array(),
2778		'vsglist' => array(),
2779		'rsplist' => array(),
2780	);
2781
2782	// specific v4 part
2783	if (strlen ($ip_bin) == 4)
2784		$ret = array_merge
2785		(
2786			$ret,
2787			array
2788			(
2789				'outpf' => array(),
2790				'inpf' => array(),
2791			)
2792		);
2793	return $ret;
2794}
2795
2796function treeApplyFunc1 (&$tree, $func)
2797{
2798	$self = __FUNCTION__;
2799	foreach (array_keys ($tree) as $key)
2800	{
2801		$func ($tree[$key]);
2802		$self ($tree[$key]['kids'], $func);
2803	}
2804}
2805
2806function treeApplyFunc2 (&$tree, $func, $stopfunc)
2807{
2808	$self = __FUNCTION__;
2809	foreach (array_keys ($tree) as $key)
2810	{
2811		$func ($tree[$key]);
2812		if (! $stopfunc ($tree[$key]))
2813			$self ($tree[$key]['kids'], $func);
2814	}
2815}
2816
2817// Note that the stop function is called after processing a tree item, not before.
2818// In other words, for a given tree node either all its sub-nodes are processed or
2819// none at all.
2820// XXX: Perhaps instead of a separate stop function it would be better to convey the
2821// feedback through the applied function's return value. This would also leave it up
2822// to the applied function whether to stop before or after modifying the current node.
2823function treeApplyFunc (&$tree, $func, $stopfunc = NULL)
2824{
2825	if (! is_callable ($func))
2826		throw new InvalidArgException ('func', $func, 'is not callable');
2827	if ($stopfunc === NULL)
2828		treeApplyFunc1 ($tree, $func);
2829	else
2830	{
2831		if (! is_callable ($stopfunc))
2832			throw new InvalidArgException ('stopfunc', $stopfunc, 'is not callable');
2833		treeApplyFunc2 ($tree, $func, $stopfunc);
2834	}
2835}
2836
2837function nodeIsCollapsed ($node)
2838{
2839	return $node['symbol'] == 'node-collapsed';
2840}
2841
2842// returns those addresses from $addrlist that do not belong to $net's subsequent networks
2843function filterOwnAddrList ($net, $addrlist)
2844{
2845	if ($net['kidc'] == 0)
2846		return $addrlist;
2847
2848	// net has children
2849	$ret = array();
2850	foreach ($net['spare_ranges'] as $mask => $spare_list)
2851		foreach ($spare_list as $spare_ip)
2852		{
2853			$spare_mask = ip_mask ($mask, strlen ($net['ip_bin']) == 16);
2854			foreach ($addrlist as $bin_ip => $addr)
2855				if (($bin_ip & $spare_mask) == $spare_ip)
2856					$ret[$bin_ip] = $addr;
2857		}
2858	return $ret;
2859}
2860
2861function getIPAddrList ($net, $flags = IPSCAN_ANY)
2862{
2863	$addrlist = scanIPNet ($net, $flags);
2864	return filterOwnAddrList ($net, $addrlist);
2865}
2866
2867// sets 'addrlist', 'own_addrlist', 'addrc', 'own_addrc' keys of $node
2868// 'addrc' and 'own_addrc' are sizes of 'addrlist' and 'own_addrlist', respectively
2869function loadIPAddrList (&$node)
2870{
2871	$node['addrlist'] = scanIPNet ($node);
2872
2873	if (! isset ($node['id']))
2874		$node['own_addrlist'] = $node['addrlist'];
2875	else
2876		$node['own_addrlist'] = filterOwnAddrList ($node, $node['addrlist']);
2877
2878	$node['addrc'] = count ($node['addrlist']);
2879	$node['own_addrc'] = count ($node['own_addrlist']);
2880}
2881
2882// returns list of PtP-typed allocs from $addrlist indexed by ip_bin
2883function getPtPNeighbors ($ip_bin, $addrlist)
2884{
2885	$ret = array();
2886	if (isset ($addrlist[$ip_bin]))
2887	{
2888		$found_ptp_alloc = FALSE;
2889		foreach ($addrlist[$ip_bin]['allocs'] as $alloc)
2890			if ($alloc['type'] == 'point2point')
2891			{
2892				$found_ptp_alloc = TRUE;
2893				break;
2894			}
2895		if ($found_ptp_alloc)
2896			foreach ($addrlist as $i_ip_bin => $i_address)
2897				if ($ip_bin !== $i_ip_bin)
2898					foreach ($i_address['allocs'] as $alloc)
2899						if ($alloc['type'] == 'point2point')
2900							$ret[$i_ip_bin][] = $alloc;
2901	}
2902	return $ret;
2903}
2904
2905// returns the array of structure described by constructIPAddress
2906function getIPAddress ($ip_bin)
2907{
2908	$scanres = scanIPSpace (array (array ('first' => $ip_bin, 'last' => $ip_bin)));
2909	if (empty ($scanres))
2910		$scanres[$ip_bin] = constructIPAddress ($ip_bin);
2911	markupIPAddrList ($scanres);
2912	return $scanres[$ip_bin];
2913}
2914
2915function getIPv4Address ($ip_bin)
2916{
2917	if (strlen ($ip_bin) != 4)
2918		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2919	return getIPAddress ($ip_bin);
2920}
2921
2922function getIPv6Address ($ip_bin)
2923{
2924	if (strlen ($ip_bin) != 16)
2925		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2926	return getIPAddress ($ip_bin);
2927}
2928
2929function makeIPTree ($netlist)
2930{
2931	// treeFromList() requires parent_id to be correct for an item to get onto the tree,
2932	// so perform necessary pre-processing to calculate parent_id of each item of $netlist.
2933	$stack = array();
2934	foreach ($netlist as $net_id => &$net)
2935	{
2936		while (count ($stack))
2937		{
2938			$top_id = $stack[count ($stack) - 1];
2939			if (! IPNetContains ($netlist[$top_id], $net)) // unless $net is a child of stack top
2940				array_pop ($stack);
2941			else
2942			{
2943				$net['parent_id'] = $top_id;
2944				break;
2945			}
2946		}
2947		if (! count ($stack))
2948			$net['parent_id'] = NULL;
2949		array_push ($stack, $net_id);
2950	}
2951	unset ($stack);
2952
2953	return treeFromList (addTraceToNodes ($netlist));
2954}
2955
2956function prepareIPTree ($netlist, $expanded_id = 0)
2957{
2958	$tree = makeIPTree ($netlist);
2959	// complement the tree before markup to make the spare networks have "symbol" set
2960	treeApplyFunc ($tree, 'iptree_fill');
2961	iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
2962	return $tree;
2963}
2964
2965# Traverse IPv4/IPv6 tree and return a list of all networks that
2966# exist in DB and don't have any sub-networks.
2967function getTerminalNetworks ($tree)
2968{
2969	$self = __FUNCTION__;
2970	$ret = array();
2971	foreach ($tree as $node)
2972		if ($node['kidc'] == 0 && isset ($node['realm']))
2973			$ret[] = $node;
2974		else
2975			$ret = array_merge ($ret, $self ($node['kids']));
2976	return $ret;
2977}
2978
2979// Check all items of the tree recursively, until the requested target id is
2980// found. Mark all items leading to this item as "expanded", collapsing all
2981// the rest that exceed the given threshold (if the threshold is given).
2982function iptree_markup_collapsion (&$tree, $threshold = 1024, $target = 0)
2983{
2984	$self = __FUNCTION__;
2985	$ret = FALSE;
2986	foreach (array_keys ($tree) as $key)
2987	{
2988		$here = $target === 'ALL' || ($target > 0 && isset ($tree[$key]['id']) && $tree[$key]['id'] == $target);
2989		$below = $self ($tree[$key]['kids'], $threshold, $target);
2990		$expand_enabled = ($target !== 'NONE');
2991		if (!$tree[$key]['kidc']) // terminal node
2992			$tree[$key]['symbol'] = 'spacer';
2993		elseif ($expand_enabled && $tree[$key]['kidc'] < $threshold)
2994			$tree[$key]['symbol'] = 'node-expanded-static';
2995		elseif ($expand_enabled && ($here || $below))
2996			$tree[$key]['symbol'] = 'node-expanded';
2997		else
2998			$tree[$key]['symbol'] = 'node-collapsed';
2999		$ret = $ret || $here || $below;
3000	}
3001	return $ret;
3002}
3003
3004// Convert entity name to human-readable value
3005function formatRealmName ($realm)
3006{
3007	$realmstr = array
3008	(
3009		'ipv4net' => 'IPv4 Network',
3010		'ipv6net' => 'IPv6 Network',
3011		'ipv4rspool' => 'IPv4 RS Pool',
3012		'ipv4vs' => 'IPv4 Virtual Service',
3013		'ipvs' => 'IP Virtual Service',
3014		'object' => 'Object',
3015		'rack' => 'Rack',
3016		'row' => 'Row',
3017		'location' => 'Location',
3018		'user' => 'User',
3019		'file' => 'File',
3020		'vst' => 'VLAN switch template',
3021	);
3022	return array_fetch ($realmstr, $realm, 'invalid');
3023}
3024
3025// Convert filesize to appropriate unit and make it human-readable
3026function formatFileSize ($bytes)
3027{
3028	// bytes
3029	if($bytes < 1024) // bytes
3030		return "${bytes} bytes";
3031
3032	// kilobytes
3033	if ($bytes < 1024000)
3034		return sprintf ("%.1fk", round (($bytes / 1024), 1));
3035
3036	// megabytes
3037	return sprintf ("%.1f MB", round (($bytes / 1024000), 1));
3038}
3039
3040// Reverse of formatFileSize, it converts human-readable value to bytes
3041function convertToBytes ($value)
3042{
3043	$value = trim($value);
3044	$last = strtolower($value[strlen($value)-1]);
3045	switch ($last)
3046	{
3047		case 'g':
3048			$value *= 1024;
3049		case 'm':
3050			$value *= 1024;
3051		case 'k':
3052			$value *= 1024;
3053	}
3054
3055	return $value;
3056}
3057
3058// make "A" HTML element
3059function mkA ($text, $nextpage, $bypass = NULL, $nexttab = NULL, $attrs = array())
3060{
3061	global $page, $tab;
3062	if ($text == '')
3063		throw new InvalidArgException ('text', $text, 'must not be empty');
3064	if (! array_key_exists ($nextpage, $page))
3065		throw new InvalidArgException ('nextpage', $nextpage, 'not a valid page name');
3066	$args = array ('page' => $nextpage);
3067	if ($nexttab !== NULL)
3068	{
3069		if (! array_key_exists ($nexttab, $tab[$nextpage]))
3070			throw new InvalidArgException ('nexttab', $nexttab, 'not a valid tab name');
3071		$args['tab'] = $nexttab;
3072	}
3073	if (array_key_exists ('bypass', $page[$nextpage]))
3074	{
3075		if ($bypass === NULL)
3076			throw new InvalidArgException ('bypass', '(NULL)', 'must be specified for the given page name');
3077		$args[$page[$nextpage]['bypass']] = $bypass;
3078	}
3079	$attrs['href'] = makeHref ($args);
3080	$ret = '<a';
3081	foreach ($attrs as $attr_name => $attr_value)
3082		$ret .= " $attr_name=" . '"' . htmlspecialchars ($attr_value, ENT_QUOTES) . '"';
3083	return $ret . '>' . $text . '</a>';
3084}
3085
3086// make "HREF" HTML attribute
3087function makeHref ($params = array())
3088{
3089	$tmp = array();
3090	foreach ($params as $key => $value)
3091	{
3092		if (is_array ($value))
3093			$key .= "[]";
3094		else
3095			$value = array ($value);
3096		if (!count ($value))
3097			$tmp[] = urlencode ($key) . '=';
3098		else
3099			foreach ($value as $sub_value)
3100				$tmp[] = urlencode ($key) . '=' . urlencode ($sub_value);
3101	}
3102	return 'index.php?' . implode ('&', $tmp);
3103}
3104
3105function makePageParams ($params = array())
3106{
3107	global $pageno, $tabno;
3108	$ret = array();
3109	// assure that page and tab keys go first
3110	$ret['page'] = isset ($params['page']) ? $params['page'] : $pageno;
3111	$ret['tab'] = isset ($params['tab']) ? $params['tab'] : $tabno;
3112	$ret += $params;
3113	if ($ret['page'] === $pageno)
3114		fillBypassValues ($pageno, $ret);
3115	return $ret;
3116}
3117
3118function makeHrefProcess ($params = array())
3119{
3120	return makeHref (array ('module' => 'redirect') + makePageParams ($params));
3121}
3122
3123// Process the given list of records to build data suitable for printNiftySelect()
3124// (like it was formerly executed by printSelect()). Screen out vendors according
3125// to VENDOR_SIEVE, if object type ID is provided. However, the OPTGROUP with already
3126// selected OPTION is protected from being screened.
3127function cookOptgroups ($recordList, $object_type_id = 0, $existing_value = 0)
3128{
3129	$ret = array();
3130	$therest = array();
3131	foreach ($recordList as $dict_key => $dict_value)
3132		if (preg_match ('/^(.*)%(GPASS|GSKIP)%/', $dict_value, $m))
3133			$ret[$m[1]][$dict_key] = execGMarker ($dict_value);
3134		else
3135			$therest[$dict_key] = $dict_value;
3136
3137	// Always keep "other" OPTGROUP at the SELECT bottom.
3138	$ret['other'] = $therest;
3139
3140	if ($object_type_id != 0)
3141	{
3142		$screenlist = array();
3143		foreach (explode (';', getConfigVar ('VENDOR_SIEVE')) as $sieve)
3144			if (preg_match ("/^([^@]+)(@${object_type_id})?\$/", trim ($sieve), $regs))
3145				$screenlist[] = $regs[1];
3146
3147		foreach (array_keys ($ret) as $vendor)
3148			if (in_array ($vendor, $screenlist))
3149			{
3150				$ok_to_screen = TRUE;
3151				if ($existing_value)
3152					foreach (array_keys ($ret[$vendor]) as $recordkey)
3153						if ($recordkey == $existing_value)
3154						{
3155							$ok_to_screen = FALSE;
3156							break;
3157						}
3158				if ($ok_to_screen)
3159					unset ($ret[$vendor]);
3160			}
3161	}
3162	return $ret;
3163}
3164
3165function dos2unix ($text)
3166{
3167	return str_replace ("\r\n", "\n", $text);
3168}
3169
3170function unix2dos ($text)
3171{
3172	return str_replace ("\n", "\r\n", $text);
3173}
3174
3175function buildPredicateTable (&$rackCode)
3176{
3177	$ret = array();
3178	$new_rackCode = array();
3179
3180	foreach ($rackCode as $sentence)
3181		if ($sentence['type'] == 'SYNT_DEFINITION')
3182			$ret[$sentence['term']] = $sentence['definition'];
3183		else
3184			$new_rackCode[] = $sentence;
3185
3186	// remove SYNT_DEFINITION statements from the original rackCode to
3187	// make permitted() calls faster.
3188	$rackCode = $new_rackCode;
3189
3190	// The latest (and the only, as the interpreter must not allow to redefine predicates)
3191	// definition of every predicate is now in the predicate table, which will remain
3192	// intact until the end of run-time.
3193	return $ret;
3194}
3195
3196// Take a list of records and filter against given RackCode expression. Return
3197// the original list intact, if there was no filter requested, but return an
3198// empty list, if there was an error.
3199function filterCellList ($list_in, $expression = array())
3200{
3201	if ($expression === NULL)
3202		return array();
3203	if (!count ($expression))
3204		return $list_in;
3205	$list_out = array();
3206	foreach ($list_in as $item_key => $item_value)
3207		if (TRUE === judgeCell ($item_value, $expression))
3208			$list_out[$item_key] = $item_value;
3209	return $list_out;
3210}
3211
3212function eval_expression ($expr, $tagchain, $silent = FALSE)
3213{
3214	$self = __FUNCTION__;
3215	global $pTable;
3216
3217	switch ($expr['type'])
3218	{
3219		// Return true, if given tag is present on the tag chain.
3220		case 'LEX_TAG':
3221			return isset ($tagchain[$expr['load']]);
3222		case 'LEX_PREDICATE': // Find given predicate in the symbol table and evaluate it.
3223			$pname = $expr['load'];
3224			if (!isset ($pTable[$pname]))
3225			{
3226				if (!$silent)
3227					showWarning ("Undefined predicate [${pname}]");
3228				return NULL;
3229			}
3230			return $self ($pTable[$pname], $tagchain, $silent);
3231		case 'LEX_BOOL':
3232			return $expr['load'];
3233		case 'SYNT_NOT_EXPR': // logical NOT
3234			$tmp = $self ($expr['load'], $tagchain, $silent);
3235			return is_bool($tmp) ? !$tmp : $tmp;
3236		case 'SYNT_AND_EXPR': // logical AND
3237			foreach ($expr['tag_args'] as $tag)
3238				if (! isset ($tagchain[$tag]))
3239					return FALSE; // early failure
3240			foreach ($expr['expr_args'] as $sub_expr)
3241				if (! $self ($sub_expr, $tagchain, $silent))
3242					return FALSE; // early failure
3243			return TRUE;
3244		case 'SYNT_EXPR': // logical OR
3245			foreach ($expr['tag_args'] as $tag)
3246				if (isset ($tagchain[$tag]))
3247					return TRUE; // early success
3248			foreach ($expr['expr_args'] as $sub_expr)
3249				if ($self ($sub_expr, $tagchain, $silent))
3250					return TRUE; // early success
3251			return FALSE;
3252		default:
3253			if (!$silent)
3254				showWarning ("Evaluation error, cannot process expression type '${expr['type']}'");
3255			return NULL;
3256	}
3257}
3258
3259// Tell, if the given expression is true for the given entity. Take complete record on input.
3260function judgeCell ($cell, $expression)
3261{
3262	$context = array_merge ($cell['etags'], $cell['itags'], $cell['atags']);
3263	$context = reindexById ($context, 'tag', TRUE);
3264	return eval_expression
3265	(
3266		$expression,
3267		$context,
3268		TRUE
3269	);
3270}
3271
3272function judgeContext ($expression)
3273{
3274	global $expl_tags, $impl_tags, $auto_tags;
3275	$context = array_merge ($expl_tags, $impl_tags, $auto_tags);
3276	$context = reindexById ($context, 'tag', TRUE);
3277	return eval_expression
3278	(
3279		$expression,
3280		$context,
3281		TRUE
3282	);
3283}
3284
3285// Tell, if a constraint from config option permits given record.
3286// An undefined $cell means current context.
3287function considerConfiguredConstraint ($cell, $varname)
3288{
3289	// Do not mask any exceptions thrown in getConfigVar().
3290	$text = getConfigVar ($varname);
3291	try
3292	{
3293		return considerGivenConstraint ($cell, $text);
3294	}
3295	catch (InvalidArgException $e)
3296	{
3297		return FALSE; // constraint set, but cannot be used due to compilation error
3298	}
3299}
3300
3301// Tell, if the given arbitrary RackCode text addresses the given record
3302// (an empty text matches any record).
3303// An undefined $cell means current context.
3304function considerGivenConstraint ($cell, $filter)
3305{
3306	if ($filter == '')
3307		return TRUE;
3308	if (! $expr = compileExpression ($filter))
3309		throw new InvalidArgException ('filter', $filter, 'not a valid RackCode expression');
3310	if (isset ($cell))
3311		return judgeCell ($cell, $expr);
3312	else
3313		return judgeContext ($expr);
3314}
3315
3316// Return list of records in the given realm that conform to
3317// the given RackCode expression. If the realm is unknown or text
3318// doesn't validate as a RackCode expression, return NULL.
3319// Otherwise (successful scan) return a list of all matched
3320// records, even if the list is empty (array() !== NULL). If the
3321// text is an empty string, return all found records in the given
3322// realm.
3323function scanRealmByText ($realm, $ftext = '')
3324{
3325	if ('' == $ftext = trim ($ftext))
3326		$fexpr = array();
3327	else
3328	{
3329		$fexpr = compileExpression ($ftext);
3330		if (! $fexpr)
3331			return NULL;
3332	}
3333	return filterCellList (listCells ($realm), $fexpr);
3334}
3335
3336function getVSTOptions()
3337{
3338	return reduceSubarraysToColumn (reindexById (listCells ('vst')), 'description');
3339}
3340
3341# Return an array in the format understood by getNiftySelect() and getOptionTree(),
3342# so that the options stand for all VLANs grouped by respective VLAN domains, except
3343# those listed on the "except" array.
3344function getAllVLANOptions ($except = array())
3345{
3346	$ret = array();
3347	foreach (getVLANDomainOptions() as $domain_id => $domain_descr)
3348	{
3349		$domain_list = array();
3350		foreach (getDomainVLANList ($domain_id, TRUE) as $vlan)
3351			$domain_list["${domain_id}-${vlan['vlan_id']}"] = "${vlan['vlan_id']} ${vlan['vlan_descr']}";
3352		if (isset ($except[$domain_id]))
3353			foreach ($except[$domain_id] as $vid)
3354				if (isset ($domain_list["${domain_id}-${vid}"]))
3355					unset ($domain_list["${domain_id}-${vid}"]);
3356		$ret[$domain_descr] = $domain_list;
3357	}
3358	return $ret;
3359}
3360
3361// This debugging helper does not depend on interface.php.
3362function dump ($var)
3363{
3364	echo '<div align=left><pre>';
3365	var_dump ($var);
3366	echo '</pre></div>';
3367}
3368
3369function getTagChart ($limit = 0, $realm = 'total', $special_tags = array())
3370{
3371	$taglist_usage = getTagUsage();
3372	// first build top-N chart...
3373	$toplist = array();
3374	foreach ($taglist_usage as $taginfo)
3375		if (isset ($taginfo['refcnt'][$realm]))
3376			$toplist[$taginfo['id']] = $taginfo['refcnt'][$realm];
3377	arsort ($toplist, SORT_NUMERIC);
3378	$ret = array();
3379	$done = 0;
3380	foreach (array_keys ($toplist) as $tag_id)
3381	{
3382		$ret[$tag_id] = $taglist_usage[$tag_id];
3383		if (++$done == $limit)
3384			break;
3385	}
3386	// ...then make sure that every item of the special list is shown
3387	// (using the same sort order)
3388	$extra = array();
3389	foreach ($special_tags as $taginfo)
3390		if (!array_key_exists ($taginfo['id'], $ret))
3391			$extra[$taginfo['id']] = $taglist_usage[$taginfo['id']]['refcnt'][$realm];
3392	arsort ($extra, SORT_NUMERIC);
3393	foreach (array_keys ($extra) as $tag_id)
3394		$ret[] = $taglist_usage[$tag_id];
3395	return $ret;
3396}
3397
3398// $style is deprecated and unused
3399function decodeObjectType ($objtype_id, $style = '')
3400{
3401	static $types;
3402	if (! isset ($types))
3403		$types = readChapter (CHAP_OBJTYPE, 'a');
3404	return $types[$objtype_id];
3405}
3406
3407// This wrapper makes it possible to call permitted() with the security context
3408// containing the given object of temporary interest without the [previously loaded]
3409// main subject.
3410function isolatedPermission ($p, $t, $cell)
3411{
3412	$saved = getContext();
3413	// retarget
3414	fixContext ($cell);
3415	// remember decision
3416	$ret = permitted ($p, $t);
3417	restoreContext ($saved);
3418	return $ret;
3419}
3420
3421function getPortListPrefs()
3422{
3423	$ret = array();
3424	if (0 >= ($ret['iif_pick'] = getConfigVar ('DEFAULT_PORT_IIF_ID')))
3425		$ret['iif_pick'] = 1;
3426	$ret['oif_picks'] = array();
3427	foreach (explode (';', getConfigVar ('DEFAULT_PORT_OIF_IDS')) as $tmp)
3428	{
3429		$tmp = explode ('=', trim ($tmp));
3430		if (count ($tmp) == 2 && $tmp[0] > 0 && $tmp[1] > 0)
3431			$ret['oif_picks'][$tmp[0]] = $tmp[1];
3432	}
3433	// enforce default value
3434	if (!array_key_exists (1, $ret['oif_picks']))
3435		$ret['oif_picks'][1] = 24;
3436	$ret['selected'] = $ret['iif_pick'] . '-' . $ret['oif_picks'][$ret['iif_pick']];
3437	return $ret;
3438}
3439
3440function getNewPortTypeOptions()
3441{
3442	return getUnlinkedPortTypeOptions (NULL);
3443}
3444
3445// Return data for printNiftySelect() with port type options. All OIF options
3446// for the default or current (passed) IIFs will be shown, but only the default
3447// OIFs will be present for each other IIFs. IIFs for that there is no default
3448// OIF will not be listed.
3449// This SELECT will be used in "manage object ports" form.
3450function getUnlinkedPortTypeOptions ($port_iif_id)
3451{
3452	static $cache;
3453	static $prefs;
3454	static $compat;
3455	if (! isset ($cache))
3456	{
3457		$cache = array();
3458		$prefs = getPortListPrefs();
3459		$compat = getPortInterfaceCompat();
3460	}
3461
3462	if (isset ($cache[$port_iif_id]))
3463		return $cache[$port_iif_id];
3464
3465	$ret = array();
3466	$seen_oifs = array();
3467	$ambiguous_oifs = array();
3468	foreach ($compat as $row)
3469	{
3470		if (isset ($seen_oifs[$row['oif_id']]))
3471			$ambiguous_oifs[$row['oif_id']] = 1;
3472		$seen_oifs[$row['oif_id']] = 1;
3473	}
3474	foreach ($compat as $row)
3475	{
3476		if ($row['iif_id'] == $prefs['iif_pick'] || $row['iif_id'] == $port_iif_id)
3477			$optgroup = $row['iif_name'];
3478		elseif (array_key_exists ($row['iif_id'], $prefs['oif_picks']) && $prefs['oif_picks'][$row['iif_id']] == $row['oif_id'])
3479			$optgroup = 'other';
3480		else
3481			continue;
3482		if (!array_key_exists ($optgroup, $ret))
3483			$ret[$optgroup] = array();
3484		if (isset ($ambiguous_oifs[$row['oif_id']]) && $optgroup == 'other')
3485			$name = $row['iif_name'] . ' / ' . $row['oif_name'];
3486		else
3487			$name = $row['oif_name'];
3488		$ret[$optgroup][$row['iif_id'] . '-' . $row['oif_id']] = $name;
3489	}
3490
3491	$cache[$port_iif_id] = $ret;
3492	return $ret;
3493}
3494
3495// Return a serialized version of VLAN configuration for a port.
3496// If a native VLAN is defined, print it first. All other VLANs
3497// are tagged and are listed after a plus sign. When no configuration
3498// is set for a port, return "default" string.
3499function serializeVLANPack ($vlanport)
3500{
3501	if (!array_key_exists ('mode', $vlanport))
3502		return 'error';
3503	switch ($vlanport['mode'])
3504	{
3505	case 'none':
3506		return 'none';
3507	case 'access':
3508		$ret = 'A';
3509		break;
3510	case 'trunk':
3511		$ret = 'T';
3512		break;
3513	case 'uplink':
3514		$ret = 'U';
3515		break;
3516	case 'downlink':
3517		$ret = 'D';
3518		break;
3519	default:
3520		return 'error';
3521	}
3522	if ($vlanport['native'])
3523		$ret .= $vlanport['native'];
3524	$tagged_bits = groupIntsToRanges ($vlanport['allowed'], $vlanport['native']);
3525	if (count ($tagged_bits))
3526		$ret .= '+' . implode (', ', $tagged_bits);
3527	return $ret != '' ? $ret : 'default';
3528}
3529
3530function groupIntsToRanges ($list, $exclude_value = NULL)
3531{
3532	$result = array();
3533	sort ($list);
3534	$id_from = $id_to = 0;
3535	$list[] = -1;
3536	foreach ($list as $next_id)
3537		if (! isset ($exclude_value) || $next_id != $exclude_value)
3538			if ($id_to && $next_id == $id_to + 1)
3539				$id_to = $next_id; // merge
3540			else
3541			{
3542				if ($id_to)
3543					$result[] = $id_from == $id_to ? $id_from : "${id_from}-${id_to}"; // flush
3544				$id_from = $id_to = $next_id; // start next pair
3545			}
3546	return $result;
3547}
3548
3549// Decode VLAN compound key (which is a string formatted DOMAINID-VLANID) and
3550// return the numbers as an array of two.
3551function decodeVLANCK ($string)
3552{
3553	$matches = array();
3554	if (1 != preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $string, $matches))
3555		throw new InvalidArgException ('VLAN compound key', $string, 'format error');
3556	if (! isNaturalNumber ($matches[1]))
3557		throw new InvalidArgException ('VLAN compound key', $string, 'domain ID cannot be 0');
3558	if (! isValidVLANID ($matches[2]))
3559		throw new InvalidArgException ('VLAN compound key', $string, 'invalid VLAN ID');
3560	return array ($matches[1], $matches[2]);
3561}
3562
3563// Return VLAN name formatted for HTML output (note that input
3564// argument comes from database unescaped).
3565function formatVLANAsOption ($vlaninfo)
3566{
3567	$ret = $vlaninfo['vlan_id'];
3568	if ($vlaninfo['vlan_descr'] != '')
3569		$ret .= ' ' . $vlaninfo['vlan_descr'];
3570	return $ret;
3571}
3572
3573function formatVLANAsLabel ($vlaninfo)
3574{
3575	$ret = $vlaninfo['vlan_id'];
3576	if ($vlaninfo['vlan_descr'] != '')
3577		$ret .= ' <i>(' . stringForLabel ($vlaninfo['vlan_descr']) . ')</i>';
3578	return $ret;
3579}
3580
3581function formatVLANAsPlainText ($vlaninfo)
3582{
3583	$ret = 'VLAN' . $vlaninfo['vlan_id'];
3584	if ($vlaninfo['vlan_descr'] != '')
3585		$ret .= ' (' . stringForLabel ($vlaninfo['vlan_descr'], 20) . ')';
3586	return $ret;
3587}
3588
3589function formatVLANAsHyperlink ($vlaninfo)
3590{
3591	return mkA (formatVLANAsRichText ($vlaninfo), 'vlan', $vlaninfo['domain_id'] . '-' . $vlaninfo['vlan_id']);
3592}
3593
3594function formatVLANAsShortLink ($vlaninfo)
3595{
3596	$title = sprintf ('VLAN %d @ %s', $vlaninfo['vlan_id'], $vlaninfo['domain_descr']);
3597	if ($vlaninfo['vlan_descr'] != '')
3598		$title .= ' (' . $vlaninfo['vlan_descr'] . ')';
3599	$attrs = array ('title' => $title);
3600	return mkA ($vlaninfo['vlan_id'], 'vlan', $vlaninfo['domain_id'] . '-' . $vlaninfo['vlan_id'], NULL, $attrs);
3601}
3602
3603function formatVLANAsRichText ($vlaninfo)
3604{
3605	$ret = 'VLAN' . $vlaninfo['vlan_id'];
3606	$ret .= ' @' . stringForLabel ($vlaninfo['domain_descr']);
3607	if ($vlaninfo['vlan_descr'] != '')
3608		$ret .= ' <i>(' . stringForLabel ($vlaninfo['vlan_descr']) . ')</i>';
3609	return $ret;
3610}
3611
3612# Produce a list of integers from a string in the following format:
3613# A,B,C-D,E-F,G,H,I-J,K ...
3614function iosParseVLANString ($string)
3615{
3616	$ret = array();
3617	foreach (explode (',', $string) as $item)
3618	{
3619		$matches = array();
3620		$item = trim ($item, ' ');
3621		if (preg_match ('/^([[:digit:]]+)$/', $item, $matches))
3622			$ret[] = intval ($matches[1]);
3623		elseif (preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $item, $matches))
3624			$ret = array_merge ($ret, range ($matches[1], $matches[2]));
3625		else
3626			throw new InvalidArgException ('string', $string, 'format mismatch');
3627	}
3628	return $ret;
3629}
3630
3631// Scan given array and return the key that addresses the first item
3632// with requested column set to given value (or NULL if there is none such).
3633// Note that 0 and NULL mean completely different things and thus
3634// require strict checking (=== and !===).
3635// Also note that this is not a 1:1 reinvention of PHP's array_search() as
3636// this function looks one level deeper (which could be done by means of
3637// array_column(), which appears only in PHP 5 >= 5.5.0).
3638function scanArrayForItem ($table, $scan_column, $scan_value)
3639{
3640	foreach ($table as $key => $row)
3641		if ($row[$scan_column] == $scan_value)
3642			return $key;
3643	return NULL;
3644}
3645
3646// Return TRUE, if every value of A1 is present in A2 and vice versa,
3647// regardless of each array's sort order and indexing.
3648// Any duplicate values in either of the arguments would be treated same
3649// as a single occurrence, IOW, there is an implicit array_unique() here.
3650function array_values_same ($a1, $a2)
3651{
3652	if (! is_array ($a1))
3653		throw new InvalidArgException ('a1', $a1, 'is not an array');
3654	if (! is_array ($a2))
3655		throw new InvalidArgException ('a2', $a2, 'is not an array');
3656	return ! count (array_diff ($a1, $a2)) && ! count (array_diff ($a2, $a1));
3657}
3658
3659# Reindex provided array of arrays by a column value that is present in
3660# each sub-array and is assumed to be unique. Most often, make "id" column in
3661# a list of cells into the key space.
3662function reindexById ($input, $column_name = 'id', $ignore_dups = FALSE)
3663{
3664	$ret = array();
3665	if (! is_array ($input))
3666		throw new InvalidArgException ('input', $input, 'must be an array');
3667	foreach ($input as $item)
3668	{
3669		if (! isset ($item[$column_name]))
3670			throw new InvalidArgException ('input', '(array)', 'ID column missing');
3671		if (isset ($ret[$item[$column_name]]))
3672		{
3673			if (!$ignore_dups)
3674				throw new InvalidArgException ('column_name', $column_name, 'duplicate ID value ' . $item[$column_name]);
3675		}
3676		else
3677			$ret[$item[$column_name]] = $item;
3678	}
3679	return $ret;
3680}
3681
3682# Given an array consisting of subarrays, each typically with the same set
3683# of keys, produce a result indexed the same way as the input array, but
3684# having each subarray replaced with one of the subarray values (using the
3685# provided subindex name, e.g.:
3686# array (10 => array ('a' => 'x1', 'b' => 'y1'), 20 => array ('a' => 'x2', 'b' => 'y2'))
3687# would map to (using subindex 'b'): array (10 => 'y1', 20 => 'y2')
3688#
3689# A similar array_column() function is available in PHP 5 >= 5.5.0.
3690function reduceSubarraysToColumn ($input, $column)
3691{
3692	$ret = array();
3693	if (! is_array ($input))
3694		throw new InvalidArgException ('input', $input, 'must be an array');
3695	foreach ($input as $key => $item)
3696		if (array_key_exists ($column, $item))
3697			$ret[$key] = $item[$column];
3698		else
3699			throw new InvalidArgException ('input', '(array)', "column '${column}' is not set for subarray at index '${key}'");
3700	return $ret;
3701}
3702
3703// Use the VLAN switch template to set VST role for each port of
3704// the provided list. Return resulting list.
3705function apply8021QOrder ($vswitch, $portlist)
3706{
3707	$hook_result = callHook ('apply8021Qrder_hook', $vswitch, $portlist);
3708	if (isset ($hook_result))
3709		return $hook_result;
3710
3711	$vst_id = $vswitch['template_id'];
3712	$vst = spotEntity ('vst', $vswitch['template_id']);
3713	amplifyCell ($vst);
3714
3715	// warm the vlan_filter cache for every rule
3716	foreach ($vst['rules'] as $i_rule => $rule)
3717		$vst['rules'][$i_rule]['vlan_filter'] = buildVLANFilter ($rule['port_role'], $rule['wrt_vlans']);
3718
3719	foreach (array_keys ($portlist) as $port_name)
3720	{
3721		foreach ($vst['rules'] as $rule)
3722			if (preg_match ($rule['port_pcre'], $port_name))
3723			{
3724				$portlist[$port_name]['vst_role'] = $rule['port_role'];
3725				$portlist[$port_name]['wrt_vlans'] = $rule['vlan_filter'];
3726				continue 2;
3727			}
3728		$portlist[$port_name]['vst_role'] = 'none';
3729	}
3730	return $portlist;
3731}
3732
3733// Return a sequence of integer ranges for given port role and VLAN filter string.
3734// FIXME: for an "anymode" port the same filter currently applies to the "trunk"
3735// and "access" configurations of the port, i.e. such a port cannot be set to "A1"
3736// by means of the user interface though as far as the data model goes "A1" is a
3737// valid configuration for an "anymode" port.
3738function buildVLANFilter ($role, $string)
3739{
3740	// set base
3741	switch ($role)
3742	{
3743	case 'access': // 1-4094
3744	case 'anymode':
3745		$min = VLAN_MIN_ID;
3746		$max = VLAN_MAX_ID;
3747		break;
3748	case 'trunk': // 2-4094
3749	case 'uplink':
3750	case 'downlink':
3751		$min = VLAN_MIN_ID + 1;
3752		$max = VLAN_MAX_ID;
3753		break;
3754	default: // none
3755		return array();
3756	}
3757	if ($string == '') // fast track
3758		return array (array ('from' => $min, 'to' => $max));
3759	// transform
3760	$vlanidlist = array();
3761	foreach (iosParseVLANString ($string) as $vlan_id)
3762		if ($min <= $vlan_id && $vlan_id <= $max)
3763			$vlanidlist[] = $vlan_id;
3764	return listToRanges ($vlanidlist);
3765}
3766
3767// pack set of integers into list of integer ranges
3768// e.g. (1, 2, 3, 5, 6, 7, 9, 11) => ((1, 3), (5, 7), (9, 9), (11, 11))
3769// The second argument, when it is different from 0, limits amount of
3770// items in each generated range.
3771function listToRanges ($vlanidlist, $limit = 0)
3772{
3773	sort ($vlanidlist);
3774	$ret = array();
3775	$from = $to = NULL;
3776	foreach ($vlanidlist as $vlan_id)
3777		if ($from == NULL)
3778		{
3779			if ($limit == 1)
3780				$ret[] = array ('from' => $vlan_id, 'to' => $vlan_id);
3781			else
3782				$from = $to = $vlan_id;
3783		}
3784		elseif ($to + 1 == $vlan_id)
3785		{
3786			$to = $vlan_id;
3787			if ($to - $from + 1 == $limit)
3788			{
3789				// cut accumulated range and start over
3790				$ret[] = array ('from' => $from, 'to' => $to);
3791				$from = $to = NULL;
3792			}
3793		}
3794		else
3795		{
3796			$ret[] = array ('from' => $from, 'to' => $to);
3797			$from = $to = $vlan_id;
3798		}
3799	if ($from != NULL)
3800		$ret[] = array ('from' => $from, 'to' => $to);
3801	return $ret;
3802}
3803
3804// return TRUE, if given VLAN ID belongs to one of filter's ranges
3805function matchVLANFilter ($vlan_id, $vfilter)
3806{
3807	foreach ($vfilter as $range)
3808		if ($range['from'] <= $vlan_id && $vlan_id <= $range['to'])
3809			return TRUE;
3810	return FALSE;
3811}
3812
3813function filterVLANList ($vlan_list, $vfilter)
3814{
3815	$ret = array();
3816	foreach ($vlan_list as $vid)
3817		if (matchVLANFilter ($vid, $vfilter))
3818			$ret[] = $vid;
3819	return $ret;
3820}
3821
3822function generate8021QDeployOps ($vswitch, $device_vlanlist, $before, $changes)
3823{
3824	$domain_vlanlist = getDomainVLANList ($vswitch['domain_id']);
3825	$employed_vlans = getEmployedVlans ($vswitch['object_id'], $domain_vlanlist);
3826
3827	// only ignore VLANs that exist and are explicitly shown as "alien"
3828	$old_managed_vlans = array();
3829	foreach ($device_vlanlist as $vlan_id)
3830		if
3831		(
3832			! array_key_exists ($vlan_id, $domain_vlanlist) ||
3833			$domain_vlanlist[$vlan_id]['vlan_type'] != 'alien'
3834		)
3835			$old_managed_vlans[] = $vlan_id;
3836	$old_managed_vlans = array_unique ($old_managed_vlans);
3837
3838	$ports_to_do = array();
3839	$ports_to_do_queue1 = array();
3840	$ports_to_do_queue2 = array();
3841	$after = $before;
3842	foreach ($changes as $port_name => $port)
3843	{
3844		$changeset = array
3845		(
3846			'old_mode' => $before[$port_name]['mode'],
3847			'old_allowed' => $before[$port_name]['allowed'],
3848			'old_native' => $before[$port_name]['native'],
3849			'new_mode' => $port['mode'],
3850			'new_allowed' => $port['allowed'],
3851			'new_native' => $port['native'],
3852		);
3853		// put the ports with employed vlans first, the others - below them
3854		if (! count (array_intersect ($changeset['old_allowed'], $employed_vlans)))
3855			$ports_to_do_queue2[$port_name] = $changeset;
3856		else
3857			$ports_to_do_queue1[$port_name] = $changeset;
3858		$after[$port_name] = $port;
3859	}
3860	# Two arrays without common keys get merged with "+" operator just fine,
3861	# with an important difference from array_merge() in that the latter
3862	# renumbers numeric keys, and "+" does not. This matters, when port name
3863	# is a number (like in XOS12 system).
3864	$ports_to_do = $ports_to_do_queue1 + $ports_to_do_queue2;
3865	// New VLAN table is a union of:
3866	// 1. all compulsory VLANs
3867	// 2. all "current" non-alien allowed VLANs of those ports that are left
3868	//    intact (regardless if a VLAN exists in VLAN domain, but looking,
3869	//    if it is present in device's own VLAN table)
3870	// 3. all "new" allowed VLANs of the ports to be "pushed" now
3871	// Like for old_managed_vlans, a VLANs is never listed, only if it
3872	// exists and belongs to "alien" type.
3873	$new_managed_vlans = array();
3874	// It is necessary to count down the number of ports still using a specific VLAN
3875	// in order to unconfigure the VLAN on the switch as soon as the VLAN membership
3876	// is removed from its last port.
3877	// This array tracks port count:
3878	//  * keys are vlan_id's;
3879	//  * values are the number of changed ports that were using this vlan in old configuration
3880	$used_vlans = array();
3881	foreach ($employed_vlans as $vlan_id)
3882		$used_vlans[$vlan_id] = 1; // prevent deletion of an employed vlan
3883
3884	// 1
3885	foreach ($domain_vlanlist as $vlan_id => $vlan)
3886		if ($vlan['vlan_type'] == 'compulsory')
3887			$new_managed_vlans[] = $vlan_id;
3888	// 2
3889	foreach ($before as $port_name => $port)
3890		if (!array_key_exists ($port_name, $changes))
3891			foreach ($port['allowed'] as $vlan_id)
3892			{
3893				if (in_array ($vlan_id, $new_managed_vlans))
3894					continue;
3895				if
3896				(
3897					array_key_exists ($vlan_id, $domain_vlanlist) &&
3898					$domain_vlanlist[$vlan_id]['vlan_type'] == 'alien'
3899				)
3900					continue;
3901				if (in_array ($vlan_id, $device_vlanlist))
3902					$new_managed_vlans[] = $vlan_id;
3903			}
3904		else
3905			foreach ($port['allowed'] as $vlan_id)
3906			{
3907				if (!array_key_exists ($vlan_id, $used_vlans))
3908					$used_vlans[$vlan_id] = 0;
3909				$used_vlans[$vlan_id]++;
3910			}
3911	// 3
3912	foreach ($changes as $port)
3913		foreach ($port['allowed'] as $vlan_id)
3914			if
3915			(
3916				isset ($domain_vlanlist[$vlan_id]) &&
3917				$domain_vlanlist[$vlan_id]['vlan_type'] == 'ondemand' &&
3918				!in_array ($vlan_id, $new_managed_vlans)
3919			)
3920				$new_managed_vlans[] = $vlan_id;
3921	$new_managed_vlans = array_unique ($new_managed_vlans);
3922
3923	$vlans_to_add = array_diff ($new_managed_vlans, $old_managed_vlans);
3924	$vlans_to_del = array_diff ($old_managed_vlans, $new_managed_vlans);
3925	$crq = array();
3926
3927	// destroy unused VLANs on device
3928	$deleted_vlans = array_diff ($vlans_to_del, array_keys ($used_vlans));
3929	foreach ($deleted_vlans as $vlan_id)
3930		$crq[] = array
3931		(
3932			'opcode' => 'destroy VLAN',
3933			'arg1' => $vlan_id,
3934		);
3935	$vlans_to_del = array_diff ($vlans_to_del, $deleted_vlans);
3936
3937	foreach (sortPortList ($ports_to_do) as $port_name => $port)
3938	{
3939		// Before removing each old VLAN as such it is necessary to unassign
3940		// ports from it (to remove VLAN from each ports' list of "allowed"
3941		// VLANs). This change in turn requires that a port's "native"
3942		// VLAN isn't set to the one being removed from its "allowed" list.
3943		switch ($port['old_mode'] . '->' . $port['new_mode'])
3944		{
3945		case 'trunk->trunk':
3946			// "old" native is set and differs from the "new" native
3947			if ($port['old_native'] && $port['old_native'] != $port['new_native'])
3948				$crq[] = array
3949				(
3950					'opcode' => 'unset native',
3951					'arg1' => $port_name,
3952					'arg2' => $port['old_native'],
3953				);
3954			$vlans_to_remove = array_diff ($port['old_allowed'], $port['new_allowed']);
3955			$queues = array();
3956			$queues[] = array_intersect ($employed_vlans, $vlans_to_remove); // remove employed vlans first
3957			$queues[] = array_diff ($vlans_to_remove, $employed_vlans);// remove other vlans afterwards
3958			foreach ($queues as $queue)
3959				if (count ($queue))
3960				{
3961					$crq[] = array
3962					(
3963						'opcode' => 'rem allowed',
3964						'port' => $port_name,
3965						'vlans' => $queue,
3966					);
3967					foreach ($queue as $vlan_id)
3968						$used_vlans[$vlan_id]--;
3969				}
3970			break;
3971		case 'access->access':
3972			if ($port['old_native'] && $port['old_native'] != $port['new_native'])
3973			{
3974				$crq[] = array
3975				(
3976					'opcode' => 'unset access',
3977					'arg1' => $port_name,
3978					'arg2' => $port['old_native'],
3979				);
3980				$used_vlans[$port['old_native']]--;
3981			}
3982			break;
3983		case 'access->trunk':
3984			$crq[] = array
3985			(
3986				'opcode' => 'unset access',
3987				'arg1' => $port_name,
3988				'arg2' => $port['old_native'],
3989			);
3990			$used_vlans[$port['old_native']]--;
3991			break;
3992		case 'trunk->access':
3993			if ($port['old_native'])
3994				$crq[] = array
3995				(
3996					'opcode' => 'unset native',
3997					'arg1' => $port_name,
3998					'arg2' => $port['old_native'],
3999				);
4000			$vlans_to_remove = $port['old_allowed'];
4001			$queues = array();
4002			$queues[] = array_intersect ($employed_vlans, $vlans_to_remove); // remove employed vlans first
4003			$queues[] = array_diff ($vlans_to_remove, $employed_vlans);// remove other vlans afterwards
4004			foreach ($queues as $queue)
4005				if (count ($queue))
4006				{
4007					$crq[] = array
4008					(
4009						'opcode' => 'rem allowed',
4010						'port' => $port_name,
4011						'vlans' => $queue,
4012					);
4013					foreach ($queue as $vlan_id)
4014						$used_vlans[$vlan_id]--;
4015				}
4016			break;
4017		default:
4018			throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
4019		}
4020
4021		// destroy unneeded VLANs on device
4022		$deleted_vlans = array();
4023		foreach ($vlans_to_del as $vlan_id)
4024			if ($used_vlans[$vlan_id] == 0)
4025			{
4026				$crq[] = array
4027				(
4028					'opcode' => 'destroy VLAN',
4029					'arg1' => $vlan_id,
4030				);
4031				$deleted_vlans[] = $vlan_id;
4032			}
4033		$vlans_to_del = array_diff ($vlans_to_del, $deleted_vlans);
4034
4035		// create new VLANs on device
4036		$added_vlans = array_intersect ($vlans_to_add, $port['new_allowed']);
4037		foreach ($added_vlans as $vlan_id)
4038			$crq[] = array
4039			(
4040				'opcode' => 'create VLAN',
4041				'arg1' => $vlan_id,
4042			);
4043		$vlans_to_add = array_diff ($vlans_to_add, $added_vlans);
4044
4045		// change port mode if needed
4046		if ($port['old_mode'] != $port['new_mode'])
4047			$crq[] = array
4048			(
4049				'opcode' => 'set mode',
4050				'arg1' => $port_name,
4051				'arg2' => $port['new_mode'],
4052			);
4053
4054		// Now, when all new VLANs are created (queued), it is safe to assign (queue)
4055		// ports to the new VLANs.
4056		switch ($port['old_mode'] . '->' . $port['new_mode'])
4057		{
4058		case 'trunk->trunk':
4059			// For each allowed VLAN that is present on the "new" list and missing from
4060			// the "old" one, queue a command to assign current port to that VLAN.
4061			if (count ($tmp = array_diff ($port['new_allowed'], $port['old_allowed'])))
4062				$crq[] = array
4063				(
4064					'opcode' => 'add allowed',
4065					'port' => $port_name,
4066					'vlans' => $tmp,
4067				);
4068			// One of the "allowed" VLANs for this port may probably be "native".
4069			// "new native" is set and differs from "old native"
4070			if ($port['new_native'] && $port['new_native'] != $port['old_native'])
4071				$crq[] = array
4072				(
4073					'opcode' => 'set native',
4074					'arg1' => $port_name,
4075					'arg2' => $port['new_native'],
4076				);
4077			break;
4078		case 'access->access':
4079			if ($port['new_native'] && $port['new_native'] != $port['old_native'])
4080				$crq[] = array
4081				(
4082					'opcode' => 'set access',
4083					'arg1' => $port_name,
4084					'arg2' => $port['new_native'],
4085				);
4086			break;
4087		case 'access->trunk':
4088			if (count ($port['new_allowed']))
4089				$crq[] = array
4090				(
4091					'opcode' => 'add allowed',
4092					'port' => $port_name,
4093					'vlans' => $port['new_allowed'],
4094				);
4095			if ($port['new_native'])
4096				$crq[] = array
4097				(
4098					'opcode' => 'set native',
4099					'arg1' => $port_name,
4100					'arg2' => $port['new_native'],
4101				);
4102			break;
4103		case 'trunk->access':
4104			$crq[] = array
4105			(
4106				'opcode' => 'set access',
4107				'arg1' => $port_name,
4108				'arg2' => $port['new_native'],
4109			);
4110			break;
4111		default:
4112			throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
4113		}
4114	}
4115
4116	// add the rest of VLANs to device (compulsory VLANs)
4117	foreach ($vlans_to_add as $vlan_id)
4118		$crq[] = array
4119		(
4120			'opcode' => 'create VLAN',
4121			'arg1' => $vlan_id,
4122		);
4123
4124	return $crq;
4125}
4126
4127function exportSwitch8021QConfig
4128(
4129	$vswitch,
4130	$running_config,
4131	$changes
4132)
4133{
4134	$crq = generate8021QDeployOps ($vswitch, $running_config['vlanlist'], $running_config['portdata'], $changes);
4135	if (count ($crq))
4136	{
4137		array_unshift ($crq, array ('opcode' => 'begin configuration'));
4138		$crq[] = array ('opcode' => 'end configuration');
4139		if (considerConfiguredConstraint (spotEntity ('object', $vswitch['object_id']), '8021Q_WRI_AFTER_CONFT_LISTSRC'))
4140			$crq[] = array ('opcode' => 'save configuration');
4141		setVLANSwitchTimestamp ($vswitch['object_id'], 'last_push_started');
4142		setDevice8021QConfig ($vswitch['object_id'], $crq, $running_config['vlannames']);
4143		setVLANSwitchTimestamp ($vswitch['object_id'], 'last_push_finished');
4144	}
4145	return count ($crq);
4146}
4147
4148// filter list of changed ports to cancel changes forbidden by VST and domain
4149function filter8021QChangeRequests
4150(
4151	$domain_vlanlist,
4152	$before,  // current saved configuration of all ports
4153	$changes  // changed ports with VST markup
4154)
4155{
4156	$domain_immune_vlans = array();
4157	foreach ($domain_vlanlist as $vlan_id => $vlan)
4158		if ($vlan['vlan_type'] == 'alien')
4159			$domain_immune_vlans[] = $vlan_id;
4160	$ret = array();
4161	foreach ($changes as $port_name => $port)
4162	{
4163		// VST violation ?
4164		if (!goodModeForVSTRole ($port['mode'], $port['vst_role']))
4165			continue; // ignore change request
4166		// find and cancel any changes regarding immune VLANs
4167		switch ($port['mode'])
4168		{
4169		case 'access':
4170			foreach ($domain_immune_vlans as $immune)
4171				// Reverting an attempt to set an access port from
4172				// "normal" VLAN to immune one (or vice versa) requires
4173				// special handling, becase the calling function has
4174				// discarded the old contents of 'allowed' for current port.
4175				if
4176				(
4177					$before[$port_name]['native'] == $immune ||
4178					$port['native'] == $immune
4179				)
4180				{
4181					$port['native'] = $before[$port_name]['native'];
4182					$port['allowed'] = array ($port['native']);
4183					// Such reversal happens either once or never for an
4184					// access port.
4185					break;
4186				}
4187			break;
4188		case 'trunk':
4189			// restrict adding alien vlans into trunk
4190			$before_vlans = isset ($before[$port_name]) ? $before[$port_name]['allowed'] : array();
4191			$added = array_diff ($port['allowed'], $before_vlans);
4192			$restricted = array_diff ($added, array_keys ($domain_vlanlist));
4193			$port['allowed'] = array_diff ($port['allowed'], $restricted);
4194			if (in_array ($port['native'], $restricted))
4195				$port['native'] = 0;
4196
4197			foreach ($domain_immune_vlans as $immune)
4198				if (in_array ($immune, $before[$port_name]['allowed'])) // was allowed before
4199				{
4200					if (!in_array ($immune, $port['allowed']))
4201						$port['allowed'][] = $immune; // restore
4202					if ($before[$port_name]['native'] == $immune) // and was native
4203						$port['native'] = $immune; // also restore
4204				}
4205				else // wasn't
4206				{
4207					if (in_array ($immune, $port['allowed']))
4208						unset ($port['allowed'][array_search ($immune, $port['allowed'])]); // cancel
4209					if ($port['native'] == $immune)
4210						$port['native'] = $before[$port_name]['native'];
4211				}
4212			break;
4213		default:
4214			throw new InvalidArgException ('mode', $port['mode']);
4215		}
4216		// save work
4217		$ret[$port_name] = $port;
4218	}
4219	return $ret;
4220}
4221
4222function getEmployedVlans ($object_id, $domain_vlanlist)
4223{
4224	$employed = array(); // keyed by vlan_id. Value is dummy int
4225	// find persistent VLANs in domain
4226	foreach ($domain_vlanlist as $vlan_id => $vlan)
4227		if ($vlan['vlan_type'] == 'compulsory')
4228			$employed[$vlan_id] = 1;
4229
4230	// find VLANs for object's L3 allocations
4231	foreach (getObjectIPAllocationList ($object_id) as $ip_bin => $alloc)
4232		if ($net = spotNetworkByIP ($ip_bin))
4233			foreach ($net['8021q'] as $vlan)
4234				if (isset ($domain_vlanlist[$vlan['vlan_id']]) && ! isset ($employed[$vlan['vlan_id']]))
4235					$employed[$vlan['vlan_id']] = 1;
4236	$ret = array_keys ($employed);
4237	$override = callHook ('getEmployedVlans_hook', $ret, $object_id, $domain_vlanlist);
4238	if (isset ($override))
4239		$ret = $override;
4240	return $ret;
4241}
4242
4243// take port list with order applied and return uplink ports in the same format
4244function produceUplinkPorts ($domain_vlanlist, $portlist, $object_id)
4245{
4246	$ret = array();
4247
4248	$employed = array();
4249	foreach (getEmployedVlans ($object_id, $domain_vlanlist) as $vlan_id)
4250		$employed[$vlan_id] = $vlan_id;
4251
4252	foreach ($portlist as $port_name => $port)
4253		if ($port['vst_role'] != 'uplink')
4254			foreach ($port['allowed'] as $vlan_id)
4255				if (! isset ($employed[$vlan_id]) && isset ($domain_vlanlist[$vlan_id]))
4256					$employed[$vlan_id] = $vlan_id;
4257
4258	foreach ($portlist as $port_name => $port)
4259		if ($port['vst_role'] == 'uplink')
4260			$ret[$port_name] = array
4261			(
4262				'vst_role' => 'uplink',
4263				'mode' => 'trunk',
4264				'allowed' => filterVLANList ($employed, $port['wrt_vlans']),
4265				'native' => 0,
4266			);
4267	return $ret;
4268}
4269
4270function same8021QConfigs ($a, $b)
4271{
4272	return	$a['mode'] == $b['mode'] &&
4273		array_values_same ($a['allowed'], $b['allowed']) &&
4274		$a['native'] == $b['native'];
4275}
4276
4277// Return TRUE, if the port can be edited by the user.
4278function editable8021QPort ($port)
4279{
4280	return in_array ($port['vst_role'], array ('trunk', 'access', 'anymode'));
4281}
4282
4283// Decide, whether the given 802.1Q port mode is permitted by
4284// VST port role.
4285function goodModeForVSTRole ($mode, $role)
4286{
4287	switch ($mode)
4288	{
4289	case 'access':
4290		return in_array ($role, array ('access', 'anymode'));
4291	case 'trunk':
4292		return in_array ($role, array ('trunk', 'uplink', 'downlink', 'anymode'));
4293	default:
4294		throw new InvalidArgException ('mode', $mode);
4295	}
4296}
4297
4298/*
4299
4300Relation between desired (D), cached (C) and running (R)
4301copies of switch ports (P) list.
4302
4303  D         C           R
4304+---+     +---+       +---+
4305| P |-----| P |-?  +--| P |
4306+---+     +---+   /   +---+
4307| P |-----| P |--+  ?-| P |
4308+---+     +---+       +---+
4309| P |-----| P |-------| P |
4310+---+     +---+       +---+
4311| P |-----| P |--+  ?-| P |
4312+---+     +---+   \   +---+
4313| P |-----| P |--+ +--| P |
4314+---+     +---+   \   +---+
4315                   +--| P |
4316                      +---+
4317                    ?-| P |
4318                      +---+
4319
4320A modified local version of a port in "conflict" state ignores remote
4321changes until remote change maintains its difference. Once both edits
4322match, the local copy "locks" on the remote and starts tracking it.
4323
4324v
4325a           "o" -- remOte version
4326l           "l" -- Local version
4327u           "b" -- Both versions
4328e
4329
4330^
4331|         o           b
4332|           o
4333| l l l l l l b     b
4334|   o   o       b
4335| o               b
4336|
4337|     o
4338|
4339|
43400----------------------------------------------> time
4341
4342*/
4343function get8021QSyncOptions
4344(
4345	$vswitch,
4346	$D, // desired config
4347	$C, // cached config
4348	$R  // running-config
4349)
4350{
4351	$default_port = array
4352	(
4353		'mode' => 'access',
4354		'allowed' => array (VLAN_DFL_ID),
4355		'native' => VLAN_DFL_ID,
4356	);
4357	$ret = array();
4358	$allports = array();
4359	foreach (array_unique (array_merge (array_keys ($C), array_keys ($R))) as $pn)
4360		$allports[$pn] = array();
4361	foreach (apply8021QOrder ($vswitch, $allports) as $pn => $port)
4362	{
4363		// catch anomalies early
4364		if ($port['vst_role'] == 'none')
4365		{
4366			if ((! array_key_exists ($pn, $R) || $R[$pn]['mode'] == 'none') && ! array_key_exists ($pn, $C))
4367				$ret[$pn] = array ('status' => 'none');
4368			else
4369				$ret[$pn] = array
4370				(
4371					'status' => 'martian_conflict',
4372					'left' => array_fetch ($C, $pn, array ('mode' => 'none')),
4373					'right' => array_fetch ($R, $pn, array ('mode' => 'none')),
4374				);
4375			continue;
4376		}
4377		elseif ((! array_key_exists ($pn, $R) || $R[$pn]['mode'] == 'none') && array_key_exists ($pn, $C))
4378		{
4379			$ret[$pn] = array
4380			(
4381				'status' => 'martian_conflict',
4382				'left' => array_fetch ($C, $pn, array ('mode' => 'none')),
4383				'right' => array_fetch ($R, $pn, array ('mode' => 'none')),
4384			);
4385			continue;
4386		}
4387		// (DC_): port missing from device
4388		if (!array_key_exists ($pn, $R))
4389		{
4390			$ret[$pn] = array ('left' => $D[$pn]);
4391			if (same8021QConfigs ($D[$pn], $default_port))
4392				$ret[$pn]['status'] = 'ok_to_delete';
4393			else
4394			{
4395				$ret[$pn]['status'] = 'delete_conflict';
4396				$ret[$pn]['lastseen'] = $C[$pn];
4397			}
4398			continue;
4399		}
4400		// (__R): port missing from DB
4401		if (!array_key_exists ($pn, $C))
4402		{
4403			// Allow importing any configuration that passes basic
4404			// validation. If port mode doesn't match its VST role,
4405			// this will be handled later WRT each port.
4406			$ret[$pn] = array
4407			(
4408				'status' => acceptable8021QConfig ($R[$pn]) ? 'ok_to_add' : 'add_conflict',
4409				'right' => $R[$pn],
4410			);
4411			continue;
4412		}
4413		$D_eq_C = same8021QConfigs ($D[$pn], $C[$pn]);
4414		$C_eq_R = same8021QConfigs ($C[$pn], $R[$pn]);
4415		// (DCR), D = C = R: data in sync
4416		if ($D_eq_C && $C_eq_R) // implies D == R
4417		{
4418			$ret[$pn] = array
4419			(
4420				'status' => 'in_sync',
4421				'both' => $R[$pn],
4422			);
4423			continue;
4424		}
4425		// (DCR), D = C: no local edit in the way
4426		if ($D_eq_C)
4427			$ret[$pn] = array
4428			(
4429				'status' => 'ok_to_pull',
4430				'left' => $D[$pn],
4431				'right' => $R[$pn],
4432			);
4433		// (DCR), C = R: no remote edit in the way
4434		elseif ($C_eq_R)
4435			$ret[$pn] = array
4436			(
4437				'status' => 'ok_to_push',
4438				'left' => $D[$pn],
4439				'right' => $R[$pn],
4440			);
4441		// (DCR), D = R: end of version conflict, restore tracking
4442		elseif (same8021QConfigs ($D[$pn], $R[$pn]))
4443			$ret[$pn] = array
4444			(
4445				'status' => 'ok_to_merge',
4446				'both' => $R[$pn],
4447			);
4448		else // D != C, C != R, D != R: version conflict
4449			$ret[$pn] = array
4450			(
4451				'status' => editable8021QPort ($port) ?
4452					// In case the port is normally updated by user, let him
4453					// resolve the conflict. If the system manages this port,
4454					// arrange the data to let remote version go down.
4455					'merge_conflict' : 'ok_to_push_with_merge',
4456				'left' => $D[$pn],
4457				'right' => $R[$pn],
4458			);
4459	}
4460	return $ret;
4461}
4462
4463// return number of records updated successfully of FALSE, if a conflict was in the way
4464function exec8021QDeploy ($object_id, $do_push)
4465{
4466	$nsaved = $npushed = $nsaved_uplinks = 0;
4467	if (NULL === $vswitch = getVLANSwitchInfo ($object_id))
4468		throw new InvalidArgException ('object_id', $object_id, 'VLAN domain is not set for this object');
4469	if (! tryDBMutex (__FUNCTION__ . "-$object_id", 10))
4470		throw new RTGatewayError ("802.1Q sync is already active for object #$object_id");
4471
4472	try
4473	{
4474		$R = getRunning8021QConfig ($vswitch['object_id']);
4475	}
4476	catch (RTGatewayError $e)
4477	{
4478		setVLANSwitchError ($object_id, E_8021Q_PULL_REMOTE_ERROR);
4479		throw $e;
4480	}
4481
4482	global $dbxlink;
4483	$dbxlink->beginTransaction();
4484	$vswitch = getVLANSwitchInfo ($object_id, 'FOR UPDATE');
4485	$D = getStored8021QConfig ($vswitch['object_id'], 'desired');
4486	$Dnew = $D;
4487	$C = getStored8021QConfig ($vswitch['object_id'], 'cached');
4488	$conflict = FALSE;
4489	$ok_to_push = array();
4490	foreach (get8021QSyncOptions ($vswitch, $D, $C, $R['portdata']) as $pn => $port)
4491	{
4492		// always update cache with new data from switch
4493		switch ($port['status'])
4494		{
4495		case 'ok_to_merge':
4496			// FIXME: this can be logged
4497			upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['both'], $C[$pn]);
4498			break;
4499		case 'ok_to_delete':
4500			$nsaved += del8021QPort ($vswitch['object_id'], $pn);
4501			unset ($Dnew[$pn]);
4502			break;
4503		case 'ok_to_add':
4504			$nsaved += add8021QPort ($vswitch['object_id'], $pn, $port['right']);
4505			$Dnew[$pn] = $port['right'];
4506			break;
4507		case 'delete_conflict':
4508		case 'merge_conflict':
4509		case 'add_conflict':
4510		case 'martian_conflict':
4511			$conflict = TRUE;
4512			break;
4513		case 'ok_to_pull':
4514			// FIXME: this can be logged
4515			$nsaved += upd8021QPort ('desired', $vswitch['object_id'], $pn, $port['right'], $D[$pn]);
4516			upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right'], $C[$pn]);
4517			$Dnew[$pn] = $port['right'];
4518			break;
4519		case 'ok_to_push_with_merge':
4520			upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right'], $C[$pn]);
4521			// fall through
4522		case 'ok_to_push':
4523			$ok_to_push[$pn] = $port['left'];
4524			break;
4525		}
4526	}
4527	// redo uplinks if some changes were pulled
4528	if ($nsaved)
4529	{
4530		$domain_vlanlist = getDomainVLANList ($vswitch['domain_id']);
4531		$Dnew = apply8021QOrder ($vswitch, $Dnew);
4532		// Take new "desired" configuration and derive uplink port configuration
4533		// from it. Then cancel changes to immune VLANs and save resulting
4534		// changes (if any left).
4535		$new_uplinks = filter8021QChangeRequests ($domain_vlanlist, $Dnew, produceUplinkPorts ($domain_vlanlist, $Dnew, $vswitch['object_id']));
4536		$nsaved_uplinks += replace8021QPorts ('desired', $vswitch['object_id'], $Dnew, $new_uplinks);
4537	}
4538
4539	$out_of_sync = FALSE;
4540	$errno = E_8021Q_NOERROR;
4541	if ($nsaved + $nsaved_uplinks || count ($ok_to_push))
4542		$out_of_sync = TRUE;
4543	if ($conflict)
4544	{
4545		$errno = E_8021Q_VERSION_CONFLICT;
4546		$out_of_sync = TRUE;
4547	}
4548	setVLANSwitchError ($object_id, $errno);
4549
4550	$mutex_rev = $vswitch['mutex_rev'];
4551	if ($vswitch['out_of_sync'] == "yes" && ! $out_of_sync)
4552		detouchVLANSwitch ($object_id, $mutex_rev);
4553	elseif ($vswitch['out_of_sync'] == "no" && $out_of_sync)
4554	{
4555		touchVLANSwitch ($object_id);
4556		++$mutex_rev;
4557	}
4558	$dbxlink->commit();
4559
4560	if ($out_of_sync && $do_push)
4561	{
4562		try
4563		{
4564			$npushed += exportSwitch8021QConfig ($vswitch, $R, $ok_to_push);
4565		}
4566		catch (RTGatewayError $r)
4567		{
4568			setVLANSwitchError ($object_id, E_8021Q_PUSH_REMOTE_ERROR);
4569			callHook ('pushErrorHandler', $object_id, $r);
4570			throw $r;
4571		}
4572
4573		// update cache for ports deployed
4574		$dbxlink->beginTransaction();
4575		replace8021QPorts ('cached', $vswitch['object_id'], $R['portdata'], $ok_to_push);
4576		setVLANSwitchError ($object_id, E_8021Q_NOERROR);
4577		detouchVLANSwitch ($object_id, $mutex_rev);
4578		$dbxlink->commit();
4579	}
4580	// start downlink work only after unlocking current object to make deadlocks less likely to happen
4581	// TODO: only process changed uplink ports
4582	if ($nsaved_uplinks)
4583		initiateUplinksReverb ($vswitch['object_id'], $new_uplinks);
4584	return $conflict ? FALSE : $nsaved + $npushed + $nsaved_uplinks;
4585}
4586
4587function strerror8021Q ($errno)
4588{
4589	$errstr = array
4590	(
4591		E_8021Q_VERSION_CONFLICT => 'pull failed due to version conflict',
4592		E_8021Q_PULL_REMOTE_ERROR => 'pull failed due to remote error',
4593		E_8021Q_PUSH_REMOTE_ERROR => 'push failed due to remote error',
4594		E_8021Q_SYNC_DISABLED => 'sync disabled by operator',
4595	);
4596	return array_fetch ($errstr, $errno, "unknown error code ${errno}");
4597}
4598
4599function saveDownlinksReverb ($object_id, $requested_changes)
4600{
4601	$nsaved = 0;
4602	global $dbxlink;
4603	$dbxlink->beginTransaction();
4604	if (NULL === $vswitch = getVLANSwitchInfo ($object_id, 'FOR UPDATE')) // not configured, bail out
4605	{
4606		$dbxlink->rollBack();
4607		return;
4608	}
4609	$domain_vlanlist = getDomainVLANList ($vswitch['domain_id']);
4610	// aplly VST to the smallest set necessary
4611	$requested_changes = apply8021QOrder ($vswitch, $requested_changes);
4612	$before = getStored8021QConfig ($object_id, 'desired', array_keys ($requested_changes));
4613	$changes_to_save = array();
4614	// first filter by wrt_vlans constraint
4615	foreach ($requested_changes as $pn => $requested)
4616		if (array_key_exists ($pn, $before) && $requested['vst_role'] == 'downlink')
4617			$changes_to_save[$pn] = array
4618			(
4619				'vst_role' => 'downlink',
4620				'mode' => 'trunk',
4621				'allowed' => filterVLANList ($requested['allowed'], $requested['wrt_vlans']),
4622				'native' => 0,
4623			);
4624	// immune VLANs filter
4625	foreach (filter8021QChangeRequests ($domain_vlanlist, $before, $changes_to_save) as $pn => $finalconfig)
4626		$nsaved += upd8021QPort ('desired', $vswitch['object_id'], $pn, $finalconfig, $before[$pn]);
4627	if ($nsaved)
4628		touchVLANSwitch ($vswitch['object_id']);
4629	$dbxlink->commit();
4630	return $nsaved;
4631}
4632
4633// Use records from Port and Link tables to run a series of tasks on remote
4634// objects. These device-specific tasks will adjust downlink ports according to
4635// the current configuration of given uplink ports.
4636function initiateUplinksReverb ($object_id, $uplink_ports)
4637{
4638	// Filter and regroup all requests (regardless of how many will succeed)
4639	// to end up with no more than one execution per remote object.
4640	$upstream_config = array();
4641	foreach (getObjectPortsAndLinks ($object_id, FALSE) as $portinfo)
4642		if
4643		(
4644			$portinfo['linked'] &&
4645			array_key_exists ($portinfo['name'], $uplink_ports)
4646		)
4647			$upstream_config[$portinfo['remote_object_id']][$portinfo['remote_name']] = $uplink_ports[$portinfo['name']];
4648	// Note that when current object has several Port records inder same name
4649	// (but with unique IIF-OIF pair), these ports can be Link'ed to different
4650	// remote objects (using different media types, perhaps). Such a case can
4651	// be considered as normal, and each remote object will show up on the
4652	// task list (with its actual remote port name, of course).
4653	$done = 0;
4654	foreach ($upstream_config as $remote_object_id => $remote_ports)
4655		if ($changed = saveDownlinksReverb ($remote_object_id, $remote_ports))
4656		{
4657			$done += $changed;
4658			$done += apply8021qChangeRequest ($remote_object_id, array(), FALSE);
4659		}
4660	return $done;
4661}
4662
4663// returns the first port from $ports that is connected and its name equals to $name
4664function findConnectedPort ($ports, $name)
4665{
4666	foreach ($ports as $portinfo)
4667		if ($portinfo['linked'] && $portinfo['name'] == $name)
4668			return $portinfo;
4669}
4670
4671// checks if the desired config of all uplink/downlink ports of that switch, and
4672// his neighbors, equals to the recalculated config. If not,
4673// sets the recalculated configs as desired and puts switches into out-of-sync state.
4674// Returns an array with object_id as key and portname subkey
4675function recalc8021QPorts ($switch_id)
4676{
4677	$ret = array
4678	(
4679		'switches' => 0,
4680		'ports' => 0,
4681	);
4682	$vswitch = getVLANSwitchInfo ($switch_id);
4683	if (! $vswitch)
4684		return $ret;
4685
4686	$ports = array(); // only linked ports appear here
4687	foreach (getObjectPortsAndLinks ($switch_id, FALSE) as $portinfo)
4688		if ($portinfo['linked'])
4689			$ports[$portinfo['name']] = $portinfo;
4690
4691	$order = apply8021QOrder ($vswitch, getStored8021QConfig ($switch_id, 'desired', array_keys ($ports)));
4692
4693	$self_processed = FALSE;
4694
4695	// calculate remote uplinks and copy them to local downlinks
4696	foreach ($ports as $portinfo)
4697		if
4698		(
4699			isset ($order[$portinfo['name']]) &&
4700			$order[$portinfo['name']]['vst_role'] == 'downlink'
4701		)
4702		{
4703			// if there is a link with remote side type 'uplink', use its vlan mask
4704			$remote_pn = $portinfo['remote_name'];
4705			$remote_vswitch = getVLANSwitchInfo ($portinfo['remote_object_id']);
4706			if (! $remote_vswitch)
4707				continue;
4708			$n = apply8021qChangeRequest ($remote_vswitch['object_id'], array(), FALSE, NULL, array($portinfo['remote_name'] => ''));
4709			$ret['switches'] += $n ? 1 : 0;
4710			$ret['ports'] += $n;
4711			if ($n > 1)
4712				$self_processed = TRUE;
4713		}
4714
4715	// if no connected downlinks found, re-calculate local uplinks
4716	// (otherwise the remote switch has already called apply8021qChangeRequest for us)
4717	if ($self_processed)
4718		$ret['switches'] += 1;
4719	elseif ($ret['ports'] == 0)
4720	{
4721		$n = apply8021qChangeRequest ($switch_id, array(), FALSE, NULL, array());
4722		$ret['switches'] += $n ? 1 : 0;
4723		$ret['ports'] += $n;
4724	}
4725	return $ret;
4726}
4727
4728// This function takes 802.1q order and the order of corresponding remote uplink port.
4729// It returns assotiative array with single row. Key = $portname, value - produced port
4730// order based on $order, and having vlan list replaced based on $uplink_order, but filtered.
4731function produceDownlinkPort ($domain_vlanlist, $portname, $order, $uplink_order)
4732{
4733	$new_order = array ($portname => $order[$portname]);
4734	$new_order[$portname]['mode'] = 'trunk';
4735	$new_order[$portname]['allowed'] = filterVLANList ($uplink_order['allowed'], $new_order[$portname]['wrt_vlans']);
4736	$new_order[$portname]['native'] = 0;
4737	return filter8021QChangeRequests ($domain_vlanlist, $order, $new_order);
4738}
4739
4740function detectVLANSwitchQueue ($vswitch)
4741{
4742	if ($vswitch['out_of_sync'] == 'no' && $vswitch['last_errno'] != E_8021Q_SYNC_DISABLED)
4743		return 'done';
4744	switch ($vswitch['last_errno'])
4745	{
4746	case E_8021Q_NOERROR:
4747		$last_change_age = time() - $vswitch['last_change'];
4748		if ($last_change_age > getConfigVar ('8021Q_DEPLOY_MAXAGE'))
4749			return 'sync_ready';
4750		elseif ($last_change_age < getConfigVar ('8021Q_DEPLOY_MINAGE'))
4751			return 'sync_aging';
4752		else
4753			return 'sync_ready';
4754	case E_8021Q_VERSION_CONFLICT:
4755	case E_8021Q_PULL_REMOTE_ERROR:
4756	case E_8021Q_PUSH_REMOTE_ERROR:
4757		$last_error_age = time() - $vswitch['last_error_ts'];
4758		if ($last_error_age < getConfigVar ('8021Q_DEPLOY_RETRY'))
4759			return 'resync_aging';
4760		else
4761			return 'resync_ready';
4762	case E_8021Q_SYNC_DISABLED:
4763		return 'disabled';
4764	}
4765	return '';
4766}
4767
4768function get8021QDeployQueues()
4769{
4770	global $dqtitle;
4771	$ret = array();
4772	foreach (array_keys ($dqtitle) as $qcode)
4773		$ret[$qcode] = array();
4774	foreach (getVLANSwitches() as $object_id)
4775	{
4776		$vswitch = getVLANSwitchInfo ($object_id);
4777		if ('' != $qcode = detectVLANSwitchQueue ($vswitch))
4778			$ret[$qcode][] = $vswitch;
4779	}
4780	return $ret;
4781}
4782
4783function acceptable8021QConfig ($port)
4784{
4785	switch ($port['mode'])
4786	{
4787	case 'trunk':
4788		return $port['native'] == 0 || in_array ($port['native'], $port['allowed']);
4789	case 'access':
4790		return $port['native'] > 0 && count ($port['allowed']) == 1 &&
4791			in_array ($port['native'], $port['allowed']);
4792	default:
4793		return FALSE;
4794	}
4795}
4796
4797function nativeVlanChangePermitted ($pn, $from_vid, $to_vid, $op = NULL)
4798{
4799	$before = array ($pn => array (
4800		'mode' => 'access',
4801		'native' => $from_vid,
4802		'allowed' => array ($from_vid),
4803	));
4804	$changes = array ($pn => array (
4805		'mode' => 'access',
4806		'native' => $to_vid,
4807		'allowed' => array ($to_vid),
4808	));
4809
4810	return count (authorize8021QChangeRequests ($before, $changes, $op)) != 0;
4811}
4812
4813function authorize8021QChangeRequests ($before, $changes, $op = NULL)
4814{
4815	if (NULL !== $ret = callHook ('authorize8021QChangeRequests_hook', $before, $changes, $op))
4816		return $ret;
4817	global $script_mode;
4818	if (isset ($script_mode) && $script_mode)
4819		return $changes;
4820	$ret = array();
4821	foreach ($changes as $pn => $new)
4822	{
4823		if (! array_key_exists($pn, $before))
4824		{
4825			$removed = array();
4826			$added = $new['allowed'];
4827		}
4828		else
4829		{
4830			$old = $before[$pn];
4831			$removed = array_diff ($old['allowed'], $new['allowed']);
4832			$added = array_diff ($new['allowed'], $old['allowed']);
4833			// treat native vlan replacement as removing/adding of old/new vlans,
4834			// even if they persist in both $old and $new allowed list
4835			if ($new['native'] != $old['native'])
4836			{
4837				if ($old['native'] && ! in_array($old['native'], $removed))
4838					$removed[] = $old['native'];
4839				if ($new['native'] && ! in_array($new['native'], $added))
4840					$added[] = $new['native'];
4841			}
4842		}
4843		foreach ($removed as $vid)
4844			if (!permitted (NULL, NULL, $op, array (array ('tag' => "\$fromvlan_{$vid}"), array ('tag' => "\$vlan_{$vid}"))))
4845				continue 2; // next port
4846		foreach ($added as $vid)
4847			if (!permitted (NULL, NULL, $op, array (array ('tag' => "\$tovlan_{$vid}"), array ('tag' => "\$vlan_{$vid}"))))
4848				continue 2; // next port
4849		$ret[$pn] = $new;
4850	}
4851	return $ret;
4852}
4853
4854function formatPortIIFOIF ($port)
4855{
4856	$ret = '';
4857	if ($port['iif_id'] != 1)
4858		$ret .= $port['iif_name'] . '/';
4859	$ret .= $port['oif_name'];
4860	return $ret;
4861}
4862
4863// Not an equivalent of the above but related.
4864function parsePortIIFOIF ($port_type)
4865{
4866	if (preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $port_type, $matches))
4867		return array ($matches[1], $matches[2]);
4868	if (preg_match ('/^([[:digit:]]+)$/', $port_type, $matches))
4869		return array (1, $matches[1]);
4870	throw new InvalidArgException ('port_type', $port_type, 'format error');
4871}
4872
4873// returns '<a...</a>' html string containing a link to specified port or object.
4874// link title is "hostname portname" if both parts are defined
4875function formatPortLink($host_id, $hostname, $port_id, $portname, $a_class = '')
4876{
4877	$href = 'index.php?page=object&object_id=' . urlencode($host_id);
4878	$additional = '';
4879	if (isset ($port_id))
4880	{
4881		$href .= '&hl_port_id=' . urlencode($port_id);
4882		$additional = "name=\"port-$port_id\"";
4883	}
4884	if (! empty($a_class))
4885		$additional .= ($additional == '' ? '' : ' '). "class='$a_class'";
4886
4887	$text_items = array();
4888	if (isset ($hostname))
4889		$text_items[] = $hostname;
4890	if (isset ($portname))
4891		$text_items[] = $portname;
4892
4893	return "<a $additional href=\"$href\">" . implode(' ', $text_items) . '</a>';
4894}
4895
4896// function returns a HTML-formatted link to the specified port
4897function formatPort ($port_info, $a_class = '')
4898{
4899	return formatPortLink
4900	(
4901		$port_info['object_id'],
4902		$port_info['object_name'],
4903		$port_info['id'],
4904		$port_info['name'],
4905		$a_class
4906	);
4907}
4908
4909// function returns a HTML-formatted link to remote port, connected to the specified port
4910function formatLinkedPort ($port_info, $a_class = '')
4911{
4912	return formatPortLink
4913	(
4914		$port_info['remote_object_id'],
4915		$port_info['remote_object_name'],
4916		$port_info['remote_id'],
4917		$port_info['remote_name'],
4918		$a_class
4919	);
4920}
4921
4922function compareDecomposedPortNames ($porta, $portb)
4923{
4924	$ret = 0;
4925
4926	$prefix_diff = strcmp ($porta['prefix'], $portb['prefix']);
4927
4928	// concatenation of 0..(n-1) numeric indices
4929	$a_parent = $porta['idx_parent'];
4930	$b_parent = $portb['idx_parent'];
4931
4932	$index_diff = 0;
4933	for ($i = 0; $i < $porta['numidx']; $i++)
4934	{
4935		if ($i >= $portb['numidx'])
4936		{
4937			$index_diff = 1; // a > b
4938			break;
4939		}
4940		if ($porta['index'][$i] != $portb['index'][$i])
4941		{
4942			$index_diff = $porta['index'][$i] - $portb['index'][$i];
4943			break;
4944		}
4945	}
4946	if ($index_diff == 0 && $porta['numidx'] < $portb['numidx'])
4947		$index_diff = -1; // a < b
4948
4949	// compare by portname fields
4950	if ($prefix_diff != 0 && ($porta['numidx'] <= 1 || $portb['numidx'] <= 1)) // if index count is lte 1, sort by prefix
4951	{
4952		$ret = $porta['numidx'] - $portb['numidx'];
4953		if ($ret == 0)
4954			$ret = $prefix_diff;
4955	}
4956	// if index count > 1 and ports have different prefixes in intersecting index sections, sort by prefix
4957	elseif ($prefix_diff != 0 && $a_parent != '' && $a_parent == $b_parent)
4958		$ret = $prefix_diff;
4959	// if indices are not equal, sort by index
4960	elseif ($index_diff != 0)
4961		$ret = $index_diff;
4962	// if all of name fields are equal, compare by some additional port fields
4963	elseif ($porta['iif_id'] != $portb['iif_id'])
4964		$ret = $porta['iif_id'] - $portb['iif_id'];
4965	elseif (0 != $result = strcmp ($porta['label'], $portb['label']))
4966		$ret = $result;
4967	elseif (0 != $result = strcmp ($porta['l2address'], $portb['l2address']))
4968		$ret = $result;
4969	elseif ($porta['id'] != $portb['id'])
4970		$ret = $porta['id'] - $portb['id'];
4971
4972	return ($ret > 0) - ($ret < 0);
4973}
4974
4975// Sort provided port list in a way based on natural. For example,
4976// switches can have ports:
4977// * fa0/1~48, gi0/1~4 (in this case 'gi' should come after 'fa'
4978// * fa1, gi0/1~48, te1/49~50 (type matters, then index)
4979// * gi5/1~3, te5/4~5 (here index matters more than type)
4980// This implementation makes port type (prefix) matter for all
4981// interfaces that have less than 2 indices, but for other ports
4982// their indices matter more than type (unless there is a clash
4983// of indices).
4984// When $name_in_value is TRUE, port name determines as $plist[$key]['name']
4985// Otherwise portname is the key of $plist
4986function sortPortList ($plist, $name_in_value = FALSE)
4987{
4988	$ret = array();
4989	$to_sort = array();
4990	$prefix_re = '/^([^0-9]*)[0-9].*$/';
4991	foreach ($plist as $pkey => $pvalue)
4992	{
4993		$pn = $name_in_value ? $pvalue['name'] : $pkey;
4994		$numbers = preg_split ('/[^0-9]+/', $pn, -1, PREG_SPLIT_NO_EMPTY);
4995		$parent = implode ('-', array_slice ($numbers, 0, count ($numbers) - 1));
4996		$to_sort[] = array
4997		(
4998			'key' => $pkey,
4999			'prefix' => preg_replace ($prefix_re, '\\1', $pn),
5000			'numidx' => count ($numbers),
5001			'index' => $numbers,
5002			'idx_parent' => $parent,
5003			'iif_id' => array_fetch ($pvalue, 'iif_id', 0),
5004			'label' => array_fetch ($pvalue, 'label', ''),
5005			'l2address' => array_fetch ($pvalue, 'l2address', ''),
5006			'id' => array_fetch ($pvalue, 'id', 0),
5007			'name' => $pn,
5008		);
5009	}
5010	usort ($to_sort, 'compareDecomposedPortNames');
5011	foreach ($to_sort as $pvalue)
5012		$ret[$pvalue['key']] = $plist[$pvalue['key']];
5013	return $ret;
5014}
5015
5016// This function works like standard php usort function and uses sortPortList.
5017function usort_portlist (&$portnames)
5018{
5019	$portnames = array_keys (sortPortList (array_fill_keys ($portnames, array())));
5020}
5021
5022// Return a "?, ?, ?, ... ?, ?" string consisting of N question marks. N > 0 because
5023// the string will be a part of an SQL query.
5024function questionMarks ($count)
5025{
5026	if (! isNaturalNumber ($count))
5027		throw new InvalidArgException ('count', $count, 'must be greater than zero');
5028	return implode (', ', array_fill (0, $count, '?'));
5029}
5030
5031// returns search results as an array keyed with realm name
5032// groups found entities by realms in 'summary'
5033function searchEntitiesByText ($terms)
5034{
5035	$summary = array();
5036
5037	if (FALSE !== ($ip_bin = ip4_checkparse ($terms)))
5038	// Search for IPv4 address.
5039	{
5040		if ($net_id = getIPv4AddressNetworkId ($ip_bin))
5041			$summary['ipv4addressbydq'][$ip_bin] = array ('net_id' => $net_id, 'ip' => $ip_bin);
5042	}
5043	elseif (FALSE !== ($ip_bin = ip6_checkparse ($terms)))
5044	// Search for IPv6 address
5045	{
5046		if ($net_id = getIPv6AddressNetworkId ($ip_bin))
5047			$summary['ipv6addressbydq'][$ip_bin] = array ('net_id' => $net_id, 'ip' => $ip_bin);
5048	}
5049	elseif (preg_match (RE_IP4_NET, $terms))
5050	// Search for IPv4 network
5051	{
5052		list ($base, $len) = explode ('/', $terms);
5053		if (NULL !== ($net_id = getIPv4AddressNetworkId (ip4_parse ($base), $len + 1)))
5054			$summary['ipv4net'][$net_id] = spotEntity('ipv4net', $net_id);
5055	}
5056	elseif (preg_match ('@(.*)/(\d+)$@', $terms, $matches) && FALSE !== ($ip_bin = ip6_checkparse ($matches[1])))
5057	// Search for IPv6 network
5058	{
5059		if (NULL !== ($net_id = getIPv6AddressNetworkId ($ip_bin, $matches[2] + 1)))
5060			$summary['ipv6net'][$net_id] = spotEntity('ipv6net', $net_id);
5061	}
5062	elseif (preg_match ('/^vlan\s*(\d+)$/i', $terms, $matches))
5063	{
5064		$byID = getSearchResultByField
5065		(
5066			'VLANDescription',
5067			array ('domain_id', 'vlan_id'),
5068			'vlan_id',
5069			$matches[1],
5070			'domain_id',
5071			1
5072		);
5073		foreach ($byID as $vlan)
5074		{
5075			// add vlans to results
5076			$vlan_ck = $vlan['domain_id'] . '-' . $vlan['vlan_id'];
5077			$vlan['id'] = $vlan_ck;
5078			$summary['vlan'][$vlan_ck] = $vlan;
5079			// add linked networks to results
5080			$vlan_info = getVLANInfo ($vlan_ck);
5081			foreach ($vlan_info['ipv4nets'] as $net_id)
5082				$summary['ipv4net'][$net_id] = spotEntity ("ipv4net", $net_id);
5083			foreach ($vlan_info['ipv6nets'] as $net_id)
5084				$summary['ipv6net'][$net_id] = spotEntity ("ipv6net", $net_id);
5085		}
5086	}
5087	else
5088	// Search for objects, addresses, networks, virtual services and RS pools by their description.
5089	{
5090		// search by FQDN has special treatment - if single object found, do not search by other fields
5091		$object_id_by_fqdn = NULL;
5092		$domains = preg_split ('/\s*,\s*/', mb_strtolower (getConfigVar ('SEARCH_DOMAINS')));
5093		if (! empty ($domains) && $object_id = searchByMgmtHostname ($terms))
5094		{
5095			// get FQDN
5096			$attrs = getAttrValues ($object_id);
5097			$fqdn = '';
5098			if (isset ($attrs[3]['value']))
5099				$fqdn = mb_strtolower (trim ($attrs[3]['value']));
5100			foreach ($domains as $domain)
5101				if ('.' . $domain === substr ($fqdn, -strlen ($domain) - 1))
5102				{
5103					$object_id_by_fqdn = $object_id;
5104					break;
5105				}
5106		}
5107		if ($object_id_by_fqdn)
5108		{
5109			$summary['object'][$object_id_by_fqdn] = array
5110			(
5111				'id' => $object_id_by_fqdn,
5112				'method' => 'fqdn',
5113			);
5114		}
5115		else
5116		{
5117			$summary['object'] = getObjectSearchResults ($terms);
5118			$summary['ipv4addressbydescr'] = getIPv4AddressSearchResult ($terms);
5119			$summary['ipv6addressbydescr'] = getIPv6AddressSearchResult ($terms);
5120			$summary['ipv4net'] = getIPv4PrefixSearchResult ($terms);
5121			$summary['ipv6net'] = getIPv6PrefixSearchResult ($terms);
5122			$summary['ipv4rspool'] = getIPv4RSPoolSearchResult ($terms);
5123			$summary['ipvs'] = getVServiceSearchResult ($terms);
5124			$summary['ipv4vs'] = getIPv4VServiceSearchResult ($terms);
5125			$summary['user'] = getAccountSearchResult ($terms);
5126			$summary['file'] = getFileSearchResult ($terms);
5127			$summary['rack'] = getRackSearchResult ($terms);
5128			$summary['row'] = getRowSearchResult ($terms);
5129			$summary['location'] = getLocationSearchResult ($terms);
5130			$summary['vlan'] = getVLANSearchResult ($terms);
5131		}
5132	}
5133	# Filter search results in a way in some realms to omit records that the
5134	# user would not be able to browse anyway.
5135	foreach (array ('object', 'ipv4net', 'ipv6net', 'ipv4rspool', 'ipv4vs', 'ipvs', 'file', 'rack', 'row', 'location') as $realm)
5136		if (isset ($summary[$realm]))
5137			foreach ($summary[$realm] as $key => $record)
5138				if (! isolatedPermission ($realm, 'default', spotEntity ($realm, $record['id'])))
5139					unset ($summary[$realm][$key]);
5140	// clear empty search result realms
5141	return array_filter ($summary, 'count');
5142}
5143
5144// returns URL to redirect to, or NULL if $result_type is unknown
5145function buildSearchRedirectURL ($result_type, $record)
5146{
5147	global $pageno_by_etype, $page;
5148	$id = isset ($record['id']) ? $record['id'] : NULL;
5149	$params = array();
5150	switch ($result_type)
5151	{
5152		case 'ipv4addressbydq':
5153		case 'ipv6addressbydq':
5154		case 'ipv4addressbydescr':
5155		case 'ipv6addressbydescr':
5156			$address = getIPAddress ($record['ip']);
5157			if (count ($address['allocs']) == 1 && isIPAddressEmpty ($address, array ('allocs')))
5158			{
5159				$next_page = 'object';
5160				$id = $address['allocs'][0]['object_id'];
5161				$params['hl_ip'] = ip_format ($record['ip']);
5162			}
5163			elseif (count ($address['vsglist'] + $address['vslist']) && isIPAddressEmpty ($address, array ('vslist', 'vsglist')))
5164			{
5165				$next_page = 'ipaddress';
5166				$id = ip_format ($record['ip']);
5167			}
5168			else
5169			{
5170				$next_page = strlen ($record['ip']) == 16 ? 'ipv6net' : 'ipv4net';
5171				$id = isset ($record['net_id']) ? $record['net_id'] : getIPAddressNetworkId ($record['ip']);
5172				$params['hl_ip'] = ip_format ($record['ip']);
5173			}
5174			break;
5175		case 'vlan':
5176			$next_page = 'vlan';
5177			$id = $record['id'];
5178			break;
5179		default:
5180			if (! isset ($pageno_by_etype[$result_type]))
5181				return NULL;
5182			$next_page = $pageno_by_etype[$result_type];
5183			if ($result_type == 'object')
5184				if (isset ($record['by_port']) && 1 == count ($record['by_port']))
5185				{
5186					$found_ports_ids = array_keys ($record['by_port']);
5187					$params['hl_port_id'] = $found_ports_ids[0];
5188				}
5189			break;
5190	}
5191	if (array_key_exists ($next_page, $page) && isset ($page[$next_page]['bypass']))
5192		$key = $page[$next_page]['bypass'];
5193	if (! isset ($key) || ! isset ($id))
5194		return NULL;
5195	$params[$key] = $id;
5196	if (isset ($_REQUEST['last_tab']) && isset ($_REQUEST['last_page']) && $next_page == $_REQUEST['last_page'])
5197		$next_tab = assertStringArg('last_tab');
5198	return buildRedirectURL ($next_page, isset ($next_tab) ? $next_tab : 'default', $params);
5199}
5200
5201// This works like explode() with space as a separator with the added difference
5202// that anything in double quotes is returned as a single word.
5203function parseSearchTerms ($terms)
5204{
5205	$ret = array();
5206	if (mb_substr_count ($terms, '"') % 2 != 0)
5207		throw new InvalidArgException ('terms', $terms, 'contains odd number of quotes');
5208	$state = 'whitespace';
5209	$buffer = '';
5210	$len = mb_strlen ($terms);
5211	for ($i = 0; $i < $len; $i++)
5212	{
5213		$c = mb_substr ($terms, $i, 1);
5214		switch ($state)
5215		{
5216		case 'whitespace':
5217			switch ($c)
5218			{
5219			case ' ':
5220				break; // nom-nom
5221			case '"':
5222				$buffer = '';
5223				$state = 'quoted_string';
5224				break;
5225			default:
5226				$buffer = $c;
5227				$state = 'word';
5228			}
5229			break;
5230
5231		case 'word':
5232			switch ($c)
5233			{
5234			case '"':
5235				throw new InvalidArgException ('terms', $terms, 'punctuation error');
5236			case ' ':
5237				$ret[] = $buffer;
5238				$buffer = '';
5239				$state = 'whitespace';
5240				break;
5241			default:
5242				$buffer .= $c;
5243			}
5244			break;
5245
5246		case 'quoted_string':
5247			switch ($c)
5248			{
5249			case '"':
5250				if (trim ($buffer) == '')
5251					throw new InvalidArgException ('terms', $terms, 'punctuation error');
5252				$ret[] = trim ($buffer);
5253				$buffer = '';
5254				$state = 'whitespace';
5255				// FIXME: this does not detect missing whitespace that would be reasonable
5256				// to expect between the closing quote and the next token, if any.
5257				break;
5258			default:
5259				$buffer .= $c;
5260			}
5261			break;
5262		}
5263	}
5264	if ($buffer != '')
5265		$ret[] = $buffer;
5266
5267	return $ret;
5268}
5269
5270// Take a parse tree and figure out if it is a valid payload or not.
5271// Depending on that return either NULL or an array filled with the load
5272// of that expression.
5273define('PARSER_ABI_VER', 2);
5274function spotPayload ($text, $reqtype = 'SYNT_CODETEXT')
5275{
5276	require_once 'code.php';
5277	try
5278	{
5279		$parser = new RackCodeParser();
5280		$tree = $parser->parse ($text, $reqtype == 'SYNT_EXPR' ? 'expr' : 'prog');
5281		return array ('result' => 'ACK', 'ABI_ver' => PARSER_ABI_VER, 'load' => $tree);
5282	}
5283	catch (RCParserError $e)
5284	{
5285		$msg = $e->getMessage();
5286		if ($reqtype != 'SYNT_EXPR' || $e->lineno != 1)
5287			$msg .= ", line {$e->lineno}";
5288		return array ('result' => 'NAK', 'ABI_ver' => PARSER_ABI_VER, 'load' => $msg);
5289	}
5290}
5291
5292// Top-level wrapper for most of the code in this file. Get a text, return a parse tree
5293// (or error message).
5294function getRackCode ($text)
5295{
5296	if ($text == '')
5297		return array ('result' => 'NAK', 'ABI_ver' => PARSER_ABI_VER, 'load' => 'The RackCode text was found empty in ' . __FUNCTION__);
5298	$text = str_replace ("\r", '', $text) . "\n";
5299	$synt = spotPayload ($text, 'SYNT_CODETEXT');
5300	if ($synt['result'] != 'ACK')
5301		return $synt;
5302	// An empty sentence list is semantically valid, yet senseless,
5303	// so checking intermediate result once more won't hurt.
5304	if (!count ($synt['load']))
5305		return array ('result' => 'NAK', 'ABI_ver' => PARSER_ABI_VER, 'load' => 'Empty parse tree found in ' . __FUNCTION__);
5306	return $synt;
5307}
5308
5309// returns array with 'from', 'length' keys.
5310// if not found, 'from' is NULL;
5311// if length is not defined (to the end of line), length is -1
5312function getColumnCoordinates ($line, $column_name, $align = 'left')
5313{
5314	$result = array ('from' => NULL, 'length' => -1);
5315	$items = preg_split('/\s+/', $line);
5316	for ($i = 0; $i < count ($items); $i++)
5317	{
5318		$item = $items[$i];
5319		if ($column_name == $item)
5320		{
5321			$current_start = strpos ($line, $items[$i]);
5322			if ($align == 'left')
5323			{
5324				$result['from'] = $current_start;
5325				if ($i < count ($items) - 1)
5326				{
5327					$next_start = strpos ($line, $items[$i + 1]);
5328					$result['length'] = $next_start - $result['from'] - 1;
5329				}
5330				else
5331					$result['length'] = -1;
5332			}
5333			elseif ($align == 'right')
5334			{
5335				if ($i > 0)
5336					$prev_end = strpos ($line, $items[$i - 1]) + strlen ($items[$i - 1]);
5337				else
5338					$prev_end = -1;
5339				$result['from'] = $prev_end + 1;
5340				$result['length'] = $current_start - $result['from'] + strlen ($column_name);
5341			}
5342			break;
5343		}
5344	}
5345	return $result;
5346}
5347
5348// Messages in the top of the page should be shown using these functions.
5349// You can call them multiple times to show multiple messages.
5350// $option can be 'inline' to echo message div, instead of putting it into $_SESSION and draw on next index page show
5351// These functions always return NULL
5352function showError   ($message, $option = '')
5353{
5354	setMessage ('error',   $message, $option == 'inline');
5355}
5356
5357function showWarning ($message, $option = '')
5358{
5359	setMessage ('warning', $message, $option == 'inline');
5360}
5361
5362function showSuccess ($message, $option = '')
5363{
5364	setMessage ('success', $message, $option == 'inline');
5365}
5366
5367function showNotice  ($message, $option = '')
5368{
5369	setMessage ('neutral', $message, $option == 'inline');
5370}
5371
5372// do not call this directly, use showError and its siblings instead
5373// $type could be 'error', 'warning', 'success' or 'neutral'
5374function setMessage ($type, $message, $direct_rendering)
5375{
5376	global $script_mode, $message_buffering;
5377	if ($direct_rendering)
5378		echo '<div class="msg_' . $type . '">' . $message . '</div>';
5379	elseif (isset ($script_mode) && $script_mode && !$message_buffering)
5380	{
5381		if ($type == 'warning' || $type == 'error')
5382			file_put_contents ('php://stderr', strtoupper ($type) . ': ' . strip_tags ($message) . "\n");
5383	}
5384	else
5385	{
5386		switch ($type)
5387		{
5388			case 'error':
5389				$code = 100;
5390				break;
5391			case 'warning':
5392				$code = 200;
5393				break;
5394			case 'success';
5395				$code = 0;
5396				break;
5397			case 'neutral':
5398			default:
5399				$code = 300;
5400				break;
5401		}
5402		showOneLiner ($code, array ($message));
5403	}
5404}
5405
5406function showOneLiner ($code, $args = array())
5407{
5408	global $log_messages;
5409	$line = array ('c' => $code);
5410	if (! empty ($args))
5411		$line['a'] = $args;
5412	$log_messages[] = $line;
5413}
5414
5415// Works only in $script_mode == TRUE
5416function setMessageBuffering ($state)
5417{
5418	global $message_buffering;
5419	$message_buffering = $state;
5420}
5421
5422function flushMessageBuffer()
5423{
5424	global $log_messages, $script_mode;
5425
5426	if (!isset ($script_mode) || !$script_mode)
5427		return;
5428
5429	$code_str_map = array
5430	(
5431		100 => 'ERROR',
5432		200 => 'WARNING',
5433	);
5434
5435	foreach ($log_messages as $line)
5436		if (isset ($code_str_map[$line['c']]))
5437		{
5438			$type = $code_str_map[$line['c']];
5439			$message = strip_tags (implode ("\n", $line['a']));
5440			file_put_contents ('php://stderr', $type . ': ' . $message . "\n");
5441		}
5442	$log_messages = array();
5443}
5444
5445function clearMessageBuffer()
5446{
5447	global $log_messages;
5448	$log_messages = array();
5449}
5450
5451function showFuncMessage ($callfunc, $status, $log_args = array())
5452{
5453	global $msgcode;
5454	if (isset ($msgcode[$callfunc][$status]))
5455		showOneLiner ($msgcode[$callfunc][$status], $log_args);
5456	else
5457		showWarning ("Message '$status' is lost in $callfunc");
5458}
5459
5460// function returns integer count of unshown messages in log buffer.
5461// message_type can be 'all', 'success', 'error', 'warning', 'neutral'.
5462function getMessagesCount ($message_type = 'all')
5463{
5464	global $log_messages;
5465	$result = 0;
5466	foreach ($log_messages as $msg)
5467		if ($msg['c'] < 100)
5468		{
5469			if ($message_type == 'success' || $message_type == 'all')
5470				++$result;
5471		}
5472		elseif ($msg['c'] < 200)
5473		{
5474			if ($message_type == 'error' || $message_type == 'all')
5475				++$result;
5476		}
5477		elseif ($msg['c'] < 300)
5478		{
5479			if ($message_type == 'warning' || $message_type == 'all')
5480				++$result;
5481		}
5482		else
5483		{
5484			if ($message_type == 'neutral' || $message_type == 'all')
5485				++$result;
5486		}
5487	return $result;
5488}
5489
5490function isEthernetPort($port)
5491{
5492	return ($port['iif_id'] != 1 || preg_match('/Base|LACP/i', $port['oif_name']));
5493}
5494
5495function loadConfigDefaults()
5496{
5497	$ret = loadConfigCache();
5498	if (!count ($ret))
5499		throw new RackTablesError ('Failed to load configuration from the database.', RackTablesError::INTERNAL);
5500	foreach (array_keys ($ret) as $varname)
5501	{
5502		$ret[$varname]['is_altered'] = 'no';
5503		if ($ret[$varname]['vartype'] == 'uint')
5504			$ret[$varname]['varvalue'] = intval ($ret[$varname]['varvalue']);
5505		$ret[$varname]['defaultvalue'] = $ret[$varname]['varvalue'];
5506	}
5507	return $ret;
5508}
5509
5510function alterConfigWithUserPreferences()
5511{
5512	global $configCache;
5513	global $remote_username;
5514	foreach (loadUserConfigCache($remote_username) as $key => $row)
5515		if ($configCache[$key]['is_userdefined'] == 'yes')
5516		{
5517			$configCache[$key]['varvalue'] = $row['varvalue'];
5518			$configCache[$key]['is_altered'] = 'yes';
5519		}
5520}
5521
5522// Returns true if varname has a different value or varname is new
5523function isConfigVarChanged ($varname, $varvalue)
5524{
5525	global $configCache;
5526	if (!isset ($configCache))
5527		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5528	if ($varname == '')
5529		throw new InvalidArgException('varname', $varname, 'Empty variable name');
5530	if (!isset ($configCache[$varname]))
5531		return TRUE;
5532	if ($configCache[$varname]['vartype'] == 'uint')
5533		return $configCache[$varname]['varvalue'] !== intval ($varvalue);
5534	else
5535		return $configCache[$varname]['varvalue'] !== $varvalue;
5536}
5537
5538// This function depends on init.php to have the cache array initialized.
5539function getConfigVar ($varname)
5540{
5541	global $configCache;
5542	if (! isset ($configCache))
5543		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5544	if (! array_key_exists ($varname, $configCache))
5545		throw new InvalidArgException ('varname', $varname, 'no such configuration variable');
5546	return $configCache[$varname]['varvalue'];
5547}
5548
5549// return portinfo array if object has a port with such name, or NULL
5550// in strict mode the resulting port name is always equal to the $portname.
5551// in non-strict mode names are compared using shortenIfName()
5552function getPortinfoByName (&$object, $portname, $strict_mode = TRUE)
5553{
5554	if (! isset ($object['ports']))
5555		$object['ports'] = getObjectPortsAndLinks ($object['id']);
5556	if (! $strict_mode)
5557	{
5558		$breed = detectDeviceBreed ($object['id']);
5559		$portname = shortenIfName ($portname, $breed);
5560	}
5561	$ret = NULL;
5562	foreach ($object['ports'] as $portinfo)
5563		if ($portname == ($strict_mode ? $portinfo['name'] : shortenIfName ($portinfo['name'], $breed)))
5564		{
5565			$ret = $portinfo;
5566			if ($ret['linked'])
5567				break;
5568		}
5569		elseif (isset ($ret))
5570			break;
5571	return $ret;
5572}
5573
5574// exclude location-related object types
5575function withoutLocationTypes ($objtypes)
5576{
5577	global $location_obj_types;
5578	return array_diff_key ($objtypes, array_fill_keys ($location_obj_types, 0));
5579}
5580
5581# For the given object ID return a getSelect-suitable list of object types
5582# compatible with the object's attributes that have an assigned value in
5583# AttributeValue (no assigned values mean full compatibility). Being compatible
5584# with an attribute means having a record in AttributeMap (with the same chapter
5585# ID, if the attribute is dictionary-based). This knowledge is required to allow
5586# the user changing object type ID in a way that leaves data in AttributeValue
5587# meeting constraints in AttributeMap upon the change.
5588function getObjectTypeChangeOptions ($object_id)
5589{
5590	$map = getAttrMap();
5591	$used = array();
5592	$ret = array();
5593	foreach (getAttrValues ($object_id) as $attr)
5594	{
5595		if (! array_key_exists ($attr['id'], $map))
5596			return array(); // inconsistent current data
5597		if ($attr['value'] != '')
5598			$used[] = $attr;
5599	}
5600	foreach (withoutLocationTypes (readChapter (CHAP_OBJTYPE, 'o')) as $test_id => $text)
5601	{
5602		foreach ($used as $attr)
5603		{
5604			$app = $map[$attr['id']]['application'];
5605			if
5606			(
5607				(NULL === $appidx = scanArrayForItem ($app, 'objtype_id', $test_id)) ||
5608				($attr['type'] == 'dict' && $attr['chapter_id'] != $app[$appidx]['chapter_no'])
5609			)
5610				continue 2; // next type ID
5611		}
5612		$ret[$test_id] = $text;
5613	}
5614	return $ret;
5615}
5616
5617// Gets the timestamp and returns human-friendly short message describing the time difference
5618// between the current system time and the specified timestamp (like '2d 5h ago')
5619function formatAgeTimestamp ($timestamp)
5620{
5621	return formatAgeSeconds (time() - $timestamp);
5622}
5623
5624// For backward compatibility.
5625function formatAge ($timestamp)
5626{
5627	return formatAgeTimestamp ($timestamp);
5628}
5629
5630function formatAgeSeconds ($seconds)
5631{
5632	switch (TRUE)
5633	{
5634		case $seconds < 1:
5635			return 'just now';
5636		case $seconds < 60:
5637			return "${seconds}s" . ' ago';
5638		case $seconds <= 300:
5639			$mins = intval ($seconds / 60);
5640			$secs = $seconds % 60;
5641			return ($secs ? "{$mins}min ${secs}s" : "{$mins}min") . ' ago';
5642		case $seconds < 3600:
5643			return round ($seconds / 60) . 'min' . ' ago';
5644		case $seconds < 3 * 3600:
5645			$hrs = intval ($seconds / 3600);
5646			$mins = round (($seconds % 3600) / 60) . '';
5647			return ($mins ? "${hrs}h ${mins}min" : "${hrs}h") . ' ago';
5648		case $seconds < 86400:
5649			return round ($seconds / 3600) . 'h' . ' ago';
5650		case $seconds < 86400 * 3:
5651			$days = intval ($seconds / 86400);
5652			$hrs = round (($seconds - $days * 86400) / 3600);
5653			return ($hrs ? "${days}d ${hrs}h" : "${days}d") . ' ago';
5654		case $seconds < 86400 * 30.4375:
5655			return round ($seconds / 86400) . 'd' . ' ago';
5656		case $seconds < 86400 * 30.4375 * 4 :
5657			$mon = intval ($seconds / 86400 / 30.4375);
5658			$days = round (($seconds - $mon * 86400 * 30.4375) / 86400);
5659			return ($days ? "${mon}mo ${days}d" : "${mon}mo") . ' ago';
5660		case $seconds < 365.25 * 86400:
5661			return (round ($seconds / 86400 / 30.4375) . 'mo') . ' ago';
5662		case $seconds < 2 * 365.25 * 86400:
5663			$yrs = intval ($seconds / 86400 / 365.25);
5664			$mon = round (($seconds - $yrs * 86400 * 365.25) / 86400 / 30.4375);
5665			return ($mon ? "${yrs}y ${mon}mo" : "${yrs}y") . ' ago';
5666		default:
5667			return (round ($seconds / 86400 / 365.25) . 'y') . ' ago';
5668	}
5669}
5670
5671// proxy function returning the output of another function. Takes any number of additional parameters
5672function getOutputOf ($func_name)
5673{
5674	ob_start();
5675	try
5676	{
5677		$params = func_get_args();
5678		array_shift($params);
5679		call_user_func_array ($func_name, $params);
5680		return ob_get_clean();
5681	}
5682	catch (Exception $e)
5683	{
5684		ob_end_clean();
5685		throw $e;
5686	}
5687}
5688
5689// Calls a function taking into account the overrides specified in the
5690// $hook array. Takes any number of additional parameters.
5691function callHook ($hook_name)
5692{
5693	global $hook;
5694	$callback = $hook_name;
5695	if (isset ($hook[$hook_name]))
5696		$callback = $hook[$hook_name];
5697	$params = func_get_args();
5698	if ($callback !== 'universalHookHandler')
5699		array_shift ($params);
5700	if (is_callable ($callback))
5701		return call_user_func_array ($callback, $params);
5702}
5703
5704// function to parse text table header, aligned by left side
5705// returns array suitable to be used by explodeTableLine
5706function guessTableStructure ($line)
5707{
5708	$ret = array();
5709	$i = 0;
5710	while ($line != '')
5711	{
5712		if (! preg_match ('/^(\s*\S+\s*)/', $line, $m))
5713			break;
5714		$header = trim ($m[1]);
5715		$ret[$header] = array ('begin' => $i, 'length' => strlen ($m[1]));
5716		$line = substr ($line, strlen ($m[1]));
5717		$i += strlen ($m[1]);
5718	}
5719	return $ret;
5720}
5721
5722// takes text-formatted table line and an array returned by guessTableStructure
5723// returns array indexed by cell name. Works for left-aligned tables only
5724function explodeTableLine ($line, $table_schema)
5725{
5726	$ret = array();
5727	foreach ($table_schema as $header => $constraints)
5728	{
5729		$value = substr ($line, $constraints['begin'], $constraints['length']);
5730		$ret[$header] = trim ($value);
5731	}
5732	return $ret;
5733}
5734
5735// returns number of changed ports (both local and remote)
5736// shows error messages unconditionally, and success messages respecting  to $verbose setting
5737// if $mutex_rev is set, checks if it is outdated
5738// if $uplinks_filter is not empty, changes only those uplink ports that are keys of this array
5739// NOTE: this function is calling itself through initiateUplinksReverb. It is important that
5740// the call to initiateUplinksReverb is outside of DB transaction scope.
5741function apply8021qChangeRequest ($switch_id, $changes, $verbose = TRUE, $mutex_rev = NULL, $uplinks_filter = array())
5742{
5743	global $dbxlink;
5744	$dbxlink->beginTransaction();
5745	try
5746	{
5747		if (NULL === $vswitch = getVLANSwitchInfo ($switch_id, 'FOR UPDATE'))
5748			throw new InvalidArgException ('object_id', $switch_id, 'VLAN domain is not set for this object');
5749		if (isset ($mutex_rev) && $vswitch['mutex_rev'] != $mutex_rev)
5750			throw new InvalidRequestArgException ('mutex_rev', $mutex_rev, 'expired form data');
5751		$after = $before = apply8021QOrder ($vswitch, getStored8021QConfig ($vswitch['object_id'], 'desired'));
5752		$domain_vlanlist = getDomainVLANList ($vswitch['domain_id']);
5753		$changes = filter8021QChangeRequests
5754		(
5755			$domain_vlanlist,
5756			$before,
5757			apply8021QOrder ($vswitch, $changes)
5758		);
5759		$desired_ports_count = count ($changes);
5760		$changes = authorize8021QChangeRequests ($before, $changes);
5761		if (count ($changes) < $desired_ports_count)
5762			showWarning (sprintf ("Permission denied to change %d ports", $desired_ports_count - count ($changes)));
5763		foreach ($changes as $port_name => $port)
5764			$after[$port_name] = $port;
5765		$new_uplinks = filter8021QChangeRequests ($domain_vlanlist, $after, produceUplinkPorts ($domain_vlanlist, $after, $vswitch['object_id']));
5766		if ($uplinks_filter)
5767			$new_uplinks = array_intersect_key ($new_uplinks, $uplinks_filter);
5768		$npulled = replace8021QPorts ('desired', $vswitch['object_id'], $before, $changes);
5769		$nsaved_uplinks = replace8021QPorts ('desired', $vswitch['object_id'], $before, $new_uplinks);
5770		if ($npulled + $nsaved_uplinks)
5771			touchVLANSwitch ($vswitch['object_id']);
5772		$dbxlink->commit();
5773	}
5774	catch (Exception $e)
5775	{
5776		$dbxlink->rollBack();
5777		showError (sprintf ("Failed to update switchports: %s", $e->getMessage()));
5778		return 0;
5779	}
5780	$nsaved_downlinks = initiateUplinksReverb ($vswitch['object_id'], $new_uplinks);
5781	// instant deploy to that switch if configured
5782	$done = 0;
5783	if ($npulled + $nsaved_uplinks > 0 && getConfigVar ('8021Q_INSTANT_DEPLOY') == 'yes')
5784	{
5785		try
5786		{
5787			if (FALSE === $done = exec8021QDeploy ($vswitch['object_id'], TRUE))
5788				showError ("deploy was blocked due to conflicting configuration versions");
5789			elseif ($verbose)
5790				showSuccess (sprintf ("Configuration for %u port(s) have been deployed", $done));
5791		}
5792		catch (Exception $e)
5793		{
5794			showError (sprintf ("Failed to deploy changes to switch: %s", $e->getMessage()));
5795		}
5796	}
5797	// report number of changed ports
5798	$total = $npulled + $nsaved_uplinks + $nsaved_downlinks;
5799	if ($verbose)
5800	{
5801		$message = sprintf ('%u port(s) have been changed', $total);
5802		if ($total > 0)
5803			showSuccess ($message);
5804		else
5805			showNotice ($message);
5806	}
5807	return $total;
5808}
5809
5810// takes a full sublist of ipv4net entities ordered by (ip,mask)
5811// fills ['spare_ranges'] and ['kidc'] fields of each item of $nets.
5812function fillIPNetsCorrelation (&$nets, $max_depth = 0)
5813{
5814	$stack = array();
5815	foreach ($nets as &$net)
5816	{
5817		$last = NULL; // last element popped from the stack
5818		while (count ($stack)) // spin stack leaving only the parents of the $net.
5819		{
5820			$top = &$stack[count ($stack) - 1];
5821			if (IPNetContains ($top, $net))
5822			{
5823				// skip the network if max_depth exceeded
5824				if ($max_depth && count ($stack) > $max_depth)
5825					continue 2;
5826				$top['kidc']++;
5827				break;
5828			}
5829			if (isset ($last))
5830				// possible hole in the end of $top
5831				fillIPSpareListBstr ($top, ip_next (ip_last ($last)), ip_last ($top));
5832			$last = array_pop ($stack);
5833		}
5834		if (count ($stack))
5835		{
5836			$top = &$stack[count ($stack) - 1];
5837			if (isset ($last))
5838				// possible hole in the middle of $top
5839				fillIPSpareListBstr ($top, ip_next (ip_last ($last)), ip_prev ($net['ip_bin']));
5840			else
5841				// possible hole in the beginning of $top
5842				fillIPSpareListBstr ($top, $top['ip_bin'], ip_prev ($net['ip_bin']));
5843		}
5844		$stack[] = &$net;
5845	}
5846	// final stack spin
5847	$last = NULL;
5848	while (count ($stack))
5849	{
5850		$top = &$stack[count ($stack) - 1];
5851		if (isset ($last))
5852		{
5853			// possible hole in the end of $top
5854			$last = ip_last ($last);
5855			$a = ip_next ($last);
5856			// check for crossing 0
5857			if (0 > strcmp ($a, $last))
5858				break;
5859			fillIPSpareListBstr ($top, $a, ip_last ($top));
5860		}
5861		$last = array_pop ($stack);
5862	}
5863}
5864
5865// $a, $b - binary strings (IP addresses) meaning the beginning and the end of IP range.
5866// $net is an entity for hole autotags to be set to.
5867function fillIPSpareListBstr (&$net, $a, $b)
5868{
5869	$len = strlen ($a) * 8;
5870	while (0 >= strcmp ($a, $b))
5871	{
5872		$max_mask = 0; // the number of common binary bits in the major of $a and $b
5873		$xor = $a ^ $b;
5874		for ($i = 0; $i < strlen ($xor); $i++)
5875		{
5876			$max_mask += 8;
5877			if ($xor[$i] != "\0")
5878			{
5879				$byte = ord ($xor[$i]);
5880				do
5881				{
5882					$max_mask--;
5883					$byte >>= 1;
5884				} while ($byte != 0);
5885				break;
5886			}
5887		}
5888
5889		for ($mask = $max_mask; $mask <= $len; $mask++)
5890		{
5891			$bmask = ip_mask ($mask, $len == 128);
5892			$last_a = $a | ~ $bmask;
5893			if ($a == ($a & $bmask) && 0 >= strcmp ($last_a, $b))
5894			{
5895				$net['spare_ranges'][$mask][] = $a;
5896				$a = ip_next ($last_a);
5897				// check for crossing 0
5898				if (0 > strcmp ($a, $last_a))
5899					break 2;
5900				break;
5901			}
5902		}
5903	}
5904}
5905
5906// returns TRUE if all of the fields set by constructIPAddress are empty
5907function isIPAddressEmpty ($addrinfo, $except_fields = array())
5908{
5909	// string fields
5910	$check_fields = array ('name', 'comment');
5911	foreach (array_diff ($check_fields, $except_fields) as $field)
5912		if (array_key_exists ($field, $addrinfo) && $addrinfo[$field] != '')
5913			return FALSE;
5914
5915	// "boolean" fields
5916	if (! in_array ('reserved', $except_fields) && $addrinfo['reserved'] != 'no')
5917		return FALSE;
5918
5919	// array fields
5920	$check_fields = array ('allocs', 'rsplist', 'vslist', 'vsglist');
5921	if (strlen ($addrinfo['ip_bin']) == 4)
5922		$check_fields = array_merge ($check_fields, array ('inpf', 'outpf'));
5923	foreach (array_diff ($check_fields, $except_fields) as $field)
5924		if (array_key_exists ($field, $addrinfo) && is_array ($addrinfo[$field]) && count ($addrinfo[$field]) > 0)
5925			return FALSE;
5926	return TRUE;
5927}
5928
5929// returns TRUE if the network cell is allowed to be deleted, FALSE otherwise
5930// $netinfo could be either ipv4net or ipv6net entity.
5931// in case of returning FALSE, $netinfo['addrlist'] is set
5932function isIPNetworkEmpty (&$netinfo)
5933{
5934	if (getConfigVar ('IPV4_JAYWALK') == 'yes')
5935		return TRUE;
5936	if (! isset ($netinfo['addrlist']))
5937		loadIPAddrList ($netinfo);
5938	$pure_array = ($netinfo['realm'] == 'ipv4net') ?
5939		array ($netinfo['ip_bin'] => 'network', ip_last ($netinfo) => 'broadcast') : // v4
5940		array ($netinfo['ip_bin'] => 'Subnet-Router anycast'); // v6
5941	$pure_auto = 0;
5942	foreach ($pure_array as $ip => $comment)
5943		if
5944		(
5945			array_key_exists ($ip, $netinfo['addrlist']) &&
5946			$netinfo['addrlist'][$ip]['name'] == $comment &&
5947			$netinfo['addrlist'][$ip]['reserved'] == 'yes' &&
5948			isIPAddressEmpty ($netinfo['addrlist'][$ip], array ('name', 'reserved'))
5949		)
5950			$pure_auto++;
5951	return ($netinfo['own_addrc'] <= $pure_auto);
5952}
5953
5954// returns the first element of given array, or NULL if array is empty
5955function array_first ($array)
5956{
5957	$single = array_slice (array_values ($array), 0, 1);
5958	if (count ($single))
5959		return $single[0];
5960}
5961
5962// returns the last element of given array, or NULL if array is empty
5963function array_last ($array)
5964{
5965	$single = array_slice (array_values ($array), -1, 1);
5966	if (count ($single))
5967		return $single[0];
5968}
5969
5970// FIXME: Remove this function at some point because it is a reimplementation
5971// of array_diff_key().
5972// returns array of key-value pairs from array $a such that keys are not present in $b
5973function array_sub ($a, $b)
5974{
5975	$ret = array();
5976	foreach ($a as $key => $value)
5977		if (! array_key_exists($key, $b))
5978			$ret[$key] = $value;
5979	return $ret;
5980}
5981
5982// returns the requested element value or the default value if not found
5983function array_fetch ($array, $key, $default_value)
5984{
5985	if (! is_array ($array))
5986		throw new InvalidArgException ('array', $array, 'is not an array');
5987	return array_key_exists ($key, $array) ? $array[$key] : $default_value;
5988}
5989
5990// Registers additional ophandler on page-tab-opname triplet.
5991// Valid $method values are 'before' and 'after'.
5992//   'before' puts your ophandler in the beginning of the list (and thus before the default)
5993//   'after' puts your ophandler to the end of the list (and thus after the default)
5994function registerOpHandler ($page, $tab, $opname, $callback, $method = 'before')
5995{
5996	global $ophandlers_stack;
5997	global $ophandler;
5998	if (! isset ($ophandler[$page][$tab][$opname]))
5999		$ophandlers_stack[$page][$tab][$opname] = array();
6000	if (isset ($ophandler[$page][$tab][$opname]) && $ophandler[$page][$tab][$opname] != 'universalOpHandler')
6001	{
6002		$ophandlers_stack[$page][$tab][$opname] = array ($ophandler[$page][$tab][$opname]);
6003		$ophandler[$page][$tab][$opname] = 'universalOpHandler';
6004	}
6005	$ophandler[$page][$tab][$opname] = 'universalOpHandler';
6006	if ($method == 'before')
6007		array_unshift ($ophandlers_stack[$page][$tab][$opname], $callback);
6008	elseif ($method == 'after')
6009		array_push ($ophandlers_stack[$page][$tab][$opname], $callback);
6010	else
6011		throw new RacktablesError ("unknown ophandler injection method '$method'", RackTablesError::INTERNAL);
6012}
6013
6014// call this from custom ophandler registered by registerOpHandler
6015// to prevent the rest of the ophanlers to run
6016function stopOpPropagation()
6017{
6018	global $ophandler_propagation_stop;
6019	$ophandler_propagation_stop = TRUE;
6020}
6021
6022function universalOpHandler()
6023{
6024	global $ophandler_propagation_stop;
6025	$ophandler_propagation_stop = FALSE;
6026	global $ophandlers_stack;
6027	global $pageno, $tabno, $op;
6028	$op_stack = $ophandlers_stack[$pageno][$tabno][$op];
6029	$ret = NULL;
6030	foreach ($op_stack as $callback)
6031	{
6032		$ret_i = call_user_func ($callback);
6033		if ($ret_i != '' || ! isset ($ret))
6034			$ret = $ret_i;
6035		if ($ophandler_propagation_stop)
6036			break;
6037	}
6038	return $ret;
6039}
6040
6041// Registers additional tabhandler on page-tab pair.
6042// Valid $method values are 'before', 'after' and 'replace'.
6043//   'before' puts your tabhandler in the beginning of the list (and thus before the default)
6044//   'after' puts your tabhandler to the end of the list (and thus after the default)
6045//   'replace': the same as 'after', but the rendered tab is replaced by your output, not appended. See also getRenderedTab
6046function registerTabHandler ($page, $tab, $callback, $method = 'after')
6047{
6048	global $tabhandlers_stack;
6049	global $tabhandler;
6050
6051	if (! isset ($tabhandlers_stack[$page][$tab]))
6052		$tabhandlers_stack[$page][$tab] = array();
6053
6054	if (isset ($tabhandler[$page][$tab]) && $tabhandler[$page][$tab] != 'universalTabHandler')
6055		array_push ($tabhandlers_stack[$page][$tab], $tabhandler[$page][$tab]);
6056	$tabhandler[$page][$tab] = 'universalTabHandler';
6057
6058	if ($method == 'before')
6059		array_unshift ($tabhandlers_stack[$page][$tab], $callback);
6060	elseif ($method == 'after')
6061		array_push ($tabhandlers_stack[$page][$tab], $callback);
6062	elseif ($method == 'replace')
6063		array_push ($tabhandlers_stack[$page][$tab], '!' . $callback);
6064	else
6065		throw new RacktablesError ("unknown tabhandler injection method '$method'", RackTablesError::INTERNAL);
6066}
6067
6068// Returns  tab content already rendered by previous tabhandlers in the chain registered by registerTabHandler.
6069// It is useful in custom tabhandlers registered with 'replace' method.
6070function getRenderedTab()
6071{
6072	global $tabhandler_output;
6073	return $tabhandler_output;
6074}
6075
6076// call this from custom tabhandler registered by registerTabHandler
6077// to prevent the rest of the tabhanlers to run
6078function stopTabPropagation()
6079{
6080	global $tabhandler_propagation_stop;
6081	$tabhandler_propagation_stop = TRUE;
6082}
6083
6084function universalTabHandler($bypass = NULL)
6085{
6086	global $tabhandler_propagation_stop;
6087	$tabhandler_propagation_stop = FALSE;
6088	global $tabhandlers_stack;
6089	global $tabhandler_output;
6090	global $pageno, $tabno;
6091	$tab_stack = $tabhandlers_stack[$pageno][$tabno];
6092	$ret = NULL;
6093	$tabhandler_output = '';
6094	foreach ($tab_stack as $callback)
6095	{
6096		$do_replace = FALSE;
6097		if ($callback[0] == '!')
6098		{
6099			$callback = substr ($callback, 1);
6100			$do_replace = TRUE;
6101		}
6102		ob_start();
6103		$ret = call_user_func ($callback, $bypass);
6104		$current_output = ob_get_contents();
6105		ob_end_clean();
6106		if ($do_replace)
6107			$tabhandler_output = $current_output;
6108		else
6109			$tabhandler_output .= $current_output;
6110		if ($tabhandler_propagation_stop)
6111			break;
6112	}
6113	echo $tabhandler_output;
6114	return $ret;
6115}
6116
6117// $method could be 'before', 'after', 'chain'
6118function registerHook ($hook_name, $callback, $method = 'after')
6119{
6120	global $hooks_stack, $hook;
6121
6122	if (! isset ($hooks_stack[$hook_name]))
6123		$hooks_stack[$hook_name] = array();
6124
6125	if (isset ($hook[$hook_name]) && $hook[$hook_name] != 'universalHookHandler')
6126		array_push ($hooks_stack[$hook_name], $hook[$hook_name]);
6127	$hook[$hook_name] = 'universalHookHandler';
6128
6129	// If the hook name is a built-in function, the function needs to be the first on the stack.
6130	if (empty ($hooks_stack[$hook_name]) && is_callable ($hook_name))
6131		array_push ($hooks_stack[$hook_name], $hook_name);
6132
6133	switch ($method)
6134	{
6135	case 'before':
6136		array_unshift ($hooks_stack[$hook_name], $callback);
6137		break;
6138	case 'after':
6139		array_push ($hooks_stack[$hook_name], $callback);
6140		break;
6141	case 'chain':
6142		array_push ($hooks_stack[$hook_name], '!' . $callback);
6143		break;
6144	default:
6145		throw new InvalidArgException ('method', $method, 'Invalid hook method');
6146	}
6147}
6148
6149// hook handlers dispatcher. registerHook leaves 'universalHookHandler' in $hook
6150function universalHookHandler()
6151{
6152	global $hook_propagation_stop;
6153	if (! isset ($hook_propagation_stop))
6154		$hook_propagation_stop = array();
6155	array_unshift ($hook_propagation_stop, FALSE);
6156	global $hooks_stack;
6157	$ret = NULL;
6158	$bk_params = func_get_args();
6159	$hook_name = array_shift ($bk_params);
6160	if (! array_key_exists ($hook_name, $hooks_stack) || ! is_array ($hooks_stack[$hook_name]))
6161		throw new RackTablesError ("malformed structure in hooks_stack['{$hook_name}']", RackTablesError::INTERNAL);
6162	foreach ($hooks_stack[$hook_name] as $callback)
6163	{
6164		$params = $bk_params;
6165		if ('!' === substr ($callback, 0, 1))
6166		{
6167			$callback = substr ($callback, 1);
6168			array_unshift ($params, $ret);
6169		}
6170		if (is_callable ($callback))
6171			$ret = call_user_func_array ($callback, $params);
6172		else
6173			throw new RackTablesError ("Call of non-existant callback '$callback'", RackTablesError::INTERNAL);
6174		if ($hook_propagation_stop[0])
6175			break;
6176	}
6177	array_shift ($hook_propagation_stop);
6178	return $ret;
6179}
6180
6181// call this from custom hook registered by registerHook
6182// to prevent the rest of the hooks to run
6183function stopHookPropagation()
6184{
6185	global $hook_propagation_stop;
6186	if ($hook_propagation_stop)
6187		$hook_propagation_stop[0] = TRUE;
6188}
6189
6190function arePortTypesCompatible ($oif1, $oif2)
6191{
6192	static $map = NULL;
6193	if (! isset ($map))
6194	{
6195		$map = array();
6196		foreach (getPortOIFCompat() as $item)
6197			$map[$item['type1']][$item['type2']] = 1;
6198	}
6199	return isset ($map[$oif1][$oif2]);
6200}
6201
6202function arePortsCompatible ($portinfo_a, $portinfo_b)
6203{
6204	return arePortTypesCompatible ($portinfo_a['oif_id'], $portinfo_b['oif_id']);
6205}
6206
6207// returns HTML-formatted link to the given entity
6208function mkCellA ($cell, $title = NULL)
6209{
6210	global $pageno_by_etype;
6211	if (! isset ($pageno_by_etype[$cell['realm']]))
6212		throw new RackTablesError ("Internal structure error in array \$pageno_by_etype. Page for realm '${cell['realm']}' is not set", RackTablesError::INTERNAL);
6213	$cell_page = $pageno_by_etype[$cell['realm']];
6214	$cell_key = $cell[$cell['realm'] == 'user' ? 'user_id' : 'id'];
6215	if ($title === NULL)
6216		switch ($cell['realm'])
6217		{
6218			case 'object':
6219			case 'ipv4vs':
6220			case 'ipv4net':
6221			case 'ipv6net':
6222				$title = formatEntityName ($cell);
6223				break;
6224			default:
6225				$title = formatRealmName ($cell['realm']) . ' ' . formatEntityName ($cell);
6226				break;
6227		}
6228	return mkA ($title, $cell_page, $cell_key);
6229}
6230
6231// Returns a list of entities of a given realm, like listCells.
6232// An optional $varname is the name of config option with constraint in RackCode.
6233function listConstraint ($realm, $varname = '')
6234{
6235	$wideList = listCells ($realm);
6236	if ($varname != '' && ('' != $filter = getConfigVar ($varname)))
6237	{
6238		$expr = compileExpression ($filter);
6239		if (! $expr)
6240			return array();
6241		$wideList = filterCellList ($wideList, $expr);
6242	}
6243	return $wideList;
6244}
6245
6246// Return a simple object list w/o related information, so that the returned value
6247// can be directly used by printSelect(). An optional argument is the name of config
6248// option with constraint in RackCode.
6249function getNarrowObjectList ($varname = '')
6250{
6251	return formatEntityList (listConstraint ('object', $varname));
6252}
6253
6254// takes an array of cells,
6255// returns an array indexed by cell id, values are simple text representation of a cell.
6256// Intended to pass its return value to printSelect routine.
6257function formatEntityList ($list)
6258{
6259	$ret = array();
6260	foreach ($list as $entity)
6261		$ret[$entity['id']] = formatEntityName ($entity);
6262	asort ($ret);
6263	return $ret;
6264}
6265
6266function formatEntityName ($entity)
6267{
6268	$ret = '';
6269	switch ($entity['realm'])
6270	{
6271		case 'object':
6272			$ret = $entity['dname'];
6273			break;
6274		case 'ipv4vs':
6275			$ret = $entity['name'] . ($entity['name'] != '' ? ' ' : '') . '(' . $entity['dname'] . ')';
6276			break;
6277		case 'ipv4net':
6278		case 'ipv6net':
6279			$ret = $entity['ip'] . '/' . $entity['mask'];
6280			break;
6281		case 'user':
6282			$ret = $entity['user_name'];
6283			break;
6284		case 'ipvs':
6285		case 'ipv4rspool':
6286		case 'file':
6287		case 'rack':
6288		case 'row':
6289		case 'location':
6290			$ret = $entity['name'];
6291			break;
6292	}
6293	if ($ret == '')
6294		$ret = '[unnamed] #' . $entity['id'];
6295	return $ret;
6296}
6297
6298// Returns reversed (top-to-bottom) $unit_no if the rack is configured to be
6299// that way (the calling function tells this), or unchanged $unit_no otherwise.
6300function inverseRackUnit ($height, $unit_no, $reverse)
6301{
6302	return $reverse ? ($height - $unit_no + 1) : $unit_no;
6303}
6304
6305// returns true either if given domains are the same
6306// or if one is a group and other is its member
6307function sameDomains ($domain_id_1, $domain_id_2)
6308{
6309	if ($domain_id_1 == $domain_id_2)
6310		return TRUE;
6311	static $cache = array();
6312	if (! isset ($cache[$domain_id_1]))
6313		$cache[$domain_id_1] = getDomainGroupMembers ($domain_id_1);
6314	if (! isset ($cache[$domain_id_2]))
6315		$cache[$domain_id_2] = getDomainGroupMembers ($domain_id_2);
6316	return in_array ($domain_id_1, $cache[$domain_id_2]) || in_array ($domain_id_2, $cache[$domain_id_1]);
6317}
6318
6319// Checks if 802.1Q port uplink/downlink feature is misconfigured.
6320// Returns FALSE if 802.1Q port role/linking is wrong, TRUE otherwise.
6321function checkPortRole ($vswitch, $portinfo, $port_name, $port_order)
6322{
6323	if (! $portinfo || ! $portinfo['linked'])
6324		return TRUE; // not linked port
6325
6326	// find linked port with the same name
6327	if ($port_name != $portinfo['name'])
6328		return FALSE; // typo in local port name
6329
6330	$local_auto = ($port_order['vst_role'] == 'uplink' || $port_order['vst_role'] == 'downlink') ?
6331		$port_order['vst_role'] :
6332		FALSE;
6333	$remote_vswitch = getVLANSwitchInfo ($portinfo['remote_object_id']);
6334	if (! $remote_vswitch)
6335		return ! $local_auto;
6336
6337	$remote_pn = $portinfo['remote_name'];
6338	$remote_ports = apply8021QOrder ($remote_vswitch, getStored8021QConfig ($remote_vswitch['object_id'], 'desired', array ($remote_pn)));
6339	if (! array_key_exists($remote_pn, $remote_ports))
6340		// linked auto-port must have corresponding remote 802.1Q port
6341		return
6342			! $local_auto &&
6343			! isset ($remote_ports[shortenIfName ($remote_pn, NULL, $portinfo['remote_object_id'])]); // typo in remote port name
6344	$remote = $remote_ports[$remote_pn];
6345
6346	$remote_auto = ($remote['vst_role'] == 'uplink' || $remote['vst_role'] == 'downlink') ?
6347		$remote['vst_role'] :
6348		FALSE;
6349
6350	if (! $remote_auto && ! $local_auto)
6351		return TRUE;
6352	if ($remote_auto && $local_auto && $local_auto != $remote_auto && sameDomains ($vswitch['domain_id'], $remote_vswitch['domain_id']))
6353		return TRUE; // auto-calc link ends must belong to the same domain
6354	return FALSE;
6355}
6356
6357# Convert InvalidArgException to InvalidRequestArgException with a choice of
6358# replacing the reference to the failed argument or leaving it unchanged.
6359#
6360# DEPRECATED, use InvalidArgException::newIRAE()
6361function convertToIRAE ($iae, $override_argname = NULL)
6362{
6363	if (! ($iae instanceof InvalidArgException))
6364		throw new InvalidArgException ('iae', '(object)', 'not an instance of InvalidArgException class');
6365	return $iae->newIRAE ($override_argname);
6366}
6367
6368# Produce a textual date/time from a given UNIX timestamp
6369# If timestamp is omitted, time() value is used
6370function datetimestrFromTimestamp ($ts = NULL)
6371{
6372	if (! isset ($ts))
6373		$ts = time();
6374	return strftime (getConfigVar ('DATETIME_FORMAT'), $ts);
6375}
6376
6377# vice versa
6378function timestampFromDatetimestr ($s)
6379{
6380	$format = getConfigVar ('DATETIME_FORMAT');
6381	if (FALSE === $tmp = strptime ($s, $format))
6382		throw new InvalidArgException ('s', $s, "not a date in format '${format}'");
6383	$ret = mktime
6384	(
6385		$tmp['tm_hour'],       # 0~23
6386		$tmp['tm_min'],        # 0~59
6387		$tmp['tm_sec'],        # 0~59
6388		$tmp['tm_mon'] + 1,    # 0~11 -> 1~12
6389		$tmp['tm_mday'],       # 1~31
6390		$tmp['tm_year'] + 1900 # 0~n -> 1900~n
6391	);
6392	# PHP UNIX time has a wider (at least on 64-bit systems) range than the unsigned
6393	# 32-bit integer type RackTables allocates in the database for UNIX time.
6394	if ($ret < 0)
6395		throw new InvalidArgException ('s', $s, 'is before 1970-01-01 00:00:00 UTC');
6396	if ($ret >= 0xFFFFFFFF)
6397		throw new InvalidArgException ('s', $s, 'is on or after 2106-02-07 06:28:15 UTC');
6398	return $ret;
6399}
6400
6401function SQLDateFromDateStr ($s, $format = NULL)
6402{
6403	if ($format === NULL)
6404		$format = getConfigVar ('DATEONLY_FORMAT');
6405	if (FALSE === $tmp = strptime ($s, $format))
6406		throw new InvalidArgException ('s', $s, "not a date in format '${format}'");
6407	$y = $tmp['tm_year'] + 1900;
6408	$m = $tmp['tm_mon'] + 1;
6409	$d = $tmp['tm_mday'];
6410	if ($y < 1000 || $y > 9999)
6411		throw new InvalidArgException ('s', $s, 'year out of range');
6412	if (! checkdate ($m, $d, $y))
6413		throw new InvalidArgException ('s', $s, 'not a Gregorian calendar date');
6414	return sprintf ('%4u-%02u-%02u', $y, $m, $d);
6415}
6416
6417// This function returns either -1 or 0 or 1, it can be used with usort().
6418function cmpSQLDates ($date1, $date2)
6419{
6420	if
6421	(
6422		! preg_match ('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $date1, $m1) ||
6423		! checkdate ($m1[2], $m1[3], $m1[1])
6424	)
6425		throw new InvalidArgException ('date1', $date1, 'not a valid SQL date');
6426	if
6427	(
6428		! preg_match ('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $date2, $m2) ||
6429		! checkdate ($m2[2], $m2[3], $m2[1])
6430	)
6431		throw new InvalidArgException ('date2', $date2, 'not a valid SQL date');
6432	if (0 != $ret = numCompare ($m1[1], $m2[1]))
6433		return $ret;
6434	if (0 != $ret = numCompare ($m1[2], $m2[2]))
6435		return $ret;
6436	return numCompare ($m1[3], $m2[3]);
6437}
6438
6439# Produce a human-readable clue, such as 'YYYY-MM-DD' for '%Y-%m-%d'.
6440function datetimeFormatHint ($format)
6441{
6442	$subst = array
6443	(
6444		# leave ISO-8601:1988 week-numbering years (%g, %G) alone
6445		'%y' => 'YY',
6446		'%Y' => 'YYYY',
6447		'%m' => 'MM',
6448		'%d' => 'DD',
6449	);
6450	return str_replace (array_keys ($subst), $subst, $format);
6451}
6452
6453// Return TRUE, if the object belongs to specified type and has
6454// specified attribute belonging to the given set of values.
6455function checkTypeAndAttribute ($object_id, $type_id, $attr_id, $values)
6456{
6457	$object = spotEntity ('object', $object_id);
6458	if ($object['objtype_id'] == $type_id)
6459		foreach (getAttrValues ($object_id) as $record)
6460			if ($record['id'] == $attr_id && in_array ($record['key'], $values))
6461				return TRUE;
6462	return FALSE;
6463}
6464
6465// The old name, remove at a later point.
6466function nullEmptyStr ($str)
6467{
6468	return nullIfEmptyStr ($str);
6469}
6470
6471function nullIfEmptyStr ($str)
6472{
6473	return $str != '' ? $str : NULL;
6474}
6475
6476function nullIfFalse ($x)
6477{
6478	return $x === FALSE ? NULL : $x;
6479}
6480
6481function nullIfZero ($x)
6482{
6483	return $x == 0 ? NULL : $x;
6484}
6485
6486function emptyStrIfZero ($x)
6487{
6488	return ($x === 0 || $x === '0') ? '' : $x;
6489}
6490
6491function printLocationChildrenSelectOptions ($location, $parent_id, $location_id = NULL, $level = 0)
6492{
6493	$self = __FUNCTION__;
6494	$level++;
6495	foreach ($location['kids'] as $subLocation)
6496	{
6497		if ($subLocation['id'] == $location_id)
6498			continue;
6499		echo "<option value=${subLocation['id']}";
6500		if ($subLocation['id'] == $parent_id)
6501			echo ' selected';
6502		echo '>' . str_repeat ('&raquo; ', $level) . "${subLocation['name']}</option>\n";
6503		if ($subLocation['kidc'] > 0)
6504			$self ($subLocation, $parent_id, $location_id, $level);
6505	}
6506}
6507
6508function validTagName ($s, $allow_autotag = FALSE)
6509{
6510	return preg_match (TAGNAME_REGEXP, $s) ||
6511		($allow_autotag && preg_match (AUTOTAGNAME_REGEXP, $s));
6512}
6513
6514// returns html string with parent location names
6515// link: if each name should be wrapped in an href
6516function getLocationTrail ($location_id, $link = TRUE, $spacer = ' : ')
6517{
6518	// XXX: $location_tree is an array, not a tree
6519	static $location_tree = array ();
6520	if (count ($location_tree) == 0)
6521		foreach (listCells ('location') as $location)
6522			$location_tree[$location['id']] = array ('parent_id' => $location['parent_id'], 'name' => $location['name']);
6523
6524	// prepend parent location(s) to given location string
6525	$names = array();
6526	$id = $location_id;
6527	$locationIdx = 0;
6528	while (isset ($id))
6529	{
6530		if ($locationIdx == 20)
6531		{
6532			showWarning ('Warning: There is likely a circular reference in the location tree.');
6533			break;
6534		}
6535		$name = $location_tree[$id]['name'];
6536		array_unshift ($names, $link ? mkA ($name, 'location', $id) : $name);
6537		$id = $location_tree[$id]['parent_id'];
6538		$locationIdx++;
6539	}
6540	return implode ($spacer, $names);
6541}
6542
6543function cmp_array_sizes ($a, $b)
6544{
6545	return numCompare (count ($a), count ($b));
6546}
6547
6548// parses the value of MGMT_PROTOS config variable and returns an array
6549// indexed by protocol name with corresponding textual RackCode values
6550function getMgmtProtosConfig ($ignore_cache = FALSE)
6551{
6552	static $cache = NULL;
6553	if (!$ignore_cache && isset ($cache))
6554		return $cache;
6555
6556	$cache = array();
6557	$config = getConfigVar ('MGMT_PROTOS');
6558	foreach (explode (';', $config) as $item)
6559	{
6560		$item = trim ($item);
6561		if ($item == '')
6562			continue;
6563		if (preg_match('/^(\S+)\s*:\s*(.*)$/', $item, $m))
6564			$cache[$m[1]] = $m[2];
6565	}
6566	return $cache;
6567}
6568
6569// returns compiled RackCode expression or NULL if syntax error occurs
6570// caches the result in $exprCache global
6571function compileExpression ($code, $do_cache_lookup = TRUE)
6572{
6573	global $exprCache;
6574	if (! is_array ($exprCache))
6575		$exprCache = array();
6576	if ($do_cache_lookup && array_key_exists($code, $exprCache))
6577		return $exprCache[$code];
6578
6579	$ret = NULL;
6580	$parse = spotPayload ($code, 'SYNT_EXPR');
6581	if ($parse['result'] == 'ACK')
6582		$ret = $parse['load'];
6583	$exprCache[$code] = $ret;
6584	return $ret;
6585}
6586
6587// a caching wrapper around detectDeviceBreed and shortenIfName
6588function shortenPortName ($if_name, $object_id)
6589{
6590	static $breed_cache = array();
6591	if (! array_key_exists($object_id, $breed_cache))
6592		$breed_cache[$object_id] = detectDeviceBreed ($object_id);
6593	$breed = $breed_cache[$object_id];
6594	return $breed == '' ? $if_name : shortenIfName ($if_name, $breed);
6595}
6596
6597// returns an array of IP ranges of size $dst_mask > $netinfo['mask'], or array ($netinfo)
6598function splitNetworkByMask ($netinfo, $dst_mask)
6599{
6600	$self = __FUNCTION__;
6601	if ($netinfo['mask'] >= $dst_mask)
6602		return array ($netinfo);
6603
6604	return array_merge
6605	(
6606		$self (constructIPRange ($netinfo['ip_bin'], $netinfo['mask'] + 1), $dst_mask),
6607		$self (constructIPRange (ip_last ($netinfo), $netinfo['mask'] + 1), $dst_mask)
6608	);
6609}
6610
6611// this function is used both to remember and to retrieve the last created entity's ID
6612// it stores given id in the static var, and returns the stored value is called without args
6613// used in plugins to make additional work on created entity in the chained ophandler
6614// returns an array of realm-ID pairs
6615function lastCreated ($realm = NULL, $id = NULL)
6616{
6617	static $last_ids = array();
6618	if (isset ($realm) && isset ($id))
6619		$last_ids[] = array('realm' => $realm, 'id' => $id);
6620	return $last_ids;
6621}
6622
6623// returns last id of a given type from lastCreated() result array
6624function getLastCreatedId ($realm)
6625{
6626	foreach (array_reverse (lastCreated()) as $item)
6627		if ($item['realm'] == $realm)
6628			return $item['id'];
6629}
6630
6631function formatPatchCableHeapAsPlainText ($heap)
6632{
6633	$text = "${heap['amount']} pcs: [${heap['end1_connector']}] ${heap['pctype']} [${heap['end2_connector']}]";
6634	if ($heap['description'] != '')
6635		$text .=  " (${heap['description']})";
6636	return stringForOption ($text, 512);
6637}
6638
6639// takes a list of structures and the field name in those structures.
6640// returns a two-dimentional list indexed by the value of the given field
6641// the subsequent index value is taken from the index of the original $list.
6642function groupBy ($list, $group_field)
6643{
6644	$ret = array();
6645	if (! is_array ($list))
6646		throw new InvalidArgException ('list', $list, 'must be an array');
6647	foreach ($list as $index => $item)
6648	{
6649		if (! is_array ($item))
6650			throw new InvalidArgException ("list[${index}]", $item, 'must be an array');
6651		$key = '';
6652		if (isset ($item[$group_field]))
6653			$key = (string) $item[$group_field];
6654		$ret[$key][$index] = $item;
6655	}
6656	return $ret;
6657}
6658
6659// returns the associative $array sorted by its keys
6660// sort order is taken from the $order array:
6661// $array[i] is arranged with $array[j] conforming to
6662// numeric comparison of $order[i] and $order[j].
6663function customKsort ($array, $order)
6664{
6665	$ret = array();
6666	foreach ($array as $key => $value)
6667		$ret[$key] = isset ($order[$key]) ? $order[$key] : array_last ($order) + 1;
6668
6669	asort ($ret, SORT_NUMERIC);
6670	foreach (array_keys ($ret) as $key)
6671		$ret[$key] = $array[$key];
6672	return $ret;
6673}
6674
6675// RT uses PHP sessions on demand and tries to minimize session lifetime
6676// to allow concurrent operations for a single user.
6677// You should call session_commit after each call of this function.
6678function startSession()
6679{
6680	if (session_status() != PHP_SESSION_ACTIVE)
6681		session_start();
6682}
6683
6684// loads session data. Use if you need to only read from _SESSION.
6685function startROSession()
6686{
6687	startSession();
6688	session_commit();
6689}
6690
6691// removes a VLAN from ports that contain it, but keep a VLAN amongst others in a range
6692function pinpointDeleteVlan ($domain_id, $vlan_id)
6693{
6694	$ret = 0;
6695	$vlan_ck = $domain_id . '-' . $vlan_id;
6696	$domain_vlanlist = getDomainVLANList ($domain_id);
6697	$used_ports = getVLANConfiguredPorts ($vlan_ck);
6698	foreach ($used_ports as $object_id => $port_list)
6699	{
6700		$vswitch = getVLANSwitchInfo ($object_id); // get mutex rev
6701		$D = getStored8021QConfig ($object_id, 'desired', $port_list);
6702		$changes = array();
6703		foreach ($port_list as $pn)
6704		{
6705			if (! isset ($D[$pn]))
6706				continue;
6707			// remove vlan from a port only if its range does not contain foreign vlans
6708			foreach (listToRanges ($D[$pn]['allowed']) as $range)
6709				if (matchVLANFilter ($vlan_id, array ($range)))
6710				{
6711					for ($i = $range['from']; $i <= $range['to']; ++$i)
6712						if (! isset ($domain_vlanlist[$i]))
6713							continue 3; // keep vlan, skip to next port
6714				}
6715			// remove vlan from port
6716			$conf = $D[$pn];
6717			$conf['allowed'] = array_diff ($conf['allowed'], array ($vlan_id));
6718			if ($conf['mode'] == 'access')
6719				$conf['mode'] = 'trunk';
6720			if ($conf['native'] == $vlan_id)
6721				$conf['native'] = 0;
6722			$changes[$pn] = $conf;
6723		}
6724		if ($changes)
6725			$ret += apply8021qChangeRequest ($object_id, $changes, FALSE, $vswitch['mutex_rev']);
6726	}
6727
6728	return $ret;
6729}
6730
6731function etypeByPageno ($pg = NULL)
6732{
6733	global $etype_by_pageno, $pageno;
6734	if ($pg === NULL)
6735		$pg = $pageno;
6736	if (! array_key_exists ($pg, $etype_by_pageno))
6737		throw new RackTablesError ('key not found', RackTablesError::INTERNAL);
6738	return $etype_by_pageno[$pg];
6739}
6740
6741function requireListOfFiles ($x)
6742{
6743	if (! is_array ($x))
6744		require_once $x;
6745	else
6746		foreach ($x as $filename)
6747			require_once $filename;
6748}
6749
6750function requireExtraFiles ($reqlist, $pageno, $tabno)
6751{
6752	if (array_key_exists ("${pageno}-${tabno}", $reqlist))
6753		requireListOfFiles ($reqlist["${pageno}-${tabno}"]);
6754	if (array_key_exists ("${pageno}-*", $reqlist))
6755		requireListOfFiles ($reqlist["${pageno}-*"]);
6756}
6757
6758// Return the text as a list of lines after removing CRs, empty lines
6759// and leading/trailing whitespace.
6760function textareaCooked ($text)
6761{
6762	$ret = dos2unix ($text);
6763	$ret = explode ("\n", $ret);
6764	$ret = array_map ('trim', $ret);
6765	$ret = array_diff ($ret, array (''));
6766	$ret = array_values ($ret); // reindex to be consistent enough for the tests
6767	return $ret;
6768}
6769
6770// Used to fill $desiredPorts argument for replaceObjectPorts.
6771// Call this function just like commitAddPort except the first argument
6772function addDesiredPort (&$desiredPorts, $port_name, $port_type_id, $port_label, $port_l2address)
6773{
6774	list ($iif_id, $oif_id) = parsePortIIFOIF ($port_type_id);
6775	$desiredPorts["{$port_name}-{$iif_id}"] = array (
6776		'name' => $port_name,
6777		'iif_id' => $iif_id,
6778		'oif_id' => $oif_id,
6779		'label' => $port_label,
6780		'l2address' => $port_l2address,
6781	);
6782}
6783
6784// synchronizes the list of the object ports to the desired list of ports
6785// $desiredPorts is a list of ports in getObjectPortsAndLinks format.
6786// required port fields are name, iif_id, oif_id, label and l2address.
6787// $desiredPorts can be filled by addDesiredPort function using commitAddPort format
6788function replaceObjectPorts ($object_id, $desiredPorts)
6789{
6790	global $dbxlink;
6791	$to_delete = $to_update = $real_ports = array();
6792
6793	// The check that does not require access to the database goes first.
6794	foreach (array_keys ($desiredPorts) as $k)
6795		$desiredPorts[$k]['l2address'] = l2addressForDatabase ($desiredPorts[$k]['l2address']);
6796
6797	// Further processing must be done with exclusive access to the table. Even when the
6798	// only changes requested are to add ports w/o MAC addresses or to update existing
6799	// ports in a way that does not introduce new MAC addresses, it is impossible to
6800	// tell reliably which ports require which actions without locking the table first.
6801	$dbxlink->exec ('LOCK TABLES Port WRITE, PortLog WRITE, Link READ');
6802	foreach (getObjectPortsAndLinksTerse ($object_id) as $port)
6803	{
6804		$key = "{$port['name']}-{$port['iif_id']}";
6805		if (! array_key_exists ($key, $desiredPorts))
6806		{
6807			$to_delete[] = $port;
6808			continue;
6809		}
6810		if ($port['l2address'] != $desiredPorts[$key]['l2address'] || $port['label'] != $desiredPorts[$key]['label'])
6811			$to_update[$key] = $port;
6812		$real_ports[$key] = 1;
6813	}
6814	$to_add = array_diff_key ($desiredPorts, $real_ports);
6815
6816	try
6817	{
6818		assertUniqueL2Addresses (reduceSubarraysToColumn (array_merge ($to_update, $to_add), 'l2address'), $object_id);
6819		// Make the actual changes.
6820		foreach ($to_delete as $port)
6821			if ($port['link_count'] != 0)
6822				showWarning (sprintf ("Port %s should be deleted, but it's used", formatPort ($port)));
6823			else
6824				usePreparedDeleteBlade ('Port', array ('id' => $port['id']));
6825		foreach ($to_update as $key => $port)
6826			commitUpdatePortReal
6827			(
6828				$object_id,
6829				$port['id'],
6830				$port['name'],
6831				$port['iif_id'],
6832				$port['oif_id'],
6833				$desiredPorts[$key]['label'],
6834				$desiredPorts[$key]['l2address'],
6835				$port['reservation_comment']
6836			);
6837		foreach ($to_add as $key => $port)
6838			commitAddPortReal
6839			(
6840				$object_id,
6841				$port['name'],
6842				$port['iif_id'],
6843				$port['oif_id'],
6844				$port['label'],
6845				$port['l2address']
6846			);
6847	}
6848	catch (Exception $e)
6849	{
6850		$dbxlink->exec ('UNLOCK TABLES');
6851		throw $e;
6852	}
6853
6854	$dbxlink->exec ('UNLOCK TABLES');
6855	showSuccess (sprintf ('Added ports: %u, changed: %u, deleted: %u', count ($to_add), count ($to_update), count ($to_delete)));
6856}
6857
6858function formatPluginState ($state)
6859{
6860	$map = array
6861	(
6862		'disabled' => 'Disabled',
6863		'enabled' => 'Enabled',
6864		'not_installed' => 'Not installed',
6865	);
6866	return array_fetch ($map, $state, 'unknown');
6867}
6868