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