r5011 bugfix: fetchPortList: wrong field name 'cable_id' renamed to 'cableid'
[racktables] / wwwroot / inc / functions.php
CommitLineData
b325120a 1<?php
cddbb9fd
DO
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
e673ee24
DO
7/*
8*
9* This file is a library of computational functions for RackTables.
10*
11*/
12
13$loclist[0] = 'front';
14$loclist[1] = 'interior';
15$loclist[2] = 'rear';
16$loclist['front'] = 0;
17$loclist['interior'] = 1;
18$loclist['rear'] = 2;
19$template[0] = array (TRUE, TRUE, TRUE);
20$template[1] = array (TRUE, TRUE, FALSE);
21$template[2] = array (FALSE, TRUE, TRUE);
22$template[3] = array (TRUE, FALSE, FALSE);
23$template[4] = array (FALSE, TRUE, FALSE);
24$template[5] = array (FALSE, FALSE, TRUE);
25$templateWidth[0] = 3;
26$templateWidth[1] = 2;
27$templateWidth[2] = 2;
28$templateWidth[3] = 1;
29$templateWidth[4] = 1;
30$templateWidth[5] = 1;
31
408b9915
DO
32define ('CHAP_OBJTYPE', 1);
33define ('CHAP_PORTTYPE', 2);
408b9915
DO
34// The latter matches both SunOS and Linux-styled formats.
35define ('RE_L2_IFCFG', '/^[0-9a-f]{1,2}(:[0-9a-f]{1,2}){5}$/i');
36define ('RE_L2_CISCO', '/^[0-9a-f]{4}(\.[0-9a-f]{4}){2}$/i');
37define ('RE_L2_HUAWEI', '/^[0-9a-f]{4}(-[0-9a-f]{4}){2}$/i');
38define ('RE_L2_SOLID', '/^[0-9a-f]{12}$/i');
39define ('RE_L2_IPCFG', '/^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/i');
40define ('RE_L2_WWN_COLON', '/^[0-9a-f]{1,2}(:[0-9a-f]{1,2}){7}$/i');
41define ('RE_L2_WWN_HYPHEN', '/^[0-9a-f]{2}(-[0-9a-f]{2}){7}$/i');
42define ('RE_L2_WWN_SOLID', '/^[0-9a-f]{16}$/i');
43define ('RE_IP4_ADDR', '#^[0-9]{1,3}(\.[0-9]{1,3}){3}$#');
44define ('RE_IP4_NET', '#^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$#');
45define ('E_8021Q_NOERROR', 0);
46define ('E_8021Q_VERSION_CONFLICT', 101);
47define ('E_8021Q_PULL_REMOTE_ERROR', 102);
48define ('E_8021Q_PUSH_REMOTE_ERROR', 103);
49define ('E_8021Q_SYNC_DISABLED', 104);
50define ('VLAN_MIN_ID', 1);
51define ('VLAN_MAX_ID', 4094);
52define ('VLAN_DFL_ID', 1);
fa5b2764
AA
53define ('TAB_REMEMBER_TIMEOUT', 300);
54
482c7f35
DO
55// Entity type by page number mapping is 1:1 atm, but may change later.
56$etype_by_pageno = array
57(
58 'ipv4net' => 'ipv4net',
21ee3351 59 'ipv6net' => 'ipv6net',
482c7f35
DO
60 'ipv4rspool' => 'ipv4rspool',
61 'ipv4vs' => 'ipv4vs',
62 'object' => 'object',
63 'rack' => 'rack',
64 'user' => 'user',
65 'file' => 'file',
cc3d6915 66 'vst' => 'vst',
482c7f35
DO
67);
68
ef4b16fb
DO
69// Rack thumbnail image width summands: "front", "interior" and "rear" elements w/o surrounding border.
70$rtwidth = array
71(
72 0 => 9,
73 1 => 21,
74 2 => 9
75);
76
9b8174d7
AD
77$location_obj_types = array
78(
79 1560,
80 1561,
81 1562
82);
83
c8824ff4
DO
84$netmaskbylen = array
85(
86 32 => '255.255.255.255',
87 31 => '255.255.255.254',
88 30 => '255.255.255.252',
89 29 => '255.255.255.248',
90 28 => '255.255.255.240',
91 27 => '255.255.255.224',
92 26 => '255.255.255.192',
93 25 => '255.255.255.128',
94 24 => '255.255.255.0',
95 23 => '255.255.254.0',
96 22 => '255.255.252.0',
97 21 => '255.255.248.0',
98 20 => '255.255.240.0',
99 19 => '255.255.224.0',
100 18 => '255.255.192.0',
101 17 => '255.255.128.0',
102 16 => '255.255.0.0',
103 15 => '255.254.0.0',
104 14 => '255.252.0.0',
105 13 => '255.248.0.0',
106 12 => '255.240.0.0',
107 11 => '255.224.0.0',
108 10 => '255.192.0.0',
109 9 => '255.128.0.0',
110 8 => '255.0.0.0',
111 7 => '254.0.0.0',
112 6 => '252.0.0.0',
113 5 => '248.0.0.0',
114 4 => '240.0.0.0',
115 3 => '224.0.0.0',
116 2 => '192.0.0.0',
117 1 => '128.0.0.0'
118);
119
120$wildcardbylen = array
121(
122 32 => '0.0.0.0',
123 31 => '0.0.0.1',
124 30 => '0.0.0.3',
125 29 => '0.0.0.7',
126 28 => '0.0.0.15',
127 27 => '0.0.0.31',
128 26 => '0.0.0.63',
129 25 => '0.0.0.127',
130 24 => '0.0.0.255',
131 23 => '0.0.1.255',
132 22 => '0.0.3.255',
133 21 => '0.0.7.255',
134 20 => '0.0.15.255',
135 19 => '0.0.31.255',
136 18 => '0.0.63.255',
137 17 => '0.0.127.255',
138 16 => '0.0.255.25',
139 15 => '0.1.255.255',
140 14 => '0.3.255.255',
141 13 => '0.7.255.255',
142 12 => '0.15.255.255',
143 11 => '0.31.255.255',
144 10 => '0.63.255.255',
145 9 => '0.127.255.255',
146 8 => '0.255.255.255',
147 7 => '1.255.255.255',
148 6 => '3.255.255.255',
149 5 => '7.255.255.255',
150 4 => '15.255.255.255',
151 3 => '31.255.255.255',
152 2 => '63.255.255.255',
153 1 => '127.255.255.255'
154);
155
fe90d767
DO
156$masklenByDQ = array
157(
158 '255.255.255.255' => 32,
159 '255.255.255.254' => 31,
160 '255.255.255.252' => 30,
161 '255.255.255.248' => 29,
162 '255.255.255.240' => 28,
163 '255.255.255.224' => 27,
164 '255.255.255.192' => 26,
165 '255.255.255.128' => 25,
166 '255.255.255.0' => 24,
167 '255.255.254.0' => 23,
168 '255.255.252.0' => 22,
169 '255.255.248.0' => 21,
170 '255.255.240.0' => 20,
171 '255.255.224.0' => 19,
172 '255.255.192.0' => 18,
173 '255.255.128.0' => 17,
174 '255.255.0.0' => 16,
175 '255.254.0.0' => 15,
176 '255.252.0.0' => 14,
177 '255.248.0.0' => 13,
178 '255.240.0.0' => 12,
179 '255.224.0.0' => 11,
180 '255.192.0.0' => 10,
181 '255.128.0.0' => 9,
182 '255.0.0.0' => 8,
183 '254.0.0.0' => 7,
184 '252.0.0.0' => 6,
185 '248.0.0.0' => 5,
186 '240.0.0.0' => 4,
187 '224.0.0.0' => 3,
188 '192.0.0.0' => 2,
189 '128.0.0.0' => 1,
190 '0.0.0.0' => 0,
191);
192
8a97919e
DO
193// 802.1Q deploy queue titles
194$dqtitle = array
195(
196 'sync_aging' => 'Normal, aging',
197 'resync_aging' => 'Version conflict, aging',
198 'sync_ready' => 'Normal, ready for sync',
199 'resync_ready' => 'Version conflict, ready for retry',
200 'failed' => 'Failed',
201 'disabled' => 'Sync disabled',
202 'done' => 'Up to date',
203);
204
e820929d
DO
205$wdm_packs = array
206(
207 '1000cwdm80' => array
208 (
209 'title' => '1000Base-CWDM80 (8 channels)',
210 'iif_ids' => array (3, 4),
211 'oif_ids' => array (1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216),
212 ),
213 '1000dwdm80' => array // ITU channels 20~61
214 (
215 'title' => '1000Base-DWDM80 (42 channels)',
216 'iif_ids' => array (3, 4),
217 'oif_ids' => array
218 (
219 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226,
220 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236,
221 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246,
222 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256,
223 1257, 1258
224 ),
225 ),
226 '10000dwdm80' => array // same channels for 10GE
227 (
228 'title' => '10GBase-ZR-DWDM80 (42 channels)',
229 'iif_ids' => array (9, 6, 5, 8, 7),
230 'oif_ids' => array
231 (
232 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268,
233 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278,
234 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288,
235 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298,
236 1299, 1300
237 ),
238 ),
52d31972
DO
239 '10000dwdm40' => array
240 (
241 'title' => '10GBase-ER-DWDM40 (42 channels)',
242 'iif_ids' => array (9, 6, 5, 8, 7),
243 'oif_ids' => array
244 (
245 1425, 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, 1434,
246 1435, 1436, 1437, 1438, 1439, 1440, 1441, 1442, 1443, 1444,
247 1445, 1446, 1447, 1448, 1449, 1450, 1451, 1452, 1453, 1454,
248 1455, 1456, 1457, 1458, 1459, 1460, 1461, 1462, 1463, 1464,
249 1465, 1466
250 ),
251 ),
e820929d
DO
252);
253
a2b16202
AA
254$log_messages = array(); // messages waiting for displaying
255
f77323f1
DO
256// This function assures that specified argument was passed
257// and is a number greater than zero.
0cc24e9a 258function assertUIntArg ($argname, $allow_zero = FALSE)
f77323f1
DO
259{
260 if (!isset ($_REQUEST[$argname]))
d986edc2 261 throw new InvalidRequestArgException($argname, '', 'parameter is missing');
f77323f1 262 if (!is_numeric ($_REQUEST[$argname]))
5847d944 263 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a number');
f77323f1 264 if ($_REQUEST[$argname] < 0)
5847d944 265 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is less than zero');
ca92dc40 266 if (!$allow_zero and $_REQUEST[$argname] == 0)
5847d944 267 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is zero');
f77323f1
DO
268}
269
09ec2e59
AA
270function isInteger ($arg, $allow_zero = FALSE)
271{
272 if (! is_numeric ($arg))
273 return FALSE;
274 if (! $allow_zero and ! $arg)
275 return FALSE;
276 return TRUE;
277}
278
f77323f1
DO
279// This function assures that specified argument was passed
280// and is a non-empty string.
0cc24e9a 281function assertStringArg ($argname, $ok_if_empty = FALSE)
f77323f1
DO
282{
283 if (!isset ($_REQUEST[$argname]))
d986edc2 284 throw new InvalidRequestArgException($argname, '', 'parameter is missing');
f77323f1 285 if (!is_string ($_REQUEST[$argname]))
5847d944 286 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
f77323f1 287 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
5847d944 288 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
f77323f1
DO
289}
290
0cc24e9a 291function assertBoolArg ($argname, $ok_if_empty = FALSE)
f77323f1
DO
292{
293 if (!isset ($_REQUEST[$argname]))
d986edc2 294 throw new InvalidRequestArgException($argname, '', 'parameter is missing');
f77323f1 295 if (!is_string ($_REQUEST[$argname]) or $_REQUEST[$argname] != 'on')
5847d944 296 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
f77323f1 297 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
5847d944 298 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
f77323f1
DO
299}
300
21ee3351
AA
301// function returns IPv6Address object, null if arg is correct IPv4, or throws an exception
302function assertIPArg ($argname, $ok_if_empty = FALSE)
303{
304 assertStringArg ($argname, $ok_if_empty);
305 $ip = $_REQUEST[$argname];
306 if (FALSE !== strpos ($ip, ':'))
307 {
308 $v6address = new IPv6Address;
309 $result = $v6address->parse ($ip);
310 $ret = $v6address;
311 }
312 else
313 {
314 $result = long2ip (ip2long ($ip)) === $ip;
315 $ret = NULL;
316 }
317 if (! $result)
318 throw new InvalidRequestArgException ($argname, $ip, 'parameter is not a valid IPv4 or IPv6 address');
319 return $ret;
320}
321
0cc24e9a 322function assertIPv4Arg ($argname, $ok_if_empty = FALSE)
f77323f1 323{
0cc24e9a 324 assertStringArg ($argname, $ok_if_empty);
f77323f1 325 if (strlen ($_REQUEST[$argname]) and long2ip (ip2long ($_REQUEST[$argname])) !== $_REQUEST[$argname])
5847d944 326 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a valid ipv4 address');
f77323f1
DO
327}
328
21ee3351
AA
329// function returns IPv6Address object, or throws an exception
330function assertIPv6Arg ($argname, $ok_if_empty = FALSE)
331{
332 assertStringArg ($argname, $ok_if_empty);
333 $ipv6 = new IPv6Address;
334 if (strlen ($_REQUEST[$argname]) and ! $ok_if_empty and ! $ipv6->parse ($_REQUEST[$argname]))
335 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a valid ipv6 address');
336 return $ipv6;
337}
338
e0d188ef
DO
339function assertPCREArg ($argname)
340{
341 assertStringArg ($argname, TRUE); // empty pattern is Ok
664ec705 342 if (FALSE === preg_match ($_REQUEST[$argname], 'test'))
e0d188ef
DO
343 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'PCRE validation failed');
344}
345
09ec2e59
AA
346function isPCRE ($arg)
347{
35906898 348 if (! isset ($arg) or FALSE === @preg_match ($arg, 'test'))
09ec2e59
AA
349 return FALSE;
350 return TRUE;
351}
352
cde28cf0
DO
353function genericAssertion ($argname, $argtype)
354{
04b1804f 355 global $sic;
cde28cf0
DO
356 switch ($argtype)
357 {
358 case 'string':
359 assertStringArg ($argname);
360 break;
4eba4f82
DO
361 case 'string0':
362 assertStringArg ($argname, TRUE);
363 break;
cde28cf0
DO
364 case 'uint':
365 assertUIntArg ($argname);
366 break;
e268ff45
DO
367 case 'uint-uint':
368 assertStringArg ($argname);
369 if (1 != preg_match ('/^[1-9][0-9]*-[1-9][0-9]*$/', $_REQUEST[$argname]))
370 throw new InvalidRequestArgException ($argname, $_REQUEST[$argname], 'illegal format');
371 break;
cde28cf0
DO
372 case 'uint0':
373 assertUIntArg ($argname, TRUE);
374 break;
375 case 'inet4':
376 assertIPv4Arg ($argname);
377 break;
9b6e7bd1
DO
378 case 'inet6':
379 assertIPv6Arg ($argname);
380 break;
381 case 'l2address':
382 assertStringArg ($argname);
383 case 'l2address0':
384 assertStringArg ($argname, TRUE);
385 try
386 {
387 l2addressForDatabase ($sic[$argname]);
388 }
389 catch (InvalidArgException $e)
390 {
391 throw new InvalidRequestArgException ($argname, $sic[$argname], 'malformed MAC/WWN address');
392 }
393 break;
04b1804f
DO
394 case 'tag':
395 assertStringArg ($argname);
396 if (!validTagName ($sic[$argname]))
397 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Invalid tag name');
398 break;
9b6e7bd1
DO
399 case 'pcre':
400 assertPCREArg ($argname);
401 break;
1d84140d
DO
402 case 'json':
403 assertStringArg ($argname);
405ac32c 404 if (NULL === json_decode ($sic[$argname], TRUE))
1d84140d
DO
405 throw new InvalidRequestArgException ($argname, '(omitted)', 'Invalid JSON code received from client');
406 break;
fab31c45
DO
407 case 'array':
408 if (! array_key_exists ($argname, $_REQUEST))
409 throw new InvalidRequestArgException ($argname, '(missing argument)');
410 if (! is_array ($_REQUEST[$argname]))
411 throw new InvalidRequestArgException ($argname, '(omitted)', 'argument is not an array');
412 break;
7299b9f7
DO
413 case 'enum/attr_type':
414 assertStringArg ($argname);
415 if (!in_array ($sic[$argname], array ('uint', 'float', 'string', 'dict')))
416 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
417 break;
10005279
DO
418 case 'enum/vlan_type':
419 assertStringArg ($argname);
420 // "Alien" type is not valid until the logic is fixed to implement it in full.
421 if (!in_array ($sic[$argname], array ('ondemand', 'compulsory')))
422 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
423 break;
1d768108
DO
424 case 'enum/wdmstd':
425 assertStringArg ($argname);
e820929d
DO
426 global $wdm_packs;
427 if (! array_key_exists ($sic[$argname], $wdm_packs))
1d768108
DO
428 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
429 break;
a9ec91cf
DO
430 case 'enum/ipproto':
431 assertStringArg ($argname);
ed38c73c
AA
432 global $vs_proto;
433 if (!array_key_exists ($sic[$argname], $vs_proto))
a9ec91cf
DO
434 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
435 break;
96597d19
DO
436 case 'enum/inet4alloc':
437 case 'enum/inet6alloc':
438 assertStringArg ($argname);
439 if (!in_array ($sic[$argname], array ('regular', 'shared', 'virtual', 'router')))
440 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
441 break;
77c80ac3
DO
442 case 'enum/dqcode':
443 assertStringArg ($argname);
444 global $dqtitle;
445 if (! array_key_exists ($sic[$argname], $dqtitle))
446 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
447 break;
1d768108
DO
448 case 'iif':
449 if (!array_key_exists ($sic[$argname], getPortIIFOptions()))
450 throw new InvalidRequestArgException ($argname, $sic[$argname], 'Unknown value');
451 break;
ccc0acff
DO
452 case 'vlan':
453 case 'vlan1':
454 genericAssertion ($argname, 'uint');
455 if ($argtype == 'vlan' and $sic[$argname] == VLAN_DFL_ID)
456 throw new InvalidRequestArgException ($argname, $sic[$argname], 'default VLAN cannot be changed');
457 if ($sic[$argname] > VLAN_MAX_ID or $sic[$argname] < VLAN_MIN_ID)
458 throw new InvalidRequestArgException ($argname, $sic[$argname], 'out of valid range');
459 break;
77c80ac3
DO
460 case 'rackcode/expr':
461 genericAssertion ($argname, 'string0');
462 if ($sic[$argname] == '')
463 return;
464 $parse = spotPayload ($sic[$argname], 'SYNT_EXPR');
465 if ($parse['result'] != 'ACK')
466 throw new InvalidRequestArgException ($argname, $sic[$argname], 'RackCode parsing error');
467 break;
cde28cf0
DO
468 default:
469 throw new InvalidArgException ('argtype', $argtype); // comes not from user's input
470 }
471}
472
473// Validate and return "bypass" value for the current context, if one is
474// defined for it, or NULL otherwise.
475function getBypassValue()
476{
477 global $page, $pageno, $sic;
478 if (!array_key_exists ('bypass', $page[$pageno]))
479 return NULL;
480 if (!array_key_exists ('bypass_type', $page[$pageno]))
481 throw new RackTablesError ("Internal structure error at node '${pageno}' (bypass_type is not set)", RackTablesError::INTERNAL);
482 genericAssertion ($page[$pageno]['bypass'], $page[$pageno]['bypass_type']);
483 return $sic[$page[$pageno]['bypass']];
484}
485
e673ee24
DO
486// Objects of some types should be explicitly shown as
487// anonymous (labelless). This function is a single place where the
488// decision about displayed name is made.
cfa8f3cf 489function setDisplayedName (&$cell)
e673ee24 490{
cfa8f3cf
DO
491 if ($cell['name'] != '')
492 $cell['dname'] = $cell['name'];
e673ee24 493 else
326dc167 494 $cell['dname'] = '[' . decodeObjectType ($cell['objtype_id'], 'o') . ']';
e673ee24
DO
495}
496
497// This function finds height of solid rectangle of atoms, which are all
498// assigned to the same object. Rectangle base is defined by specified
499// template.
500function rectHeight ($rackData, $startRow, $template_idx)
501{
502 $height = 0;
503 // The first met object_id is used to match all the folowing IDs.
504 $object_id = 0;
505 global $template;
506 do
507 {
508 for ($locidx = 0; $locidx < 3; $locidx++)
509 {
510 // At least one value in template is TRUE, but the following block
511 // can meet 'skipped' atoms. Let's ensure we have something after processing
512 // the first row.
513 if ($template[$template_idx][$locidx])
514 {
515 if (isset ($rackData[$startRow - $height][$locidx]['skipped']))
516 break 2;
93e02204
DO
517 if (isset ($rackData[$startRow - $height][$locidx]['rowspan']))
518 break 2;
519 if (isset ($rackData[$startRow - $height][$locidx]['colspan']))
520 break 2;
e673ee24
DO
521 if ($rackData[$startRow - $height][$locidx]['state'] != 'T')
522 break 2;
523 if ($object_id == 0)
524 $object_id = $rackData[$startRow - $height][$locidx]['object_id'];
525 if ($object_id != $rackData[$startRow - $height][$locidx]['object_id'])
526 break 2;
527 }
528 }
529 // If the first row can't offer anything, bail out.
530 if ($height == 0 and $object_id == 0)
531 break;
532 $height++;
533 }
534 while ($startRow - $height > 0);
93e02204
DO
535# echo "for startRow==${startRow} and template==(" . ($template[$template_idx][0] ? 'T' : 'F');
536# echo ', ' . ($template[$template_idx][1] ? 'T' : 'F') . ', ' . ($template[$template_idx][2] ? 'T' : 'F');
537# echo ") height==${height}<br>\n";
e673ee24
DO
538 return $height;
539}
540
541// This function marks atoms to be avoided by rectHeight() and assigns rowspan/colspan
542// attributes.
543function markSpan (&$rackData, $startRow, $maxheight, $template_idx)
544{
545 global $template, $templateWidth;
546 $colspan = 0;
547 for ($height = 0; $height < $maxheight; $height++)
548 {
549 for ($locidx = 0; $locidx < 3; $locidx++)
550 {
551 if ($template[$template_idx][$locidx])
552 {
553 // Add colspan/rowspan to the first row met and mark the following ones to skip.
93e02204
DO
554 // Explicitly show even single-cell spanned atoms, because rectHeight()
555 // is expeciting this data for correct calculation.
e673ee24
DO
556 if ($colspan != 0)
557 $rackData[$startRow - $height][$locidx]['skipped'] = TRUE;
558 else
559 {
560 $colspan = $templateWidth[$template_idx];
93e02204 561 if ($colspan >= 1)
e673ee24 562 $rackData[$startRow - $height][$locidx]['colspan'] = $colspan;
93e02204 563 if ($maxheight >= 1)
e673ee24
DO
564 $rackData[$startRow - $height][$locidx]['rowspan'] = $maxheight;
565 }
566 }
567 }
568 }
569 return;
570}
571
93e02204
DO
572// This function sets rowspan/solspan/skipped atom attributes for renderRack()
573// What we actually have to do is to find _all_ possible rectangles for each unit
574// and then select the widest of those with the maximal square.
04d16213 575function markAllSpans (&$rackData)
e673ee24 576{
e673ee24 577 for ($i = $rackData['height']; $i > 0; $i--)
93e02204
DO
578 while (markBestSpan ($rackData, $i));
579}
580
581// Calculate height of 6 possible span templates (array is presorted by width
582// descending) and mark the best (if any).
583function markBestSpan (&$rackData, $i)
584{
585 global $template, $templateWidth;
586 for ($j = 0; $j < 6; $j++)
e673ee24 587 {
93e02204
DO
588 $height[$j] = rectHeight ($rackData, $i, $j);
589 $square[$j] = $height[$j] * $templateWidth[$j];
590 }
591 // find the widest rectangle of those with maximal height
592 $maxsquare = max ($square);
593 if (!$maxsquare)
594 return FALSE;
595 $best_template_index = 0;
596 for ($j = 0; $j < 6; $j++)
597 if ($square[$j] == $maxsquare)
e673ee24 598 {
93e02204
DO
599 $best_template_index = $j;
600 $bestheight = $height[$j];
601 break;
e673ee24 602 }
93e02204
DO
603 // distribute span marks
604 markSpan ($rackData, $i, $bestheight, $best_template_index);
605 return TRUE;
e673ee24
DO
606}
607
e673ee24
DO
608// We can mount 'F' atoms and unmount our own 'T' atoms.
609function applyObjectMountMask (&$rackData, $object_id)
610{
611 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
612 for ($locidx = 0; $locidx < 3; $locidx++)
613 switch ($rackData[$unit_no][$locidx]['state'])
614 {
615 case 'F':
616 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
617 break;
618 case 'T':
619 $rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
620 break;
621 default:
622 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
623 }
624}
625
626// Design change means transition between 'F' and 'A' and back.
627function applyRackDesignMask (&$rackData)
628{
629 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
630 for ($locidx = 0; $locidx < 3; $locidx++)
631 switch ($rackData[$unit_no][$locidx]['state'])
632 {
633 case 'F':
634 case 'A':
635 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
636 break;
637 default:
638 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
639 }
640}
641
642// The same for 'F' and 'U'.
643function applyRackProblemMask (&$rackData)
644{
645 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
646 for ($locidx = 0; $locidx < 3; $locidx++)
647 switch ($rackData[$unit_no][$locidx]['state'])
648 {
649 case 'F':
650 case 'U':
651 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
652 break;
653 default:
654 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
655 }
656}
657
e673ee24
DO
658// This function highlights specified object (and removes previous highlight).
659function highlightObject (&$rackData, $object_id)
660{
453cce7e
AD
661 // Also highlight parent objects
662 $parents = getEntityRelatives ('parents', 'object', $object_id);
663 $parent_ids = array();
664 foreach ($parents as $parent)
665 $parent_ids[] = $parent['entity_id'];
666
e673ee24
DO
667 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
668 for ($locidx = 0; $locidx < 3; $locidx++)
669 if
670 (
671 $rackData[$unit_no][$locidx]['state'] == 'T' and
453cce7e 672 ($rackData[$unit_no][$locidx]['object_id'] == $object_id or in_array($rackData[$unit_no][$locidx]['object_id'], $parent_ids))
e673ee24
DO
673 )
674 $rackData[$unit_no][$locidx]['hl'] = 'h';
675 else
676 unset ($rackData[$unit_no][$locidx]['hl']);
677}
678
679// This function marks atoms to selected or not depending on their current state.
680function markupAtomGrid (&$data, $checked_state)
681{
682 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
683 for ($locidx = 0; $locidx < 3; $locidx++)
684 {
685 if (!($data[$unit_no][$locidx]['enabled'] === TRUE))
686 continue;
687 if ($data[$unit_no][$locidx]['state'] == $checked_state)
688 $data[$unit_no][$locidx]['checked'] = ' checked';
689 else
690 $data[$unit_no][$locidx]['checked'] = '';
691 }
692}
693
694// This function is almost a clone of processGridForm(), but doesn't save anything to database
695// Return value is the changed rack data.
696// Here we assume that correct filter has already been applied, so we just
697// set or unset checkbox inputs w/o changing atom state.
698function mergeGridFormToRack (&$rackData)
699{
700 $rack_id = $rackData['id'];
701 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
702 for ($locidx = 0; $locidx < 3; $locidx++)
703 {
704 if ($rackData[$unit_no][$locidx]['enabled'] != TRUE)
705 continue;
706 $inputname = "atom_${rack_id}_${unit_no}_${locidx}";
707 if (isset ($_REQUEST[$inputname]) and $_REQUEST[$inputname] == 'on')
708 $rackData[$unit_no][$locidx]['checked'] = ' checked';
709 else
710 $rackData[$unit_no][$locidx]['checked'] = '';
711 }
712}
713
bb0a44e9 714// netmask conversion from length to number
e673ee24
DO
715function binMaskFromDec ($maskL)
716{
bb0a44e9
DO
717 $map_straight = array (
718 0 => 0x00000000,
719 1 => 0x80000000,
720 2 => 0xc0000000,
721 3 => 0xe0000000,
722 4 => 0xf0000000,
723 5 => 0xf8000000,
724 6 => 0xfc000000,
725 7 => 0xfe000000,
726 8 => 0xff000000,
727 9 => 0xff800000,
728 10 => 0xffc00000,
729 11 => 0xffe00000,
730 12 => 0xfff00000,
731 13 => 0xfff80000,
732 14 => 0xfffc0000,
733 15 => 0xfffe0000,
734 16 => 0xffff0000,
735 17 => 0xffff8000,
736 18 => 0xffffc000,
737 19 => 0xffffe000,
738 20 => 0xfffff000,
739 21 => 0xfffff800,
740 22 => 0xfffffc00,
741 23 => 0xfffffe00,
742 24 => 0xffffff00,
743 25 => 0xffffff80,
744 26 => 0xffffffc0,
745 27 => 0xffffffe0,
746 28 => 0xfffffff0,
747 29 => 0xfffffff8,
748 30 => 0xfffffffc,
749 31 => 0xfffffffe,
750 32 => 0xffffffff,
751 );
752 return $map_straight[$maskL];
e673ee24
DO
753}
754
bb0a44e9 755// complementary value
e673ee24
DO
756function binInvMaskFromDec ($maskL)
757{
bb0a44e9
DO
758 $map_compl = array (
759 0 => 0xffffffff,
760 1 => 0x7fffffff,
761 2 => 0x3fffffff,
762 3 => 0x1fffffff,
763 4 => 0x0fffffff,
764 5 => 0x07ffffff,
765 6 => 0x03ffffff,
766 7 => 0x01ffffff,
767 8 => 0x00ffffff,
768 9 => 0x007fffff,
769 10 => 0x003fffff,
770 11 => 0x001fffff,
771 12 => 0x000fffff,
772 13 => 0x0007ffff,
773 14 => 0x0003ffff,
774 15 => 0x0001ffff,
775 16 => 0x0000ffff,
776 17 => 0x00007fff,
777 18 => 0x00003fff,
778 19 => 0x00001fff,
779 20 => 0x00000fff,
780 21 => 0x000007ff,
781 22 => 0x000003ff,
782 23 => 0x000001ff,
783 24 => 0x000000ff,
784 25 => 0x0000007f,
785 26 => 0x0000003f,
786 27 => 0x0000001f,
787 28 => 0x0000000f,
788 29 => 0x00000007,
789 30 => 0x00000003,
790 31 => 0x00000001,
791 32 => 0x00000000,
792 );
793 return $map_compl[$maskL];
e673ee24
DO
794}
795
e673ee24
DO
796// This function looks up 'has_problems' flag for 'T' atoms
797// and modifies 'hl' key. May be, this should be better done
61a1d996 798// in amplifyCell(). We don't honour 'skipped' key, because
e673ee24
DO
799// the function is also used for thumb creation.
800function markupObjectProblems (&$rackData)
801{
802 for ($i = $rackData['height']; $i > 0; $i--)
803 for ($locidx = 0; $locidx < 3; $locidx++)
804 if ($rackData[$i][$locidx]['state'] == 'T')
805 {
6297d584 806 $object = spotEntity ('object', $rackData[$i][$locidx]['object_id']);
e673ee24
DO
807 if ($object['has_problems'] == 'yes')
808 {
809 // Object can be already highlighted.
810 if (isset ($rackData[$i][$locidx]['hl']))
811 $rackData[$i][$locidx]['hl'] = $rackData[$i][$locidx]['hl'] . 'w';
812 else
813 $rackData[$i][$locidx]['hl'] = 'w';
814 }
815 }
816}
817
d516d719 818// Return a uniformly (010203040506 or 0102030405060708) formatted address, if it is present
9b6e7bd1 819// in the provided string, an empty string for an empty string or raise an exception.
e673ee24
DO
820function l2addressForDatabase ($string)
821{
e673ee24 822 $string = strtoupper ($string);
05771508
DO
823 switch (TRUE)
824 {
d516d719 825 case ($string == '' or preg_match (RE_L2_SOLID, $string) or preg_match (RE_L2_WWN_SOLID, $string)):
05771508 826 return $string;
d516d719
DO
827 case (preg_match (RE_L2_IFCFG, $string) or preg_match (RE_L2_WWN_COLON, $string)):
828 // reformat output of SunOS ifconfig
829 $ret = '';
830 foreach (explode (':', $string) as $byte)
831 $ret .= (strlen ($byte) == 1 ? '0' : '') . $byte;
832 return $ret;
05771508 833 case (preg_match (RE_L2_CISCO, $string)):
d516d719 834 return str_replace ('.', '', $string);
c5553818
DO
835 case (preg_match (RE_L2_HUAWEI, $string)):
836 return str_replace ('-', '', $string);
d516d719
DO
837 case (preg_match (RE_L2_IPCFG, $string) or preg_match (RE_L2_WWN_HYPHEN, $string)):
838 return str_replace ('-', '', $string);
05771508 839 default:
9b6e7bd1 840 throw new InvalidArgException ('$string', $string, 'malformed MAC/WWN address');
05771508 841 }
e673ee24
DO
842}
843
844function l2addressFromDatabase ($string)
845{
846 switch (strlen ($string))
847 {
848 case 12: // Ethernet
d516d719 849 case 16: // FireWire/Fibre Channel
e673ee24
DO
850 $ret = implode (':', str_split ($string, 2));
851 break;
852 default:
853 $ret = $string;
854 break;
855 }
856 return $ret;
857}
858
859// The following 2 functions return previous and next rack IDs for
860// a given rack ID. The order of racks is the same as in renderRackspace()
861// or renderRow().
04d16213 862function getPrevIDforRack ($row_id, $rack_id)
e673ee24 863{
a8efc03e 864 $rackList = listCells ('rack', $row_id);
e673ee24
DO
865 doubleLink ($rackList);
866 if (isset ($rackList[$rack_id]['prev_key']))
867 return $rackList[$rack_id]['prev_key'];
868 return NULL;
869}
870
04d16213 871function getNextIDforRack ($row_id, $rack_id)
e673ee24 872{
a8efc03e 873 $rackList = listCells ('rack', $row_id);
e673ee24
DO
874 doubleLink ($rackList);
875 if (isset ($rackList[$rack_id]['next_key']))
876 return $rackList[$rack_id]['next_key'];
877 return NULL;
878}
879
880// This function finds previous and next array keys for each array key and
881// modifies its argument accordingly.
882function doubleLink (&$array)
883{
884 $prev_key = NULL;
885 foreach (array_keys ($array) as $key)
886 {
887 if ($prev_key)
888 {
889 $array[$key]['prev_key'] = $prev_key;
890 $array[$prev_key]['next_key'] = $key;
891 }
892 $prev_key = $key;
893 }
894}
895
e673ee24
DO
896function sortTokenize ($a, $b)
897{
898 $aold='';
899 while ($a != $aold)
900 {
901 $aold=$a;
84986395
DO
902 $a = preg_replace('/[^a-zA-Z0-9]/',' ',$a);
903 $a = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$a);
904 $a = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$a);
e673ee24
DO
905 }
906
907 $bold='';
908 while ($b != $bold)
909 {
910 $bold=$b;
84986395
DO
911 $b = preg_replace('/[^a-zA-Z0-9]/',' ',$b);
912 $b = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$b);
913 $b = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$b);
e673ee24
DO
914 }
915
916
917
918 $ar = explode(' ', $a);
919 $br = explode(' ', $b);
920 for ($i=0; $i<count($ar) && $i<count($br); $i++)
921 {
922 $ret = 0;
923 if (is_numeric($ar[$i]) and is_numeric($br[$i]))
924 $ret = ($ar[$i]==$br[$i])?0:($ar[$i]<$br[$i]?-1:1);
925 else
926 $ret = strcasecmp($ar[$i], $br[$i]);
927 if ($ret != 0)
928 return $ret;
929 }
930 if ($i<count($ar))
931 return 1;
932 if ($i<count($br))
933 return -1;
934 return 0;
935}
936
c31cd72c 937// This function returns an array of single element of object's FQDN attribute,
f321b50a
DO
938// if FQDN is set. The next choice is object's common name, if it looks like a
939// hostname. Otherwise an array of all 'regular' IP addresses of the
c31cd72c 940// object is returned (which may appear 0 and more elements long).
f321b50a 941function findAllEndpoints ($object_id, $fallback = '')
c31cd72c 942{
7fa7047a
DO
943 foreach (getAttrValues ($object_id) as $record)
944 if ($record['id'] == 3 && strlen ($record['value'])) // FQDN
c31cd72c 945 return array ($record['value']);
c31cd72c 946 $regular = array();
85970da2
DO
947 foreach (getObjectIPv4Allocations ($object_id) as $dottedquad => $alloc)
948 if ($alloc['type'] == 'regular')
949 $regular[] = $dottedquad;
59a83bd8 950 if (!count ($regular) && strlen ($fallback))
f321b50a 951 return array ($fallback);
c31cd72c
DO
952 return $regular;
953}
954
83ba6670
DO
955// Some records in the dictionary may be written as plain text or as Wiki
956// link in the following syntax:
957// 1. word
958// 2. [[word URL]] // FIXME: this isn't working
959// 3. [[word word word | URL]]
8ad59489
AA
960// This function parses the line in $record['value'] and modifies $record:
961// $record['o_value'] is set to be the first part of link (word word word)
962// $record['a_value'] is the same, but with %GPASS and %GSKIP macros applied
963// $record['href'] is set to URL if it is specified in the input value
964function parseWikiLink (&$record)
965{
966 if (! preg_match ('/^\[\[(.+)\]\]$/', $record['value'], $matches))
967 $record['o_value'] = $record['value'];
968 else
24cbe8af 969 {
8ad59489
AA
970 $s = explode ('|', $matches[1]);
971 if (isset ($s[1]))
972 $record['href'] = trim ($s[1]);
973 $record['o_value'] = trim ($s[0]);
24cbe8af 974 }
8ad59489 975 $record['a_value'] = execGMarker ($record['o_value']);
83ba6670
DO
976}
977
c9edf725
DO
978// FIXME: should this be saved as "P-data"?
979function execGMarker ($line)
980{
84986395 981 return preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $line));
c9edf725
DO
982}
983
177b1e9b
DO
984// rackspace usage for a single rack
985// (T + W + U) / (height * 3 - A)
04d16213 986function getRSUforRack ($data)
177b1e9b 987{
6ffba290 988 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
9e60f7df
DO
989 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
990 for ($locidx = 0; $locidx < 3; $locidx++)
991 $counter[$data[$unit_no][$locidx]['state']]++;
dfa3c075 992 return ($counter['T'] + $counter['W'] + $counter['U']) / ($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
177b1e9b
DO
993}
994
11df133a 995// Same for row.
9b8174d7 996function getRSUforRow ($rowData)
11df133a 997{
bb26a59e
DO
998 if (!count ($rowData))
999 return 0;
11df133a 1000 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
f81a2012 1001 $total_height = 0;
dfa3c075
DO
1002 foreach (array_keys ($rowData) as $rack_id)
1003 {
61a1d996
DO
1004 $data = spotEntity ('rack', $rack_id);
1005 amplifyCell ($data);
dfa3c075 1006 $total_height += $data['height'];
11df133a
DO
1007 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
1008 for ($locidx = 0; $locidx < 3; $locidx++)
1009 $counter[$data[$unit_no][$locidx]['state']]++;
dfa3c075
DO
1010 }
1011 return ($counter['T'] + $counter['W'] + $counter['U']) / ($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
11df133a
DO
1012}
1013
4d7c0c70
AD
1014// Find any URL in a string and replace it with a clickable link
1015// Adopted from UrlLinker: https://bitbucket.org/kwi/urllinker/src
e6e7d8b3
DO
1016function string_insert_hrefs ($s)
1017{
1018 if (getConfigVar ('DETECT_URLS') != 'yes')
1019 return $s;
e6e7d8b3 1020
4d7c0c70
AD
1021 $rexProtocol = '(https?://)?';
1022 $rexDomain = '(?:[-a-zA-Z0-9]{1,63}\.)+[a-zA-Z][-a-zA-Z0-9]{1,62}';
1023 $rexIp = '(?:[1-9][0-9]{0,2}\.|0\.){3}(?:[1-9][0-9]{0,2}|0)'; // doesn't support IPv6 addresses
1024 $rexPort = '(:[0-9]{1,5})?';
1025 $rexPath = '(/[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]*?)?';
1026 $rexQuery = '(\?[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';
1027 $rexFragment = '(#[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';
1028 $rexUsername = '[^]\\\\\x00-\x20\"(),:-<>[\x7f-\xff]{1,64}';
1029 $rexPassword = $rexUsername; // allow the same characters as in the username
1030 $rexUrl = "$rexProtocol(?:($rexUsername)(:$rexPassword)?@)?($rexDomain|$rexIp)($rexPort$rexPath$rexQuery$rexFragment)";
1031 $rexUrlLinker = "{\\b$rexUrl(?=[?.!,;:\"]?(\s|$))}";
1032
1033 $html = '';
1034 $position = 0;
1035 while (preg_match($rexUrlLinker, $s, $match, PREG_OFFSET_CAPTURE, $position))
1036 {
1037 list($url, $urlPosition) = $match[0];
1038
1039 // Add the text leading up to the URL.
1040 $html .= substr($s, $position, $urlPosition - $position);
1041
1042 $protocol = $match[1][0];
1043 $username = $match[2][0];
1044 $password = $match[3][0];
1045 $domain = $match[4][0];
1046 $afterDomain = $match[5][0]; // everything following the domain
1047 $port = $match[6][0];
1048 $path = $match[7][0];
1049
1050 // Do not permit implicit protocol if a password is specified, as
1051 // this causes too many errors (e.g. "my email:foo@example.org").
1052 if (!$protocol && $password)
1053 {
1054 $html .= htmlspecialchars($username);
1055
1056 // Continue text parsing at the ':' following the "username".
1057 $position = $urlPosition + strlen($username);
1058 continue;
1059 }
1060
1061 if (!$protocol && $username && !$password && !$afterDomain)
1062 {
1063 // Looks like an email address.
1064 $emailUrl = TRUE;
1065 $completeUrl = "mailto:$url";
1066 $linkText = $url;
1067 }
1068 else
1069 {
1070 // Prepend http:// if no protocol specified
1071 $completeUrl = $protocol ? $url : "http://$url";
1072 $linkText = "$protocol$domain$port$path";
1073 }
1074
1075 $linkHtml = '<a href="' . htmlspecialchars($completeUrl) . '">'
1076 . htmlspecialchars($linkText)
1077 . '</a>';
1078
1079 // It's not an e-mail address, provide an additional link with a new window/tab as the target
1080 if (!isset($emailUrl))
1081 $linkHtml .= ' [<a href="' . htmlspecialchars($completeUrl) . '" target="_blank">^</a>]';
1082 unset($emailUrl);
1083
1084 // Add the hyperlink.
1085 $html .= $linkHtml;
1086
1087 // Continue text parsing from after the URL.
1088 $position = $urlPosition + strlen($url);
1089 }
1090
1091 // Add the remainder of the text.
1092 $html .= substr($s, $position);
1093 return $html;
e6e7d8b3
DO
1094}
1095
118e4c38
DO
1096// Parse AUTOPORTS_CONFIG and return a list of generated pairs (port_type, port_name)
1097// for the requested object_type_id.
1098function getAutoPorts ($type_id)
1099{
1100 $ret = array();
1101 $typemap = explode (';', str_replace (' ', '', getConfigVar ('AUTOPORTS_CONFIG')));
1102 foreach ($typemap as $equation)
1103 {
1104 $tmp = explode ('=', $equation);
1105 if (count ($tmp) != 2)
1106 continue;
1107 $objtype_id = $tmp[0];
1108 if ($objtype_id != $type_id)
1109 continue;
1110 $portlist = $tmp[1];
1111 foreach (explode ('+', $portlist) as $product)
1112 {
1113 $tmp = explode ('*', $product);
e24e4549 1114 if (count ($tmp) > 4 || count ($tmp) < 3)
118e4c38 1115 continue;
e24e4549 1116 # format: <number of ports>*<port_type_id>[*<sprintf_name>*<startnumber>]
118e4c38
DO
1117 $nports = $tmp[0];
1118 $port_type = $tmp[1];
1119 $format = $tmp[2];
e24e4549 1120 $startnum = isset ($tmp[3]) ? $tmp[3] : 0;
118e4c38 1121 for ($i = 0; $i < $nports; $i++)
e24e4549 1122 $ret[] = array ('type' => $port_type, 'name' => @sprintf ($format, $i + $startnum));
118e4c38
DO
1123 }
1124 }
1125 return $ret;
1126}
1127
86256b96
DO
1128// Use pre-served trace to traverse the tree, then place given node where it belongs.
1129function pokeNode (&$tree, $trace, $key, $value, $threshold = 0)
1130{
1131 // This function needs the trace to be followed FIFO-way. The fastest
1132 // way to do so is to use array_push() for putting values into the
1133 // list and array_shift() for getting them out. This exposed up to 11%
1134 // performance gain compared to other patterns of array_push/array_unshift/
1135 // array_reverse/array_pop/array_shift conjunction.
1136 $myid = array_shift ($trace);
1137 if (!count ($trace)) // reached the target
9dfd4cc9 1138 {
86256b96
DO
1139 if (!$threshold or ($threshold and $tree[$myid]['kidc'] + 1 < $threshold))
1140 $tree[$myid]['kids'][$key] = $value;
1141 // Reset accumulated records once, when the limit is reached, not each time
1142 // after that.
1143 if (++$tree[$myid]['kidc'] == $threshold)
1144 $tree[$myid]['kids'] = array();
1145 }
1146 else // not yet
1147 {
1148 $self = __FUNCTION__;
1149 $self ($tree[$myid]['kids'], $trace, $key, $value, $threshold);
9dfd4cc9 1150 }
9dfd4cc9
DO
1151}
1152
0ba76ca2
DO
1153// Likewise traverse the tree with the trace and return the final node.
1154function peekNode ($tree, $trace, $target_id)
1155{
1156 $self = __FUNCTION__;
1157 if (NULL === ($next = array_shift ($trace))) // warm
1158 {
1159 foreach ($tree as $node)
f1e27fe5 1160 if (array_key_exists ('id', $node) and $node['id'] == $target_id) // hot
0ba76ca2
DO
1161 return $node;
1162 }
1163 else // cold
1164 {
1165 foreach ($tree as $node)
3e576410 1166 if (array_key_exists ('id', $node) and $node['id'] == $next) // warmer
0ba76ca2
DO
1167 return $self ($node['kids'], $trace, $target_id);
1168 }
99e09736 1169 throw new RackTablesError ('inconsistent tree data', RackTablesError::INTERNAL);
0ba76ca2
DO
1170}
1171
d65353ad
DO
1172// Build a tree from the item list and return it. Input and output data is
1173// indexed by item id (nested items in output are recursively stored in 'kids'
51b6651a
DO
1174// key, which is in turn indexed by id. Functions, which are ready to handle
1175// tree collapsion/expansion themselves, may request non-zero threshold value
1176// for smaller resulting tree.
3fb336f6 1177function treeFromList (&$orig_nodelist, $threshold = 0, $return_main_payload = TRUE)
d65353ad 1178{
86256b96 1179 $tree = array();
3fb336f6 1180 $nodelist = $orig_nodelist;
86256b96
DO
1181 // Array equivalent of traceEntity() function.
1182 $trace = array();
1183 // set kidc and kids only once
1184 foreach (array_keys ($nodelist) as $nodeid)
1185 {
1186 $nodelist[$nodeid]['kidc'] = 0;
1187 $nodelist[$nodeid]['kids'] = array();
1188 }
1189 do
9dfd4cc9 1190 {
86256b96
DO
1191 $nextpass = FALSE;
1192 foreach (array_keys ($nodelist) as $nodeid)
9dfd4cc9 1193 {
86256b96
DO
1194 // When adding a node to the working tree, book another
1195 // iteration, because the new item could make a way for
1196 // others onto the tree. Also remove any item added from
1197 // the input list, so iteration base shrinks.
1198 // First check if we can assign directly.
1199 if ($nodelist[$nodeid]['parent_id'] == NULL)
9dfd4cc9 1200 {
86256b96
DO
1201 $tree[$nodeid] = $nodelist[$nodeid];
1202 $trace[$nodeid] = array(); // Trace to root node is empty
1203 unset ($nodelist[$nodeid]);
1204 $nextpass = TRUE;
9dfd4cc9 1205 }
86256b96
DO
1206 // Now look if it fits somewhere on already built tree.
1207 elseif (isset ($trace[$nodelist[$nodeid]['parent_id']]))
9dfd4cc9 1208 {
86256b96
DO
1209 // Trace to a node is a trace to its parent plus parent id.
1210 $trace[$nodeid] = $trace[$nodelist[$nodeid]['parent_id']];
1211 $trace[$nodeid][] = $nodelist[$nodeid]['parent_id'];
1212 pokeNode ($tree, $trace[$nodeid], $nodeid, $nodelist[$nodeid], $threshold);
1213 // path to any other node is made of all parent nodes plus the added node itself
1214 unset ($nodelist[$nodeid]);
1215 $nextpass = TRUE;
9dfd4cc9
DO
1216 }
1217 }
9dfd4cc9 1218 }
86256b96 1219 while ($nextpass);
3fb336f6 1220 if (!$return_main_payload)
86256b96 1221 return $nodelist;
3fb336f6
DO
1222 // update each input node with its backtrace route
1223 foreach ($trace as $nodeid => $route)
1224 $orig_nodelist[$nodeid]['trace'] = $route;
1225 return $tree;
9dfd4cc9
DO
1226}
1227
49fb1027 1228// Build a tree from the tag list and return everything _except_ the tree.
573214e0
DO
1229// IOW, return taginfo items, which have parent_id set and pointing outside
1230// of the "normal" tree, which originates from the root.
49fb1027
DO
1231function getOrphanedTags ()
1232{
1233 global $taglist;
86256b96 1234 return treeFromList ($taglist, 0, FALSE);
49fb1027
DO
1235}
1236
ba93bd98
DO
1237// Return the list of missing implicit tags.
1238function getImplicitTags ($oldtags)
1239{
3fb336f6
DO
1240 global $taglist;
1241 $tmp = array();
1242 foreach ($oldtags as $taginfo)
1243 $tmp = array_merge ($tmp, $taglist[$taginfo['id']]['trace']);
1244 // don't call array_unique here, it is in the function we will call now
1245 return buildTagChainFromIds ($tmp);
ba93bd98
DO
1246}
1247
6e49bd1f 1248// Minimize the chain: exclude all implicit tags and return the result.
5fd2a004
DO
1249// This function makes use of an external cache with a miss/hit ratio
1250// about 3/7 (ticket:255).
3fb336f6 1251function getExplicitTagsOnly ($chain)
ab379543 1252{
5fd2a004 1253 global $taglist, $tagRelCache;
ab379543 1254 $ret = array();
5fd2a004
DO
1255 foreach (array_keys ($chain) as $keyA) // check each A
1256 {
1257 $tagidA = $chain[$keyA]['id'];
1258 // do not include A in result, if A is seen on the trace of any B!=A
1259 foreach (array_keys ($chain) as $keyB)
1260 {
1261 $tagidB = $chain[$keyB]['id'];
1262 if ($tagidA == $tagidB)
1263 continue;
1264 if (!isset ($tagRelCache[$tagidA][$tagidB]))
0745db4d 1265 $tagRelCache[$tagidA][$tagidB] = in_array ($tagidA, $taglist[$tagidB]['trace']);
5fd2a004
DO
1266 if ($tagRelCache[$tagidA][$tagidB] === TRUE) // A is ancestor of B
1267 continue 2; // skip this A
1268 }
1269 $ret[] = $chain[$keyA];
1270 }
74ccacff
DO
1271 return $ret;
1272}
1273
abef7149
DO
1274// Check, if the given tag is present on the chain (will only work
1275// for regular tags with tag ID set.
1276function tagOnChain ($taginfo, $tagchain)
1277{
1278 if (!isset ($taginfo['id']))
1279 return FALSE;
1280 foreach ($tagchain as $test)
1281 if ($test['id'] == $taginfo['id'])
1282 return TRUE;
1283 return FALSE;
1284}
1285
f8821b96
DO
1286function tagNameOnChain ($tagname, $tagchain)
1287{
1288 foreach ($tagchain as $test)
1289 if ($test['tag'] == $tagname)
1290 return TRUE;
1291 return FALSE;
1292}
1293
abef7149
DO
1294// Return TRUE, if two tags chains differ (order of tags doesn't matter).
1295// Assume, that neither of the lists contains duplicates.
1296// FIXME: a faster, than O(x^2) method is possible for this calculation.
1297function tagChainCmp ($chain1, $chain2)
1298{
1299 if (count ($chain1) != count ($chain2))
1300 return TRUE;
1301 foreach ($chain1 as $taginfo1)
1302 if (!tagOnChain ($taginfo1, $chain2))
1303 return TRUE;
1304 return FALSE;
1305}
1306
0df8c52b 1307function redirectIfNecessary ()
da958e52 1308{
750d26d2
DO
1309 global
1310 $trigger,
1311 $pageno,
1312 $tabno;
a2b16202 1313 @session_start();
fa5b2764
AA
1314 if
1315 (
1316 ! isset ($_REQUEST['tab']) and
1317 isset ($_SESSION['RTLT'][$pageno]) and
1318 getConfigVar ('SHOW_LAST_TAB') == 'yes' and
1319 permitted ($pageno, $_SESSION['RTLT'][$pageno]['tabname']) and
1320 time() - $_SESSION['RTLT'][$pageno]['time'] <= TAB_REMEMBER_TIMEOUT
1321 )
a2b16202 1322 redirectUser (buildRedirectURL ($pageno, $_SESSION['RTLT'][$pageno]['tabname']));
fa5b2764 1323
750d26d2 1324 // check if we accidentaly got on a dynamic tab that shouldn't be shown for this object
3a7bdcc6
DO
1325 if
1326 (
1327 isset ($trigger[$pageno][$tabno]) and
1328 !strlen (call_user_func ($trigger[$pageno][$tabno]))
1329 )
fa5b2764
AA
1330 {
1331 $_SESSION['RTLT'][$pageno]['dont_remember'] = 1;
a2b16202 1332 redirectUser (buildRedirectURL ($pageno, 'default'));
fa5b2764 1333 }
a2b16202
AA
1334 if (isset ($_SESSION['RTLT'][$pageno]['dont_remember']))
1335 unset ($_SESSION['RTLT'][$pageno]['dont_remember']);
1336 // store the last visited tab name
1337 if (isset ($_REQUEST['tab']))
1338 $_SESSION['RTLT'][$pageno] = array ('tabname' => $tabno, 'time' => time());
1339 session_commit(); // if we are continuing to run, unlock session data
0df8c52b
DO
1340}
1341
0c714908
DO
1342function prepareNavigation()
1343{
329ec966
DY
1344 global
1345 $pageno,
1346 $tabno;
329ec966
DY
1347 $pageno = (isset ($_REQUEST['page'])) ? $_REQUEST['page'] : 'index';
1348
0c714908 1349 if (isset ($_REQUEST['tab']))
329ec966 1350 $tabno = $_REQUEST['tab'];
0c714908 1351 else
329ec966 1352 $tabno = 'default';
329ec966
DY
1353}
1354
0df8c52b
DO
1355function fixContext ($target = NULL)
1356{
1357 global
1358 $pageno,
1359 $auto_tags,
1360 $expl_tags,
1361 $impl_tags,
1362 $target_given_tags,
1363 $user_given_tags,
1364 $etype_by_pageno,
1365 $page;
53bae67b 1366
0df8c52b
DO
1367 if ($target !== NULL)
1368 {
1369 $target_given_tags = $target['etags'];
1370 // Don't reset autochain, because auth procedures could push stuff there in.
1371 // Another important point is to ignore 'user' realm, so we don't infuse effective
1372 // context with autotags of the displayed account.
1373 if ($target['realm'] != 'user')
1374 $auto_tags = array_merge ($auto_tags, $target['atags']);
1375 }
1376 elseif (array_key_exists ($pageno, $etype_by_pageno))
53da7dca
DO
1377 {
1378 // Each page listed in the map above requires one uint argument.
0a50efb4 1379 $target_realm = $etype_by_pageno[$pageno];
0cc24e9a 1380 assertUIntArg ($page[$pageno]['bypass']);
0a50efb4 1381 $target_id = $_REQUEST[$page[$pageno]['bypass']];
b135a49d 1382 $target = spotEntity ($target_realm, $target_id);
53da7dca 1383 $target_given_tags = $target['etags'];
0df8c52b 1384 if ($target['realm'] != 'user')
53da7dca
DO
1385 $auto_tags = array_merge ($auto_tags, $target['atags']);
1386 }
4c9b513a
DO
1387 // Explicit and implicit chains should be normally empty at this point, so
1388 // overwrite the contents anyway.
1389 $expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1390 $impl_tags = getImplicitTags ($expl_tags);
da958e52
DO
1391}
1392
75a01117
DO
1393# Merge e/i/a-tags of the given cell structures into current context, when
1394# these aren't there yet.
1395function spreadContext ($extracell)
1396{
1397 global
1398 $auto_tags,
1399 $expl_tags,
1400 $impl_tags,
1401 $target_given_tags,
1402 $user_given_tags;
1403 foreach ($extracell['atags'] as $taginfo)
1404 if (! tagNameOnChain ($taginfo['tag'], $auto_tags))
1405 $auto_tags[] = $taginfo;
1406 $target_given_tags = mergeTagChains ($target_given_tags, $extracell['etags']);
1407 $expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1408 $impl_tags = getImplicitTags ($expl_tags);
1409}
1410
1411# return a structure suitable for feeding into restoreContext()
1412function getContext()
1413{
1414 global
1415 $auto_tags,
1416 $expl_tags,
1417 $impl_tags,
1418 $target_given_tags;
1419 return array
1420 (
1421 'auto_tags' => $auto_tags,
1422 'expl_tags' => $expl_tags,
1423 'impl_tags' => $impl_tags,
1424 'target_given_tags' => $target_given_tags,
1425 );
1426}
1427
1428function restoreContext ($ctx)
1429{
1430 global
1431 $auto_tags,
1432 $expl_tags,
1433 $impl_tags,
1434 $target_given_tags;
1435 $auto_tags = $ctx['auto_tags'];
1436 $expl_tags = $ctx['expl_tags'];
1437 $impl_tags = $ctx['impl_tags'];
1438 $target_given_tags = $ctx['target_given_tags'];
1439}
1440
abef7149
DO
1441// Take a list of user-supplied tag IDs to build a list of valid taginfo
1442// records indexed by tag IDs (tag chain).
6e49bd1f 1443function buildTagChainFromIds ($tagidlist)
ab379543 1444{
20c901a7 1445 global $taglist;
ab379543 1446 $ret = array();
abef7149 1447 foreach (array_unique ($tagidlist) as $tag_id)
ab379543
DO
1448 if (isset ($taglist[$tag_id]))
1449 $ret[] = $taglist[$tag_id];
1450 return $ret;
1451}
1452
7cc02fc1
DO
1453// Process a given tag tree and return only meaningful branches. The resulting
1454// (sub)tree will have refcnt leaves on every last branch.
a26a6ccc 1455function getObjectiveTagTree ($tree, $realm, $preselect)
7cc02fc1 1456{
51b6651a 1457 $self = __FUNCTION__;
7cc02fc1
DO
1458 $ret = array();
1459 foreach ($tree as $taginfo)
1460 {
a26a6ccc
DO
1461 $subsearch = $self ($taginfo['kids'], $realm, $preselect);
1462 // If the current node addresses something, add it to the result
1463 // regardless of how many sub-nodes it features.
1464 if
7cc02fc1 1465 (
a26a6ccc
DO
1466 isset ($taginfo['refcnt'][$realm]) or
1467 count ($subsearch) > 1 or
3fb336f6 1468 in_array ($taginfo['id'], $preselect)
a26a6ccc
DO
1469 )
1470 $ret[] = array
1471 (
1472 'id' => $taginfo['id'],
1473 'tag' => $taginfo['tag'],
1474 'parent_id' => $taginfo['parent_id'],
1475 'refcnt' => $taginfo['refcnt'],
1476 'kids' => $subsearch
1477 );
1478 else
1479 $ret = array_merge ($ret, $subsearch);
7cc02fc1
DO
1480 }
1481 return $ret;
1482}
1483
95857b5c
DO
1484// Preprocess tag tree to get only tags which can effectively reduce given filter result,
1485// than passes shrinked tag tree to getObjectiveTagTree and return its result.
21ee3351 1486// This makes sense only if andor mode is 'and', otherwise function does not modify tree.
95857b5c
DO
1487// 'Given filter' is a pair of $entity_list(filter result) and $preselect(filter data).
1488// 'Effectively' means reduce to non-empty result.
1489function getShrinkedTagTree($entity_list, $realm, $preselect) {
1490 global $tagtree;
3dcf526a 1491 if ($preselect['andor'] != 'and' || empty($entity_list) && $preselect['is_empty'])
95857b5c
DO
1492 return getObjectiveTagTree($tagtree, $realm, $preselect['tagidlist']);
1493
1494 $used_tags = array(); //associative, keys - tag ids, values - taginfos
1495 foreach ($entity_list as $entity)
1496 {
1497 foreach ($entity['etags'] as $etag)
1498 if (! array_key_exists($etag['id'], $used_tags))
1499 $used_tags[$etag['id']] = 1;
1500 else
1501 $used_tags[$etag['id']]++;
1502
1503 foreach ($entity['itags'] as $itag)
1504 if (! array_key_exists($itag['id'], $used_tags))
1505 $used_tags[$itag['id']] = 0;
1506 }
1507
1508 $shrinked_tree = shrinkSubtree($tagtree, $used_tags, $preselect, $realm);
1509 return getObjectiveTagTree($shrinked_tree, $realm, $preselect['tagidlist']);
1510}
1511
1512// deletes item from tag subtree unless it exists in $used_tags and not preselected
1513function shrinkSubtree($tree, $used_tags, $preselect, $realm) {
1514 $self = __FUNCTION__;
1515
1516 foreach($tree as $i => &$item) {
1517 $item['kids'] = $self($item['kids'], $used_tags, $preselect, $realm);
1518 $item['kidc'] = count($item['kids']);
1519 if
1520 (
1521 ! array_key_exists($item['id'], $used_tags) &&
1522 ! in_array($item['id'], $preselect['tagidlist']) &&
1523 ! $item['kidc']
1524 )
1525 unset($tree[$i]);
1526 else {
444953cc
AA
1527 if (isset ($used_tags[$item['id']]) && $used_tags[$item['id']])
1528 $item['refcnt'][$realm] = $used_tags[$item['id']];
1529 else
95857b5c
DO
1530 unset($item['refcnt'][$realm]);
1531 }
1532 }
1533 return $tree;
1534}
1535
7ddb2c05
DO
1536// Get taginfo record by tag name, return NULL, if record doesn't exist.
1537function getTagByName ($target_name)
1538{
1539 global $taglist;
1540 foreach ($taglist as $taginfo)
1541 if ($taginfo['tag'] == $target_name)
1542 return $taginfo;
1543 return NULL;
1544}
1545
fc73c734
DO
1546// Merge two chains, filtering dupes out. Return the resulting superset.
1547function mergeTagChains ($chainA, $chainB)
1548{
1549 // $ret = $chainA;
1550 // Reindex by tag id in any case.
1551 $ret = array();
1552 foreach ($chainA as $tag)
1553 $ret[$tag['id']] = $tag;
1554 foreach ($chainB as $tag)
1555 if (!isset ($ret[$tag['id']]))
1556 $ret[$tag['id']] = $tag;
1557 return $ret;
1558}
1559
201204fb
DO
1560# Return a list consisting of tag ID of the given tree node and IDs of all
1561# nodes it contains.
1562function getTagIDListForNode ($treenode)
1563{
1564 $self = __FUNCTION__;
1565 $ret = array ($treenode['id']);
1566 foreach ($treenode['kids'] as $item)
1567 $ret = array_merge ($ret, $self ($item));
1568 return $ret;
1569}
1570
31c941ec
DO
1571function getCellFilter ()
1572{
7da7450c 1573 global $sic;
eca0114c 1574 global $pageno;
3dcf526a 1575 $andor_used = FALSE;
a2b16202 1576 @session_start();
444953cc 1577 // if the page is submitted we get an andor value so we know they are trying to start a new filter or clearing the existing one.
b48268d8
RF
1578 if(isset($_REQUEST['andor']))
1579 {
3dcf526a 1580 $andor_used = TRUE;
444953cc 1581 unset($_SESSION[$pageno]); // delete saved filter
b8970c7e 1582 }
444953cc
AA
1583 // otherwise inject saved filter to the $_REQUEST and $sic vars
1584 elseif (isset ($_SESSION[$pageno]['filter']) and is_array ($_SESSION[$pageno]['filter']) and getConfigVar ('STATIC_FILTER') == 'yes')
1585 foreach (array('andor', 'cfe', 'cft[]', 'cfp[]', 'nft[]', 'nfp[]') as $param)
1586 {
1587 $param = str_replace ('[]', '', $param, $is_array);
1588 if (! isset ($_REQUEST[$param]) and isset ($_SESSION[$pageno]['filter'][$param]) and (!$is_array or is_array ($_SESSION[$pageno]['filter'][$param])))
1589 {
1590 $_REQUEST[$param] = $_SESSION[$pageno]['filter'][$param];
1591 if (! $is_array)
1592 $sic[$param] = $_REQUEST[$param];
1593 }
1594 }
eca0114c 1595
3d670bba
DO
1596 $ret = array
1597 (
23cdc7e9
DO
1598 'tagidlist' => array(),
1599 'tnamelist' => array(),
1600 'pnamelist' => array(),
444953cc 1601 'negatedlist' => array(),
23cdc7e9 1602 'andor' => '',
3d670bba 1603 'text' => '',
23cdc7e9
DO
1604 'extratext' => '',
1605 'expression' => array(),
a8efc03e 1606 'urlextra' => '', // Just put text here and let makeHref call urlencode().
3dcf526a 1607 'is_empty' => TRUE,
3d670bba 1608 );
23cdc7e9 1609 switch (TRUE)
31c941ec 1610 {
23cdc7e9 1611 case (!isset ($_REQUEST['andor'])):
444953cc 1612 $andor = getConfigVar ('FILTER_DEFAULT_ANDOR');
3d670bba 1613 break;
23cdc7e9
DO
1614 case ($_REQUEST['andor'] == 'and'):
1615 case ($_REQUEST['andor'] == 'or'):
444953cc
AA
1616 $_SESSION[$pageno]['filter']['andor'] = $_REQUEST['andor'];
1617 $ret['andor'] = $andor = $_REQUEST['andor'];
3d670bba
DO
1618 break;
1619 default:
08408472 1620 showWarning ('Invalid and/or switch value in submitted form');
3d670bba 1621 return NULL;
31c941ec 1622 }
23cdc7e9
DO
1623 // Both tags and predicates, which don't exist, should be
1624 // handled somehow. Discard them silently for now.
444953cc
AA
1625 global $taglist, $pTable;
1626 foreach (array ('cft', 'cfp', 'nft', 'nfp') as $param)
1627 if (isset ($_REQUEST[$param]) and is_array ($_REQUEST[$param]))
1628 {
1629 $_SESSION[$pageno]['filter'][$param] = $_REQUEST[$param];
1630 foreach ($_REQUEST[$param] as $req_key)
23cdc7e9 1631 {
444953cc
AA
1632 if (strpos ($param, 'ft') !== FALSE)
1633 {
1634 // param is a taglist
1635 if (! isset ($taglist[$req_key]))
1636 continue;
1637 $ret['tagidlist'][] = $req_key;
1638 $ret['tnamelist'][] = $taglist[$req_key]['tag'];
1639 $text = '{' . $taglist[$req_key]['tag'] . '}';
1640 }
1641 else
1642 {
1643 // param is a predicate list
1644 if (! isset ($pTable[$req_key]))
1645 continue;
1646 $ret['pnamelist'][] = $req_key;
1647 $text = '[' . $req_key . ']';
1648 }
1649 if (strpos ($param, 'nf') === 0)
1650 {
1651 $text = "not $text";
1652 $ret['negatedlist'][] = $req_key;
1653 }
1654 if (! empty ($ret['text']))
1655 {
1656 $andor_used = TRUE;
1657 $ret['text'] .= " $andor ";
1658 }
1659 $ret['text'] .= $text;
1660 $ret['urlextra'] .= '&' . $param . '[]=' . $req_key;
23cdc7e9 1661 }
444953cc 1662 }
7da7450c
DO
1663 // Extra text comes from TEXTAREA and is easily screwed by standard escaping function.
1664 if (isset ($sic['cfe']))
a8efc03e 1665 {
444953cc 1666 $_SESSION[$pageno]['filter']['cfe'] = $sic['cfe'];
7da7450c
DO
1667 // Only consider extra text, when it is a correct RackCode expression.
1668 $parse = spotPayload ($sic['cfe'], 'SYNT_EXPR');
1669 if ($parse['result'] == 'ACK')
1670 {
1671 $ret['extratext'] = trim ($sic['cfe']);
1672 $ret['urlextra'] .= '&cfe=' . $ret['extratext'];
1673 }
a8efc03e 1674 }
23cdc7e9
DO
1675 $finaltext = array();
1676 if (strlen ($ret['text']))
1677 $finaltext[] = '(' . $ret['text'] . ')';
1678 if (strlen ($ret['extratext']))
1679 $finaltext[] = '(' . $ret['extratext'] . ')';
3dcf526a 1680 $andor_used = $andor_used || (count($finaltext) > 1);
444953cc 1681 $finaltext = implode (' ' . $andor . ' ', $finaltext);
23cdc7e9
DO
1682 if (strlen ($finaltext))
1683 {
3dcf526a 1684 $ret['is_empty'] = FALSE;
23cdc7e9
DO
1685 $parse = spotPayload ($finaltext, 'SYNT_EXPR');
1686 $ret['expression'] = $parse['result'] == 'ACK' ? $parse['load'] : NULL;
8c699525
DO
1687 // It's not quite fair enough to put the blame of the whole text onto
1688 // non-empty "extra" portion of it, but it's the only user-generated portion
1689 // of it, thus the most probable cause of parse error.
1690 if (strlen ($ret['extratext']))
1691 $ret['extraclass'] = $parse['result'] == 'ACK' ? 'validation-success' : 'validation-error';
31c941ec 1692 }
3dcf526a
DO
1693 if (! $andor_used)
1694 $ret['andor'] = getConfigVar ('FILTER_DEFAULT_ANDOR');
1695 else
1696 $ret['urlextra'] .= '&andor=' . $ret['andor'];
3d670bba 1697 return $ret;
31c941ec
DO
1698}
1699
a2b16202 1700function buildRedirectURL ($nextpage = NULL, $nexttab = NULL, $moreArgs = array())
53bae67b 1701{
a2b16202
AA
1702 global $page, $pageno, $tabno;
1703 if ($nextpage === NULL)
1704 $nextpage = $pageno;
1705 if ($nexttab === NULL)
1706 $nexttab = $tabno;
1707 $url = "index.php?page=${nextpage}&tab=${nexttab}";
1708 if (isset ($page[$nextpage]['bypass']))
1709 $url .= '&' . $page[$nextpage]['bypass'] . '=' . $_REQUEST[$page[$nextpage]['bypass']];
1710 if (isset ($page[$nextpage]['bypass_tabs']))
1711 foreach ($page[$nextpage]['bypass_tabs'] as $param_name)
fa5b2764 1712 if (isset ($_REQUEST[$param_name]))
a2b16202
AA
1713 $url .= '&' . urlencode ($param_name) . '=' . urlencode ($_REQUEST[$param_name]);
1714
1715 if (count ($moreArgs) > 0)
1716 foreach ($moreArgs as $arg => $value)
1717 if (is_array ($value))
1718 foreach ($value as $v)
1719 $url .= '&' . urlencode ($arg . '[]') . '=' . urlencode ($v);
1720 elseif ($arg != 'module')
1721 $url .= '&' . urlencode ($arg) . '=' . urlencode ($value);
1722 return $url;
1723}
1724
1725function redirectUser ($url)
1726{
1727 global $log_messages;
1728 if (! empty ($log_messages))
1729 {
1730 @session_start();
1731 $_SESSION['log'] = $log_messages;
1732 }
1733 header ("Location: " . $url);
53bae67b
DO
1734 die;
1735}
1736
9f3e5caa
DO
1737function getRackCodeStats ()
1738{
1739 global $rackCode;
914b439b 1740 $defc = $grantc = $modc = 0;
9f3e5caa
DO
1741 foreach ($rackCode as $s)
1742 switch ($s['type'])
1743 {
1744 case 'SYNT_DEFINITION':
1745 $defc++;
1746 break;
1747 case 'SYNT_GRANT':
1748 $grantc++;
1749 break;
914b439b
DO
1750 case 'SYNT_CTXMOD':
1751 $modc++;
1752 break;
9f3e5caa
DO
1753 default:
1754 break;
1755 }
914b439b
DO
1756 $ret = array
1757 (
1758 'Definition sentences' => $defc,
1759 'Grant sentences' => $grantc,
1760 'Context mod sentences' => $modc
1761 );
9f3e5caa
DO
1762 return $ret;
1763}
1764
d5157018
DO
1765function getRackImageWidth ()
1766{
529eac25
DO
1767 global $rtwidth;
1768 return 3 + $rtwidth[0] + $rtwidth[1] + $rtwidth[2] + 3;
d5157018
DO
1769}
1770
1771function getRackImageHeight ($units)
1772{
1773 return 3 + 3 + $units * 2;
1774}
1775
2d318652 1776// Indicate occupation state of each IP address: none, ordinary or problematic.
21ee3351 1777function markupIPAddrList (&$addrlist)
2d318652
DO
1778{
1779 foreach (array_keys ($addrlist) as $ip_bin)
1780 {
d983f70a
DO
1781 $refc = array
1782 (
00c5d8cb
DO
1783 'shared' => 0, // virtual
1784 'virtual' => 0, // loopback
1785 'regular' => 0, // connected host
1786 'router' => 0 // connected gateway
d983f70a
DO
1787 );
1788 foreach ($addrlist[$ip_bin]['allocs'] as $a)
1789 $refc[$a['type']]++;
79ddc250 1790 $nvirtloopback = ($refc['shared'] + $refc['virtual'] + $refc['router'] > 0) ? 1 : 0; // modulus of virtual + shared + router
d983f70a 1791 $nreserved = ($addrlist[$ip_bin]['reserved'] == 'yes') ? 1 : 0; // only one reservation is possible ever
79ddc250
AD
1792 $nrealms = $nreserved + $nvirtloopback + $refc['regular']; // last is connected allocation
1793
2d318652
DO
1794 if ($nrealms == 1)
1795 $addrlist[$ip_bin]['class'] = 'trbusy';
1796 elseif ($nrealms > 1)
1797 $addrlist[$ip_bin]['class'] = 'trerror';
c8020acc 1798 elseif (! empty ($addrlist[$ip_bin]['vslist']) or ! empty ($addrlist[$ip_bin]['rsplist']))
212b565a 1799 $addrlist[$ip_bin]['class'] = 'trbusy';
2d318652
DO
1800 else
1801 $addrlist[$ip_bin]['class'] = '';
1802 }
1803}
1804
21ee3351 1805// Scan the given address list (returned by scanIPv4Space/scanIPv6Space) and return a list of all routers found.
04d619d0
DO
1806function findRouters ($addrlist)
1807{
1808 $ret = array();
1809 foreach ($addrlist as $addr)
1810 foreach ($addr['allocs'] as $alloc)
1811 if ($alloc['type'] == 'router')
1812 $ret[] = array
1813 (
1814 'id' => $alloc['object_id'],
1815 'iface' => $alloc['name'],
1816 'dname' => $alloc['object_name'],
1817 'addr' => $addr['ip']
1818 );
1819 return $ret;
1820}
1821
fb7a4967
DO
1822// Assist in tag chain sorting.
1823function taginfoCmp ($tagA, $tagB)
1824{
1825 return $tagA['ci'] - $tagB['ci'];
1826}
1827
1327d9dd
DO
1828// Compare networks. When sorting a tree, the records on the list will have
1829// distinct base IP addresses.
3444ecf2
DO
1830// "The comparison function must return an integer less than, equal to, or greater
1831// than zero if the first argument is considered to be respectively less than,
1832// equal to, or greater than the second." (c) PHP manual
1327d9dd
DO
1833function IPv4NetworkCmp ($netA, $netB)
1834{
7f68bc8b
DO
1835 // On 64-bit systems this function can be reduced to just this:
1836 if (PHP_INT_SIZE == 8)
1837 return $netA['ip_bin'] - $netB['ip_bin'];
2d75c30b
DO
1838 // There's a problem just substracting one u32 integer from another,
1839 // because the result may happen big enough to become a negative i32
1840 // integer itself (PHP tries to cast everything it sees to signed int)
3444ecf2
DO
1841 // The comparison below must treat positive and negative values of both
1842 // arguments.
1843 // Equal values give instant decision regardless of their [equal] sign.
1844 if ($netA['ip_bin'] == $netB['ip_bin'])
2d75c30b 1845 return 0;
3444ecf2
DO
1846 // Same-signed values compete arithmetically within one of i32 contiguous ranges:
1847 // 0x00000001~0x7fffffff 1~2147483647
1848 // 0 doesn't have any sign, and network 0.0.0.0 isn't allowed
1849 // 0x80000000~0xffffffff -2147483648~-1
1850 $signA = $netA['ip_bin'] / abs ($netA['ip_bin']);
1851 $signB = $netB['ip_bin'] / abs ($netB['ip_bin']);
1852 if ($signA == $signB)
1853 {
1854 if ($netA['ip_bin'] > $netB['ip_bin'])
1855 return 1;
1856 else
1857 return -1;
1858 }
1859 else // With only one of two values being negative, it... wins!
1860 {
1861 if ($netA['ip_bin'] < $netB['ip_bin'])
1862 return 1;
1863 else
1864 return -1;
1865 }
1327d9dd
DO
1866}
1867
21ee3351
AA
1868function IPv6NetworkCmp ($netA, $netB)
1869{
522b6f90 1870 return strcmp ($netA['ip_bin']->getBin(), $netB['ip_bin']->getBin());
21ee3351
AA
1871}
1872
fb7a4967 1873// Modify the given tag tree so, that each level's items are sorted alphabetically.
1327d9dd 1874function sortTree (&$tree, $sortfunc = '')
fb7a4967 1875{
59a83bd8 1876 if (!strlen ($sortfunc))
1327d9dd 1877 return;
51b6651a 1878 $self = __FUNCTION__;
1327d9dd 1879 usort ($tree, $sortfunc);
fb7a4967
DO
1880 // Don't make a mistake of directly iterating over the items of current level, because this way
1881 // the sorting will be performed on a _copy_ if each item, not the item itself.
1882 foreach (array_keys ($tree) as $tagid)
51b6651a 1883 $self ($tree[$tagid]['kids'], $sortfunc);
fb7a4967
DO
1884}
1885
0137d53c
DO
1886function iptree_fill (&$netdata)
1887{
59a83bd8 1888 if (!isset ($netdata['kids']) or !count ($netdata['kids']))
0137d53c 1889 return;
a987ff52 1890 // If we really have nested prefixes, they must fit into the tree.
0137d53c
DO
1891 $worktree = array
1892 (
1893 'ip_bin' => $netdata['ip_bin'],
1894 'mask' => $netdata['mask']
1895 );
1896 foreach ($netdata['kids'] as $pfx)
1897 iptree_embed ($worktree, $pfx);
1898 $netdata['kids'] = iptree_construct ($worktree);
1899 $netdata['kidc'] = count ($netdata['kids']);
1900}
1901
21ee3351
AA
1902function ipv6tree_fill (&$netdata)
1903{
1904 if (!isset ($netdata['kids']) or !count ($netdata['kids']))
1905 return;
1906 // If we really have nested prefixes, they must fit into the tree.
1907 $worktree = array
1908 (
1909 'ip_bin' => $netdata['ip_bin'],
1910 'mask' => $netdata['mask']
1911 );
1912 foreach ($netdata['kids'] as $pfx)
1913 ipv6tree_embed ($worktree, $pfx);
1914 $netdata['kids'] = ipv6tree_construct ($worktree);
1915 $netdata['kidc'] = count ($netdata['kids']);
1916}
1917
0137d53c
DO
1918function iptree_construct ($node)
1919{
1920 $self = __FUNCTION__;
1921
1922 if (!isset ($node['right']))
1923 {
1924 if (!isset ($node['ip']))
1925 {
1926 $node['ip'] = long2ip ($node['ip_bin']);
1927 $node['kids'] = array();
fec0c8da 1928 $node['kidc'] = 0;
0137d53c
DO
1929 $node['name'] = '';
1930 }
1931 return array ($node);
1932 }
1933 else
1934 return array_merge ($self ($node['left']), $self ($node['right']));
1935}
1936
21ee3351
AA
1937function ipv6tree_construct ($node)
1938{
1939 $self = __FUNCTION__;
1940
1941 if (!isset ($node['right']))
1942 {
1943 if (!isset ($node['ip']))
1944 {
1945 $node['ip'] = $node['ip_bin']->format();
1946 $node['kids'] = array();
1947 $node['kidc'] = 0;
1948 $node['name'] = '';
1949 }
1950 return array ($node);
1951 }
1952 else
1953 return array_merge ($self ($node['left']), $self ($node['right']));
1954}
1955
0137d53c
DO
1956function iptree_embed (&$node, $pfx)
1957{
1958 $self = __FUNCTION__;
1959
1960 // hit?
1961 if ($node['ip_bin'] == $pfx['ip_bin'] and $node['mask'] == $pfx['mask'])
1962 {
1963 $node = $pfx;
1964 return;
1965 }
1966 if ($node['mask'] == $pfx['mask'])
3a089a44 1967 throw new RackTablesError ('the recurring loop lost control', RackTablesError::INTERNAL);
0137d53c
DO
1968
1969 // split?
1970 if (!isset ($node['right']))
1971 {
2eb52ed1 1972 // Fill in db_first/db_last to make it possible to run scanIPv4Space() on the node.
a987ff52 1973 $node['left']['mask'] = $node['mask'] + 1;
0137d53c 1974 $node['left']['ip_bin'] = $node['ip_bin'];
2eb52ed1
AA
1975 $node['left']['db_first'] = sprintf ('%u', $node['left']['ip_bin']);
1976 $node['left']['db_last'] = sprintf ('%u', $node['left']['ip_bin'] | binInvMaskFromDec ($node['left']['mask']));
a987ff52
DO
1977
1978 $node['right']['mask'] = $node['mask'] + 1;
0137d53c 1979 $node['right']['ip_bin'] = $node['ip_bin'] + binInvMaskFromDec ($node['mask'] + 1) + 1;
2eb52ed1
AA
1980 $node['right']['db_first'] = sprintf ('%u', $node['right']['ip_bin']);
1981 $node['right']['db_last'] = sprintf ('%u', $node['right']['ip_bin'] | binInvMaskFromDec ($node['right']['mask']));
0137d53c
DO
1982 }
1983
1984 // repeat!
1985 if (($node['left']['ip_bin'] & binMaskFromDec ($node['left']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1986 $self ($node['left'], $pfx);
1987 elseif (($node['right']['ip_bin'] & binMaskFromDec ($node['right']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1988 $self ($node['right'], $pfx);
1989 else
3a089a44 1990 throw new RackTablesError ('cannot decide between left and right', RackTablesError::INTERNAL);
0137d53c
DO
1991}
1992
21ee3351
AA
1993function ipv6tree_embed (&$node, $pfx)
1994{
1995 $self = __FUNCTION__;
1996
1997 // hit?
1998 if ($node['ip_bin'] == $pfx['ip_bin'] and $node['mask'] == $pfx['mask'])
1999 {
2000 $node = $pfx;
2001 return;
2002 }
2003 if ($node['mask'] == $pfx['mask'])
2004 throw new RackTablesError ('the recurring loop lost control', RackTablesError::INTERNAL);
2005
2006 // split?
2007 if (!isset ($node['right']))
2008 {
2009 $node['left']['mask'] = $node['mask'] + 1;
2010 $node['left']['ip_bin'] = $node['ip_bin'];
2eb52ed1
AA
2011 $node['left']['db_first'] = $node['ip_bin']->get_first_subnet_address ($node['mask'] + 1);
2012 $node['left']['db_last'] = $node['ip_bin']->get_last_subnet_address ($node['mask'] + 1);
21ee3351
AA
2013
2014 $node['right']['mask'] = $node['mask'] + 1;
2015 $node['right']['ip_bin'] = $node['ip_bin']->get_last_subnet_address ($node['mask'] + 1)->next();
2eb52ed1
AA
2016 $node['right']['db_first'] = $node['right']['ip_bin'];
2017 $node['right']['db_last'] = $node['right']['ip_bin']->get_last_subnet_address ($node['mask'] + 1);
21ee3351
AA
2018 }
2019
2020 // repeat!
2eb52ed1 2021 if ($node['left']['db_first'] == $pfx['ip_bin']->get_first_subnet_address ($node['left']['mask']))
21ee3351 2022 $self ($node['left'], $pfx);
2eb52ed1 2023 elseif ($node['right']['db_first'] == $pfx['ip_bin']->get_first_subnet_address ($node['left']['mask']))
21ee3351
AA
2024 $self ($node['right'], $pfx);
2025 else
2026 throw new RackTablesError ('cannot decide between left and right', RackTablesError::INTERNAL);
2027}
2028
3b81cb98 2029function treeApplyFunc (&$tree, $func = '', $stopfunc = '')
0137d53c 2030{
59a83bd8 2031 if (!strlen ($func))
0137d53c
DO
2032 return;
2033 $self = __FUNCTION__;
2034 foreach (array_keys ($tree) as $key)
2035 {
2036 $func ($tree[$key]);
59a83bd8 2037 if (strlen ($stopfunc) and $stopfunc ($tree[$key]))
c3315231 2038 continue;
0137d53c
DO
2039 $self ($tree[$key]['kids'], $func);
2040 }
2041}
2042
a987ff52
DO
2043function loadIPv4AddrList (&$netinfo)
2044{
2045 loadOwnIPv4Addresses ($netinfo);
21ee3351 2046 markupIPAddrList ($netinfo['addrlist']);
a987ff52 2047}
b18d26dc 2048
b6b87070 2049function countOwnIPv4Addresses (&$node)
b18d26dc 2050{
737a3f72 2051 $node['addrt'] = 0;
92ee2b01 2052 if (empty ($node['kids']))
737a3f72 2053 $node['addrt'] = binInvMaskFromDec ($node['mask']) + 1;
b18d26dc 2054 else
b6b87070 2055 foreach ($node['kids'] as $nested)
b18d26dc 2056 if (!isset ($nested['id'])) // spare
737a3f72 2057 $node['addrt'] += binInvMaskFromDec ($nested['mask']) + 1;
b6b87070
DO
2058}
2059
c3315231
DO
2060function nodeIsCollapsed ($node)
2061{
2062 return $node['symbol'] == 'node-collapsed';
2063}
2064
21ee3351 2065// implies countOwnIPv4Addresses
b6b87070
DO
2066function loadOwnIPv4Addresses (&$node)
2067{
2068 $toscan = array();
21ee3351 2069 $node['addrt'] = 0;
2eb52ed1 2070 if (!isset ($node['kids']) or !count ($node['kids']))
21ee3351 2071 {
2eb52ed1
AA
2072 $toscan[] = array ('i32_first' => $node['db_first'], 'i32_last' => $node['db_last']);
2073 $node['addrt'] = $node['db_last'] - $node['db_first'] + 1;
21ee3351 2074 }
b6b87070 2075 else
2eb52ed1
AA
2076 {
2077 $node['addrt'] = 0;
b6b87070
DO
2078 foreach ($node['kids'] as $nested)
2079 if (!isset ($nested['id'])) // spare
21ee3351 2080 {
2eb52ed1 2081 $toscan[] = array ('i32_first' => $nested['db_first'], 'i32_last' => $nested['db_last']);
beb2fb65 2082 $node['addrt'] += $nested['db_last'] - $nested['db_first'] + 1;
21ee3351 2083 }
2eb52ed1 2084 }
f7414fa5 2085 $node['addrlist'] = scanIPv4Space ($toscan);
b6b87070 2086 $node['addrc'] = count ($node['addrlist']);
b18d26dc
DO
2087}
2088
21ee3351
AA
2089function loadIPv6AddrList (&$netinfo)
2090{
2091 loadOwnIPv6Addresses ($netinfo);
2092 markupIPAddrList ($netinfo['addrlist']);
2093}
2094
2095function loadOwnIPv6Addresses (&$node)
2096{
2097 $toscan = array();
2098 $node['addrt'] = 0;
2099 if (empty ($node['kids']))
2100 $toscan[] = array ('first' => $node['ip_bin'], 'last' => $node['ip_bin']->get_last_subnet_address ($node['mask']));
2101 else
2102 foreach ($node['kids'] as $nested)
2103 if (!isset ($nested['id'])) // spare
2104 $toscan[] = array ('first' => $nested['ip_bin'], 'last' => $nested['ip_bin']->get_last_subnet_address ($nested['mask']));
2105 $node['addrlist'] = scanIPv6Space ($toscan);
2106 $node['addrc'] = count ($node['addrlist']);
2107}
2108
fec0c8da
DO
2109function prepareIPv4Tree ($netlist, $expanded_id = 0)
2110{
573214e0
DO
2111 // treeFromList() requires parent_id to be correct for an item to get onto the tree,
2112 // so perform necessary pre-processing to make orphans belong to root. This trick
2113 // was earlier performed by getIPv4NetworkList().
2114 $netids = array_keys ($netlist);
2115 foreach ($netids as $cid)
2116 if (!in_array ($netlist[$cid]['parent_id'], $netids))
2117 $netlist[$cid]['parent_id'] = NULL;
fec0c8da
DO
2118 $tree = treeFromList ($netlist); // medium call
2119 sortTree ($tree, 'IPv4NetworkCmp');
fec0c8da 2120 // complement the tree before markup to make the spare networks have "symbol" set
c3315231
DO
2121 treeApplyFunc ($tree, 'iptree_fill');
2122 iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
2123 // count addresses after the markup to skip computation for hidden tree nodes
2124 treeApplyFunc ($tree, 'countOwnIPv4Addresses', 'nodeIsCollapsed');
fec0c8da
DO
2125 return $tree;
2126}
2127
21ee3351
AA
2128function prepareIPv6Tree ($netlist, $expanded_id = 0)
2129{
2130 // treeFromList() requires parent_id to be correct for an item to get onto the tree,
2131 // so perform necessary pre-processing to make orphans belong to root. This trick
2132 // was earlier performed by getIPv4NetworkList().
2133 $netids = array_keys ($netlist);
2134 foreach ($netids as $cid)
2135 if (!in_array ($netlist[$cid]['parent_id'], $netids))
2136 $netlist[$cid]['parent_id'] = NULL;
2137 $tree = treeFromList ($netlist); // medium call
2138 sortTree ($tree, 'IPv6NetworkCmp');
2139 // complement the tree before markup to make the spare networks have "symbol" set
2140 treeApplyFunc ($tree, 'ipv6tree_fill');
2141 iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
2142 return $tree;
2143}
2144
a280c8b4
DO
2145# Traverse IPv4/IPv6 tree and return a list of all networks, which
2146# exist in DB and don't have any sub-networks.
2147function getTerminalNetworks ($tree)
2148{
2149 $self = __FUNCTION__;
2150 $ret = array();
2151 foreach ($tree as $node)
2152 if ($node['kidc'] == 0 and isset ($node['realm']))
2153 $ret[] = $node;
2154 else
2155 $ret = array_merge ($ret, $self ($node['kids']));
2156 return $ret;
2157}
2158
fec0c8da
DO
2159// Check all items of the tree recursively, until the requested target id is
2160// found. Mark all items leading to this item as "expanded", collapsing all
2161// the rest, which exceed the given threshold (if the threshold is given).
2162function iptree_markup_collapsion (&$tree, $threshold = 1024, $target = 0)
2163{
2164 $self = __FUNCTION__;
2165 $ret = FALSE;
2166 foreach (array_keys ($tree) as $key)
2167 {
5388794d 2168 $here = ($target === 'ALL' or ($target > 0 and isset ($tree[$key]['id']) and $tree[$key]['id'] == $target));
fec0c8da
DO
2169 $below = $self ($tree[$key]['kids'], $threshold, $target);
2170 if (!$tree[$key]['kidc']) // terminal node
2171 $tree[$key]['symbol'] = 'spacer';
2172 elseif ($tree[$key]['kidc'] < $threshold)
2173 $tree[$key]['symbol'] = 'node-expanded-static';
2174 elseif ($here or $below)
2175 $tree[$key]['symbol'] = 'node-expanded';
2176 else
2177 $tree[$key]['symbol'] = 'node-collapsed';
2178 $ret = ($ret or $here or $below); // parentheses are necessary for this to be computed correctly
2179 }
2180 return $ret;
2181}
2182
e1ae3fb4
AD
2183// Convert entity name to human-readable value
2184function formatEntityName ($name) {
2185 switch ($name)
2186 {
2187 case 'ipv4net':
2188 return 'IPv4 Network';
7fa4b0e4
AA
2189 case 'ipv6net':
2190 return 'IPv6 Network';
e1ae3fb4
AD
2191 case 'ipv4rspool':
2192 return 'IPv4 RS Pool';
2193 case 'ipv4vs':
2194 return 'IPv4 Virtual Service';
2195 case 'object':
2196 return 'Object';
2197 case 'rack':
2198 return 'Rack';
2199 case 'user':
2200 return 'User';
2201 }
2202 return 'invalid';
2203}
2204
8bc5d1e4
DO
2205// Display hrefs for all of a file's parents. If scissors are requested,
2206// prepend cutting button to each of them.
2207function serializeFileLinks ($links, $scissors = FALSE)
e1ae3fb4 2208{
e1ae3fb4
AD
2209 $comma = '';
2210 $ret = '';
2211 foreach ($links as $link_id => $li)
2212 {
2213 switch ($li['entity_type'])
2214 {
2215 case 'ipv4net':
2216 $params = "page=ipv4net&id=";
2217 break;
7fa4b0e4
AA
2218 case 'ipv6net':
2219 $params = "page=ipv6net&id=";
2220 break;
e1ae3fb4
AD
2221 case 'ipv4rspool':
2222 $params = "page=ipv4rspool&pool_id=";
2223 break;
2224 case 'ipv4vs':
2225 $params = "page=ipv4vs&vs_id=";
2226 break;
2227 case 'object':
2228 $params = "page=object&object_id=";
2229 break;
2230 case 'rack':
2231 $params = "page=rack&rack_id=";
2232 break;
2233 case 'user':
2234 $params = "page=user&user_id=";
2235 break;
2236 }
8bc5d1e4
DO
2237 $ret .= $comma;
2238 if ($scissors)
2239 {
2240 $ret .= "<a href='" . makeHrefProcess(array('op'=>'unlinkFile', 'link_id'=>$link_id)) . "'";
2241 $ret .= getImageHREF ('cut') . '</a> ';
2242 }
790a60e8 2243 $ret .= sprintf("<a href='index.php?%s%s'>%s</a>", $params, $li['entity_id'], $li['name']);
8bc5d1e4 2244 $comma = '<br>';
e1ae3fb4 2245 }
e1ae3fb4
AD
2246 return $ret;
2247}
2248
2249// Convert filesize to appropriate unit and make it human-readable
2250function formatFileSize ($bytes) {
2251 // bytes
2252 if($bytes < 1024) // bytes
2253 return "${bytes} bytes";
2254
2255 // kilobytes
2256 if ($bytes < 1024000)
2257 return sprintf ("%.1fk", round (($bytes / 1024), 1));
2258
2259 // megabytes
2260 return sprintf ("%.1f MB", round (($bytes / 1024000), 1));
2261}
2262
2263// Reverse of formatFileSize, it converts human-readable value to bytes
2264function convertToBytes ($value) {
2265 $value = trim($value);
2266 $last = strtolower($value[strlen($value)-1]);
2267 switch ($last)
2268 {
2269 case 'g':
2270 $value *= 1024;
2271 case 'm':
2272 $value *= 1024;
2273 case 'k':
2274 $value *= 1024;
2275 }
2276
2277 return $value;
2278}
2279
4fbb5a00
DY
2280function ip_quad2long ($ip)
2281{
2282 return sprintf("%u", ip2long($ip));
2283}
2284
2285function ip_long2quad ($quad)
2286{
2287 return long2ip($quad);
2288}
2289
36ef72d9 2290// make "A" HTML element
3b3c5e21
DO
2291function mkA ($text, $nextpage, $bypass = NULL, $nexttab = NULL)
2292{
2293 global $page, $tab;
ae61a274 2294 if ($text == '')
3b3c5e21 2295 throw new InvalidArgException ('text', $text);
ae61a274
DO
2296 if (! array_key_exists ($nextpage, $page))
2297 throw new InvalidArgException ('nextpage', $nextpage, 'not found');
3b3c5e21
DO
2298 $args = array ('page' => $nextpage);
2299 if ($nexttab !== NULL)
2300 {
ae61a274
DO
2301 if (! array_key_exists ($nexttab, $tab[$nextpage]))
2302 throw new InvalidArgException ('nexttab', $nexttab, 'not found');
3b3c5e21
DO
2303 $args['tab'] = $nexttab;
2304 }
2305 if (array_key_exists ('bypass', $page[$nextpage]))
2306 {
2307 if ($bypass === NULL)
ae61a274 2308 throw new InvalidArgException ('bypass', '(NULL)');
3b3c5e21
DO
2309 $args[$page[$nextpage]['bypass']] = $bypass;
2310 }
2311 return '<a href="' . makeHref ($args) . '">' . $text . '</a>';
2312}
2313
36ef72d9 2314// make "HREF" HTML attribute
ae61a274 2315function makeHref ($params = array())
4fbb5a00 2316{
ae61a274
DO
2317 $tmp = array();
2318 foreach ($params as $key => $value)
3bbf8c03
AA
2319 {
2320 if (is_array ($value))
2321 $key .= "[]";
2322 else
2323 $value = array ($value);
73348a60
AA
2324 if (!count ($value))
2325 $tmp[] = urlencode ($key) . '=';
2326 else
2327 foreach ($value as $sub_value)
2328 $tmp[] = urlencode ($key) . '=' . urlencode ($sub_value);
3bbf8c03 2329 }
ae61a274 2330 return 'index.php?' . implode ('&', $tmp);
4fbb5a00
DY
2331}
2332
ae61a274 2333function makeHrefProcess ($params = array())
4fbb5a00 2334{
aa017c43 2335 global $pageno, $tabno, $page;
ae61a274
DO
2336 $tmp = array();
2337 if (! array_key_exists ('page', $params))
9f14a7ef 2338 $params['page'] = $pageno;
ae61a274 2339 if (! array_key_exists ('tab', $params))
9f14a7ef 2340 $params['tab'] = $tabno;
aa017c43
AA
2341 if (isset ($page[$pageno]['bypass']))
2342 {
2343 $bypass = $page[$pageno]['bypass'];
2344 if (! array_key_exists ($bypass, $params) and array_key_exists ($bypass, $_REQUEST))
2345 $params[$bypass] = $_REQUEST[$bypass];
2346 }
ae61a274
DO
2347 foreach ($params as $key => $value)
2348 $tmp[] = urlencode ($key) . '=' . urlencode ($value);
2349 return '?module=redirect&' . implode ('&', $tmp);
4fbb5a00
DY
2350}
2351
39106006 2352function makeHrefForHelper ($helper_name, $params = array())
4fbb5a00 2353{
e0ce8064 2354 $ret = '?module=popup&helper=' . $helper_name;
4fbb5a00 2355 foreach($params as $key=>$value)
39106006 2356 $ret .= '&'.urlencode($key).'='.urlencode($value);
4fbb5a00
DY
2357 return $ret;
2358}
2359
f3d274bf
DO
2360// Process the given list of records to build data suitable for printNiftySelect()
2361// (like it was formerly executed by printSelect()). Screen out vendors according
2362// to VENDOR_SIEVE, if object type ID is provided. However, the OPTGROUP with already
2363// selected OPTION is protected from being screened.
2364function cookOptgroups ($recordList, $object_type_id = 0, $existing_value = 0)
2365{
2366 $ret = array();
2367 // Always keep "other" OPTGROUP at the SELECT bottom.
2368 $therest = array();
2369 foreach ($recordList as $dict_key => $dict_value)
2370 if (strpos ($dict_value, '%GSKIP%') !== FALSE)
2371 {
2372 $tmp = explode ('%GSKIP%', $dict_value, 2);
2373 $ret[$tmp[0]][$dict_key] = $tmp[1];
2374 }
2375 elseif (strpos ($dict_value, '%GPASS%') !== FALSE)
2376 {
2377 $tmp = explode ('%GPASS%', $dict_value, 2);
2378 $ret[$tmp[0]][$dict_key] = $tmp[1];
2379 }
2380 else
2381 $therest[$dict_key] = $dict_value;
2382 if ($object_type_id != 0)
2383 {
2384 $screenlist = array();
2385 foreach (explode (';', getConfigVar ('VENDOR_SIEVE')) as $sieve)
eea3ca5e 2386 if (preg_match ("/^([^@]+)(@${object_type_id})?\$/", trim ($sieve), $regs)){
f3d274bf 2387 $screenlist[] = $regs[1];
eea3ca5e 2388 }
f3d274bf
DO
2389 foreach (array_keys ($ret) as $vendor)
2390 if (in_array ($vendor, $screenlist))
2391 {
2392 $ok_to_screen = TRUE;
2393 if ($existing_value)
2394 foreach (array_keys ($ret[$vendor]) as $recordkey)
2395 if ($recordkey == $existing_value)
2396 {
2397 $ok_to_screen = FALSE;
2398 break;
2399 }
2400 if ($ok_to_screen)
2401 unset ($ret[$vendor]);
2402 }
2403 }
2404 $ret['other'] = $therest;
2405 return $ret;
2406}
2407
9dd73255
DO
2408function unix2dos ($text)
2409{
2410 return str_replace ("\n", "\r\n", $text);
2411}
2412
a0527aef 2413function buildPredicateTable ($parsetree)
2c21a10c 2414{
a0527aef
DO
2415 $ret = array();
2416 foreach ($parsetree as $sentence)
72e8baf6 2417 if ($sentence['type'] == 'SYNT_DEFINITION')
a0527aef 2418 $ret[$sentence['term']] = $sentence['definition'];
72e8baf6
DO
2419 // Now we have predicate table filled in with the latest definitions of each
2420 // particular predicate met. This isn't as chik, as on-the-fly predicate
2421 // overloading during allow/deny scan, but quite sufficient for this task.
a0527aef
DO
2422 return $ret;
2423}
2424
2425// Take a list of records and filter against given RackCode expression. Return
2426// the original list intact, if there was no filter requested, but return an
2427// empty list, if there was an error.
573214e0 2428function filterCellList ($list_in, $expression = array())
d08d766d
DO
2429{
2430 if ($expression === NULL)
2431 return array();
2432 if (!count ($expression))
2433 return $list_in;
d08d766d
DO
2434 $list_out = array();
2435 foreach ($list_in as $item_key => $item_value)
573214e0 2436 if (TRUE === judgeCell ($item_value, $expression))
d08d766d
DO
2437 $list_out[$item_key] = $item_value;
2438 return $list_out;
2439}
2440
ef0503fc
DO
2441function eval_expression ($expr, $tagchain, $ptable, $silent = FALSE)
2442{
2443 $self = __FUNCTION__;
2444 switch ($expr['type'])
2445 {
2446 // Return true, if given tag is present on the tag chain.
2447 case 'LEX_TAG':
2448 case 'LEX_AUTOTAG':
2449 foreach ($tagchain as $tagInfo)
2450 if ($expr['load'] == $tagInfo['tag'])
2451 return TRUE;
2452 return FALSE;
2453 case 'LEX_PREDICATE': // Find given predicate in the symbol table and evaluate it.
2454 $pname = $expr['load'];
2455 if (!isset ($ptable[$pname]))
2456 {
2457 if (!$silent)
08408472 2458 showWarning ("Predicate '${pname}' is referenced before declaration");
ef0503fc
DO
2459 return NULL;
2460 }
2461 return $self ($ptable[$pname], $tagchain, $ptable);
2462 case 'LEX_TRUE':
2463 return TRUE;
2464 case 'LEX_FALSE':
2465 return FALSE;
2466 case 'SYNT_NOT_EXPR':
2467 $tmp = $self ($expr['load'], $tagchain, $ptable);
2468 if ($tmp === TRUE)
2469 return FALSE;
2470 elseif ($tmp === FALSE)
2471 return TRUE;
2472 else
2473 return $tmp;
2474 case 'SYNT_AND_EXPR': // binary AND
2475 if (FALSE == $self ($expr['left'], $tagchain, $ptable))
2476 return FALSE; // early failure
2477 return $self ($expr['right'], $tagchain, $ptable);
2478 case 'SYNT_EXPR': // binary OR
2479 if (TRUE == $self ($expr['left'], $tagchain, $ptable))
2480 return TRUE; // early success
2481 return $self ($expr['right'], $tagchain, $ptable);
2482 default:
2483 if (!$silent)
08408472 2484 showWarning ("Evaluation error, cannot process expression type '${expr['type']}'");
ef0503fc
DO
2485 return NULL;
2486 break;
2487 }
2488}
2489
212c9d8a 2490// Tell, if the given expression is true for the given entity. Take complete record on input.
573214e0 2491function judgeCell ($cell, $expression)
d08d766d
DO
2492{
2493 global $pTable;
2494 return eval_expression
2495 (
2496 $expression,
2497 array_merge
2498 (
573214e0
DO
2499 $cell['etags'],
2500 $cell['itags'],
2501 $cell['atags']
d08d766d
DO
2502 ),
2503 $pTable,
2504 TRUE
2505 );
2506}
2507
9d7e7b8b
AA
2508function judgeContext ($expression)
2509{
2510 global $pTable, $expl_tags, $impl_tags, $auto_tags;
2511 return eval_expression
2512 (
2513 $expression,
2514 array_merge
2515 (
2516 $expl_tags,
2517 $impl_tags,
2518 $auto_tags
2519 ),
2520 $pTable,
2521 TRUE
2522 );
2523}
2524
7ddbcf59 2525// Tell, if a constraint from config option permits given record.
99446891 2526// An undefined $cell means current context.
212c9d8a 2527function considerConfiguredConstraint ($cell, $varname)
c6bc0ac5 2528{
7ddbcf59 2529 if (!strlen (getConfigVar ($varname)))
c6bc0ac5 2530 return TRUE; // no restriction
7ddbcf59
DO
2531 global $parseCache;
2532 if (!isset ($parseCache[$varname]))
2533 // getConfigVar() doesn't re-read the value from DB because of its
2534 // own cache, so there is no race condition here between two calls.
2535 $parseCache[$varname] = spotPayload (getConfigVar ($varname), 'SYNT_EXPR');
2536 if ($parseCache[$varname]['result'] != 'ACK')
2537 return FALSE; // constraint set, but cannot be used due to compilation error
99446891
AA
2538 if (isset ($cell))
2539 return judgeCell ($cell, $parseCache[$varname]['load']);
2540 else
2541 return judgeContext ($parseCache[$varname]['load']);
c6bc0ac5
DO
2542}
2543
682e77d1
DO
2544// Tell, if the given arbitrary RackCode text addresses the given record
2545// (an empty text matches any record).
68292868 2546// An undefined $cell means current context.
682e77d1
DO
2547function considerGivenConstraint ($cell, $filtertext)
2548{
2549 if ($filtertext == '')
2550 return TRUE;
2551 $parse = spotPayload ($filtertext, 'SYNT_EXPR');
2552 if ($parse['result'] != 'ACK')
2553 throw new InvalidRequestArgException ('filtertext', $filtertext, 'RackCode parsing error');
68292868
AA
2554 if (isset ($cell))
2555 return judgeCell ($cell, $parse['load']);
2556 else
2557 return judgeContext ($parse['load']);
682e77d1
DO
2558}
2559
17112e81
DO
2560// Return list of records in the given realm, which conform to
2561// the given RackCode expression. If the realm is unknown or text
2562// doesn't validate as a RackCode expression, return NULL.
2563// Otherwise (successful scan) return a list of all matched
2564// records, even if the list is empty (array() !== NULL). If the
2565// text is an empty string, return all found records in the given
2566// realm.
2567function scanRealmByText ($realm = NULL, $ftext = '')
2568{
2569 switch ($realm)
2570 {
2571 case 'object':
95e4b0f5 2572 case 'rack':
17112e81
DO
2573 case 'user':
2574 case 'ipv4net':
7fa4b0e4 2575 case 'ipv6net':
17112e81 2576 case 'file':
39776127 2577 case 'ipv4vs':
41780975 2578 case 'ipv4rspool':
883bd06c 2579 case 'vst':
17112e81
DO
2580 if (!strlen ($ftext = trim ($ftext)))
2581 $fexpr = array();
2582 else
2583 {
2584 $fparse = spotPayload ($ftext, 'SYNT_EXPR');
2585 if ($fparse['result'] != 'ACK')
2586 return NULL;
2587 $fexpr = $fparse['load'];
2588 }
2589 return filterCellList (listCells ($realm), $fexpr);
2590 default:
c5f84f48 2591 throw new InvalidArgException ('$realm', $realm);
17112e81
DO
2592 }
2593
2594}
39776127 2595
cc3d6915
DO
2596function getVSTOptions()
2597{
2598 $ret = array();
2599 foreach (listCells ('vst') as $vst)
2600 $ret[$vst['id']] = niftyString ($vst['description'], 30, FALSE);
2601 return $ret;
2602}
2603
c7e370ed
DO
2604# Return a 2-dimensional array in the format understood by getNiftySelect(),
2605# which would contain all VLANs of all VLAN domains except domains, which IDs
2606# are present in $except_domains argument.
2607function getAllVLANOptions ($except_domains = array())
52e38709 2608{
c7e370ed
DO
2609 $ret = array();
2610 foreach (getVLANDomainStats() as $domain)
2611 if (! in_array ($domain['id'], $except_domains))
2612 foreach (getDomainVLANs ($domain['id']) as $vlan)
2613 $ret[$domain['description']]["${domain['id']}-${vlan['vlan_id']}"] =
2614 "${vlan['vlan_id']} (${vlan['netc']}) ${vlan['vlan_descr']}";
52e38709
DO
2615 return $ret;
2616}
2617
56a797ef
DO
2618// Let's have this debug helper here to enable debugging of process.php w/o interface.php.
2619function dump ($var)
2620{
2621 echo '<div align=left><pre>';
2622 print_r ($var);
2623 echo '</pre></div>';
2624}
2625
9e51318b
DO
2626function getTagChart ($limit = 0, $realm = 'total', $special_tags = array())
2627{
2628 global $taglist;
2629 // first build top-N chart...
2630 $toplist = array();
2631 foreach ($taglist as $taginfo)
2632 if (isset ($taginfo['refcnt'][$realm]))
2633 $toplist[$taginfo['id']] = $taginfo['refcnt'][$realm];
2634 arsort ($toplist, SORT_NUMERIC);
2635 $ret = array();
2636 $done = 0;
2637 foreach (array_keys ($toplist) as $tag_id)
2638 {
2639 $ret[$tag_id] = $taglist[$tag_id];
2640 if (++$done == $limit)
2641 break;
2642 }
2643 // ...then make sure, that every item of the special list is shown
2644 // (using the same sort order)
2645 $extra = array();
2646 foreach ($special_tags as $taginfo)
2647 if (!array_key_exists ($taginfo['id'], $ret))
2648 $extra[$taginfo['id']] = $taglist[$taginfo['id']]['refcnt'][$realm];
2649 arsort ($extra, SORT_NUMERIC);
2650 foreach (array_keys ($extra) as $tag_id)
2651 $ret[] = $taglist[$tag_id];
2652 return $ret;
2653}
2654
7fa7047a
DO
2655function decodeObjectType ($objtype_id, $style = 'r')
2656{
2657 static $types = array();
2658 if (!count ($types))
2659 $types = array
2660 (
2661 'r' => readChapter (CHAP_OBJTYPE),
2662 'a' => readChapter (CHAP_OBJTYPE, 'a'),
2663 'o' => readChapter (CHAP_OBJTYPE, 'o')
2664 );
2665 return $types[$style][$objtype_id];
2666}
2667
0df8c52b
DO
2668function isolatedPermission ($p, $t, $cell)
2669{
2670 // This function is called from both "file" page and a number of other pages,
2671 // which have already fixed security context and authorized the user for it.
2672 // OTOH, it is necessary here to authorize against the current file, which
2673 // means saving the current context and building a new one.
2674 global
2675 $expl_tags,
2676 $impl_tags,
2677 $target_given_tags,
2678 $auto_tags;
2679 // push current context
2680 $orig_expl_tags = $expl_tags;
2681 $orig_impl_tags = $impl_tags;
2682 $orig_target_given_tags = $target_given_tags;
2683 $orig_auto_tags = $auto_tags;
2684 // retarget
2685 fixContext ($cell);
2686 // remember decision
2687 $ret = permitted ($p, $t);
2688 // pop context
2689 $expl_tags = $orig_expl_tags;
2690 $impl_tags = $orig_impl_tags;
2691 $target_given_tags = $orig_target_given_tags;
2692 $auto_tags = $orig_auto_tags;
2693 return $ret;
2694}
2695
3153a326
DO
2696function getPortListPrefs()
2697{
2698 $ret = array();
2699 if (0 >= ($ret['iif_pick'] = getConfigVar ('DEFAULT_PORT_IIF_ID')))
2700 $ret['iif_pick'] = 1;
2701 $ret['oif_picks'] = array();
2702 foreach (explode (';', getConfigVar ('DEFAULT_PORT_OIF_IDS')) as $tmp)
2703 {
2704 $tmp = explode ('=', trim ($tmp));
2705 if (count ($tmp) == 2 and $tmp[0] > 0 and $tmp[1] > 0)
2706 $ret['oif_picks'][$tmp[0]] = $tmp[1];
2707 }
2708 // enforce default value
2709 if (!array_key_exists (1, $ret['oif_picks']))
2710 $ret['oif_picks'][1] = 24;
2711 $ret['selected'] = $ret['iif_pick'] . '-' . $ret['oif_picks'][$ret['iif_pick']];
2712 return $ret;
2713}
2714
2dfa1b73
DO
2715// Return data for printNiftySelect() with port type options. All OIF options
2716// for the default IIF will be shown, but only the default OIFs will be present
2717// for each other IIFs. IIFs, for which there is no default OIF, will not
2718// be listed.
2719// This SELECT will be used for the "add new port" form.
2720function getNewPortTypeOptions()
2721{
2722 $ret = array();
2723 $prefs = getPortListPrefs();
2724 foreach (getPortInterfaceCompat() as $row)
2725 {
2726 if ($row['iif_id'] == $prefs['iif_pick'])
2727 $optgroup = $row['iif_name'];
2728 elseif (array_key_exists ($row['iif_id'], $prefs['oif_picks']) and $prefs['oif_picks'][$row['iif_id']] == $row['oif_id'])
2729 $optgroup = 'other';
2730 else
2731 continue;
2732 if (!array_key_exists ($optgroup, $ret))
2733 $ret[$optgroup] = array();
2734 $ret[$optgroup][$row['iif_id'] . '-' . $row['oif_id']] = $row['oif_name'];
2735 }
2736 return $ret;
2737}
2738
8198f2c6
DO
2739// Return a serialized version of VLAN configuration for a port.
2740// If a native VLAN is defined, print it first. All other VLANs
d0dadd80
DO
2741// are tagged and are listed after a plus sign. When no configuration
2742// is set for a port, return "default" string.
de1e0987 2743function serializeVLANPack ($vlanport)
8198f2c6 2744{
4f860864
DO
2745 if (!array_key_exists ('mode', $vlanport))
2746 return 'error';
2747 switch ($vlanport['mode'])
2748 {
2749 case 'none':
2750 return 'none';
2751 case 'access':
2752 $ret = 'A';
2753 break;
2754 case 'trunk':
2755 $ret = 'T';
2756 break;
36a70b71
DO
2757 case 'uplink':
2758 $ret = 'U';
2759 break;
2760 case 'downlink':
2761 $ret = 'D';
2762 break;
4f860864
DO
2763 default:
2764 return 'error';
2765 }
0ab9ec25
AA
2766 if ($vlanport['native'])
2767 $ret .= $vlanport['native'];
e55917ed
AA
2768 $tagged_bits = groupIntsToRanges ($vlanport['allowed'], $vlanport['native']);
2769 if (count ($tagged_bits))
de1e0987 2770 $ret .= '+' . implode (', ', $tagged_bits);
e55917ed
AA
2771 return strlen ($ret) ? $ret : 'default';
2772}
2773
2774function groupIntsToRanges ($list, $exclude_value = NULL)
2775{
2776 $result = array();
2777 sort ($list);
e9be55de 2778 $id_from = $id_to = 0;
e55917ed
AA
2779 $list[] = -1;
2780 foreach ($list as $next_id)
2781 if (!isset ($exclude_value) or $next_id != $exclude_value)
2782 if ($id_to && $next_id == $id_to + 1)
2783 $id_to = $next_id; // merge
2784 else
e9be55de 2785 {
e55917ed
AA
2786 if ($id_to)
2787 $result[] = $id_from == $id_to ? $id_from : "${id_from}-${id_to}"; // flush
2788 $id_from = $id_to = $next_id; // start next pair
e9be55de 2789 }
e55917ed 2790 return $result;
8198f2c6
DO
2791}
2792
8846b060
DO
2793// Decode VLAN compound key (which is a string formatted DOMAINID-VLANID) and
2794// return the numbers as an array of two.
2795function decodeVLANCK ($string)
2796{
2797 $matches = array();
2798 if (1 != preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $string, $matches))
2799 throw new InvalidArgException ('VLAN compound key', $string);
2800 return array ($matches[1], $matches[2]);
2801}
2802
ce85f5c8
DO
2803// Return VLAN name formatted for HTML output (note, that input
2804// argument comes from database unescaped).
a72aa89f
DO
2805function formatVLANName ($vlaninfo, $context = 'markup long')
2806{
2807 switch ($context)
2808 {
2809 case 'option':
2810 $ret = $vlaninfo['vlan_id'];
2811 if ($vlaninfo['vlan_descr'] != '')
2812 $ret .= ' ' . niftyString ($vlaninfo['vlan_descr']);
2813 return $ret;
2814 case 'label':
2815 $ret = $vlaninfo['vlan_id'];
2816 if ($vlaninfo['vlan_descr'] != '')
2817 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2818 return $ret;
0812b506
DO
2819 case 'plain long':
2820 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2821 if ($vlaninfo['vlan_descr'] != '')
1f54e1ba
DO
2822 $ret .= ' (' . niftyString ($vlaninfo['vlan_descr'], 20, FALSE) . ')';
2823 return $ret;
2824 case 'hyperlink':
2825 $ret = '<a href="';
2826 $ret .= makeHref (array ('page' => 'vlan', 'vlan_ck' => $vlaninfo['domain_id'] . '-' . $vlaninfo['vlan_id']));
2827 $ret .= '">' . formatVLANName ($vlaninfo, 'markup long') . '</a>';
0812b506 2828 return $ret;
a72aa89f
DO
2829 case 'markup long':
2830 default:
2831 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2832 $ret .= ' @' . niftyString ($vlaninfo['domain_descr']);
2833 if ($vlaninfo['vlan_descr'] != '')
2834 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2835 return $ret;
2836 }
ce85f5c8
DO
2837}
2838
e9d357e1
DO
2839// map interface name
2840function ios12ShortenIfName ($ifname)
2841{
680c6167
AA
2842 if (preg_match ('@^eth-trunk(\d+)$@i', $ifname, $m))
2843 return "Eth-Trunk${m[1]}";
20cd20ad 2844 $ifname = preg_replace ('@^(?:[Ee]thernet|Eth)(.+)$@', 'e\\1', $ifname);
e9d357e1 2845 $ifname = preg_replace ('@^FastEthernet(.+)$@', 'fa\\1', $ifname);
65e557dd 2846 $ifname = preg_replace ('@^(?:GigabitEthernet|GE)(.+)$@', 'gi\\1', $ifname);
e9d357e1 2847 $ifname = preg_replace ('@^TenGigabitEthernet(.+)$@', 'te\\1', $ifname);
fa9e68cd 2848 $ifname = preg_replace ('@^[Pp]ort-channel(.+)$@', 'po\\1', $ifname);
65e557dd 2849 $ifname = preg_replace ('@^(?:XGigabitEthernet|XGE)(.+)$@', 'xg\\1', $ifname);
1ebbf889 2850 $ifname = strtolower ($ifname);
e9d357e1
DO
2851 return $ifname;
2852}
2853
f10dd5cc
DO
2854function iosParseVLANString ($string)
2855{
2856 $ret = array();
2857 foreach (explode (',', $string) as $item)
2858 {
2859 $matches = array();
735e323f 2860 $item = trim ($item, ' ');
f10dd5cc
DO
2861 if (preg_match ('/^([[:digit:]]+)$/', $item, $matches))
2862 $ret[] = $matches[1];
2863 elseif (preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $item, $matches))
2864 $ret = array_merge ($ret, range ($matches[1], $matches[2]));
2865 }
2866 return $ret;
2867}
2868
bb35bb93
DO
2869// Scan given array and return the key, which addresses the first item
2870// with requested column set to given value (or NULL if there is none such).
19350222
DO
2871// Note that 0 and NULL mean completely different things and thus
2872// require strict checking (=== and !===).
dbc00990
DO
2873function scanArrayForItem ($table, $scan_column, $scan_value)
2874{
2875 foreach ($table as $key => $row)
2876 if ($row[$scan_column] == $scan_value)
2877 return $key;
2878 return NULL;
2879}
2880
66658512
DO
2881// Return TRUE, if every value of A1 is present in A2 and vice versa,
2882// regardless of each array's sort order and indexing.
2883function array_values_same ($a1, $a2)
2884{
2885 return !count (array_diff ($a1, $a2)) and !count (array_diff ($a2, $a1));
2886}
2887
56561657
DO
2888# Reindex provided array of arrays by a column value, which is present in
2889# each sub-array and is assumed to be unique. Most often, make "id" column in
2890# a list of cells into the key space.
2891function reindexById ($input, $column_name = 'id')
2892{
6c0bf066 2893 $ret = array();
56561657
DO
2894 foreach ($input as $item)
2895 {
2896 if (! array_key_exists ($column_name, $item))
2897 throw new InvalidArgException ('input', '(array)', 'ID column missing');
2898 if (array_key_exists ($item[$column_name], $ret))
2899 throw new InvalidArgException ('column_name', $column_name, 'duplicate ID value ' . $item[$column_name]);
2900 $ret[$item[$column_name]] = $item;
2901 }
2902 return $ret;
2903}
2904
1ce258f7
DO
2905// Use the VLAN switch template to set VST role for each port of
2906// the provided list. Return resulting list.
bc254f49 2907function apply8021QOrder ($vst_id, $portlist)
25930440 2908{
cc3d6915
DO
2909 $vst = spotEntity ('vst', $vst_id);
2910 amplifyCell ($vst);
1ce258f7
DO
2911 foreach (array_keys ($portlist) as $port_name)
2912 {
2913 foreach ($vst['rules'] as $rule)
2914 if (preg_match ($rule['port_pcre'], $port_name))
25930440
DO
2915 {
2916 $portlist[$port_name]['vst_role'] = $rule['port_role'];
091768aa 2917 $portlist[$port_name]['wrt_vlans'] = buildVLANFilter ($rule['port_role'], $rule['wrt_vlans']);
1ce258f7 2918 continue 2;
25930440 2919 }
1ce258f7
DO
2920 $portlist[$port_name]['vst_role'] = 'none';
2921 }
25930440
DO
2922 return $portlist;
2923}
2924
091768aa
DO
2925// return a sequence of ranges for given string form and port role
2926function buildVLANFilter ($role, $string)
be28b696 2927{
091768aa 2928 // set base
1ce258f7 2929 switch ($role)
be28b696 2930 {
1ce258f7
DO
2931 case 'access': // 1-4094
2932 $min = VLAN_MIN_ID;
2933 $max = VLAN_MAX_ID;
2934 break;
2935 case 'trunk': // 2-4094
2936 case 'uplink':
65da0c15 2937 case 'downlink':
ec523868 2938 case 'anymode':
1ce258f7
DO
2939 $min = VLAN_MIN_ID + 1;
2940 $max = VLAN_MAX_ID;
2941 break;
65da0c15 2942 default: // none
1ce258f7 2943 return array();
be28b696 2944 }
091768aa
DO
2945 if ($string == '') // fast track
2946 return array (array ('from' => $min, 'to' => $max));
2947 // transform
2948 $vlanidlist = array();
1ce258f7
DO
2949 foreach (iosParseVLANString ($string) as $vlan_id)
2950 if ($min <= $vlan_id and $vlan_id <= $max)
091768aa 2951 $vlanidlist[] = $vlan_id;
cc6a6c4e
DO
2952 return listToRanges ($vlanidlist);
2953}
2954
2955// pack set of integers into list of integer ranges
2956// e.g. (1, 2, 3, 5, 6, 7, 9, 11) => ((1, 3), (5, 7), (9, 9), (11, 11))
ec523868
DO
2957// The second argument, when it is different from 0, limits amount of
2958// items in each generated range.
2959function listToRanges ($vlanidlist, $limit = 0)
cc6a6c4e 2960{
091768aa
DO
2961 sort ($vlanidlist);
2962 $ret = array();
2963 $from = $to = NULL;
2964 foreach ($vlanidlist as $vlan_id)
2965 if ($from == NULL)
ec523868
DO
2966 {
2967 if ($limit == 1)
2968 $ret[] = array ('from' => $vlan_id, 'to' => $vlan_id);
2969 else
2970 $from = $to = $vlan_id;
2971 }
091768aa 2972 elseif ($to + 1 == $vlan_id)
ec523868 2973 {
091768aa 2974 $to = $vlan_id;
ec523868
DO
2975 if ($to - $from + 1 == $limit)
2976 {
2977 // cut accumulated range and start over
2978 $ret[] = array ('from' => $from, 'to' => $to);
2979 $from = $to = NULL;
2980 }
2981 }
091768aa
DO
2982 else
2983 {
2984 $ret[] = array ('from' => $from, 'to' => $to);
2985 $from = $to = $vlan_id;
2986 }
2987 if ($from != NULL)
2988 $ret[] = array ('from' => $from, 'to' => $to);
be28b696
DO
2989 return $ret;
2990}
2991
091768aa
DO
2992// return TRUE, if given VLAN ID belongs to one of filter's ranges
2993function matchVLANFilter ($vlan_id, $vfilter)
2994{
2995 foreach ($vfilter as $range)
2996 if ($range['from'] <= $vlan_id and $vlan_id <= $range['to'])
2997 return TRUE;
2998 return FALSE;
2999}
3000
651c0804 3001function generate8021QDeployOps ($vswitch, $device_vlanlist, $before, $changes)
bcd14540 3002{
651c0804
AA
3003 $domain_vlanlist = getDomainVLANs ($vswitch['domain_id']);
3004 $employed_vlans = getEmployedVlans ($vswitch['object_id'], $domain_vlanlist);
3005
bcd14540
DO
3006 // only ignore VLANs, which exist and are explicitly shown as "alien"
3007 $old_managed_vlans = array();
bcd14540
DO
3008 foreach ($device_vlanlist as $vlan_id)
3009 if
3010 (
3011 !array_key_exists ($vlan_id, $domain_vlanlist) or
3012 $domain_vlanlist[$vlan_id]['vlan_type'] != 'alien'
3013 )
3014 $old_managed_vlans[] = $vlan_id;
3015 $ports_to_do = array();
651c0804
AA
3016 $ports_to_do_queue1 = array();
3017 $ports_to_do_queue2 = array();
bcd14540
DO
3018 $after = $before;
3019 foreach ($changes as $port_name => $port)
3020 {
651c0804 3021 $changeset = array
bcd14540
DO
3022 (
3023 'old_mode' => $before[$port_name]['mode'],
3024 'old_allowed' => $before[$port_name]['allowed'],
3025 'old_native' => $before[$port_name]['native'],
3026 'new_mode' => $port['mode'],
3027 'new_allowed' => $port['allowed'],
3028 'new_native' => $port['native'],
3029 );
651c0804
AA
3030 // put the ports with employed vlans first, the others - below them
3031 if (! count (array_intersect ($changeset['old_allowed'], $employed_vlans)))
3032 $ports_to_do_queue2[$port_name] = $changeset;
3033 else
3034 $ports_to_do_queue1[$port_name] = $changeset;
bcd14540
DO
3035 $after[$port_name] = $port;
3036 }
f95bc64f
DO
3037 # Two arrays without common keys get merged with "+" operator just fine,
3038 # with an important difference from array_merge() in that the latter
3039 # renumbers numeric keys, and "+" does not. This matters, when port name
3040 # is a number (like in XOS12 system).
3041 $ports_to_do = $ports_to_do_queue1 + $ports_to_do_queue2;
bcd14540
DO
3042 // New VLAN table is a union of:
3043 // 1. all compulsory VLANs
3044 // 2. all "current" non-alien allowed VLANs of those ports, which are left
3045 // intact (regardless if a VLAN exists in VLAN domain, but looking,
3046 // if it is present in device's own VLAN table)
3047 // 3. all "new" allowed VLANs of those ports, which we do "push" now
3048 // Like for old_managed_vlans, a VLANs is never listed, only if it
3049 // exists and belongs to "alien" type.
3050 $new_managed_vlans = array();
3051 // 1
3052 foreach ($domain_vlanlist as $vlan_id => $vlan)
3053 if ($vlan['vlan_type'] == 'compulsory')
3054 $new_managed_vlans[] = $vlan_id;
3055 // 2
3056 foreach ($before as $port_name => $port)
3057 if (!array_key_exists ($port_name, $changes))
3058 foreach ($port['allowed'] as $vlan_id)
3059 {
3060 if (in_array ($vlan_id, $new_managed_vlans))
3061 continue;
3062 if
3063 (
3064 array_key_exists ($vlan_id, $domain_vlanlist) and
3065 $domain_vlanlist[$vlan_id]['vlan_type'] == 'alien'
3066 )
3067 continue;
3068 if (in_array ($vlan_id, $device_vlanlist))
3069 $new_managed_vlans[] = $vlan_id;
3070 }
3071 // 3
3072 foreach ($changes as $port)
10b6b476 3073 foreach ($port['allowed'] as $vlan_id)
bcd14540
DO
3074 if
3075 (
e405f256 3076 isset ($domain_vlanlist[$vlan_id]) and
bcd14540
DO
3077 $domain_vlanlist[$vlan_id]['vlan_type'] == 'ondemand' and
3078 !in_array ($vlan_id, $new_managed_vlans)
3079 )
3080 $new_managed_vlans[] = $vlan_id;
3081 $crq = array();
3082 // Before removing each old VLAN as such it is necessary to unassign
3083 // ports from it (to remove VLAN from each ports' list of "allowed"
3084 // VLANs). This change in turn requires, that a port's "native"
3085 // VLAN isn't set to the one being removed from its "allowed" list.
3086 foreach ($ports_to_do as $port_name => $port)
3087 switch ($port['old_mode'] . '->' . $port['new_mode'])
3088 {
3089 case 'trunk->trunk':
3090 // "old" native is set and differs from the "new" native
3091 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
3092 $crq[] = array
3093 (
3094 'opcode' => 'unset native',
3095 'arg1' => $port_name,
3096 'arg2' => $port['old_native'],
3097 );
651c0804
AA
3098 $vlans_to_remove = array_diff ($port['old_allowed'], $port['new_allowed']);
3099 $queues = array();
3100 $queues[] = array_intersect ($employed_vlans, $vlans_to_remove); // remove employed vlans first
3101 $queues[] = array_diff ($vlans_to_remove, $employed_vlans);// remove other vlans afterwards
3102 foreach ($queues as $queue)
3103 if (! empty ($queue))
3104 $crq[] = array
3105 (
3106 'opcode' => 'rem allowed',
3107 'port' => $port_name,
3108 'vlans' => $queue,
3109 );
bcd14540
DO
3110 break;
3111 case 'access->access':
3112 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
3113 $crq[] = array
3114 (
3115 'opcode' => 'unset access',
3116 'arg1' => $port_name,
3117 'arg2' => $port['old_native'],
3118 );
3119 break;
3120 case 'access->trunk':
3121 $crq[] = array
3122 (
3123 'opcode' => 'unset access',
3124 'arg1' => $port_name,
3125 'arg2' => $port['old_native'],
3126 );
3127 break;
3128 case 'trunk->access':
453c8fb8
DO
3129 if ($port['old_native'])
3130 $crq[] = array
3131 (
3132 'opcode' => 'unset native',
3133 'arg1' => $port_name,
3134 'arg2' => $port['old_native'],
3135 );
651c0804
AA
3136 $vlans_to_remove = $port['old_allowed'];
3137 $queues = array();
3138 $queues[] = array_intersect ($employed_vlans, $vlans_to_remove); // remove employed vlans first
3139 $queues[] = array_diff ($vlans_to_remove, $employed_vlans);// remove other vlans afterwards
3140 foreach ($queues as $queue)
3141 if (! empty ($queue))
3142 $crq[] = array
3143 (
3144 'opcode' => 'rem allowed',
3145 'port' => $port_name,
3146 'vlans' => $queue,
3147 );
bcd14540
DO
3148 break;
3149 default:
164ba494 3150 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
bcd14540
DO
3151 }
3152 // Now it is safe to unconfigure VLANs, which still exist on device,
3153 // but are not present on the "new" list.
3154 // FIXME: put all IDs into one pseudo-command to make it easier
3155 // for translators to create/destroy VLANs in batches, where
3156 // target platform allows them to do.
3157 foreach (array_diff ($old_managed_vlans, $new_managed_vlans) as $vlan_id)
3158 $crq[] = array
3159 (
3160 'opcode' => 'destroy VLAN',
3161 'arg1' => $vlan_id,
3162 );
3163 // Configure VLANs, which must be present on the device, but are not yet.
3164 foreach (array_diff ($new_managed_vlans, $old_managed_vlans) as $vlan_id)
3165 $crq[] = array
3166 (
3167 'opcode' => 'create VLAN',
3168 'arg1' => $vlan_id,
3169 );
3170 // Now, when all new VLANs are created (queued), it is safe to assign (queue)
3171 // ports to the new VLANs.
3172 foreach ($ports_to_do as $port_name => $port)
3173 switch ($port['old_mode'] . '->' . $port['new_mode'])
3174 {
3175 case 'trunk->trunk':
3176 // For each allowed VLAN, which is present on the "new" list and missing from
3177 // the "old" one, queue a command to assign current port to that VLAN.
cc6a6c4e 3178 if (count ($tmp = array_diff ($port['new_allowed'], $port['old_allowed'])))
bcd14540
DO
3179 $crq[] = array
3180 (
3181 'opcode' => 'add allowed',
cc6a6c4e
DO
3182 'port' => $port_name,
3183 'vlans' => $tmp,
bcd14540
DO
3184 );
3185 // One of the "allowed" VLANs for this port may probably be "native".
3186 // "new native" is set and differs from "old native"
3187 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
3188 $crq[] = array
3189 (
3190 'opcode' => 'set native',
3191 'arg1' => $port_name,
3192 'arg2' => $port['new_native'],
3193 );
3194 break;
3195 case 'access->access':
3196 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
3197 $crq[] = array
3198 (
3199 'opcode' => 'set access',
3200 'arg1' => $port_name,
3201 'arg2' => $port['new_native'],
3202 );
3203 break;
3204 case 'access->trunk':
3205 $crq[] = array
3206 (
3207 'opcode' => 'set mode',
3208 'arg1' => $port_name,
3209 'arg2' => $port['new_mode'],
3210 );
cc6a6c4e 3211 if (count ($port['new_allowed']))
bcd14540
DO
3212 $crq[] = array
3213 (
3214 'opcode' => 'add allowed',
cc6a6c4e
DO
3215 'port' => $port_name,
3216 'vlans' => $port['new_allowed'],
bcd14540 3217 );
453c8fb8
DO
3218 if ($port['new_native'])
3219 $crq[] = array
3220 (
3221 'opcode' => 'set native',
3222 'arg1' => $port_name,
3223 'arg2' => $port['new_native'],
3224 );
bcd14540
DO
3225 break;
3226 case 'trunk->access':
3227 $crq[] = array
3228 (
3229 'opcode' => 'set mode',
3230 'arg1' => $port_name,
3231 'arg2' => $port['new_mode'],
3232 );
3233 $crq[] = array
3234 (
3235 'opcode' => 'set access',
3236 'arg1' => $port_name,
3237 'arg2' => $port['new_native'],
3238 );
3239 break;
3240 default:
164ba494 3241 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
bcd14540 3242 }
0b24ab60
AA
3243 return $crq;
3244}
3245
3246function exportSwitch8021QConfig
3247(
3248 $vswitch,
3249 $device_vlanlist,
3250 $before,
8d6c3668
AA
3251 $changes,
3252 $vlan_names
0b24ab60
AA
3253)
3254{
651c0804 3255 $crq = generate8021QDeployOps ($vswitch, $device_vlanlist, $before, $changes);
5017142e
DO
3256 if (count ($crq))
3257 {
3258 array_unshift ($crq, array ('opcode' => 'begin configuration'));
3259 $crq[] = array ('opcode' => 'end configuration');
3260 if (considerConfiguredConstraint (spotEntity ('object', $vswitch['object_id']), '8021Q_WRI_AFTER_CONFT_LISTSRC'))
3261 $crq[] = array ('opcode' => 'save configuration');
8d6c3668 3262 setDevice8021QConfig ($vswitch['object_id'], $crq, $vlan_names);
5017142e 3263 }
bcd14540
DO
3264 return count ($crq);
3265}
3266
af204724 3267// filter list of changed ports to cancel changes forbidden by VST and domain
af204724
DO
3268function filter8021QChangeRequests
3269(
3270 $domain_vlanlist,
3271 $before, // current saved configuration of all ports
3272 $changes // changed ports with VST markup
3273)
3274{
3275 $domain_immune_vlans = array();
3276 foreach ($domain_vlanlist as $vlan_id => $vlan)
3277 if ($vlan['vlan_type'] == 'alien')
3278 $domain_immune_vlans[] = $vlan_id;
3279 $ret = array();
3280 foreach ($changes as $port_name => $port)
3281 {
ec523868
DO
3282 // VST violation ?
3283 if (!goodModeForVSTRole ($port['mode'], $port['vst_role']))
3284 continue; // ignore change request
af204724 3285 // find and cancel any changes regarding immune VLANs
ec523868 3286 switch ($port['mode'])
034a61c9
DO
3287 {
3288 case 'access':
034a61c9 3289 foreach ($domain_immune_vlans as $immune)
af204724
DO
3290 // Reverting an attempt to set an access port from
3291 // "normal" VLAN to immune one (or vice versa) requires
3292 // special handling, becase the calling function has
3293 // discarded the old contents of 'allowed' for current port.
af204724
DO
3294 if
3295 (
3296 $before[$port_name]['native'] == $immune or
3297 $port['native'] == $immune
3298 )
3299 {
3300 $port['native'] = $before[$port_name]['native'];
3301 $port['allowed'] = array ($port['native']);
034a61c9
DO
3302 // Such reversal happens either once or never for an
3303 // access port.
3304 break;
af204724 3305 }
034a61c9
DO
3306 break;
3307 case 'trunk':
034a61c9 3308 foreach ($domain_immune_vlans as $immune)
af204724
DO
3309 if (in_array ($immune, $before[$port_name]['allowed'])) // was allowed before
3310 {
3311 if (!in_array ($immune, $port['allowed']))
3312 $port['allowed'][] = $immune; // restore
3313 if ($before[$port_name]['native'] == $immune) // and was native
3314 $port['native'] = $immune; // also restore
3315 }
3316 else // wasn't
3317 {
3318 if (in_array ($immune, $port['allowed']))
3319 unset ($port['allowed'][array_search ($immune, $port['allowed'])]); // cancel
3320 if ($port['native'] == $immune)
3321 $port['native'] = $before[$port_name]['native'];
3322 }
034a61c9
DO
3323 break;
3324 default:
ec523868 3325 throw new InvalidArgException ('mode', $port['mode']);
034a61c9
DO
3326 }
3327 // save work
af204724
DO
3328 $ret[$port_name] = $port;
3329 }
3330 return $ret;
3331}
3332
651c0804 3333function getEmployedVlans ($object_id, $domain_vlanlist)
4741e9c3 3334{
651c0804
AA
3335 $employed = array(); // keyed by vlan_id. Value is dummy int
3336 // find persistent VLANs in domain
3337 foreach ($domain_vlanlist as $vlan_id => $vlan)
3338 if ($vlan['vlan_type'] == 'compulsory')
3339 $employed[$vlan_id] = 1;
9ce5cf73
AA
3340
3341 // find VLANs for object's L3 allocations
3342 $cell = spotEntity ('object', $object_id);
3343 amplifyCell ($cell);
3344 foreach (array ('ipv4', 'ipv6') as $family)
3345 {
3346 $seen_nets = array();
3347 foreach ($cell[$family] as $ip => $allocation)
3348 {
3349 if ($family == 'ipv6')
3350 $ip = new IPv6Address ($ip);
3351 if ($net_id = ($family == 'ipv6' ? getIPv6AddressNetworkId ($ip) : getIPv4AddressNetworkId ($ip)))
3352 {
3353 if (! isset($seen_nets[$net_id]))
3354 $seen_nets[$net_id] = 1;
3355 else
3356 continue;
3357 $net = spotEntity ("${family}net", $net_id);
9ce5cf73 3358 foreach ($net['8021q'] as $vlan)
651c0804
AA
3359 if (! isset ($employed[$vlan['vlan_id']]))
3360 $employed[$vlan['vlan_id']] = 1;
9ce5cf73
AA
3361 }
3362 }
3363 }
651c0804
AA
3364 return array_keys ($employed);
3365}
9ce5cf73 3366
651c0804
AA
3367// take port list with order applied and return uplink ports in the same format
3368function produceUplinkPorts ($domain_vlanlist, $portlist, $object_id)
3369{
3370 $ret = array();
3371
3372 $employed = getEmployedVlans ($object_id, $domain_vlanlist);
ca7f0af4
DO
3373 foreach ($portlist as $port_name => $port)
3374 if ($port['vst_role'] != 'uplink')
3375 foreach ($port['allowed'] as $vlan_id)
3376 if (!in_array ($vlan_id, $employed))
3377 $employed[] = $vlan_id;
651c0804 3378
ca7f0af4
DO
3379 foreach ($portlist as $port_name => $port)
3380 if ($port['vst_role'] == 'uplink')
3381 {
1ce258f7
DO
3382 $employed_here = array();
3383 foreach ($employed as $vlan_id)
091768aa 3384 if (matchVLANFilter ($vlan_id, $port['wrt_vlans']))
1ce258f7 3385 $employed_here[] = $vlan_id;
ca7f0af4
DO
3386 $ret[$port_name] = array
3387 (
034a61c9 3388 'vst_role' => 'uplink',
ca7f0af4
DO
3389 'mode' => 'trunk',
3390 'allowed' => $employed_here,
3391 'native' => 0,
3392 );
3393 }
4741e9c3
DO
3394 return $ret;
3395}
3396
9c45ea37
DO
3397function same8021QConfigs ($a, $b)
3398{
3399 return $a['mode'] == $b['mode'] &&
3400 array_values_same ($a['allowed'], $b['allowed']) &&
3401 $a['native'] == $b['native'];
3402}
3403
ec523868
DO
3404// Return TRUE, if the port can be edited by the user.
3405function editable8021QPort ($port)
3406{
3407 return in_array ($port['vst_role'], array ('trunk', 'access', 'anymode'));
3408}
3409
3410// Decide, whether the given 802.1Q port mode is permitted by
3411// VST port role.
3412function goodModeForVSTRole ($mode, $role)
3413{
3414 switch ($mode)
3415 {
3416 case 'access':
3417 return in_array ($role, array ('access', 'anymode'));
3418 case 'trunk':
3419 return in_array ($role, array ('trunk', 'uplink', 'downlink', 'anymode'));
3420 default:
3421 throw new InvalidArgException ('mode', $mode);
3422 }
3423}
3424
26dec09b
DO
3425/*
3426
3427Relation between desired (D), cached (C) and running (R)
3428copies of switch ports (P) list.
3429
3430 D C R
3431+---+ +---+ +---+
3432| P |-----| P |-? +--| P |
3433+---+ +---+ / +---+
3434| P |-----| P |--+ ?-| P |
3435+---+ +---+ +---+
3436| P |-----| P |-------| P |
3437+---+ +---+ +---+
3438| P |-----| P |--+ ?-| P |
3439+---+ +---+ \ +---+
3440| P |-----| P |--+ +--| P |
3441+---+ +---+ \ +---+
3442 +--| P |
3443 +---+
3444 ?-| P |
3445 +---+
3446
3447A modified local version of a port in "conflict" state ignores remote
3448changes until remote change maintains its difference. Once both edits
3449match, the local copy "locks" on the remote and starts tracking it.
3450
3451v
3452a "o" -- remOte version
3453l "l" -- Local version
3454u "b" -- Both versions
3455e
3456
3457^
3458| o b
3459| o
3460| l l l l l l b b
3461| o o b
3462| o b
3463|
3464| o
3465|
3466|
34670----------------------------------------------> time
3468
3469*/
be28b696
DO
3470function get8021QSyncOptions
3471(
3472 $vswitch,
3473 $D, // desired config
3474 $C, // cached config
3475 $R // running-config
3476)
3477{
3478 $default_port = array
3479 (
3480 'mode' => 'access',
3481 'allowed' => array (VLAN_DFL_ID),
3482 'native' => VLAN_DFL_ID,
3483 );
ab25b0d0 3484 $ret = array();
24832534 3485 $allports = array();
ab25b0d0 3486 foreach (array_unique (array_merge (array_keys ($C), array_keys ($R))) as $pn)
d3082829
DO
3487 $allports[$pn] = array();
3488 foreach (apply8021QOrder ($vswitch['template_id'], $allports) as $pn => $port)
be28b696 3489 {
7a375475
DO
3490 // catch anomalies early
3491 if ($port['vst_role'] == 'none')
3492 {
3493 if ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and !array_key_exists ($pn, $C))
3494 $ret[$pn] = array ('status' => 'none');
3495 else
3496 $ret[$pn] = array
3497 (
3498 'status' => 'martian_conflict',
3499 'left' => array_key_exists ($pn, $C) ? $C[$pn] : array ('mode' => 'none'),
3500 'right' => array_key_exists ($pn, $R) ? $R[$pn] : array ('mode' => 'none'),
3501 );
3502 continue;
3503 }
3504 elseif ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and array_key_exists ($pn, $C))
3505 {
3506 $ret[$pn] = array
3507 (
3508 'status' => 'martian_conflict',
3509 'left' => array_key_exists ($pn, $C) ? $C[$pn] : array ('mode' => 'none'),
3510 'right' => array_key_exists ($pn, $R) ? $R[$pn] : array ('mode' => 'none'),
3511 );
3512 continue;
3513 }
26dec09b
DO
3514 // (DC_): port missing from device
3515 if (!array_key_exists ($pn, $R))
be28b696 3516 {
ef016293
DO
3517 $ret[$pn] = array ('left' => $D[$pn]);
3518 if (same8021QConfigs ($D[$pn], $default_port))
3519 $ret[$pn]['status'] = 'ok_to_delete';
3520 else
3521 {
3522 $ret[$pn]['status'] = 'delete_conflict';
3523 $ret[$pn]['lastseen'] = $C[$pn];
3524 }
be28b696
DO
3525 continue;
3526 }
26dec09b
DO
3527 // (__R): port missing from DB
3528 if (!array_key_exists ($pn, $C))
be28b696 3529 {
7a375475
DO
3530 // Allow importing any configuration, which passes basic
3531 // validation. If port mode doesn't match its VST role,
3532 // this will be handled later WRT each port.
ab25b0d0
DO
3533 $ret[$pn] = array
3534 (
7a375475 3535 'status' => acceptable8021QConfig ($R[$pn]) ? 'ok_to_add' : 'add_conflict',
ab25b0d0
DO
3536 'right' => $R[$pn],
3537 );
be28b696
DO
3538 continue;
3539 }
3540 $D_eq_C = same8021QConfigs ($D[$pn], $C[$pn]);
3541 $C_eq_R = same8021QConfigs ($C[$pn], $R[$pn]);
26dec09b 3542 // (DCR), D = C = R: data in sync
be28b696 3543 if ($D_eq_C and $C_eq_R) // implies D == R
ab25b0d0
DO
3544 {
3545 $ret[$pn] = array
3546 (
3547 'status' => 'in_sync',
3548 'both' => $R[$pn],
3549 );
be28b696 3550 continue;
ab25b0d0 3551 }
26dec09b 3552 // (DCR), D = C: no local edit in the way
be28b696 3553 if ($D_eq_C)
ab25b0d0
DO
3554 $ret[$pn] = array
3555 (
3556 'status' => 'ok_to_pull',
3557 'left' => $D[$pn],
3558 'right' => $R[$pn],
3559 );
26dec09b 3560 // (DCR), C = R: no remote edit in the way
be28b696 3561 elseif ($C_eq_R)
ab25b0d0
DO
3562 $ret[$pn] = array
3563 (
3564 'status' => 'ok_to_push',
3565 'left' => $D[$pn],
3566 'right' => $R[$pn],
3567 );
26dec09b 3568 // (DCR), D = R: end of version conflict, restore tracking
be28b696 3569 elseif (same8021QConfigs ($D[$pn], $R[$pn]))
ab25b0d0
DO
3570 $ret[$pn] = array
3571 (
d973196a 3572 'status' => 'ok_to_merge',
ab25b0d0
DO
3573 'both' => $R[$pn],
3574 );
26dec09b 3575 else // D != C, C != R, D != R: version conflict
ab25b0d0
DO
3576 $ret[$pn] = array
3577 (
ec523868 3578 'status' => editable8021QPort ($port) ?
d3082829
DO
3579 // In case the port is normally updated by user, let him
3580 // resolve the conflict. If the system manages this port,
3581 // arrange the data to let remote version go down.
3582 'merge_conflict' : 'ok_to_push_with_merge',
ab25b0d0
DO
3583 'left' => $D[$pn],
3584 'right' => $R[$pn],
3585 );
be28b696 3586 }
ab25b0d0 3587 return $ret;
be28b696
DO
3588}
3589
ca5d4cbc 3590// return number of records updated successfully of FALSE, if a conflict was in the way
d973196a 3591function exec8021QDeploy ($object_id, $do_push)
ca5d4cbc
DO
3592{
3593 global $dbxlink;
b3a27170 3594 $nsaved = $npushed = $nsaved_uplinks = 0;
ca5d4cbc
DO
3595 $dbxlink->beginTransaction();
3596 if (NULL === $vswitch = getVLANSwitchInfo ($object_id, 'FOR UPDATE'))
3597 throw new InvalidArgException ('object_id', $object_id, 'VLAN domain is not set for this object');
3598 $D = getStored8021QConfig ($vswitch['object_id'], 'desired');
3599 $C = getStored8021QConfig ($vswitch['object_id'], 'cached');
d973196a
DO
3600 try
3601 {
3602 $R = getRunning8021QConfig ($vswitch['object_id']);
3603 }
3a089a44 3604 catch (RTGatewayError $e)
d973196a 3605 {
c1aa3ada
DO
3606 usePreparedExecuteBlade
3607 (
3608 'UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3609 array (E_8021Q_PULL_REMOTE_ERROR, $vswitch['object_id'])
3610 );
d973196a
DO
3611 $dbxlink->commit();
3612 return 0;
3613 }
ca5d4cbc 3614 $conflict = FALSE;
ca5d4cbc 3615 $ok_to_push = array();
d973196a 3616 foreach (get8021QSyncOptions ($vswitch, $D, $C, $R['portdata']) as $pn => $port)
ca5d4cbc
DO
3617 {
3618 // always update cache with new data from switch
3619 switch ($port['status'])
3620 {
d973196a 3621 case 'ok_to_merge':
f82a94da 3622 // FIXME: this can be logged
d973196a 3623 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['both']);
ca5d4cbc
DO
3624 break;
3625 case 'ok_to_delete':
29c2e036
AA
3626 del8021QPort ($vswitch['object_id'], $pn);
3627 $nsaved++;
ca5d4cbc
DO
3628 break;
3629 case 'ok_to_add':
29c2e036
AA
3630 add8021QPort ($vswitch['object_id'], $pn, $port['right']);
3631 $nsaved++;
ca5d4cbc
DO
3632 break;
3633 case 'delete_conflict':
3634 case 'merge_conflict':
7a375475
DO
3635 case 'add_conflict':
3636 case 'martian_conflict':
ca5d4cbc
DO
3637 $conflict = TRUE;
3638 break;
3639 case 'ok_to_pull':
f82a94da 3640 // FIXME: this can be logged
d973196a
DO
3641 upd8021QPort ('desired', $vswitch['object_id'], $pn, $port['right']);
3642 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right']);
3643 $nsaved++;
ca5d4cbc 3644 break;
d3082829
DO
3645 case 'ok_to_push_with_merge':
3646 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right']);
3647 // fall through
ca5d4cbc
DO
3648 case 'ok_to_push':
3649 $ok_to_push[$pn] = $port['left'];
3650 break;
3651 }
3652 }
32c7fad1
DO
3653 // redo uplinks unconditionally
3654 $domain_vlanlist = getDomainVLANs ($vswitch['domain_id']);
3655 $Dnew = apply8021QOrder ($vswitch['template_id'], getStored8021QConfig ($vswitch['object_id'], 'desired'));
034a61c9
DO
3656 // Take new "desired" configuration and derive uplink port configuration
3657 // from it. Then cancel changes to immune VLANs and save resulting
3658 // changes (if any left).
9ce5cf73 3659 $new_uplinks = filter8021QChangeRequests ($domain_vlanlist, $Dnew, produceUplinkPorts ($domain_vlanlist, $Dnew, $vswitch['object_id']));
b3a27170
DO
3660 $nsaved_uplinks += replace8021QPorts ('desired', $vswitch['object_id'], $Dnew, $new_uplinks);
3661 if ($nsaved + $nsaved_uplinks)
32c7fad1
DO
3662 // saved configuration has changed (either "user" ports have changed,
3663 // or uplinks, or both), so bump revision number up)
9a357bcc 3664 touchVLANSwitch ($vswitch['object_id']);
d973196a 3665 if ($conflict)
c1aa3ada
DO
3666 usePreparedExecuteBlade
3667 (
3668 'UPDATE VLANSwitch SET out_of_sync="yes", last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3669 array (E_8021Q_VERSION_CONFLICT, $vswitch['object_id'])
3670 );
52ce8bd7 3671 else
d973196a 3672 {
c1aa3ada
DO
3673 usePreparedExecuteBlade
3674 (
3675 'UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3676 array (E_8021Q_NOERROR, $vswitch['object_id'])
3677 );
4799a8df
DO
3678 // Modified uplinks are very likely to differ from those in R-copy,
3679 // so don't mark device as clean, if this happened. This can cost
3680 // us an additional, empty round of sync, but at least out_of_sync
3681 // won't be mistakenly set to 'no'.
3682 // FIXME: A cleaner way of coupling pull and push operations would
3683 // be to split this function into two.
3684 if (!count ($ok_to_push) and !$nsaved_uplinks)
c1aa3ada
DO
3685 usePreparedExecuteBlade
3686 (
3687 'UPDATE VLANSwitch SET out_of_sync="no" WHERE object_id=?',
3688 array ($vswitch['object_id'])
3689 );
52ce8bd7 3690 elseif ($do_push)
d973196a 3691 {
c1aa3ada
DO
3692 usePreparedExecuteBlade
3693 (
3694 'UPDATE VLANSwitch SET last_push_started=NOW() WHERE object_id=?',
3695 array ($vswitch['object_id'])
3696 );
52ce8bd7
DO
3697 try
3698 {
8cbdec04 3699 $vlan_names = isset ($R['vlannames']) ? $R['vlannames'] : array();
8d6c3668 3700 $npushed += exportSwitch8021QConfig ($vswitch, $R['vlanlist'], $R['portdata'], $ok_to_push, $vlan_names);
52ce8bd7
DO
3701 // update cache for ports deployed
3702 replace8021QPorts ('cached', $vswitch['object_id'], $R['portdata'], $ok_to_push);
c1aa3ada
DO
3703 usePreparedExecuteBlade
3704 (
3705 'UPDATE VLANSwitch SET last_push_finished=NOW(), out_of_sync="no", last_errno=? WHERE object_id=?',
3706 array (E_8021Q_NOERROR, $vswitch['object_id'])
3707 );
52ce8bd7 3708 }
3a089a44 3709 catch (RTGatewayError $r)
52ce8bd7 3710 {
c1aa3ada
DO
3711 usePreparedExecuteBlade
3712 (