4 * This file is a library of computational functions for RackTables.
9 $loclist[1] = 'interior';
11 $loclist['front'] = 0;
12 $loclist['interior'] = 1;
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;
27 // Entity type by page number mapping is 1:1 atm, but may change later.
28 $etype_by_pageno = array
30 'ipv4net' => 'ipv4net',
31 'ipv6net' => 'ipv6net',
32 'ipv4rspool' => 'ipv4rspool',
40 // Rack thumbnail image width summands: "front", "interior" and "rear" elements w/o surrounding border.
50 32 => '255.255.255.255',
51 31 => '255.255.255.254',
52 30 => '255.255.255.252',
53 29 => '255.255.255.248',
54 28 => '255.255.255.240',
55 27 => '255.255.255.224',
56 26 => '255.255.255.192',
57 25 => '255.255.255.128',
58 24 => '255.255.255.0',
59 23 => '255.255.254.0',
60 22 => '255.255.252.0',
61 21 => '255.255.248.0',
62 20 => '255.255.240.0',
63 19 => '255.255.224.0',
64 18 => '255.255.192.0',
65 17 => '255.255.128.0',
84 $wildcardbylen = array
106 12 => '0.15.255.255',
107 11 => '0.31.255.255',
108 10 => '0.63.255.255',
109 9 => '0.127.255.255',
110 8 => '0.255.255.255',
111 7 => '1.255.255.255',
112 6 => '3.255.255.255',
113 5 => '7.255.255.255',
114 4 => '15.255.255.255',
115 3 => '31.255.255.255',
116 2 => '63.255.255.255',
117 1 => '127.255.255.255'
120 // This function assures that specified argument was passed
121 // and is a number greater than zero.
122 function assertUIntArg ($argname, $allow_zero = FALSE)
124 if (!isset ($_REQUEST[$argname]))
125 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
126 if (!is_numeric ($_REQUEST[$argname]))
127 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a number');
128 if ($_REQUEST[$argname] < 0)
129 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is less than zero');
130 if (!$allow_zero and $_REQUEST[$argname] == 0)
131 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is zero');
134 // This function assures that specified argument was passed
135 // and is a non-empty string.
136 function assertStringArg ($argname, $ok_if_empty = FALSE)
138 if (!isset ($_REQUEST[$argname]))
139 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
140 if (!is_string ($_REQUEST[$argname]))
141 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
142 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
143 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
146 function assertBoolArg ($argname, $ok_if_empty = FALSE)
148 if (!isset ($_REQUEST[$argname]))
149 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
150 if (!is_string ($_REQUEST[$argname]) or $_REQUEST[$argname] != 'on')
151 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
152 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
153 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
156 // function returns IPv6Address object, null if arg is correct IPv4, or throws an exception
157 function assertIPArg ($argname, $ok_if_empty = FALSE)
159 assertStringArg ($argname, $ok_if_empty);
160 $ip = $_REQUEST[$argname];
161 if (FALSE !== strpos ($ip, ':'))
163 $v6address = new IPv6Address
;
164 $result = $v6address->parse ($ip);
169 $result = long2ip (ip2long ($ip)) === $ip;
173 throw new InvalidRequestArgException ($argname, $ip, 'parameter is not a valid IPv4 or IPv6 address');
177 function assertIPv4Arg ($argname, $ok_if_empty = FALSE)
179 assertStringArg ($argname, $ok_if_empty);
180 if (strlen ($_REQUEST[$argname]) and long2ip (ip2long ($_REQUEST[$argname])) !== $_REQUEST[$argname])
181 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a valid ipv4 address');
184 // function returns IPv6Address object, or throws an exception
185 function assertIPv6Arg ($argname, $ok_if_empty = FALSE)
187 assertStringArg ($argname, $ok_if_empty);
188 $ipv6 = new IPv6Address
;
189 if (strlen ($_REQUEST[$argname]) and ! $ok_if_empty and ! $ipv6->parse ($_REQUEST[$argname]))
190 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a valid ipv6 address');
194 function assertPCREArg ($argname)
196 assertStringArg ($argname, TRUE); // empty pattern is Ok
197 if (FALSE === preg_match ($_REQUEST[$argname], 'test'))
198 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'PCRE validation failed');
201 // Objects of some types should be explicitly shown as
202 // anonymous (labelless). This function is a single place where the
203 // decision about displayed name is made.
204 function setDisplayedName (&$cell)
206 if ($cell['name'] != '')
207 $cell['dname'] = $cell['name'];
210 $cell['atags'][] = array ('tag' => '$nameless');
211 if (considerConfiguredConstraint ($cell, 'NAMEWARN_LISTSRC'))
212 $cell['dname'] = 'ANONYMOUS ' . decodeObjectType ($cell['objtype_id'], 'o');
214 $cell['dname'] = '[' . decodeObjectType ($cell['objtype_id'], 'o') . ']';
218 // This function finds height of solid rectangle of atoms, which are all
219 // assigned to the same object. Rectangle base is defined by specified
221 function rectHeight ($rackData, $startRow, $template_idx)
224 // The first met object_id is used to match all the folowing IDs.
229 for ($locidx = 0; $locidx < 3; $locidx++
)
231 // At least one value in template is TRUE, but the following block
232 // can meet 'skipped' atoms. Let's ensure we have something after processing
234 if ($template[$template_idx][$locidx])
236 if (isset ($rackData[$startRow - $height][$locidx]['skipped']))
238 if (isset ($rackData[$startRow - $height][$locidx]['rowspan']))
240 if (isset ($rackData[$startRow - $height][$locidx]['colspan']))
242 if ($rackData[$startRow - $height][$locidx]['state'] != 'T')
245 $object_id = $rackData[$startRow - $height][$locidx]['object_id'];
246 if ($object_id != $rackData[$startRow - $height][$locidx]['object_id'])
250 // If the first row can't offer anything, bail out.
251 if ($height == 0 and $object_id == 0)
255 while ($startRow - $height > 0);
256 # echo "for startRow==${startRow} and template==(" . ($template[$template_idx][0] ? 'T' : 'F');
257 # echo ', ' . ($template[$template_idx][1] ? 'T' : 'F') . ', ' . ($template[$template_idx][2] ? 'T' : 'F');
258 # echo ") height==${height}<br>\n";
262 // This function marks atoms to be avoided by rectHeight() and assigns rowspan/colspan
264 function markSpan (&$rackData, $startRow, $maxheight, $template_idx)
266 global $template, $templateWidth;
268 for ($height = 0; $height < $maxheight; $height++
)
270 for ($locidx = 0; $locidx < 3; $locidx++
)
272 if ($template[$template_idx][$locidx])
274 // Add colspan/rowspan to the first row met and mark the following ones to skip.
275 // Explicitly show even single-cell spanned atoms, because rectHeight()
276 // is expeciting this data for correct calculation.
278 $rackData[$startRow - $height][$locidx]['skipped'] = TRUE;
281 $colspan = $templateWidth[$template_idx];
283 $rackData[$startRow - $height][$locidx]['colspan'] = $colspan;
285 $rackData[$startRow - $height][$locidx]['rowspan'] = $maxheight;
293 // This function sets rowspan/solspan/skipped atom attributes for renderRack()
294 // What we actually have to do is to find _all_ possible rectangles for each unit
295 // and then select the widest of those with the maximal square.
296 function markAllSpans (&$rackData = NULL)
298 if ($rackData == NULL)
300 showWarning ('Invalid rackData', __FUNCTION__
);
303 for ($i = $rackData['height']; $i > 0; $i--)
304 while (markBestSpan ($rackData, $i));
307 // Calculate height of 6 possible span templates (array is presorted by width
308 // descending) and mark the best (if any).
309 function markBestSpan (&$rackData, $i)
311 global $template, $templateWidth;
312 for ($j = 0; $j < 6; $j++
)
314 $height[$j] = rectHeight ($rackData, $i, $j);
315 $square[$j] = $height[$j] * $templateWidth[$j];
317 // find the widest rectangle of those with maximal height
318 $maxsquare = max ($square);
321 $best_template_index = 0;
322 for ($j = 0; $j < 6; $j++
)
323 if ($square[$j] == $maxsquare)
325 $best_template_index = $j;
326 $bestheight = $height[$j];
329 // distribute span marks
330 markSpan ($rackData, $i, $bestheight, $best_template_index);
334 // We can mount 'F' atoms and unmount our own 'T' atoms.
335 function applyObjectMountMask (&$rackData, $object_id)
337 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
338 for ($locidx = 0; $locidx < 3; $locidx++
)
339 switch ($rackData[$unit_no][$locidx]['state'])
342 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
345 $rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
348 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
352 // Design change means transition between 'F' and 'A' and back.
353 function applyRackDesignMask (&$rackData)
355 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
356 for ($locidx = 0; $locidx < 3; $locidx++
)
357 switch ($rackData[$unit_no][$locidx]['state'])
361 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
364 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
368 // The same for 'F' and 'U'.
369 function applyRackProblemMask (&$rackData)
371 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
372 for ($locidx = 0; $locidx < 3; $locidx++
)
373 switch ($rackData[$unit_no][$locidx]['state'])
377 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
380 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
384 // This function highlights specified object (and removes previous highlight).
385 function highlightObject (&$rackData, $object_id)
387 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
388 for ($locidx = 0; $locidx < 3; $locidx++
)
391 $rackData[$unit_no][$locidx]['state'] == 'T' and
392 $rackData[$unit_no][$locidx]['object_id'] == $object_id
394 $rackData[$unit_no][$locidx]['hl'] = 'h';
396 unset ($rackData[$unit_no][$locidx]['hl']);
399 // This function marks atoms to selected or not depending on their current state.
400 function markupAtomGrid (&$data, $checked_state)
402 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
403 for ($locidx = 0; $locidx < 3; $locidx++
)
405 if (!($data[$unit_no][$locidx]['enabled'] === TRUE))
407 if ($data[$unit_no][$locidx]['state'] == $checked_state)
408 $data[$unit_no][$locidx]['checked'] = ' checked';
410 $data[$unit_no][$locidx]['checked'] = '';
414 // This function is almost a clone of processGridForm(), but doesn't save anything to database
415 // Return value is the changed rack data.
416 // Here we assume that correct filter has already been applied, so we just
417 // set or unset checkbox inputs w/o changing atom state.
418 function mergeGridFormToRack (&$rackData)
420 $rack_id = $rackData['id'];
421 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
422 for ($locidx = 0; $locidx < 3; $locidx++
)
424 if ($rackData[$unit_no][$locidx]['enabled'] != TRUE)
426 $inputname = "atom_${rack_id}_${unit_no}_${locidx}";
427 if (isset ($_REQUEST[$inputname]) and $_REQUEST[$inputname] == 'on')
428 $rackData[$unit_no][$locidx]['checked'] = ' checked';
430 $rackData[$unit_no][$locidx]['checked'] = '';
434 // netmask conversion from length to number
435 function binMaskFromDec ($maskL)
437 $map_straight = array (
472 return $map_straight[$maskL];
475 // complementary value
476 function binInvMaskFromDec ($maskL)
513 return $map_compl[$maskL];
516 // This function looks up 'has_problems' flag for 'T' atoms
517 // and modifies 'hl' key. May be, this should be better done
518 // in amplifyCell(). We don't honour 'skipped' key, because
519 // the function is also used for thumb creation.
520 function markupObjectProblems (&$rackData)
522 for ($i = $rackData['height']; $i > 0; $i--)
523 for ($locidx = 0; $locidx < 3; $locidx++
)
524 if ($rackData[$i][$locidx]['state'] == 'T')
526 $object = spotEntity ('object', $rackData[$i][$locidx]['object_id']);
527 if ($object['has_problems'] == 'yes')
529 // Object can be already highlighted.
530 if (isset ($rackData[$i][$locidx]['hl']))
531 $rackData[$i][$locidx]['hl'] = $rackData[$i][$locidx]['hl'] . 'w';
533 $rackData[$i][$locidx]['hl'] = 'w';
538 // Return a uniformly (010203040506 or 0102030405060708) formatted address, if it is present
539 // in the provided string, an empty string for an empty string or NULL for error.
540 function l2addressForDatabase ($string)
542 $string = strtoupper ($string);
545 case ($string == '' or preg_match (RE_L2_SOLID
, $string) or preg_match (RE_L2_WWN_SOLID
, $string)):
547 case (preg_match (RE_L2_IFCFG
, $string) or preg_match (RE_L2_WWN_COLON
, $string)):
548 // reformat output of SunOS ifconfig
550 foreach (explode (':', $string) as $byte)
551 $ret .= (strlen ($byte) == 1 ?
'0' : '') . $byte;
553 case (preg_match (RE_L2_CISCO
, $string)):
554 return str_replace ('.', '', $string);
555 case (preg_match (RE_L2_HUAWEI
, $string)):
556 return str_replace ('-', '', $string);
557 case (preg_match (RE_L2_IPCFG
, $string) or preg_match (RE_L2_WWN_HYPHEN
, $string)):
558 return str_replace ('-', '', $string);
564 function l2addressFromDatabase ($string)
566 switch (strlen ($string))
569 case 16: // FireWire/Fibre Channel
570 $ret = implode (':', str_split ($string, 2));
579 // The following 2 functions return previous and next rack IDs for
580 // a given rack ID. The order of racks is the same as in renderRackspace()
582 function getPrevIDforRack ($row_id = 0, $rack_id = 0)
584 if ($row_id <= 0 or $rack_id <= 0)
586 showWarning ('Invalid arguments passed', __FUNCTION__
);
589 $rackList = listCells ('rack', $row_id);
590 doubleLink ($rackList);
591 if (isset ($rackList[$rack_id]['prev_key']))
592 return $rackList[$rack_id]['prev_key'];
596 function getNextIDforRack ($row_id = 0, $rack_id = 0)
598 if ($row_id <= 0 or $rack_id <= 0)
600 showWarning ('Invalid arguments passed', __FUNCTION__
);
603 $rackList = listCells ('rack', $row_id);
604 doubleLink ($rackList);
605 if (isset ($rackList[$rack_id]['next_key']))
606 return $rackList[$rack_id]['next_key'];
610 // This function finds previous and next array keys for each array key and
611 // modifies its argument accordingly.
612 function doubleLink (&$array)
615 foreach (array_keys ($array) as $key)
619 $array[$key]['prev_key'] = $prev_key;
620 $array[$prev_key]['next_key'] = $key;
626 function sortTokenize ($a, $b)
632 $a = preg_replace('/[^a-zA-Z0-9]/',' ',$a);
633 $a = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$a);
634 $a = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$a);
641 $b = preg_replace('/[^a-zA-Z0-9]/',' ',$b);
642 $b = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$b);
643 $b = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$b);
648 $ar = explode(' ', $a);
649 $br = explode(' ', $b);
650 for ($i=0; $i<count($ar) && $i<count($br); $i++
)
653 if (is_numeric($ar[$i]) and is_numeric($br[$i]))
654 $ret = ($ar[$i]==$br[$i])?
0:($ar[$i]<$br[$i]?
-1:1);
656 $ret = strcasecmp($ar[$i], $br[$i]);
667 // This function returns an array of single element of object's FQDN attribute,
668 // if FQDN is set. The next choice is object's common name, if it looks like a
669 // hostname. Otherwise an array of all 'regular' IP addresses of the
670 // object is returned (which may appear 0 and more elements long).
671 function findAllEndpoints ($object_id, $fallback = '')
673 foreach (getAttrValues ($object_id) as $record)
674 if ($record['id'] == 3 && strlen ($record['value'])) // FQDN
675 return array ($record['value']);
677 foreach (getObjectIPv4Allocations ($object_id) as $dottedquad => $alloc)
678 if ($alloc['type'] == 'regular')
679 $regular[] = $dottedquad;
680 if (!count ($regular) && strlen ($fallback))
681 return array ($fallback);
685 // Some records in the dictionary may be written as plain text or as Wiki
686 // link in the following syntax:
688 // 2. [[word URL]] // FIXME: this isn't working
689 // 3. [[word word word | URL]]
690 // This function parses the line and returns text suitable for either A
691 // (rendering <A HREF>) or O (for <OPTION>).
692 function parseWikiLink ($line, $which)
694 if (preg_match ('/^\[\[.+\]\]$/', $line) == 0)
696 // always strip the marker for A-data, but let cookOptgroup()
697 // do this later (otherwise it can't sort groups out)
699 return preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $line));
703 $line = preg_replace ('/^\[\[(.+)\]\]$/', '$1', $line);
704 $s = explode ('|', $line);
705 $o_value = trim ($s[0]);
708 $o_value = preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $o_value));
709 $a_value = trim ($s[1]);
710 return "<a href='${a_value}'>${o_value}</a>";
713 // FIXME: should this be saved as "P-data"?
714 function execGMarker ($line)
716 return preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $line));
719 // rackspace usage for a single rack
720 // (T + W + U) / (height * 3 - A)
721 function getRSUforRack ($data = NULL)
725 showWarning ('Invalid argument', __FUNCTION__
);
728 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
729 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
730 for ($locidx = 0; $locidx < 3; $locidx++
)
731 $counter[$data[$unit_no][$locidx]['state']]++
;
732 return ($counter['T'] +
$counter['W'] +
$counter['U']) / ($counter['T'] +
$counter['W'] +
$counter['U'] +
$counter['F']);
736 function getRSUforRackRow ($rowData = NULL)
738 if ($rowData === NULL)
740 showWarning ('Invalid argument', __FUNCTION__
);
743 if (!count ($rowData))
745 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
747 foreach (array_keys ($rowData) as $rack_id)
749 $data = spotEntity ('rack', $rack_id);
751 $total_height +
= $data['height'];
752 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
753 for ($locidx = 0; $locidx < 3; $locidx++
)
754 $counter[$data[$unit_no][$locidx]['state']]++
;
756 return ($counter['T'] +
$counter['W'] +
$counter['U']) / ($counter['T'] +
$counter['W'] +
$counter['U'] +
$counter['F']);
759 // Make sure the string is always wrapped with LF characters
760 function lf_wrap ($str)
762 $ret = trim ($str, "\r\n");
768 // Adopted from Mantis BTS code.
769 function string_insert_hrefs ($s)
771 if (getConfigVar ('DETECT_URLS') != 'yes')
773 # Find any URL in a string and replace it by a clickable link
774 $s = preg_replace( '/(([[:alpha:]][-+.[:alnum:]]*):\/\/(%[[:digit:]A-Fa-f]{2}|[-_.!~*\';\/?%^\\\\:@&={\|}+$#\(\),\[\][:alnum:]])+)/se',
775 "'<a href=\"'.rtrim('\\1','.').'\">\\1</a> [<a href=\"'.rtrim('\\1','.').'\" target=\"_blank\">^</a>]'",
777 $s = preg_replace( '/\b' . email_regex_simple() . '\b/i',
778 '<a href="mailto:\0">\0</a>',
784 function email_regex_simple ()
786 return "(([a-z0-9!#*+\/=?^_{|}~-]+(?:\.[a-z0-9!#*+\/=?^_{|}~-]+)*)" . # recipient
787 "\@((?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?))"; # @domain
790 // Parse AUTOPORTS_CONFIG and return a list of generated pairs (port_type, port_name)
791 // for the requested object_type_id.
792 function getAutoPorts ($type_id)
795 $typemap = explode (';', str_replace (' ', '', getConfigVar ('AUTOPORTS_CONFIG')));
796 foreach ($typemap as $equation)
798 $tmp = explode ('=', $equation);
799 if (count ($tmp) != 2)
801 $objtype_id = $tmp[0];
802 if ($objtype_id != $type_id)
805 foreach (explode ('+', $portlist) as $product)
807 $tmp = explode ('*', $product);
808 if (count ($tmp) != 3)
811 $port_type = $tmp[1];
813 for ($i = 0; $i < $nports; $i++
)
814 $ret[] = array ('type' => $port_type, 'name' => @sprintf
($format, $i));
820 // Use pre-served trace to traverse the tree, then place given node where it belongs.
821 function pokeNode (&$tree, $trace, $key, $value, $threshold = 0)
823 // This function needs the trace to be followed FIFO-way. The fastest
824 // way to do so is to use array_push() for putting values into the
825 // list and array_shift() for getting them out. This exposed up to 11%
826 // performance gain compared to other patterns of array_push/array_unshift/
827 // array_reverse/array_pop/array_shift conjunction.
828 $myid = array_shift ($trace);
829 if (!count ($trace)) // reached the target
831 if (!$threshold or ($threshold and $tree[$myid]['kidc'] +
1 < $threshold))
832 $tree[$myid]['kids'][$key] = $value;
833 // Reset accumulated records once, when the limit is reached, not each time
835 if (++
$tree[$myid]['kidc'] == $threshold)
836 $tree[$myid]['kids'] = array();
840 $self = __FUNCTION__
;
841 $self ($tree[$myid]['kids'], $trace, $key, $value, $threshold);
845 // Likewise traverse the tree with the trace and return the final node.
846 function peekNode ($tree, $trace, $target_id)
848 $self = __FUNCTION__
;
849 if (NULL === ($next = array_shift ($trace))) // warm
851 foreach ($tree as $node)
852 if (array_key_exists ('id', $node) and $node['id'] == $target_id) // hot
857 foreach ($tree as $node)
858 if (array_key_exists ('id', $node) and $node['id'] == $next) // warmer
859 return $self ($node['kids'], $trace, $target_id);
865 // Build a tree from the item list and return it. Input and output data is
866 // indexed by item id (nested items in output are recursively stored in 'kids'
867 // key, which is in turn indexed by id. Functions, which are ready to handle
868 // tree collapsion/expansion themselves, may request non-zero threshold value
869 // for smaller resulting tree.
870 function treeFromList (&$orig_nodelist, $threshold = 0, $return_main_payload = TRUE)
873 $nodelist = $orig_nodelist;
874 // Array equivalent of traceEntity() function.
876 // set kidc and kids only once
877 foreach (array_keys ($nodelist) as $nodeid)
879 $nodelist[$nodeid]['kidc'] = 0;
880 $nodelist[$nodeid]['kids'] = array();
885 foreach (array_keys ($nodelist) as $nodeid)
887 // When adding a node to the working tree, book another
888 // iteration, because the new item could make a way for
889 // others onto the tree. Also remove any item added from
890 // the input list, so iteration base shrinks.
891 // First check if we can assign directly.
892 if ($nodelist[$nodeid]['parent_id'] == NULL)
894 $tree[$nodeid] = $nodelist[$nodeid];
895 $trace[$nodeid] = array(); // Trace to root node is empty
896 unset ($nodelist[$nodeid]);
899 // Now look if it fits somewhere on already built tree.
900 elseif (isset ($trace[$nodelist[$nodeid]['parent_id']]))
902 // Trace to a node is a trace to its parent plus parent id.
903 $trace[$nodeid] = $trace[$nodelist[$nodeid]['parent_id']];
904 $trace[$nodeid][] = $nodelist[$nodeid]['parent_id'];
905 pokeNode ($tree, $trace[$nodeid], $nodeid, $nodelist[$nodeid], $threshold);
906 // path to any other node is made of all parent nodes plus the added node itself
907 unset ($nodelist[$nodeid]);
913 if (!$return_main_payload)
915 // update each input node with its backtrace route
916 foreach ($trace as $nodeid => $route)
917 $orig_nodelist[$nodeid]['trace'] = $route;
921 // Build a tree from the tag list and return everything _except_ the tree.
922 // IOW, return taginfo items, which have parent_id set and pointing outside
923 // of the "normal" tree, which originates from the root.
924 function getOrphanedTags ()
927 return treeFromList ($taglist, 0, FALSE);
930 function serializeTags ($chain, $baseurl = '')
934 foreach ($chain as $taginfo)
937 ($baseurl == '' ?
'' : "<a href='${baseurl}cft[]=${taginfo['id']}'>") .
939 ($baseurl == '' ?
'' : '</a>');
945 // Return the list of missing implicit tags.
946 function getImplicitTags ($oldtags)
950 foreach ($oldtags as $taginfo)
951 $tmp = array_merge ($tmp, $taglist[$taginfo['id']]['trace']);
952 // don't call array_unique here, it is in the function we will call now
953 return buildTagChainFromIds ($tmp);
956 // Minimize the chain: exclude all implicit tags and return the result.
957 // This function makes use of an external cache with a miss/hit ratio
958 // about 3/7 (ticket:255).
959 function getExplicitTagsOnly ($chain)
961 global $taglist, $tagRelCache;
963 foreach (array_keys ($chain) as $keyA) // check each A
965 $tagidA = $chain[$keyA]['id'];
966 // do not include A in result, if A is seen on the trace of any B!=A
967 foreach (array_keys ($chain) as $keyB)
969 $tagidB = $chain[$keyB]['id'];
970 if ($tagidA == $tagidB)
972 if (!isset ($tagRelCache[$tagidA][$tagidB]))
973 $tagRelCache[$tagidA][$tagidB] = in_array ($tagidA, $taglist[$tagidB]['trace']);
974 if ($tagRelCache[$tagidA][$tagidB] === TRUE) // A is ancestor of B
975 continue 2; // skip this A
977 $ret[] = $chain[$keyA];
982 // Universal autotags generator, a complementing function for loadEntityTags().
983 // Bypass key isn't strictly typed, but interpreted depending on the realm.
984 function generateEntityAutoTags ($cell)
987 switch ($cell['realm'])
990 $ret[] = array ('tag' => '$rackid_' . $cell['id']);
991 $ret[] = array ('tag' => '$any_rack');
994 $ret[] = array ('tag' => '$id_' . $cell['id']);
995 $ret[] = array ('tag' => '$typeid_' . $cell['objtype_id']);
996 $ret[] = array ('tag' => '$any_object');
997 if (validTagName ('$cn_' . $cell['name'], TRUE))
998 $ret[] = array ('tag' => '$cn_' . $cell['name']);
999 if (!strlen ($cell['rack_id']))
1000 $ret[] = array ('tag' => '$unmounted');
1001 if (!$cell['nports'])
1002 $ret[] = array ('tag' => '$portless');
1003 if ($cell['asset_no'] == '')
1004 $ret[] = array ('tag' => '$no_asset_tag');
1005 if ($cell['runs8021Q'])
1006 $ret[] = array ('tag' => '$runs_8021Q');
1008 // dictionary attribute autotags '$attr_X_Y'
1009 $attrs = getAttrValues($cell['id']);
1010 foreach ($attrs as $attr_id => $attr_record)
1011 if (isset ($attr_record['key']))
1012 $ret[] = array ('tag' => "\$attr_{$attr_id}_{$attr_record['key']}");
1015 $ret[] = array ('tag' => '$ip4netid_' . $cell['id']);
1016 $ret[] = array ('tag' => '$ip4net-' . str_replace ('.', '-', $cell['ip']) . '-' . $cell['mask']);
1017 for ($i = 8; $i < 32; $i++
)
1019 // these conditions hit 1 to 3 times per each i
1020 if ($cell['mask'] >= $i)
1021 $ret[] = array ('tag' => '$masklen_ge_' . $i);
1022 if ($cell['mask'] <= $i)
1023 $ret[] = array ('tag' => '$masklen_le_' . $i);
1024 if ($cell['mask'] == $i)
1025 $ret[] = array ('tag' => '$masklen_eq_' . $i);
1027 $ret[] = array ('tag' => '$any_ip4net');
1028 $ret[] = array ('tag' => '$any_net');
1031 $ret[] = array ('tag' => '$ip6netid_' . $cell['id']);
1032 $ret[] = array ('tag' => '$any_ip6net');
1033 $ret[] = array ('tag' => '$any_net');
1036 $ret[] = array ('tag' => '$ipv4vsid_' . $cell['id']);
1037 $ret[] = array ('tag' => '$any_ipv4vs');
1038 $ret[] = array ('tag' => '$any_vs');
1041 $ret[] = array ('tag' => '$ipv4rspid_' . $cell['id']);
1042 $ret[] = array ('tag' => '$any_ipv4rsp');
1043 $ret[] = array ('tag' => '$any_rsp');
1046 // {$username_XXX} autotag is generated always, but {$userid_XXX}
1047 // appears only for accounts, which exist in local database.
1048 $ret[] = array ('tag' => '$username_' . $cell['user_name']);
1049 if (isset ($cell['user_id']))
1050 $ret[] = array ('tag' => '$userid_' . $cell['user_id']);
1053 $ret[] = array ('tag' => '$fileid_' . $cell['id']);
1054 $ret[] = array ('tag' => '$any_file');
1059 // {$tagless} doesn't apply to users
1060 switch ($cell['realm'])
1068 if (!count ($cell['etags']))
1069 $ret[] = array ('tag' => '$untagged');
1077 // Check, if the given tag is present on the chain (will only work
1078 // for regular tags with tag ID set.
1079 function tagOnChain ($taginfo, $tagchain)
1081 if (!isset ($taginfo['id']))
1083 foreach ($tagchain as $test)
1084 if ($test['id'] == $taginfo['id'])
1089 function tagNameOnChain ($tagname, $tagchain)
1091 foreach ($tagchain as $test)
1092 if ($test['tag'] == $tagname)
1097 // Return TRUE, if two tags chains differ (order of tags doesn't matter).
1098 // Assume, that neither of the lists contains duplicates.
1099 // FIXME: a faster, than O(x^2) method is possible for this calculation.
1100 function tagChainCmp ($chain1, $chain2)
1102 if (count ($chain1) != count ($chain2))
1104 foreach ($chain1 as $taginfo1)
1105 if (!tagOnChain ($taginfo1, $chain2))
1110 function redirectIfNecessary ()
1118 'accounts' => 'userlist',
1119 'rspools' => 'ipv4rsplist',
1120 'rspool' => 'ipv4rsp',
1121 'vservices' => 'ipv4vslist',
1122 'vservice' => 'ipv4vs',
1123 'objects' => 'depot',
1124 'objgroup' => 'depot',
1127 $tmap['objects']['newmulti'] = 'addmore';
1128 $tmap['objects']['newobj'] = 'addmore';
1129 $tmap['object']['switchvlans'] = 'livevlans';
1130 $tmap['object']['slb'] = 'editrspvs';
1131 $tmap['object']['portfwrd'] = 'nat4';
1132 $tmap['object']['network'] = 'ipv4';
1133 if (isset ($pmap[$pageno]))
1134 redirectUser ($pmap[$pageno], $tabno);
1135 if (isset ($tmap[$pageno][$tabno]))
1136 redirectUser ($pageno, $tmap[$pageno][$tabno]);
1137 // check if we accidentaly got on a dynamic tab that shouldn't be shown for this object
1140 isset ($trigger[$pageno][$tabno]) and
1141 !strlen (call_user_func ($trigger[$pageno][$tabno]))
1143 redirectUser ($pageno, 'default');
1146 function prepareNavigation()
1152 $pageno = (isset ($_REQUEST['page'])) ?
$_REQUEST['page'] : 'index';
1154 // Special handling of tab number to substitute the "last" index where applicable.
1155 // Always show explicitly requested tab, substitute the last used name in case
1156 // it is awailable, fall back to the default one.
1158 if (isset ($_REQUEST['tab']))
1159 $tabno = $_REQUEST['tab'];
1162 basename($_SERVER['PHP_SELF']) == 'index.php' and
1163 getConfigVar ('SHOW_LAST_TAB') == 'yes' and
1164 isset ($_SESSION['RTLT'][$pageno]) and
1165 permitted ($pageno, $_SESSION['RTLT'][$pageno])
1167 redirectUser ($pageno, $_SESSION['RTLT'][$pageno]);
1172 function fixContext ($target = NULL)
1184 if ($target !== NULL)
1186 $target_given_tags = $target['etags'];
1187 // Don't reset autochain, because auth procedures could push stuff there in.
1188 // Another important point is to ignore 'user' realm, so we don't infuse effective
1189 // context with autotags of the displayed account.
1190 if ($target['realm'] != 'user')
1191 $auto_tags = array_merge ($auto_tags, $target['atags']);
1193 elseif (array_key_exists ($pageno, $etype_by_pageno))
1195 // Each page listed in the map above requires one uint argument.
1196 $target_realm = $etype_by_pageno[$pageno];
1197 assertUIntArg ($page[$pageno]['bypass']);
1198 $target_id = $_REQUEST[$page[$pageno]['bypass']];
1199 $target = spotEntity ($target_realm, $target_id);
1200 $target_given_tags = $target['etags'];
1201 if ($target['realm'] != 'user')
1202 $auto_tags = array_merge ($auto_tags, $target['atags']);
1204 // Explicit and implicit chains should be normally empty at this point, so
1205 // overwrite the contents anyway.
1206 $expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1207 $impl_tags = getImplicitTags ($expl_tags);
1210 // Take a list of user-supplied tag IDs to build a list of valid taginfo
1211 // records indexed by tag IDs (tag chain).
1212 function buildTagChainFromIds ($tagidlist)
1216 foreach (array_unique ($tagidlist) as $tag_id)
1217 if (isset ($taglist[$tag_id]))
1218 $ret[] = $taglist[$tag_id];
1222 // Process a given tag tree and return only meaningful branches. The resulting
1223 // (sub)tree will have refcnt leaves on every last branch.
1224 function getObjectiveTagTree ($tree, $realm, $preselect)
1226 $self = __FUNCTION__
;
1228 foreach ($tree as $taginfo)
1230 $subsearch = $self ($taginfo['kids'], $realm, $preselect);
1231 // If the current node addresses something, add it to the result
1232 // regardless of how many sub-nodes it features.
1235 isset ($taginfo['refcnt'][$realm]) or
1236 count ($subsearch) > 1 or
1237 in_array ($taginfo['id'], $preselect)
1241 'id' => $taginfo['id'],
1242 'tag' => $taginfo['tag'],
1243 'parent_id' => $taginfo['parent_id'],
1244 'refcnt' => $taginfo['refcnt'],
1245 'kids' => $subsearch
1248 $ret = array_merge ($ret, $subsearch);
1253 // Preprocess tag tree to get only tags which can effectively reduce given filter result,
1254 // than passes shrinked tag tree to getObjectiveTagTree and return its result.
1255 // This makes sense only if andor mode is 'and', otherwise function does not modify tree.
1256 // 'Given filter' is a pair of $entity_list(filter result) and $preselect(filter data).
1257 // 'Effectively' means reduce to non-empty result.
1258 function getShrinkedTagTree($entity_list, $realm, $preselect) {
1260 if ($preselect['andor'] != 'and' ||
empty($entity_list) && $preselect['is_empty'])
1261 return getObjectiveTagTree($tagtree, $realm, $preselect['tagidlist']);
1263 $used_tags = array(); //associative, keys - tag ids, values - taginfos
1264 foreach ($entity_list as $entity)
1266 foreach ($entity['etags'] as $etag)
1267 if (! array_key_exists($etag['id'], $used_tags))
1268 $used_tags[$etag['id']] = 1;
1270 $used_tags[$etag['id']]++
;
1272 foreach ($entity['itags'] as $itag)
1273 if (! array_key_exists($itag['id'], $used_tags))
1274 $used_tags[$itag['id']] = 0;
1277 $shrinked_tree = shrinkSubtree($tagtree, $used_tags, $preselect, $realm);
1278 return getObjectiveTagTree($shrinked_tree, $realm, $preselect['tagidlist']);
1281 // deletes item from tag subtree unless it exists in $used_tags and not preselected
1282 function shrinkSubtree($tree, $used_tags, $preselect, $realm) {
1283 $self = __FUNCTION__
;
1285 foreach($tree as $i => &$item) {
1286 $item['kids'] = $self($item['kids'], $used_tags, $preselect, $realm);
1287 $item['kidc'] = count($item['kids']);
1290 ! array_key_exists($item['id'], $used_tags) &&
1291 ! in_array($item['id'], $preselect['tagidlist']) &&
1296 $item['refcnt'][$realm] = $used_tags[$item['id']];
1297 if (! $item['refcnt'][$realm])
1298 unset($item['refcnt'][$realm]);
1304 // Get taginfo record by tag name, return NULL, if record doesn't exist.
1305 function getTagByName ($target_name)
1308 foreach ($taglist as $taginfo)
1309 if ($taginfo['tag'] == $target_name)
1314 // Merge two chains, filtering dupes out. Return the resulting superset.
1315 function mergeTagChains ($chainA, $chainB)
1318 // Reindex by tag id in any case.
1320 foreach ($chainA as $tag)
1321 $ret[$tag['id']] = $tag;
1322 foreach ($chainB as $tag)
1323 if (!isset ($ret[$tag['id']]))
1324 $ret[$tag['id']] = $tag;
1328 function getCellFilter ()
1332 $staticFilter = getConfigVar ('STATIC_FILTER');
1333 if (isset ($_REQUEST['tagfilter']) and is_array ($_REQUEST['tagfilter']))
1335 $_REQUEST['cft'] = $_REQUEST['tagfilter'];
1336 unset ($_REQUEST['tagfilter']);
1338 $andor_used = FALSE;
1339 //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.
1340 if(isset($_REQUEST['andor']))
1343 unset($_SESSION[$pageno]);
1345 if (isset ($_SESSION[$pageno]['tagfilter']) and is_array ($_SESSION[$pageno]['tagfilter']) and !(isset($_REQUEST['cft'])) and $staticFilter == 'yes')
1347 $_REQUEST['cft'] = $_SESSION[$pageno]['tagfilter'];
1349 if (isset ($_SESSION[$pageno]['cfe']) and !(isset($sic['cfe'])) and $staticFilter == 'yes')
1351 $sic['cfe'] = $_SESSION[$pageno]['cfe'];
1353 if (isset ($_SESSION[$pageno]['andor']) and !(isset($_REQUEST['andor'])) and $staticFilter == 'yes')
1355 $_REQUEST['andor'] = $_SESSION[$pageno]['andor'];
1361 'tagidlist' => array(),
1362 'tnamelist' => array(),
1363 'pnamelist' => array(),
1367 'expression' => array(),
1368 'urlextra' => '', // Just put text here and let makeHref call urlencode().
1373 case (!isset ($_REQUEST['andor'])):
1374 $andor2 = getConfigVar ('FILTER_DEFAULT_ANDOR');
1376 case ($_REQUEST['andor'] == 'and'):
1377 case ($_REQUEST['andor'] == 'or'):
1378 $_SESSION[$pageno]['andor'] = $_REQUEST['andor'];
1379 $ret['andor'] = $andor2 = $_REQUEST['andor'];
1382 showWarning ('Invalid and/or switch value in submitted form', __FUNCTION__
);
1386 // Both tags and predicates, which don't exist, should be
1387 // handled somehow. Discard them silently for now.
1388 if (isset ($_REQUEST['cft']) and is_array ($_REQUEST['cft']))
1390 $_SESSION[$pageno]['tagfilter'] = $_REQUEST['cft'];
1392 foreach ($_REQUEST['cft'] as $req_id)
1393 if (isset ($taglist[$req_id]))
1395 $ret['tagidlist'][] = $req_id;
1396 $ret['tnamelist'][] = $taglist[$req_id]['tag'];
1397 $andor_used = $andor_used ||
(trim($andor1) != '');
1398 $ret['text'] .= $andor1 . '{' . $taglist[$req_id]['tag'] . '}';
1399 $andor1 = ' ' . $andor2 . ' ';
1400 $ret['urlextra'] .= '&cft[]=' . $req_id;
1403 if (isset ($_REQUEST['cfp']) and is_array ($_REQUEST['cfp']))
1406 foreach ($_REQUEST['cfp'] as $req_name)
1407 if (isset ($pTable[$req_name]))
1409 $ret['pnamelist'][] = $req_name;
1410 $andor_used = $andor_used ||
(trim($andor1) != '');
1411 $ret['text'] .= $andor1 . '[' . $req_name . ']';
1412 $andor1 = ' ' . $andor2 . ' ';
1413 $ret['urlextra'] .= '&cfp[]=' . $req_name;
1416 // Extra text comes from TEXTAREA and is easily screwed by standard escaping function.
1417 if (isset ($sic['cfe']))
1419 $_SESSION[$pageno]['cfe'] = $sic['cfe'];
1420 // Only consider extra text, when it is a correct RackCode expression.
1421 $parse = spotPayload ($sic['cfe'], 'SYNT_EXPR');
1422 if ($parse['result'] == 'ACK')
1424 $ret['extratext'] = trim ($sic['cfe']);
1425 $ret['urlextra'] .= '&cfe=' . $ret['extratext'];
1428 $finaltext = array();
1429 if (strlen ($ret['text']))
1430 $finaltext[] = '(' . $ret['text'] . ')';
1431 if (strlen ($ret['extratext']))
1432 $finaltext[] = '(' . $ret['extratext'] . ')';
1433 $andor_used = $andor_used ||
(count($finaltext) > 1);
1434 $finaltext = implode (' ' . $andor2 . ' ', $finaltext);
1435 if (strlen ($finaltext))
1437 $ret['is_empty'] = FALSE;
1438 $parse = spotPayload ($finaltext, 'SYNT_EXPR');
1439 $ret['expression'] = $parse['result'] == 'ACK' ?
$parse['load'] : NULL;
1440 // It's not quite fair enough to put the blame of the whole text onto
1441 // non-empty "extra" portion of it, but it's the only user-generated portion
1442 // of it, thus the most probable cause of parse error.
1443 if (strlen ($ret['extratext']))
1444 $ret['extraclass'] = $parse['result'] == 'ACK' ?
'validation-success' : 'validation-error';
1447 $ret['andor'] = getConfigVar ('FILTER_DEFAULT_ANDOR');
1449 $ret['urlextra'] .= '&andor=' . $ret['andor'];
1453 // Return an empty message log.
1454 function emptyLog ()
1463 // Return a message log consisting of only one message.
1464 function oneLiner ($code, $args = array())
1467 $ret['m'][] = count ($args) ?
array ('c' => $code, 'a' => $args) : array ('c' => $code);
1471 // Merge message payload from two message logs given and return the result.
1472 function mergeLogs ($log1, $log2)
1475 $ret['m'] = array_merge ($log1['m'], $log2['m']);
1479 function validTagName ($s, $allow_autotag = FALSE)
1481 if (1 == preg_match (TAGNAME_REGEXP
, $s))
1483 if ($allow_autotag and 1 == preg_match (AUTOTAGNAME_REGEXP
, $s))
1488 function redirectUser ($p, $t)
1491 $l = "index.php?page=${p}&tab=${t}";
1492 if (isset ($page[$p]['bypass']) and isset ($_REQUEST[$page[$p]['bypass']]))
1493 $l .= '&' . $page[$p]['bypass'] . '=' . $_REQUEST[$page[$p]['bypass']];
1494 header ("Location: " . $l);
1498 function getRackCodeStats ()
1501 $defc = $grantc = $modc = 0;
1502 foreach ($rackCode as $s)
1505 case 'SYNT_DEFINITION':
1519 'Definition sentences' => $defc,
1520 'Grant sentences' => $grantc,
1521 'Context mod sentences' => $modc
1526 function getRackImageWidth ()
1529 return 3 +
$rtwidth[0] +
$rtwidth[1] +
$rtwidth[2] +
3;
1532 function getRackImageHeight ($units)
1534 return 3 +
3 +
$units * 2;
1537 // Perform substitutions and return resulting string
1538 // used solely by buildLVSConfig()
1539 function apply_macros ($macros, $subject, &$error_macro_stat)
1541 // clear all text before last %RESET% macro
1542 $reset_keyword = '%RESET%';
1543 $reset_position = mb_strpos($subject, $reset_keyword, 0);
1544 if ($reset_position === FALSE)
1549 mb_substr($subject, $reset_position +
mb_strlen($reset_keyword)),
1553 foreach ($macros as $search => $replace)
1555 if (empty($replace))
1557 $replace = "<span class=\"msg_error\">$search</span>";
1559 $ret = str_replace ($search, $replace, $ret, $count);
1562 if (array_key_exists($search, $error_macro_stat))
1563 $error_macro_stat[$search] +
= $count;
1565 $error_macro_stat[$search] = $count;
1569 $ret = str_replace ($search, $replace, $ret);
1574 // throws RTBuildLVSConfigError exception if undefined macros found
1575 function buildLVSConfig ($object_id = 0)
1577 if ($object_id <= 0)
1579 showWarning ('Invalid argument', __FUNCTION__
);
1582 $oInfo = spotEntity ('object', $object_id);
1583 $defaults = getSLBDefaults (TRUE);
1584 $lbconfig = getSLBConfig ($object_id);
1585 if ($lbconfig === NULL)
1587 showWarning ('getSLBConfig() failed', __FUNCTION__
);
1590 $newconfig = "#\n#\n# This configuration has been generated automatically by RackTables\n";
1591 $newconfig .= "# for object_id == ${object_id}\n# object name: ${oInfo['name']}\n#\n#\n\n\n";
1593 $error_stat = array();
1594 foreach ($lbconfig as $vs_id => $vsinfo)
1596 $newconfig .= "########################################################\n" .
1597 "# VS (id == ${vs_id}): " . (!strlen ($vsinfo['vs_name']) ?
'NO NAME' : $vsinfo['vs_name']) . "\n" .
1598 "# RS pool (id == ${vsinfo['pool_id']}): " . (!strlen ($vsinfo['pool_name']) ?
'ANONYMOUS' : $vsinfo['pool_name']) . "\n" .
1599 "########################################################\n";
1600 # The order of inheritance is: VS -> LB -> pool [ -> RS ]
1603 '%VIP%' => $vsinfo['vip'],
1604 '%VPORT%' => $vsinfo['vport'],
1605 '%PROTO%' => $vsinfo['proto'],
1606 '%VNAME%' => $vsinfo['vs_name'],
1607 '%RSPOOLNAME%' => $vsinfo['pool_name'],
1608 '%PRIO%' => $vsinfo['prio']
1610 $newconfig .= "virtual_server ${vsinfo['vip']} ${vsinfo['vport']} {\n";
1611 $newconfig .= "\tprotocol ${vsinfo['proto']}\n";
1612 $newconfig .= lf_wrap (apply_macros
1615 lf_wrap ($defaults['vs']) .
1616 lf_wrap ($vsinfo['vs_vsconfig']) .
1617 lf_wrap ($vsinfo['lb_vsconfig']) .
1618 lf_wrap ($vsinfo['pool_vsconfig']),
1621 foreach ($vsinfo['rslist'] as $rs)
1623 if (!strlen ($rs['rsport']))
1624 $rs['rsport'] = $vsinfo['vport'];
1625 $macros['%RSIP%'] = $rs['rsip'];
1626 $macros['%RSPORT%'] = $rs['rsport'];
1627 $newconfig .= "\treal_server ${rs['rsip']} ${rs['rsport']} {\n";
1628 $newconfig .= lf_wrap (apply_macros
1631 lf_wrap ($defaults['rs']) .
1632 lf_wrap ($vsinfo['vs_rsconfig']) .
1633 lf_wrap ($vsinfo['lb_rsconfig']) .
1634 lf_wrap ($vsinfo['pool_rsconfig']) .
1635 lf_wrap ($rs['rs_rsconfig']),
1638 $newconfig .= "\t}\n";
1640 $newconfig .= "}\n\n\n";
1642 if (! empty($error_stat))
1644 $error_messages = array();
1645 foreach ($error_stat as $macro => $count)
1646 $error_messages[] = "Error: macro $macro can not be empty ($count occurences)";
1647 throw new RTBuildLVSConfigError($error_messages, $newconfig, $object_id);
1650 // FIXME: deal somehow with Mac-styled text, the below replacement will screw it up
1651 return dos2unix ($newconfig);
1654 // Indicate occupation state of each IP address: none, ordinary or problematic.
1655 function markupIPAddrList (&$addrlist)
1657 foreach (array_keys ($addrlist) as $ip_bin)
1661 'shared' => 0, // virtual
1662 'virtual' => 0, // loopback
1663 'regular' => 0, // connected host
1664 'router' => 0 // connected gateway
1666 foreach ($addrlist[$ip_bin]['allocs'] as $a)
1667 $refc[$a['type']]++
;
1668 $nvirtloopback = ($refc['shared'] +
$refc['virtual'] > 0) ?
1 : 0; // modulus of virtual + shared
1669 $nreserved = ($addrlist[$ip_bin]['reserved'] == 'yes') ?
1 : 0; // only one reservation is possible ever
1670 $nrealms = $nreserved +
$nvirtloopback +
$refc['regular'] +
$refc['router']; // latter two are connected and router allocations
1673 $addrlist[$ip_bin]['class'] = 'trbusy';
1674 elseif ($nrealms > 1)
1675 $addrlist[$ip_bin]['class'] = 'trerror';
1677 $addrlist[$ip_bin]['class'] = '';
1681 // Scan the given address list (returned by scanIPv4Space/scanIPv6Space) and return a list of all routers found.
1682 function findRouters ($addrlist)
1685 foreach ($addrlist as $addr)
1686 foreach ($addr['allocs'] as $alloc)
1687 if ($alloc['type'] == 'router')
1690 'id' => $alloc['object_id'],
1691 'iface' => $alloc['name'],
1692 'dname' => $alloc['object_name'],
1693 'addr' => $addr['ip']
1698 // Assist in tag chain sorting.
1699 function taginfoCmp ($tagA, $tagB)
1701 return $tagA['ci'] - $tagB['ci'];
1704 // Compare networks. When sorting a tree, the records on the list will have
1705 // distinct base IP addresses.
1706 // "The comparison function must return an integer less than, equal to, or greater
1707 // than zero if the first argument is considered to be respectively less than,
1708 // equal to, or greater than the second." (c) PHP manual
1709 function IPv4NetworkCmp ($netA, $netB)
1711 // On 64-bit systems this function can be reduced to just this:
1712 if (PHP_INT_SIZE
== 8)
1713 return $netA['ip_bin'] - $netB['ip_bin'];
1714 // There's a problem just substracting one u32 integer from another,
1715 // because the result may happen big enough to become a negative i32
1716 // integer itself (PHP tries to cast everything it sees to signed int)
1717 // The comparison below must treat positive and negative values of both
1719 // Equal values give instant decision regardless of their [equal] sign.
1720 if ($netA['ip_bin'] == $netB['ip_bin'])
1722 // Same-signed values compete arithmetically within one of i32 contiguous ranges:
1723 // 0x00000001~0x7fffffff 1~2147483647
1724 // 0 doesn't have any sign, and network 0.0.0.0 isn't allowed
1725 // 0x80000000~0xffffffff -2147483648~-1
1726 $signA = $netA['ip_bin'] / abs ($netA['ip_bin']);
1727 $signB = $netB['ip_bin'] / abs ($netB['ip_bin']);
1728 if ($signA == $signB)
1730 if ($netA['ip_bin'] > $netB['ip_bin'])
1735 else // With only one of two values being negative, it... wins!
1737 if ($netA['ip_bin'] < $netB['ip_bin'])
1744 function IPv6NetworkCmp ($netA, $netB)
1746 return strcmp ($netA['ip_bin'], $netB['ip_bin']);
1749 // Modify the given tag tree so, that each level's items are sorted alphabetically.
1750 function sortTree (&$tree, $sortfunc = '')
1752 if (!strlen ($sortfunc))
1754 $self = __FUNCTION__
;
1755 usort ($tree, $sortfunc);
1756 // Don't make a mistake of directly iterating over the items of current level, because this way
1757 // the sorting will be performed on a _copy_ if each item, not the item itself.
1758 foreach (array_keys ($tree) as $tagid)
1759 $self ($tree[$tagid]['kids'], $sortfunc);
1762 function iptree_fill (&$netdata)
1764 if (!isset ($netdata['kids']) or !count ($netdata['kids']))
1766 // If we really have nested prefixes, they must fit into the tree.
1769 'ip_bin' => $netdata['ip_bin'],
1770 'mask' => $netdata['mask']
1772 foreach ($netdata['kids'] as $pfx)
1773 iptree_embed ($worktree, $pfx);
1774 $netdata['kids'] = iptree_construct ($worktree);
1775 $netdata['kidc'] = count ($netdata['kids']);
1778 function ipv6tree_fill (&$netdata)
1780 if (!isset ($netdata['kids']) or !count ($netdata['kids']))
1782 // If we really have nested prefixes, they must fit into the tree.
1785 'ip_bin' => $netdata['ip_bin'],
1786 'mask' => $netdata['mask']
1788 foreach ($netdata['kids'] as $pfx)
1789 ipv6tree_embed ($worktree, $pfx);
1790 $netdata['kids'] = ipv6tree_construct ($worktree);
1791 $netdata['kidc'] = count ($netdata['kids']);
1794 function iptree_construct ($node)
1796 $self = __FUNCTION__
;
1798 if (!isset ($node['right']))
1800 if (!isset ($node['ip']))
1802 $node['ip'] = long2ip ($node['ip_bin']);
1803 $node['kids'] = array();
1807 return array ($node);
1810 return array_merge ($self ($node['left']), $self ($node['right']));
1813 function ipv6tree_construct ($node)
1815 $self = __FUNCTION__
;
1817 if (!isset ($node['right']))
1819 if (!isset ($node['ip']))
1821 $node['ip'] = $node['ip_bin']->format();
1822 $node['kids'] = array();
1826 return array ($node);
1829 return array_merge ($self ($node['left']), $self ($node['right']));
1832 function iptree_embed (&$node, $pfx)
1834 $self = __FUNCTION__
;
1837 if ($node['ip_bin'] == $pfx['ip_bin'] and $node['mask'] == $pfx['mask'])
1842 if ($node['mask'] == $pfx['mask'])
1843 throw new RackTablesError ('the recurring loop lost control', RackTablesError
::INTERNAL
);
1846 if (!isset ($node['right']))
1848 // Fill in db_first/db_last to make it possible to run scanIPv4Space() on the node.
1849 $node['left']['mask'] = $node['mask'] +
1;
1850 $node['left']['ip_bin'] = $node['ip_bin'];
1851 $node['left']['db_first'] = sprintf ('%u', $node['left']['ip_bin']);
1852 $node['left']['db_last'] = sprintf ('%u', $node['left']['ip_bin'] |
binInvMaskFromDec ($node['left']['mask']));
1854 $node['right']['mask'] = $node['mask'] +
1;
1855 $node['right']['ip_bin'] = $node['ip_bin'] +
binInvMaskFromDec ($node['mask'] +
1) +
1;
1856 $node['right']['db_first'] = sprintf ('%u', $node['right']['ip_bin']);
1857 $node['right']['db_last'] = sprintf ('%u', $node['right']['ip_bin'] |
binInvMaskFromDec ($node['right']['mask']));
1861 if (($node['left']['ip_bin'] & binMaskFromDec ($node['left']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1862 $self ($node['left'], $pfx);
1863 elseif (($node['right']['ip_bin'] & binMaskFromDec ($node['right']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1864 $self ($node['right'], $pfx);
1866 throw new RackTablesError ('cannot decide between left and right', RackTablesError
::INTERNAL
);
1869 function ipv6tree_embed (&$node, $pfx)
1871 $self = __FUNCTION__
;
1874 if ($node['ip_bin'] == $pfx['ip_bin'] and $node['mask'] == $pfx['mask'])
1879 if ($node['mask'] == $pfx['mask'])
1880 throw new RackTablesError ('the recurring loop lost control', RackTablesError
::INTERNAL
);
1883 if (!isset ($node['right']))
1885 $node['left']['mask'] = $node['mask'] +
1;
1886 $node['left']['ip_bin'] = $node['ip_bin'];
1887 $node['left']['db_first'] = $node['ip_bin']->get_first_subnet_address ($node['mask'] +
1);
1888 $node['left']['db_last'] = $node['ip_bin']->get_last_subnet_address ($node['mask'] +
1);
1890 $node['right']['mask'] = $node['mask'] +
1;
1891 $node['right']['ip_bin'] = $node['ip_bin']->get_last_subnet_address ($node['mask'] +
1)->next();
1892 $node['right']['db_first'] = $node['right']['ip_bin'];
1893 $node['right']['db_last'] = $node['right']['ip_bin']->get_last_subnet_address ($node['mask'] +
1);
1897 if ($node['left']['db_first'] == $pfx['ip_bin']->get_first_subnet_address ($node['left']['mask']))
1898 $self ($node['left'], $pfx);
1899 elseif ($node['right']['db_first'] == $pfx['ip_bin']->get_first_subnet_address ($node['left']['mask']))
1900 $self ($node['right'], $pfx);
1902 throw new RackTablesError ('cannot decide between left and right', RackTablesError
::INTERNAL
);
1905 function treeApplyFunc (&$tree, $func = '', $stopfunc = '')
1907 if (!strlen ($func))
1909 $self = __FUNCTION__
;
1910 foreach (array_keys ($tree) as $key)
1912 $func ($tree[$key]);
1913 if (strlen ($stopfunc) and $stopfunc ($tree[$key]))
1915 $self ($tree[$key]['kids'], $func);
1919 function loadIPv4AddrList (&$netinfo)
1921 loadOwnIPv4Addresses ($netinfo);
1922 markupIPAddrList ($netinfo['addrlist']);
1925 function countOwnIPv4Addresses (&$node)
1928 if (empty ($node['kids']))
1929 $node['addrt'] = binInvMaskFromDec ($node['mask']) +
1;
1931 foreach ($node['kids'] as $nested)
1932 if (!isset ($nested['id'])) // spare
1933 $node['addrt'] +
= binInvMaskFromDec ($nested['mask']) +
1;
1936 function nodeIsCollapsed ($node)
1938 return $node['symbol'] == 'node-collapsed';
1941 // implies countOwnIPv4Addresses
1942 function loadOwnIPv4Addresses (&$node)
1946 if (!isset ($node['kids']) or !count ($node['kids']))
1948 $toscan[] = array ('i32_first' => $node['db_first'], 'i32_last' => $node['db_last']);
1949 $node['addrt'] = $node['db_last'] - $node['db_first'] +
1;
1954 foreach ($node['kids'] as $nested)
1955 if (!isset ($nested['id'])) // spare
1957 $toscan[] = array ('i32_first' => $nested['db_first'], 'i32_last' => $nested['db_last']);
1958 $node['addrt'] +
= $node['db_last'] - $node['db_first'] +
1;
1961 $node['addrlist'] = scanIPv4Space ($toscan);
1962 $node['addrc'] = count ($node['addrlist']);
1965 function loadIPv6AddrList (&$netinfo)
1967 loadOwnIPv6Addresses ($netinfo);
1968 markupIPAddrList ($netinfo['addrlist']);
1971 function loadOwnIPv6Addresses (&$node)
1975 if (empty ($node['kids']))
1976 $toscan[] = array ('first' => $node['ip_bin'], 'last' => $node['ip_bin']->get_last_subnet_address ($node['mask']));
1978 foreach ($node['kids'] as $nested)
1979 if (!isset ($nested['id'])) // spare
1980 $toscan[] = array ('first' => $nested['ip_bin'], 'last' => $nested['ip_bin']->get_last_subnet_address ($nested['mask']));
1981 $node['addrlist'] = scanIPv6Space ($toscan);
1982 $node['addrc'] = count ($node['addrlist']);
1985 function prepareIPv4Tree ($netlist, $expanded_id = 0)
1987 // treeFromList() requires parent_id to be correct for an item to get onto the tree,
1988 // so perform necessary pre-processing to make orphans belong to root. This trick
1989 // was earlier performed by getIPv4NetworkList().
1990 $netids = array_keys ($netlist);
1991 foreach ($netids as $cid)
1992 if (!in_array ($netlist[$cid]['parent_id'], $netids))
1993 $netlist[$cid]['parent_id'] = NULL;
1994 $tree = treeFromList ($netlist); // medium call
1995 sortTree ($tree, 'IPv4NetworkCmp');
1996 // complement the tree before markup to make the spare networks have "symbol" set
1997 treeApplyFunc ($tree, 'iptree_fill');
1998 iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
1999 // count addresses after the markup to skip computation for hidden tree nodes
2000 treeApplyFunc ($tree, 'countOwnIPv4Addresses', 'nodeIsCollapsed');
2004 function prepareIPv6Tree ($netlist, $expanded_id = 0)
2006 // treeFromList() requires parent_id to be correct for an item to get onto the tree,
2007 // so perform necessary pre-processing to make orphans belong to root. This trick
2008 // was earlier performed by getIPv4NetworkList().
2009 $netids = array_keys ($netlist);
2010 foreach ($netids as $cid)
2011 if (!in_array ($netlist[$cid]['parent_id'], $netids))
2012 $netlist[$cid]['parent_id'] = NULL;
2013 $tree = treeFromList ($netlist); // medium call
2014 sortTree ($tree, 'IPv6NetworkCmp');
2015 // complement the tree before markup to make the spare networks have "symbol" set
2016 treeApplyFunc ($tree, 'ipv6tree_fill');
2017 iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
2021 // Check all items of the tree recursively, until the requested target id is
2022 // found. Mark all items leading to this item as "expanded", collapsing all
2023 // the rest, which exceed the given threshold (if the threshold is given).
2024 function iptree_markup_collapsion (&$tree, $threshold = 1024, $target = 0)
2026 $self = __FUNCTION__
;
2028 foreach (array_keys ($tree) as $key)
2030 $here = ($target === 'ALL' or ($target > 0 and isset ($tree[$key]['id']) and $tree[$key]['id'] == $target));
2031 $below = $self ($tree[$key]['kids'], $threshold, $target);
2032 if (!$tree[$key]['kidc']) // terminal node
2033 $tree[$key]['symbol'] = 'spacer';
2034 elseif ($tree[$key]['kidc'] < $threshold)
2035 $tree[$key]['symbol'] = 'node-expanded-static';
2036 elseif ($here or $below)
2037 $tree[$key]['symbol'] = 'node-expanded';
2039 $tree[$key]['symbol'] = 'node-collapsed';
2040 $ret = ($ret or $here or $below); // parentheses are necessary for this to be computed correctly
2045 // Convert entity name to human-readable value
2046 function formatEntityName ($name) {
2050 return 'IPv4 Network';
2052 return 'IPv4 RS Pool';
2054 return 'IPv4 Virtual Service';
2065 // Take a MySQL or other generic timestamp and make it prettier
2066 function formatTimestamp ($timestamp) {
2067 return date('n/j/y g:iA', strtotime($timestamp));
2070 // Display hrefs for all of a file's parents. If scissors are requested,
2071 // prepend cutting button to each of them.
2072 function serializeFileLinks ($links, $scissors = FALSE)
2076 foreach ($links as $link_id => $li)
2078 switch ($li['entity_type'])
2081 $params = "page=ipv4net&id=";
2084 $params = "page=ipv4rspool&pool_id=";
2087 $params = "page=ipv4vs&vs_id=";
2090 $params = "page=object&object_id=";
2093 $params = "page=rack&rack_id=";
2096 $params = "page=user&user_id=";
2102 $ret .= "<a href='" . makeHrefProcess(array('op'=>'unlinkFile', 'link_id'=>$link_id)) . "'";
2103 $ret .= getImageHREF ('cut') . '</a> ';
2105 $ret .= sprintf("<a href='index.php?%s%s'>%s</a>", $params, $li['entity_id'], $li['name']);
2111 // Convert filesize to appropriate unit and make it human-readable
2112 function formatFileSize ($bytes) {
2114 if($bytes < 1024) // bytes
2115 return "${bytes} bytes";
2118 if ($bytes < 1024000)
2119 return sprintf ("%.1fk", round (($bytes / 1024), 1));
2122 return sprintf ("%.1f MB", round (($bytes / 1024000), 1));
2125 // Reverse of formatFileSize, it converts human-readable value to bytes
2126 function convertToBytes ($value) {
2127 $value = trim($value);
2128 $last = strtolower($value[strlen($value)-1]);
2142 function ip_quad2long ($ip)
2144 return sprintf("%u", ip2long($ip));
2147 function ip_long2quad ($quad)
2149 return long2ip($quad);
2152 function makeHref($params = array())
2154 $ret = 'index.php?';
2156 foreach($params as $key=>$value)
2160 $ret .= urlencode($key).'='.urlencode($value);
2166 function makeHrefProcess($params = array())
2168 global $pageno, $tabno;
2169 $ret = 'process.php?';
2171 if (!isset($params['page']))
2172 $params['page'] = $pageno;
2173 if (!isset($params['tab']))
2174 $params['tab'] = $tabno;
2175 foreach($params as $key=>$value)
2179 $ret .= urlencode($key).'='.urlencode($value);
2185 function makeHrefForHelper ($helper_name, $params = array())
2187 $ret = 'popup.php?helper=' . $helper_name;
2188 foreach($params as $key=>$value)
2189 $ret .= '&'.urlencode($key).'='.urlencode($value);
2193 // Process the given list of records to build data suitable for printNiftySelect()
2194 // (like it was formerly executed by printSelect()). Screen out vendors according
2195 // to VENDOR_SIEVE, if object type ID is provided. However, the OPTGROUP with already
2196 // selected OPTION is protected from being screened.
2197 function cookOptgroups ($recordList, $object_type_id = 0, $existing_value = 0)
2200 // Always keep "other" OPTGROUP at the SELECT bottom.
2202 foreach ($recordList as $dict_key => $dict_value)
2203 if (strpos ($dict_value, '%GSKIP%') !== FALSE)
2205 $tmp = explode ('%GSKIP%', $dict_value, 2);
2206 $ret[$tmp[0]][$dict_key] = $tmp[1];
2208 elseif (strpos ($dict_value, '%GPASS%') !== FALSE)
2210 $tmp = explode ('%GPASS%', $dict_value, 2);
2211 $ret[$tmp[0]][$dict_key] = $tmp[1];
2214 $therest[$dict_key] = $dict_value;
2215 if ($object_type_id != 0)
2217 $screenlist = array();
2218 foreach (explode (';', getConfigVar ('VENDOR_SIEVE')) as $sieve)
2219 if (preg_match ("/^([^@]+)(@${object_type_id})?\$/", trim ($sieve), $regs)){
2220 $screenlist[] = $regs[1];
2222 foreach (array_keys ($ret) as $vendor)
2223 if (in_array ($vendor, $screenlist))
2225 $ok_to_screen = TRUE;
2226 if ($existing_value)
2227 foreach (array_keys ($ret[$vendor]) as $recordkey)
2228 if ($recordkey == $existing_value)
2230 $ok_to_screen = FALSE;
2234 unset ($ret[$vendor]);
2237 $ret['other'] = $therest;
2241 function dos2unix ($text)
2243 return str_replace ("\r\n", "\n", $text);
2246 function unix2dos ($text)
2248 return str_replace ("\n", "\r\n", $text);
2251 function buildPredicateTable ($parsetree)
2254 foreach ($parsetree as $sentence)
2255 if ($sentence['type'] == 'SYNT_DEFINITION')
2256 $ret[$sentence['term']] = $sentence['definition'];
2257 // Now we have predicate table filled in with the latest definitions of each
2258 // particular predicate met. This isn't as chik, as on-the-fly predicate
2259 // overloading during allow/deny scan, but quite sufficient for this task.
2263 // Take a list of records and filter against given RackCode expression. Return
2264 // the original list intact, if there was no filter requested, but return an
2265 // empty list, if there was an error.
2266 function filterCellList ($list_in, $expression = array())
2268 if ($expression === NULL)
2270 if (!count ($expression))
2272 $list_out = array();
2273 foreach ($list_in as $item_key => $item_value)
2274 if (TRUE === judgeCell ($item_value, $expression))
2275 $list_out[$item_key] = $item_value;
2279 // Tell, if the given expression is true for the given entity. Take complete record on input.
2280 function judgeCell ($cell, $expression)
2283 return eval_expression
2297 // Tell, if a constraint from config option permits given record.
2298 function considerConfiguredConstraint ($cell, $varname)
2300 if (!strlen (getConfigVar ($varname)))
2301 return TRUE; // no restriction
2303 if (!isset ($parseCache[$varname]))
2304 // getConfigVar() doesn't re-read the value from DB because of its
2305 // own cache, so there is no race condition here between two calls.
2306 $parseCache[$varname] = spotPayload (getConfigVar ($varname), 'SYNT_EXPR');
2307 if ($parseCache[$varname]['result'] != 'ACK')
2308 return FALSE; // constraint set, but cannot be used due to compilation error
2309 return judgeCell ($cell, $parseCache[$varname]['load']);
2312 // Return list of records in the given realm, which conform to
2313 // the given RackCode expression. If the realm is unknown or text
2314 // doesn't validate as a RackCode expression, return NULL.
2315 // Otherwise (successful scan) return a list of all matched
2316 // records, even if the list is empty (array() !== NULL). If the
2317 // text is an empty string, return all found records in the given
2319 function scanRealmByText ($realm = NULL, $ftext = '')
2329 if (!strlen ($ftext = trim ($ftext)))
2333 $fparse = spotPayload ($ftext, 'SYNT_EXPR');
2334 if ($fparse['result'] != 'ACK')
2336 $fexpr = $fparse['load'];
2338 return filterCellList (listCells ($realm), $fexpr);
2340 throw new InvalidArgException ('$realm', $realm);
2345 function getIPv4VSOptions ()
2348 foreach (listCells ('ipv4vs') as $vsid => $vsinfo)
2349 $ret[$vsid] = $vsinfo['dname'] . (!strlen ($vsinfo['name']) ?
'' : " (${vsinfo['name']})");
2353 function getIPv4RSPoolOptions ()
2356 foreach (listCells ('ipv4rspool') as $pool_id => $poolInfo)
2357 $ret[$pool_id] = $poolInfo['name'];
2361 // Derive a complete cell structure from the given username regardless
2362 // if it is a local account or not.
2363 function constructUserCell ($username)
2365 if (NULL !== ($userid = getUserIDByUsername ($username)))
2366 return spotEntity ('user', $userid);
2370 'user_name' => $username,
2371 'user_realname' => '',
2375 $ret['atags'] = generateEntityAutoTags ($ret);
2379 // Let's have this debug helper here to enable debugging of process.php w/o interface.php.
2380 function dump ($var)
2382 echo '<div align=left><pre>';
2384 echo '</pre></div>';
2387 function getTagChart ($limit = 0, $realm = 'total', $special_tags = array())
2390 // first build top-N chart...
2392 foreach ($taglist as $taginfo)
2393 if (isset ($taginfo['refcnt'][$realm]))
2394 $toplist[$taginfo['id']] = $taginfo['refcnt'][$realm];
2395 arsort ($toplist, SORT_NUMERIC
);
2398 foreach (array_keys ($toplist) as $tag_id)
2400 $ret[$tag_id] = $taglist[$tag_id];
2401 if (++
$done == $limit)
2404 // ...then make sure, that every item of the special list is shown
2405 // (using the same sort order)
2407 foreach ($special_tags as $taginfo)
2408 if (!array_key_exists ($taginfo['id'], $ret))
2409 $extra[$taginfo['id']] = $taglist[$taginfo['id']]['refcnt'][$realm];
2410 arsort ($extra, SORT_NUMERIC
);
2411 foreach (array_keys ($extra) as $tag_id)
2412 $ret[] = $taglist[$tag_id];
2416 function decodeObjectType ($objtype_id, $style = 'r')
2418 static $types = array();
2419 if (!count ($types))
2422 'r' => readChapter (CHAP_OBJTYPE
),
2423 'a' => readChapter (CHAP_OBJTYPE
, 'a'),
2424 'o' => readChapter (CHAP_OBJTYPE
, 'o')
2426 return $types[$style][$objtype_id];
2429 function isolatedPermission ($p, $t, $cell)
2431 // This function is called from both "file" page and a number of other pages,
2432 // which have already fixed security context and authorized the user for it.
2433 // OTOH, it is necessary here to authorize against the current file, which
2434 // means saving the current context and building a new one.
2440 // push current context
2441 $orig_expl_tags = $expl_tags;
2442 $orig_impl_tags = $impl_tags;
2443 $orig_target_given_tags = $target_given_tags;
2444 $orig_auto_tags = $auto_tags;
2447 // remember decision
2448 $ret = permitted ($p, $t);
2450 $expl_tags = $orig_expl_tags;
2451 $impl_tags = $orig_impl_tags;
2452 $target_given_tags = $orig_target_given_tags;
2453 $auto_tags = $orig_auto_tags;
2457 function getPortListPrefs()
2460 if (0 >= ($ret['iif_pick'] = getConfigVar ('DEFAULT_PORT_IIF_ID')))
2461 $ret['iif_pick'] = 1;
2462 $ret['oif_picks'] = array();
2463 foreach (explode (';', getConfigVar ('DEFAULT_PORT_OIF_IDS')) as $tmp)
2465 $tmp = explode ('=', trim ($tmp));
2466 if (count ($tmp) == 2 and $tmp[0] > 0 and $tmp[1] > 0)
2467 $ret['oif_picks'][$tmp[0]] = $tmp[1];
2469 // enforce default value
2470 if (!array_key_exists (1, $ret['oif_picks']))
2471 $ret['oif_picks'][1] = 24;
2472 $ret['selected'] = $ret['iif_pick'] . '-' . $ret['oif_picks'][$ret['iif_pick']];
2476 // Return data for printNiftySelect() with port type options. All OIF options
2477 // for the default IIF will be shown, but only the default OIFs will be present
2478 // for each other IIFs. IIFs, for which there is no default OIF, will not
2480 // This SELECT will be used for the "add new port" form.
2481 function getNewPortTypeOptions()
2484 $prefs = getPortListPrefs();
2485 foreach (getPortInterfaceCompat() as $row)
2487 if ($row['iif_id'] == $prefs['iif_pick'])
2488 $optgroup = $row['iif_name'];
2489 elseif (array_key_exists ($row['iif_id'], $prefs['oif_picks']) and $prefs['oif_picks'][$row['iif_id']] == $row['oif_id'])
2490 $optgroup = 'other';
2493 if (!array_key_exists ($optgroup, $ret))
2494 $ret[$optgroup] = array();
2495 $ret[$optgroup][$row['iif_id'] . '-' . $row['oif_id']] = $row['oif_name'];
2500 // Return a serialized version of VLAN configuration for a port.
2501 // If a native VLAN is defined, print it first. All other VLANs
2502 // are tagged and are listed after a plus sign. When no configuration
2503 // is set for a port, return "default" string.
2504 function serializeVLANPack ($vlanport)
2506 if (!array_key_exists ('mode', $vlanport))
2508 switch ($vlanport['mode'])
2528 foreach ($vlanport['allowed'] as $vlan_id)
2529 if ($vlan_id != $vlanport['native'])
2530 $tagged[] = $vlan_id;
2532 $ret .= $vlanport['native'] ?
$vlanport['native'] : '';
2533 $tagged_bits = array();
2534 $id_from = $id_to = 0;
2535 foreach ($tagged as $next_id)
2539 if ($next_id == $id_to +
1) // merge
2545 $tagged_bits[] = $id_from == $id_to ?
$id_from : "${id_from}-${id_to}";
2547 $id_from = $id_to = $next_id; // start next pair
2551 $tagged_bits[] = $id_from == $id_to ?
$id_from : "${id_from}-${id_to}";
2552 if (count ($tagged))
2553 $ret .= '+' . implode (', ', $tagged_bits);
2554 return strlen ($ret) ?
$ret : 'default';
2557 // Decode VLAN compound key (which is a string formatted DOMAINID-VLANID) and
2558 // return the numbers as an array of two.
2559 function decodeVLANCK ($string)
2562 if (1 != preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $string, $matches))
2563 throw new InvalidArgException ('VLAN compound key', $string);
2564 return array ($matches[1], $matches[2]);
2567 // Return VLAN name formatted for HTML output (note, that input
2568 // argument comes from database unescaped).
2569 function formatVLANName ($vlaninfo, $context = 'markup long')
2574 $ret = $vlaninfo['vlan_id'];
2575 if ($vlaninfo['vlan_descr'] != '')
2576 $ret .= ' ' . niftyString ($vlaninfo['vlan_descr']);
2579 $ret = $vlaninfo['vlan_id'];
2580 if ($vlaninfo['vlan_descr'] != '')
2581 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2584 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2585 if ($vlaninfo['vlan_descr'] != '')
2586 $ret .= ' (' . niftyString ($vlaninfo['vlan_descr'], 20, FALSE) . ')';
2590 $ret .= makeHref (array ('page' => 'vlan', 'vlan_ck' => $vlaninfo['domain_id'] . '-' . $vlaninfo['vlan_id']));
2591 $ret .= '">' . formatVLANName ($vlaninfo, 'markup long') . '</a>';
2595 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2596 $ret .= ' @' . niftyString ($vlaninfo['domain_descr']);
2597 if ($vlaninfo['vlan_descr'] != '')
2598 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2603 // map interface name
2604 function ios12ShortenIfName ($ifname)
2606 $ifname = preg_replace ('@^Eth(?:ernet)?(.+)$@', 'e\\1', $ifname);
2607 $ifname = preg_replace ('@^FastEthernet(.+)$@', 'fa\\1', $ifname);
2608 $ifname = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $ifname);
2609 $ifname = preg_replace ('@^TenGigabitEthernet(.+)$@', 'te\\1', $ifname);
2610 $ifname = preg_replace ('@^Port-channel(.+)$@', 'po\\1', $ifname);
2611 $ifname = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $ifname);
2612 $ifname = strtolower ($ifname);
2616 function iosParseVLANString ($string)
2619 foreach (explode (',', $string) as $item)
2622 $item = trim ($item, ' ');
2623 if (preg_match ('/^([[:digit:]]+)$/', $item, $matches))
2624 $ret[] = $matches[1];
2625 elseif (preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $item, $matches))
2626 $ret = array_merge ($ret, range ($matches[1], $matches[2]));
2631 // Scan given array and return the key, which addresses the first item
2632 // with requested column set to given value (or NULL if there is none such).
2633 // Note that 0 and NULL mean completely different things and thus
2634 // require strict checking (=== and !===).
2635 function scanArrayForItem ($table, $scan_column, $scan_value)
2637 foreach ($table as $key => $row)
2638 if ($row[$scan_column] == $scan_value)
2643 // Return TRUE, if every value of A1 is present in A2 and vice versa,
2644 // regardless of each array's sort order and indexing.
2645 function array_values_same ($a1, $a2)
2647 return !count (array_diff ($a1, $a2)) and !count (array_diff ($a2, $a1));
2650 // Use the VLAN switch template to set VST role for each port of
2651 // the provided list. Return resulting list.
2652 function apply8021QOrder ($vst_id, $portlist)
2654 $vst = getVLANSwitchTemplate ($vst_id);
2655 foreach (array_keys ($portlist) as $port_name)
2657 foreach ($vst['rules'] as $rule)
2658 if (preg_match ($rule['port_pcre'], $port_name))
2660 $portlist[$port_name]['vst_role'] = $rule['port_role'];
2661 $portlist[$port_name]['wrt_vlans'] = buildVLANFilter ($rule['port_role'], $rule['wrt_vlans']);
2664 $portlist[$port_name]['vst_role'] = 'none';
2669 // return a sequence of ranges for given string form and port role
2670 function buildVLANFilter ($role, $string)
2675 case 'access': // 1-4094
2679 case 'trunk': // 2-4094
2683 $min = VLAN_MIN_ID +
1;
2689 if ($string == '') // fast track
2690 return array (array ('from' => $min, 'to' => $max));
2692 $vlanidlist = array();
2693 foreach (iosParseVLANString ($string) as $vlan_id)
2694 if ($min <= $vlan_id and $vlan_id <= $max)
2695 $vlanidlist[] = $vlan_id;
2696 return listToRanges ($vlanidlist);
2699 // pack set of integers into list of integer ranges
2700 // e.g. (1, 2, 3, 5, 6, 7, 9, 11) => ((1, 3), (5, 7), (9, 9), (11, 11))
2701 // The second argument, when it is different from 0, limits amount of
2702 // items in each generated range.
2703 function listToRanges ($vlanidlist, $limit = 0)
2708 foreach ($vlanidlist as $vlan_id)
2712 $ret[] = array ('from' => $vlan_id, 'to' => $vlan_id);
2714 $from = $to = $vlan_id;
2716 elseif ($to +
1 == $vlan_id)
2719 if ($to - $from +
1 == $limit)
2721 // cut accumulated range and start over
2722 $ret[] = array ('from' => $from, 'to' => $to);
2728 $ret[] = array ('from' => $from, 'to' => $to);
2729 $from = $to = $vlan_id;
2732 $ret[] = array ('from' => $from, 'to' => $to);
2736 // return TRUE, if given VLAN ID belongs to one of filter's ranges
2737 function matchVLANFilter ($vlan_id, $vfilter)
2739 foreach ($vfilter as $range)
2740 if ($range['from'] <= $vlan_id and $vlan_id <= $range['to'])
2745 function exportSwitch8021QConfig
2753 // only ignore VLANs, which exist and are explicitly shown as "alien"
2754 $old_managed_vlans = array();
2755 $domain_vlanlist = getDomainVLANs ($vswitch['domain_id']);
2756 foreach ($device_vlanlist as $vlan_id)
2759 !array_key_exists ($vlan_id, $domain_vlanlist) or
2760 $domain_vlanlist[$vlan_id]['vlan_type'] != 'alien'
2762 $old_managed_vlans[] = $vlan_id;
2763 $ports_to_do = array();
2765 foreach ($changes as $port_name => $port)
2767 $ports_to_do[$port_name] = array
2769 'old_mode' => $before[$port_name]['mode'],
2770 'old_allowed' => $before[$port_name]['allowed'],
2771 'old_native' => $before[$port_name]['native'],
2772 'new_mode' => $port['mode'],
2773 'new_allowed' => $port['allowed'],
2774 'new_native' => $port['native'],
2776 $after[$port_name] = $port;
2778 // New VLAN table is a union of:
2779 // 1. all compulsory VLANs
2780 // 2. all "current" non-alien allowed VLANs of those ports, which are left
2781 // intact (regardless if a VLAN exists in VLAN domain, but looking,
2782 // if it is present in device's own VLAN table)
2783 // 3. all "new" allowed VLANs of those ports, which we do "push" now
2784 // Like for old_managed_vlans, a VLANs is never listed, only if it
2785 // exists and belongs to "alien" type.
2786 $new_managed_vlans = array();
2788 foreach ($domain_vlanlist as $vlan_id => $vlan)
2789 if ($vlan['vlan_type'] == 'compulsory')
2790 $new_managed_vlans[] = $vlan_id;
2792 foreach ($before as $port_name => $port)
2793 if (!array_key_exists ($port_name, $changes))
2794 foreach ($port['allowed'] as $vlan_id)
2796 if (in_array ($vlan_id, $new_managed_vlans))
2800 array_key_exists ($vlan_id, $domain_vlanlist) and
2801 $domain_vlanlist[$vlan_id]['vlan_type'] == 'alien'
2804 if (in_array ($vlan_id, $device_vlanlist))
2805 $new_managed_vlans[] = $vlan_id;
2808 foreach ($changes as $port)
2809 foreach ($port['allowed'] as $vlan_id)
2812 $domain_vlanlist[$vlan_id]['vlan_type'] == 'ondemand' and
2813 !in_array ($vlan_id, $new_managed_vlans)
2815 $new_managed_vlans[] = $vlan_id;
2817 // Before removing each old VLAN as such it is necessary to unassign
2818 // ports from it (to remove VLAN from each ports' list of "allowed"
2819 // VLANs). This change in turn requires, that a port's "native"
2820 // VLAN isn't set to the one being removed from its "allowed" list.
2821 foreach ($ports_to_do as $port_name => $port)
2822 switch ($port['old_mode'] . '->' . $port['new_mode'])
2824 case 'trunk->trunk':
2825 // "old" native is set and differs from the "new" native
2826 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
2829 'opcode' => 'unset native',
2830 'arg1' => $port_name,
2831 'arg2' => $port['old_native'],
2833 if (count ($tmp = array_diff ($port['old_allowed'], $port['new_allowed'])))
2836 'opcode' => 'rem allowed',
2837 'port' => $port_name,
2841 case 'access->access':
2842 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
2845 'opcode' => 'unset access',
2846 'arg1' => $port_name,
2847 'arg2' => $port['old_native'],
2850 case 'access->trunk':
2853 'opcode' => 'unset access',
2854 'arg1' => $port_name,
2855 'arg2' => $port['old_native'],
2858 case 'trunk->access':
2861 'opcode' => 'unset native',
2862 'arg1' => $port_name,
2863 'arg2' => $port['old_native'],
2865 if (count ($port['old_allowed']))
2868 'opcode' => 'rem allowed',
2869 'port' => $port_name,
2870 'vlans' => $port['old_allowed'],
2874 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
2876 // Now it is safe to unconfigure VLANs, which still exist on device,
2877 // but are not present on the "new" list.
2878 // FIXME: put all IDs into one pseudo-command to make it easier
2879 // for translators to create/destroy VLANs in batches, where
2880 // target platform allows them to do.
2881 foreach (array_diff ($old_managed_vlans, $new_managed_vlans) as $vlan_id)
2884 'opcode' => 'destroy VLAN',
2887 // Configure VLANs, which must be present on the device, but are not yet.
2888 foreach (array_diff ($new_managed_vlans, $old_managed_vlans) as $vlan_id)
2891 'opcode' => 'create VLAN',
2894 // Now, when all new VLANs are created (queued), it is safe to assign (queue)
2895 // ports to the new VLANs.
2896 foreach ($ports_to_do as $port_name => $port)
2897 switch ($port['old_mode'] . '->' . $port['new_mode'])
2899 case 'trunk->trunk':
2900 // For each allowed VLAN, which is present on the "new" list and missing from
2901 // the "old" one, queue a command to assign current port to that VLAN.
2902 if (count ($tmp = array_diff ($port['new_allowed'], $port['old_allowed'])))
2905 'opcode' => 'add allowed',
2906 'port' => $port_name,
2909 // One of the "allowed" VLANs for this port may probably be "native".
2910 // "new native" is set and differs from "old native"
2911 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
2914 'opcode' => 'set native',
2915 'arg1' => $port_name,
2916 'arg2' => $port['new_native'],
2919 case 'access->access':
2920 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
2923 'opcode' => 'set access',
2924 'arg1' => $port_name,
2925 'arg2' => $port['new_native'],
2928 case 'access->trunk':
2931 'opcode' => 'set mode',
2932 'arg1' => $port_name,
2933 'arg2' => $port['new_mode'],
2935 if (count ($port['new_allowed']))
2938 'opcode' => 'add allowed',
2939 'port' => $port_name,
2940 'vlans' => $port['new_allowed'],
2944 'opcode' => 'set native',
2945 'arg1' => $port_name,
2946 'arg2' => $port['new_native'],
2949 case 'trunk->access':
2952 'opcode' => 'set mode',
2953 'arg1' => $port_name,
2954 'arg2' => $port['new_mode'],
2958 'opcode' => 'set access',
2959 'arg1' => $port_name,
2960 'arg2' => $port['new_native'],
2964 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
2968 array_unshift ($crq, array ('opcode' => 'begin configuration'));
2969 $crq[] = array ('opcode' => 'end configuration');
2970 if (considerConfiguredConstraint (spotEntity ('object', $vswitch['object_id']), '8021Q_WRI_AFTER_CONFT_LISTSRC'))
2971 $crq[] = array ('opcode' => 'save configuration');
2972 setDevice8021QConfig ($vswitch['object_id'], $crq);
2974 return count ($crq);
2977 // filter list of changed ports to cancel changes forbidden by VST and domain
2978 function filter8021QChangeRequests
2981 $before, // current saved configuration of all ports
2982 $changes // changed ports with VST markup
2985 $domain_immune_vlans = array();
2986 foreach ($domain_vlanlist as $vlan_id => $vlan)
2987 if ($vlan['vlan_type'] == 'alien')
2988 $domain_immune_vlans[] = $vlan_id;
2990 foreach ($changes as $port_name => $port)
2993 if (!goodModeForVSTRole ($port['mode'], $port['vst_role']))
2994 continue; // ignore change request
2995 // find and cancel any changes regarding immune VLANs
2996 switch ($port['mode'])
2999 foreach ($domain_immune_vlans as $immune)
3000 // Reverting an attempt to set an access port from
3001 // "normal" VLAN to immune one (or vice versa) requires
3002 // special handling, becase the calling function has
3003 // discarded the old contents of 'allowed' for current port.
3006 $before[$port_name]['native'] == $immune or
3007 $port['native'] == $immune
3010 $port['native'] = $before[$port_name]['native'];
3011 $port['allowed'] = array ($port['native']);
3012 // Such reversal happens either once or never for an
3018 foreach ($domain_immune_vlans as $immune)
3019 if (in_array ($immune, $before[$port_name]['allowed'])) // was allowed before
3021 if (!in_array ($immune, $port['allowed']))
3022 $port['allowed'][] = $immune; // restore
3023 if ($before[$port_name]['native'] == $immune) // and was native
3024 $port['native'] = $immune; // also restore
3028 if (in_array ($immune, $port['allowed']))
3029 unset ($port['allowed'][array_search ($immune, $port['allowed'])]); // cancel
3030 if ($port['native'] == $immune)
3031 $port['native'] = $before[$port_name]['native'];
3035 throw new InvalidArgException ('mode', $port['mode']);
3038 $ret[$port_name] = $port;
3043 // take port list with order applied and return uplink ports in the same format
3044 function produceUplinkPorts ($domain_vlanlist, $portlist)
3047 $employed = array();
3048 foreach ($domain_vlanlist as $vlan_id => $vlan)
3049 if ($vlan['vlan_type'] == 'compulsory')
3050 $employed[] = $vlan_id;
3051 foreach ($portlist as $port_name => $port)
3052 if ($port['vst_role'] != 'uplink')
3053 foreach ($port['allowed'] as $vlan_id)
3054 if (!in_array ($vlan_id, $employed))
3055 $employed[] = $vlan_id;
3056 foreach ($portlist as $port_name => $port)
3057 if ($port['vst_role'] == 'uplink')
3059 $employed_here = array();
3060 foreach ($employed as $vlan_id)
3061 if (matchVLANFilter ($vlan_id, $port['wrt_vlans']))
3062 $employed_here[] = $vlan_id;
3063 $ret[$port_name] = array
3065 'vst_role' => 'uplink',
3067 'allowed' => $employed_here,
3074 function same8021QConfigs ($a, $b)
3076 return $a['mode'] == $b['mode'] &&
3077 array_values_same ($a['allowed'], $b['allowed']) &&
3078 $a['native'] == $b['native'];
3081 // Return TRUE, if the port can be edited by the user.
3082 function editable8021QPort ($port)
3084 return in_array ($port['vst_role'], array ('trunk', 'access', 'anymode'));
3087 // Decide, whether the given 802.1Q port mode is permitted by
3089 function goodModeForVSTRole ($mode, $role)
3094 return in_array ($role, array ('access', 'anymode'));
3096 return in_array ($role, array ('trunk', 'uplink', 'downlink', 'anymode'));
3098 throw new InvalidArgException ('mode', $mode);
3104 Relation between desired (D), cached (C) and running (R)
3105 copies of switch ports (P) list.
3109 | P |-----| P |-? +--| P |
3111 | P |-----| P |--+ ?-| P |
3113 | P |-----| P |-------| P |
3115 | P |-----| P |--+ ?-| P |
3117 | P |-----| P |--+ +--| P |
3124 A modified local version of a port in "conflict" state ignores remote
3125 changes until remote change maintains its difference. Once both edits
3126 match, the local copy "locks" on the remote and starts tracking it.
3129 a "o" -- remOte version
3130 l "l" -- Local version
3131 u "b" -- Both versions
3144 0----------------------------------------------> time
3147 function get8021QSyncOptions
3150 $D, // desired config
3151 $C, // cached config
3152 $R // running-config
3155 $default_port = array
3158 'allowed' => array (VLAN_DFL_ID
),
3159 'native' => VLAN_DFL_ID
,
3162 $allports = array();
3163 foreach (array_unique (array_merge (array_keys ($C), array_keys ($R))) as $pn)
3164 $allports[$pn] = array();
3165 foreach (apply8021QOrder ($vswitch['template_id'], $allports) as $pn => $port)
3167 // catch anomalies early
3168 if ($port['vst_role'] == 'none')
3170 if ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and !array_key_exists ($pn, $C))
3171 $ret[$pn] = array ('status' => 'none');
3175 'status' => 'martian_conflict',
3176 'left' => array_key_exists ($pn, $C) ?
$C[$pn] : array ('mode' => 'none'),
3177 'right' => array_key_exists ($pn, $R) ?
$R[$pn] : array ('mode' => 'none'),
3181 elseif ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and array_key_exists ($pn, $C))
3185 'status' => 'martian_conflict',
3186 'left' => array_key_exists ($pn, $C) ?
$C[$pn] : array ('mode' => 'none'),
3187 'right' => array_key_exists ($pn, $R) ?
$R[$pn] : array ('mode' => 'none'),
3191 // (DC_): port missing from device
3192 if (!array_key_exists ($pn, $R))
3194 $ret[$pn] = array ('left' => $D[$pn]);
3195 if (same8021QConfigs ($D[$pn], $default_port))
3196 $ret[$pn]['status'] = 'ok_to_delete';
3199 $ret[$pn]['status'] = 'delete_conflict';
3200 $ret[$pn]['lastseen'] = $C[$pn];
3204 // (__R): port missing from DB
3205 if (!array_key_exists ($pn, $C))
3207 // Allow importing any configuration, which passes basic
3208 // validation. If port mode doesn't match its VST role,
3209 // this will be handled later WRT each port.
3212 'status' => acceptable8021QConfig ($R[$pn]) ?
'ok_to_add' : 'add_conflict',
3217 $D_eq_C = same8021QConfigs ($D[$pn], $C[$pn]);
3218 $C_eq_R = same8021QConfigs ($C[$pn], $R[$pn]);
3219 // (DCR), D = C = R: data in sync
3220 if ($D_eq_C and $C_eq_R) // implies D == R
3224 'status' => 'in_sync',
3229 // (DCR), D = C: no local edit in the way
3233 'status' => 'ok_to_pull',
3237 // (DCR), C = R: no remote edit in the way
3241 'status' => 'ok_to_push',
3245 // (DCR), D = R: end of version conflict, restore tracking
3246 elseif (same8021QConfigs ($D[$pn], $R[$pn]))
3249 'status' => 'ok_to_merge',
3252 else // D != C, C != R, D != R: version conflict