<?php

# This file is a part of RackTables, a datacenter and server room management
# framework. See accompanying file "COPYING" for the full copyright and
# licensing information.

// Let's have it here, so extensions can add their own images.
$image = array();
$image['rackspace']['path'] = 'pix/racks.png';
$image['rackspace']['width'] = 218;
$image['rackspace']['height'] = 200;
$image['objects']['path'] = 'pix/server.png';
$image['objects']['width'] = 218;
$image['objects']['height'] = 200;
$image['depot']['path'] = 'pix/server.png';
$image['depot']['width'] = 218;
$image['depot']['height'] = 200;
$image['files']['path'] = 'pix/files.png';
$image['files']['width'] = 218;
$image['files']['height'] = 200;
$image['ipv4space']['path'] = 'pix/addressspace.png';
$image['ipv4space']['width'] = 218;
$image['ipv4space']['height'] = 200;
$image['ipv6space']['path'] = 'pix/addressspacev6.png';
$image['ipv6space']['width'] = 218;
$image['ipv6space']['height'] = 200;
$image['ipv4slb']['path'] = 'pix/slb.png';
$image['ipv4slb']['width'] = 218;
$image['ipv4slb']['height'] = 200;
$image['config']['path'] = 'pix/configuration.png';
$image['config']['width'] = 218;
$image['config']['height'] = 200;
$image['reports']['path'] = 'pix/report.png';
$image['reports']['width'] = 218;
$image['reports']['height'] = 200;
$image['8021q']['path'] = 'pix/8021q.png';
$image['8021q']['width'] = 218;
$image['8021q']['height'] = 200;
$image['objectlog']['path'] = 'pix/crystal-mimetypes-shellscript-218x200.png';
$image['objectlog']['width'] = 218;
$image['objectlog']['height'] = 200;
$image['virtual']['path'] = 'pix/virtualresources.png';
$image['virtual']['width'] = 218;
$image['virtual']['height'] = 200;
$image['cables']['path'] = 'pix/patch_cables.png';
$image['cables']['width'] = 218;
$image['cables']['height'] = 200;
$image['download']['path'] = 'pix/download.png';
$image['download']['width'] = 16;
$image['download']['height'] = 16;
$image['DOWNLOAD']['path'] = 'pix/download-big.png';
$image['DOWNLOAD']['width'] = 32;
$image['DOWNLOAD']['height'] = 32;
$image['plug']['path'] = 'pix/tango-network-wired.png';
$image['plug']['width'] = 16;
$image['plug']['height'] = 16;
$image['cut']['path'] = 'pix/tango-edit-cut-16x16.png';
$image['cut']['width'] = 16;
$image['cut']['height'] = 16;
$image['Cut']['path'] = 'pix/tango-edit-cut-22x22.png';
$image['Cut']['width'] = 22;
$image['Cut']['height'] = 22;
$image['Cut gray']['path'] = 'pix/tango-edit-cut-22x22-gray.png';
$image['Cut gray']['width'] = 22;
$image['Cut gray']['height'] = 22;
$image['CUT']['path'] = 'pix/tango-edit-cut-32x32.png';
$image['CUT']['width'] = 32;
$image['CUT']['height'] = 32;
$image['CUT gray']['path'] = 'pix/tango-edit-cut-32x32-gray.png';
$image['CUT gray']['width'] = 32;
$image['CUT gray']['height'] = 32;
$image['add']['path'] = 'pix/tango-list-add.png';
$image['add']['width'] = 16;
$image['add']['height'] = 16;
$image['ADD']['path'] = 'pix/tango-list-add-big.png';
$image['ADD']['width'] = 32;
$image['ADD']['height'] = 32;
$image['delete']['path'] = 'pix/tango-list-remove.png';
$image['delete']['width'] = 16;
$image['delete']['height'] = 16;
$image['DELETE']['path'] = 'pix/tango-list-remove-32x32.png';
$image['DELETE']['width'] = 32;
$image['DELETE']['height'] = 32;
$image['destroy']['path'] = 'pix/tango-user-trash-16x16.png';
$image['destroy']['width'] = 16;
$image['destroy']['height'] = 16;
$image['nodestroy']['path'] = 'pix/tango-user-trash-16x16-gray.png';
$image['nodestroy']['width'] = 16;
$image['nodestroy']['height'] = 16;
$image['NODESTROY']['path'] = 'pix/tango-user-trash-32x32-gray.png';
$image['NODESTROY']['width'] = 32;
$image['NODESTROY']['height'] = 32;
$image['DESTROY']['path'] = 'pix/tango-user-trash-32x32.png';
$image['DESTROY']['width'] = 32;
$image['DESTROY']['height'] = 32;
$image['nodelete']['path'] = 'pix/tango-list-remove-shadow.png';
$image['nodelete']['width'] = 16;
$image['nodelete']['height'] = 16;
$image['inservice']['path'] = 'pix/tango-emblem-system.png';
$image['inservice']['width'] = 16;
$image['inservice']['height'] = 16;
$image['notinservice']['path'] = 'pix/tango-dialog-error.png';
$image['notinservice']['width'] = 16;
$image['notinservice']['height'] = 16;
$image['find']['path'] = 'pix/tango-system-search.png';
$image['find']['width'] = 16;
$image['find']['height'] = 16;
$image['next']['path'] = 'pix/tango-go-next.png';
$image['next']['width'] = 32;
$image['next']['height'] = 32;
$image['prev']['path'] = 'pix/tango-go-previous.png';
$image['prev']['width'] = 32;
$image['prev']['height'] = 32;
$image['COMMIT']['path'] = 'pix/tango-go-prev-next-32x32.png';
$image['COMMIT']['width'] = 32;
$image['COMMIT']['height'] = 32;
$image['COMMIT gray']['path'] = 'pix/tango-go-prev-next-gray-32x32.png';
$image['COMMIT gray']['width'] = 32;
$image['COMMIT gray']['height'] = 32;
$image['RECALC']['path'] = 'pix/tango-view-refresh-32x32.png';
$image['RECALC']['width'] = 32;
$image['RECALC']['height'] = 32;
$image['recalc']['path'] = 'pix/tango-view-refresh-16x16.png';
$image['recalc']['width'] = 16;
$image['recalc']['height'] = 16;
$image['clear']['path'] = 'pix/tango-edit-clear.png';
$image['clear']['width'] = 16;
$image['clear']['height'] = 16;
$image['CLEAR']['path'] = 'pix/tango-edit-clear-big.png';
$image['CLEAR']['width'] = 32;
$image['CLEAR']['height'] = 32;
$image['CLEAR gray']['path'] = 'pix/tango-edit-clear-gray-32x32.png';
$image['CLEAR gray']['width'] = 32;
$image['CLEAR gray']['height'] = 32;
$image['save']['path'] = 'pix/tango-document-save-16x16.png';
$image['save']['width'] = 16;
$image['save']['height'] = 16;
$image['SAVE']['path'] = 'pix/tango-document-save-32x32.png';
$image['SAVE']['width'] = 32;
$image['SAVE']['height'] = 32;
$image['NOSAVE']['path'] = 'pix/tango-document-save-32x32-gray.png';
$image['NOSAVE']['width'] = 32;
$image['NOSAVE']['height'] = 32;
$image['create']['path'] = 'pix/tango-document-new.png';
$image['create']['width'] = 16;
$image['create']['height'] = 16;
$image['CREATE']['path'] = 'pix/tango-document-new-big.png';
$image['CREATE']['width'] = 32;
$image['CREATE']['height'] = 32;
$image['DENIED']['path'] = 'pix/tango-dialog-error-big.png';
$image['DENIED']['width'] = 32;
$image['DENIED']['height'] = 32;
$image['node-collapsed']['path'] = 'pix/node-collapsed.png';
$image['node-collapsed']['width'] = 16;
$image['node-collapsed']['height'] = 16;
$image['node-expanded']['path'] = 'pix/node-expanded.png';
$image['node-expanded']['width'] = 16;
$image['node-expanded']['height'] = 16;
$image['node-expanded-static']['path'] = 'pix/node-expanded-static.png';
$image['node-expanded-static']['width'] = 16;
$image['node-expanded-static']['height'] = 16;
$image['dragons']['path'] = 'pix/mitsudragon.png';
$image['dragons']['width'] = 195;
$image['dragons']['height'] = 33;
$image['LB']['path'] = 'pix/loadbalancer.png';
$image['LB']['width'] = 32;
$image['LB']['height'] = 32;
$image['RS pool']['path'] = 'pix/serverpool.png';
$image['RS pool']['width'] = 48;
$image['RS pool']['height'] = 16;
$image['VS']['path'] = 'pix/servicesign.png';
$image['VS']['width'] = 39;
$image['VS']['height'] = 62;
$image['router']['path'] = 'pix/router.png';
$image['router']['width'] = 32;
$image['router']['height'] = 32;
$image['object']['path'] = 'pix/bracket-16x16.png';
$image['object']['width'] = 16;
$image['object']['height'] = 16;
$image['OBJECT']['path'] = 'pix/bracket-32x32.png';
$image['OBJECT']['width'] = 32;
$image['OBJECT']['height'] = 32;
$image['LOCATION']['path'] = 'pix/tango-internet-32x32.png';
$image['LOCATION']['width'] = 32;
$image['LOCATION']['height'] = 32;
$image['attach']['path'] = 'pix/tango-mail-attachment-16x16.png';
$image['attach']['width'] = 16;
$image['attach']['height'] = 16;
$image['Attach']['path'] = 'pix/tango-mail-attachment-22x22.png';
$image['Attach']['width'] = 22;
$image['Attach']['height'] = 22;
$image['ATTACH']['path'] = 'pix/tango-mail-attachment-32x32.png';
$image['ATTACH']['width'] = 32;
$image['ATTACH']['height'] = 32;
$image['favorite']['path'] = 'pix/tango-emblem-favorite.png';
$image['favorite']['width'] = 16;
$image['favorite']['height'] = 16;
$image['computer']['path'] = 'pix/tango-computer.png';
$image['computer']['width'] = 16;
$image['computer']['height'] = 16;
$image['empty file']['path'] = 'pix/crystal-file-empty-32x32.png';
$image['empty file']['width'] = 32;
$image['empty file']['height'] = 32;
$image['text file']['path'] = 'pix/crystal-file-text-32x32.png';
$image['text file']['width'] = 32;
$image['text file']['height'] = 32;
$image['image file']['path'] = 'pix/crystal-file-image-32x32.png';
$image['image file']['width'] = 32;
$image['image file']['height'] = 32;
$image['text']['path'] = 'pix/tango-text-x-generic-16x16.png';
$image['text']['width'] = 16;
$image['text']['height'] = 16;
$image['NET']['path'] = 'pix/crystal-network_local-32x32.png';
$image['NET']['width'] = 32;
$image['NET']['height'] = 32;
$image['net']['path'] = 'pix/crystal-network_local-16x16.png';
$image['net']['width'] = 16;
$image['net']['height'] = 16;
$image['USER']['path'] = 'pix/crystal-edit-user-32x32.png';
$image['USER']['width'] = 32;
$image['USER']['height'] = 32;
$image['setfilter']['path'] = 'pix/pgadmin3-viewfiltereddata.png';
$image['setfilter']['width'] = 32;
$image['setfilter']['height'] = 32;
$image['setfilter gray']['path'] = 'pix/pgadmin3-viewfiltereddata-grayscale.png';
$image['setfilter gray']['width'] = 32;
$image['setfilter gray']['height'] = 32;
$image['resetfilter']['path'] = 'pix/pgadmin3-viewdata.png';
$image['resetfilter']['width'] = 32;
$image['resetfilter']['height'] = 32;
$image['resetfilter gray']['path'] = 'pix/pgadmin3-viewdata-grayscale.png';
$image['resetfilter gray']['width'] = 32;
$image['resetfilter gray']['height'] = 32;
$image['knight']['path'] = 'pix/smiley_knight.png';
$image['knight']['width'] = 72;
$image['knight']['height'] = 33;
$image['Zoom']['path'] = 'pix/tango-system-search-22x22.png';
$image['Zoom']['width'] = 22;
$image['Zoom']['height'] = 22;
$image['Zooming']['path'] = 'pix/tango-view-fullscreen-22x22.png';
$image['Zooming']['width'] = 22;
$image['Zooming']['height'] = 22;
$image['UNLOCK']['path'] = 'pix/crystal-actions-unlock-32x32.png';
$image['UNLOCK']['width'] = 32;
$image['UNLOCK']['height'] = 32;
$image['CLOCK']['path'] = 'pix/tango-appointment-32x32.png';
$image['CLOCK']['width'] = 32;
$image['CLOCK']['height'] = 32;
$image['DQUEUE done']['path'] = 'pix/crystal-ok-32x32.png';
$image['DQUEUE done']['width'] = 32;
$image['DQUEUE done']['height'] = 32;
$image['DQUEUE sync_aging']['path'] = 'pix/tango-appointment-32x32.png';
$image['DQUEUE sync_aging']['width'] = 32;
$image['DQUEUE sync_aging']['height'] = 32;
$image['DQUEUE resync_aging']['path'] = 'pix/tango-appointment-32x32.png';
$image['DQUEUE resync_aging']['width'] = 32;
$image['DQUEUE resync_aging']['height'] = 32;
$image['DQUEUE sync_ready']['path'] = 'pix/tango-emblem-system-32x32.png';
$image['DQUEUE sync_ready']['width'] = 32;
$image['DQUEUE sync_ready']['height'] = 32;
$image['DQUEUE resync_ready']['path'] = 'pix/tango-emblem-important-32x32.png';
$image['DQUEUE resync_ready']['width'] = 32;
$image['DQUEUE resync_ready']['height'] = 32;
$image['DQUEUE failed']['path'] = 'pix/tango-emblem-unreadable-32x32.png';
$image['DQUEUE failed']['width'] = 32;
$image['DQUEUE failed']['height'] = 32;
$image['DQUEUE disabled']['path'] = 'pix/tango-emblem-readonly-32x32.png';
$image['DQUEUE disabled']['width'] = 32;
$image['DQUEUE disabled']['height'] = 32;
$image['copy']['path'] = 'pix/tango-edit-copy-16x16.png';
$image['copy']['width'] = 16;
$image['copy']['height'] = 16;
$image['COPY']['path'] = 'pix/tango-edit-copy-32x32.png';
$image['COPY']['width'] = 32;
$image['COPY']['height'] = 32;
$image['html']['path'] = 'pix/tango-text-html.png';
$image['html']['width'] = 16;
$image['html']['height'] = 16;
$image['pencil']['path'] = 'pix/pencil-icon.png';
$image['pencil']['width'] = 12;
$image['pencil']['height'] = 12;
$image['link up']['path'] = 'pix/link-up.png';
$image['link up']['width'] = 16;
$image['link up']['height'] = 16;
$image['link down']['path'] = 'pix/link-down.png';
$image['link down']['width'] = 16;
$image['link down']['height'] = 16;
$image['link disabled']['path'] = 'pix/link-disabled.png';
$image['link disabled']['width'] = 16;
$image['link disabled']['height'] = 16;
$image['16x16t']['path'] = 'pix/1x1t.gif';
$image['16x16t']['width'] = 16;
$image['16x16t']['height'] = 16;
$image['disable']['path'] = 'pix/link-disabled.png';
$image['disable']['width'] = 16;
$image['disable']['height'] = 16;
$image['enable']['path'] = 'pix/link-up.png';
$image['enable']['width'] = 16;
$image['enable']['height'] = 16;
$image['upgrade']['path'] = 'pix/tango-go-up.png';
$image['upgrade']['width'] = 16;
$image['upgrade']['height'] = 16;

// Use the same workflow as for the array above:
// 1. This file is required early, it assigns the default elements.
// 2. Then plugin(s) load and can modify the array if necessary.
// 3. Then interface-config.php, when conditionally required, can read it.
$tag_palette = array
(
	'FFFFFF',
	'C0C0C0',
	'808080',
	'000000',
	'FF0000',
	'800000',
	'FF8000',
	'FFFF00',
	'808000',
	'00FF00',
	'008000',
	'00FFFF',
	'008080',
	'0000FF',
	'000080',
	'FF00FF',
	'800080',
);

$page_by_realm = array();
$page_by_realm['object'] = 'depot';
$page_by_realm['rack'] = 'rackspace';
$page_by_realm['ipv4net'] = 'ipv4space';
$page_by_realm['ipv6net'] = 'ipv6space';
$page_by_realm['ipv4vs'] = 'ipv4slb';
$page_by_realm['ipv4rspool'] = 'ipv4slb';
$page_by_realm['file'] = 'files';
$page_by_realm['user'] = 'userlist';

function getSelectOptions ($options, $selected_id = NULL)
{
	$ret = '';
	foreach ($options as $key => $value)
	{
		$selected = is_array ($selected_id) ? in_array ($key, $selected_id) : $key == $selected_id;
		$ret .= "<option value='${key}'" . ($selected ? ' selected' : '') . '>';
		$ret .= stringForOption ($value) . '</option>';
	}
	return $ret;
}

function printSelect ($optionList, $select_attrs = array(), $selected_id = NULL)
{
	echo getSelect ($optionList, $select_attrs, $selected_id);
}

// $selected_id can be an array if you want to use multiselect
// in order to use multiselect $select_attrs should contain smth like this:
// 		[ 'name' => 'some_name[]', 'multiple' => 'multiple', 'size' => 5 ]
// Input array keys are OPTION VALUEs and input array values are OPTION text.
function getSelect ($optionList, $select_attrs = array(), $selected_id = NULL, $treat_single_special = TRUE)
{
	$ret = '';
	if (!array_key_exists ('name', $select_attrs))
		throw new InvalidArgException ('select_attrs[\'name\']', '(not set)', 'must be set');
	// handle two corner cases in a specific way
	if (count ($optionList) == 0)
		return '(none)';
	if (count ($optionList) == 1 && $treat_single_special)
	{
		foreach ($optionList as $key => $value)
			break;
		return "<input type=hidden name=${select_attrs['name']} id=${select_attrs['name']} value=${key}>" .
			stringForLabel ($value, 64);
	}
	if (!array_key_exists ('id', $select_attrs))
		$select_attrs['id'] = $select_attrs['name'];
	$ret .= '<select';
	foreach ($select_attrs as $attr_name => $attr_value)
		$ret .= " ${attr_name}=${attr_value}";
	$ret .= '>' . getSelectOptions ($optionList, $selected_id) . '</select>';
	return $ret;
}

function printNiftySelect ($groupList, $select_attrs = array(), $selected_id = NULL)
{
	echo getNiftySelect ($groupList, $select_attrs, $selected_id);
}

// Input is a cooked list of OPTGROUPs, each with own sub-list of OPTIONs in the same
// format as printSelect() expects.
// If tree is true, hierarchical drop-boxes are used, otherwise optgroups are used.
function getNiftySelect ($groupList, $select_attrs, $selected_id = NULL)
{
	// special treatment for ungrouped data
	if (count ($groupList) == 1 && isset ($groupList['other']))
		return getSelect ($groupList['other'], $select_attrs, $selected_id);
	if (!array_key_exists ('name', $select_attrs))
		throw new InvalidArgException ('select_attrs[\'name\']', '(not set)', 'must be set');
	if (!array_key_exists ('id', $select_attrs))
		$select_attrs['id'] = $select_attrs['name'];

	$ret = '<select';
	foreach ($select_attrs as $attr_name => $attr_value)
		$ret .= " ${attr_name}=${attr_value}";
	$ret .= ">\n";
	foreach ($groupList as $groupname => $groupdata)
	{
		$ret .= "<optgroup label='${groupname}'>\n";
		foreach ($groupdata as $dict_key => $dict_value)
		{
			if (is_array ($selected_id))
				$is_selected = in_array ($dict_key, $selected_id);
			else
				$is_selected = $dict_key == $selected_id;
			$ret .= "<option value='${dict_key}'" . ($is_selected ? ' selected' : '') . ">${dict_value}</option>\n";
		}
		$ret .= "</optgroup>\n";
	}
	$ret .= "</select>\n";
	return $ret;
}

function getOptionTree ($tree_name, $tree_options, $tree_config = array())
{
	$default_config = array
	(
		'choose' => 'select...',
		'empty_value' => '',
		'indexed' => TRUE,
	);
	addJSInternal ('js/jquery.optionTree.js');
	addJSText ("
$(function() {
	var option_tree = " . json_encode ($tree_options) . ";
	var options = " . json_encode ($tree_config + $default_config) . ";
	$('input[name=${tree_name}]').optionTree(option_tree, options);
});"
	); // addJSText()

	return "<input type=hidden name=${tree_name}>";
}

function printImageHREF ($tag, $title = '', $do_input = FALSE)
{
	echo getImageHREF ($tag, $title, $do_input);
}

// this would be better called mkIMG(), make "IMG" HTML element
function getImageHREF ($tag, $title = '', $do_input = FALSE)
{
	global $image;

	$src = NULL;
	if (array_key_exists ($tag, $image))
	{
		$srctype = array_fetch ($image[$tag], 'srctype', 'intpath');
		if ($srctype == 'intpath' && array_key_exists ('path', $image[$tag]))
			$src = '?module=chrome&uri=' . $image[$tag]['path'];
		elseif ($srctype == 'dataurl' && array_key_exists ('data', $image[$tag]))
			$src = 'data:' . $image[$tag]['data'];
		elseif ($srctype == 'exturl' && array_key_exists ('url', $image[$tag]))
			$src = $image[$tag]['url'];
	}
	if ($src === NULL)
		throw new RackTablesError ("malformed \$image declaration for '$tag'");

	$attrs = array
	(
		'src' => $src,
		'border' => 0,
	);
	if ($title != '')
		$attrs['title'] = $title;
	if ($do_input)
	{
		$element = 'input';
		$attrs['type'] = 'image';
		$attrs['name'] = 'submit';
		$attrs['class'] = 'icon';
		// Width and height for INPUT only appear in HTML 5.
	}
	else
	{
		$element = 'img';
		foreach (array ('width', 'height') as $each)
		{
			if (! array_key_exists ($each, $image[$tag]))
				throw new RackTablesError ("no '$each' in \$image['$tag']");
			$attrs[$each] = $image[$tag][$each];
		}
	}
	return makeHtmlTag ($element, $attrs);
}

// This function is DEPRECATED and will be removed in 0.22.0. See the set
// of stringForXXXXX() functions for a proper replacement.
function escapeString ($value, $do_db_escape = FALSE)
{
	$ret = htmlspecialchars ($value, ENT_QUOTES, 'UTF-8');
	if ($do_db_escape)
	{
		global $dbxlink;
		$ret = substr ($dbxlink->quote ($ret), 1, -1);
	}
	return $ret;
}

function transformRequestData()
{
	global $sic;
	$seen_keys = array();

	// Escape all globals before using and keep a copy of the original values.
	$sic = array();
	// walk through merged GET and POST instead of REQUEST array because it
	// can contain cookies with data that could not be decoded from UTF-8
	foreach (($_POST + $_GET) as $key => $value)
	{
		if (is_array ($value))
			$_REQUEST[$key] = $value;
		else
		{
			$value = dos2unix ($value);
			$_REQUEST[$key] = htmlspecialchars ($value, ENT_QUOTES, 'UTF-8');
		}
		$sic[$key] = $value;
		$seen_keys[$key] = 1;
	}

	// delete cookie information from the $_REQUEST array
	foreach (array_keys ($_REQUEST) as $key)
		if (! isset ($seen_keys[$key]))
			unset ($_REQUEST[$key]);

	if (isset ($_SERVER['PHP_AUTH_USER']))
		$_SERVER['PHP_AUTH_USER'] = htmlspecialchars ($_SERVER['PHP_AUTH_USER'], ENT_QUOTES, 'UTF-8');
	if (isset ($_SERVER['REMOTE_USER']))
		$_SERVER['REMOTE_USER'] = htmlspecialchars ($_SERVER['REMOTE_USER'], ENT_QUOTES, 'UTF-8');
}

// Return whether value passed is likely to be a URI.
function isUri ($uri)
{
	return preg_match (RE_STATIC_URI, $uri);
}

// Return whether value passed is likely to be a URL.
function isUrl ($url)
{
	return preg_match ('~^[\w]+:\/\/[\w \-\.\_\/]+$~', $url);
}

// JS scripts should be included through the addJS* functions. They will automatically appear in the <head> of your page.
// the addJS function has been replaced by addJSInternal, addJSExternal and addJSText() functions.  This function will be
// removed in 2.22.0
function addJS ($data, $inline = FALSE, $group = 'default')
{
	if ($inline)
		addJSText ($data, $group);
	else if (isURL ($data))
		addJSExternal ($data, $group);
	else
		addJSInternal ($data, $group);
}

// addJSExternal links to external scripts via URL and must be prefixed with
// either http:// or https://
function addJSExternal ($url, $group = 'default')
{
	if (! isUrl ($url))
		throw new InvalidArgException ('url', $url, 'Value passed is not a URL');

	addPageHeader ("<script type='text/javascript' src='${url}'></script>\n", $group);
}

// addJSInternal adds links that go through the Chrome module of index.php
function addJSInternal ($uri, $group = 'default')
{
	global $html_headers;

	// Add jquery.js and racktables.js the first time a Javascript file is added.
	// FIXME: Would it be better to do this initialization elsewhere?
	if (! array_key_exists ('a_core', $html_headers))
	{
		// Prevent infinite recursion.
		$html_headers['a_core'] = array();

		addJSInternal('js/jquery-1.4.4.min.js', 'a_core');
		addJSInternal('js/racktables.js',       'a_core');
	}

	if (! isUri ($uri))
		throw new InvalidArgException ('uri', $uri, 'Value passed is not a valid URI');

	addPageHeader ("<script type='text/javascript' src='?module=chrome&uri=${uri}'></script>\n", $group);
}

// This function adds script blocks that automatically appear in the <head> of your page.
// Scripts are included in the order of adding within the same group, and groups are sorted alphabetically.
function addJSText ($text, $group = 'default')
{
	if (isUrl ($text))
		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URL and should use addJSExternal()');

	if (isUri ($text))
		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URI and should use addJSInternal()');

	addPageHeader ('<script type="text/javascript">' . "\n" . trim ($text, "\r\n") . "\n</script>\n", $group);
}

// CSS styles should be included through this function.
// They automatically appear in the <head> of your page.
// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
// Styles are included in the order of adding.
function addCSS ($data, $inline = FALSE, $group = 'default')
{
	if ($inline)
		addCSSText ($data, $group);
	else if (isURL ($data))
		addCSSExternal ($data, $group);
	else
		addCSSInternal ($data, $group);
}

// CSS styles should be included through this function.
// They automatically appear in the <head> of your page.
// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
// Styles are included in the order of adding.
function addCSSExternal ($url, $group = 'default')
{
	if (! isUrl ($url))
		throw new InvalidArgException ('url', $url, 'Value passed is not a valid URL');

	addPageHeader ("<link rel=stylesheet type='text/css' href='$url' />\n", $group);
}

// CSS styles should be included through this function.
// They automatically appear in the <head> of your page.
// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
// Styles are included in the order of adding.
function addCSSInternal ($uri, $group = 'default')
{
	if (! isUri ($uri))
		throw new InvalidArgException ('uri', $uri, 'Value passed is not a valid URI');

	addPageHeader ("<link rel=stylesheet type='text/css' href='?module=chrome&uri=$uri' />\n", $group);
}

// CSS styles should be included through this function.
// They automatically appear in the <head> of your page.
// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
// Styles are included in the order of adding.
function addCSSText ($text, $group = 'default')
{
	if (isUrl ($text))
		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URL and should use addCSSExternal()');

	if (isUri ($text))
		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URI and should use addCSSInternal()');

	addPageHeader ('<style type="text/css">' . "\n" . trim ($text, "\r\n") . "\n</style>\n", $group);
}

function getRenderedIPNetCapacity ($range)
{
	switch (strlen ($range['ip_bin']))
	{
		case 4:  return getRenderedIPv4NetCapacity ($range);
		case 16: return getRenderedIPv6NetCapacity ($range);
		default: throw new InvalidArgException ('range["ip_bin"]', $range['ip_bin'], "Invalid binary IP");
	}
}

function getRenderedIPv4NetCapacity ($range)
{
	$class = 'net-usage';
	if (isset ($range['own_addrlist']))
	{
		// full mode
		// $a is "aquamarine zone", $b is "gray zone"
		$total = ip4_range_size ($range);

		// compute $a_total: own range size, without subranges
		if (! isset ($range['kidc']) || $range['kidc'] == 0)
			$a_total = $total;
		else
		{
			$a_total = 0;
			foreach ($range['spare_ranges'] as $mask => $spare_list)
				$a_total = bcadd ($a_total, bcmul (count ($spare_list), ip4_mask_size ($mask)), 0);
		}
		$a_used = markupIPAddrList ($range['own_addrlist']);
		$b_total = bcsub ($total, $a_total, 0);
		$b_used = markupIPAddrList ($range['addrlist']) - $a_used;

		// generate link to progress bar image
		$width = 100;
		if ($total != 0)
		{
			$px_a = round (bcdiv ($a_total, $total, 4) * $width);
			$px1 = round (bcdiv ($a_used, $total, 4) * $width);
			$px2 = $px_a - $px1;
			$px3 = round (bcdiv ($b_used, $total, 4) * $width);
			if ($px3 + $px1 + $px2 > $width)
				$px3 = $width - $px1 - $px2;
		}
		else
			$px1 = $px2 = $px3 = 0;

		$title_items = array();
		$title2_items = array();
		if ($a_total != 0)
		{
			$title_items[] = "$a_used / $a_total";
			$title2_items[] = sprintf ("%d%% used", bcdiv ($a_used, $a_total, 4) * 100);
		}
		if ($b_total != 0)
		{
			$title_items[] = ($b_used ? "$b_used / " : "") . $b_total;
			$title2_items[] = sprintf ("%d%% sub-allocated", bcdiv ($b_total, $total, 4) * 100);
		}
		$title = implode (', ', $title_items);
		$title2 = implode (', ', $title2_items);
		$text = "<img width='$width' height=10 border=0 title='$title2' src='?module=progressbar4&px1=$px1&px2=$px2&px3=$px3'>" .
			" <small class='title'>$title</small>";
	}
	else
	{
		// fast mode
		$class .= ' pending';
		addJSInternal ('js/net-usage.js');

		$free_text = '';
		if (isset ($range['kidc']) && $range['kidc'] > 0)
		{
			$free_masks = array_keys ($range['spare_ranges']);
			sort ($free_masks, SORT_NUMERIC);
			if ($mask = array_shift ($free_masks))
			{
				$cnt = count ($range['spare_ranges'][$mask]);
				$free_text = ', ' . ($cnt > 1 ? "<small>${cnt}&times;</small>" : "") . "/$mask free";
			}
		}
		$text = ip4_range_size ($range) . $free_text;
	}

	$div_id = $range['ip'] . '/' . $range['mask'];

	return "<div class=\"$class\" id=\"$div_id\">" . $text . "</div>";
}

function getRenderedIPv6NetCapacity ($range)
{
	$div_id = $range['ip'] . '/' . $range['mask'];
	$class = 'net-usage';
	if (isset ($range['addrlist']))
		$used = markupIPAddrList ($range['addrlist']);
	else
	{
		$used = NULL;
		$class .= ' pending';
		addJSInternal ('js/net-usage.js');
	}

	static $prefixes = array
	(
		0 =>  '',
		3 =>  'k',
		6 =>  'M',
		9 =>  'G',
		12 => 'T',
		15 => 'P',
		18 => 'E',
		21 => 'Z',
		24 => 'Y',
	);

	if ($range['mask'] <= 64)
	{
		$what = 'net';
		$preposition = 'in';
		$range['mask'] += 64;
	}
	else
	{
		$what = 'IP';
		$preposition = 'of';
	}
	$what .= (0 == $range['mask'] % 64 ? '' : 's');
	$addrc = isset ($used) ? "$used $preposition " : '';

	$dec_order = intval ((128 - $range['mask']) / 10) * 3;
	$mult = isset ($prefixes[$dec_order]) ? $prefixes[$dec_order] : '??';

	$cnt = 1 << ((128 - $range['mask']) % 10);
	if ($cnt == 1 && $mult == '')
		$cnt = '1';

	return "<div class=\"$class\" id=\"$div_id\">" . "{$addrc}${cnt}${mult} ${what}" . "</div>";
}

// Buffer the header only once. Disregard subsequent calls even if they
// are made for a different group.
function addPageHeader ($header, $group)
{
	global $html_headers;

	foreach ($html_headers as $group_contents)
		if (in_array ($header, $group_contents))
			return;
	$html_headers[$group][] = $header;
}

// print part of HTML HEAD block
function printPageHeaders ()
{
	global $pageheaders;
	ksort ($pageheaders);
	foreach ($pageheaders as $s)
		echo $s . "\n";
	// add tabindex to all input forms
	addJSInternal ('js/tabindex_auto.js');

	// add JS/CSS headers
	global $html_headers;
	ksort($html_headers);

	foreach ($html_headers as $group_name => $list)
		foreach ($list as $index => $item)
			echo "<!-- $group_name:$index -->\n" . $item;
}

function cmpTags ($a, $b)
{
	global $taglist;
	if (isset ($a['id']) && isset ($b['id']))
	{
		$a_root = array_first ($taglist[$a['id']]['trace']);
		$b_root = array_first ($taglist[$b['id']]['trace']);
		if ($a_root < $b_root)
			return -1;
		elseif ($a_root > $b_root)
			return 1;
	}
	elseif (isset ($a['id']))
		return -1;
	elseif (isset ($b['id']))
		return 1;

	return strcmp ($a['tag'], $b['tag']);
}

function getTagClassName ($tagid)
{
	global $taglist;

	$class = '';
	foreach ($taglist[$tagid]['trace'] as $parent)
		$class .= 'tag-' . $parent . ' ';
	$class .= 'tag-' . $tagid . ' etag-' . $tagid;

	$class .= getTagClass ($taglist[$tagid]);

	return $class;
}

function serializeTags ($chain, $baseurl = '')
{
	global $taglist;
	$tmp = array();
	usort ($chain, 'cmpTags');
	foreach ($chain as $taginfo)
	{
		$title = '';
		if (isset ($taginfo['user']) && isset ($taginfo['time']))
			$title = htmlspecialchars ($taginfo['user'] . ', ' . formatAge ($taginfo['time']), ENT_QUOTES);
		if (isset($taginfo['parent_id']))
		{
			$parent_info = array();
			foreach ($taglist[$taginfo['id']]['trace'] as $tag_id)
				$parent_info[] = $taglist[$tag_id]['tag'];
			$parent_info[] = $taginfo['tag'];
			if ($title != '')
				$title .= "\n";
			$title .= implode (" &rarr;  ", $parent_info);
		}
		$has_descr = array_key_exists ('description', $taginfo) && $taginfo['description'] !== NULL;
		if ($has_descr)
			$title .= ($title == '' ? '' : "\n\n") . stringForOption ($taginfo['description'], 0);
		if ($title != '')
			$title = "title='$title'";

		if (! array_key_exists ('id', $taginfo))
			$class = '';
		else
		{
			$class = $has_descr ? 'tag-descr ' : '';
			$class .= getTagClassName ($taginfo['id']);
			$class = "class='${class}'";
		}

		$href = '';
		if ($baseurl == '')
			$tag = 'span';
		else
		{
			$tag = 'a';
			$href = "href='${baseurl}cft[]=${taginfo['id']}'";
		}
		$tmp[] = "<$tag $href $title $class>" . $taginfo['tag'] . "</$tag>";
	}
	return implode (', ', $tmp);
}

function startPortlet ($title = '')
{
	echo "<div class=portlet><h2>${title}</h2>";
}

function finishPortlet ()
{
	echo "</div>\n";
}

function getPageName ($page_code)
{
	global $page;
	$title = isset ($page[$page_code]['title']) ? $page[$page_code]['title'] : callHook ('dynamic_title_decoder', $page_code);
	if (is_array ($title))
		$title = $title['name'];
	return $title;
}

function printTagTRs ($cell, $baseurl = '')
{
	if (getConfigVar ('SHOW_EXPLICIT_TAGS') == 'yes' && count ($cell['etags']))
	{
		echo "<tr><th width='50%' class=tagchain>Explicit tags:</th><td class=tagchain>";
		echo serializeTags ($cell['etags'], $baseurl) . "</td></tr>\n";
	}
	if (getConfigVar ('SHOW_IMPLICIT_TAGS') == 'yes' && count ($cell['itags']))
	{
		echo "<tr><th width='50%' class=tagchain>Implicit tags:</th><td class=tagchain>";
		echo serializeTags ($cell['itags'], $baseurl) . "</td></tr>\n";
	}
	if (getConfigVar ('SHOW_AUTOMATIC_TAGS') == 'yes' && count ($cell['atags']))
	{
		echo "<tr><th width='50%' class=tagchain>Automatic tags:</th><td class=tagchain>";
		echo serializeTags ($cell['atags']) . "</td></tr>\n";
	}
}

// stub function to override it by chain-connected hooks
function modifyEntitySummary ($cell, $summary)
{
	return $summary;
}

// renders 'summary' portlet, which persist on default tab of every realm page.
// $values is a tricky array.
// if its value is a string, it is treated as right td inner html, and the key is treated as left th text, colon appends there automatically.
// 'tags' key has a special meaning: instead of value, the result of printTagTRs call is appended to output
// if the value is a single-element array, its value rendered as-is instead of <tr> tag and all its contents.
// if the value is an array, its first 2 items are treated as left and right contents of row, no colon is appended. Used to enable non-unique titles
function renderEntitySummary ($cell, $title, $values = array())
{
	global $page_by_realm;
	// allow plugins to override summary table
	$values = callHook ('modifyEntitySummary', $cell, $values);

	startPortlet ($title);
	echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
	foreach ($values as $name => $value)
	{
		if (is_array ($value) && count ($value) == 1)
		{
			$value = array_shift ($value);
			echo $value;
			continue;
		}
		if (is_array ($value))
		{
			$name = array_shift ($value);
			$value = array_shift ($value);
		}
		elseif (! is_array ($value))
			$name .= ':';
		$class = 'tdright';
		$m = array();
		if (preg_match('/^\{(.*?)\}(.*)/', $name, $m))
		{
			$class .= ' ' . $m[1];
			$name = $m[2];
		}
		if ($name == 'tags:')
		{
			$baseurl = '';
			if (isset ($page_by_realm[$cell['realm']]))
				$baseurl = makeHref(array('page'=>$page_by_realm[$cell['realm']], 'tab'=>'default'))."&";
			printTagTRs ($cell, $baseurl);
		}
		else
			echo "<tr><th width='50%' class='$class'>$name</th><td class=tdleft>$value</td></tr>";
	}
	echo "</table>\n";
	finishPortlet();
}

function getOpLink ($params, $title,  $img_name = '', $comment = '', $class = '')
{
	if (isset ($params))
	{
		$ret = '<a href="' . makeHrefProcess ($params) . '"';
		$class .= ' input';
	}
	else
	{
		$ret = '<a href="#" onclick="return false;"';
		$class .= ' noclick';
	}
	if ($comment != '')
		$ret .= ' title="' . htmlspecialchars ($comment, ENT_QUOTES) . '"';
	$class = trim ($class);
	if ($class != '')
		$ret .= ' class="' . htmlspecialchars ($class, ENT_QUOTES) . '"';
	$ret .= '>';
	if ($img_name != '')
	{
		$ret .= getImageHREF ($img_name, $comment);
		if ($title != '')
			$ret .= ' ';
	}
	if (FALSE !== strpos ($class, 'need-confirmation'))
		addJSInternal ('js/racktables.js');
	$ret .= $title . '</a>';
	return $ret;
}

function getPopupLink ($helper, $params, $window_name = '', $img_name = '', $title = '', $comment = '', $class = '')
{
	$ret = '';
	$popup_args = 'height=700, width=700, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
	$ret .= '<a href="#"';
	$class = trim ($class);
	if ($class != '')
		$ret .= ' class="' . htmlspecialchars ($class, ENT_QUOTES) . '"';
	if ($comment != '')
		$ret .= 'title="' . htmlspecialchars ($comment, ENT_QUOTES) . '"';
	$href = makeHref (array ('module' => 'popup', 'helper' => $helper) + makePageParams ($params));
	$ret .= " onclick=\"window.open('$href', '$window_name', '$popup_args'); return false\">";

	if ($img_name != '')
	{
		$ret .= getImageHREF ($img_name, $comment);
		if ($title != '')
			$ret .= ' ';
	}
	$ret .= $title;
	$ret .= '</a>';
	return $ret;
}

function renderProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
{
	echo getProgressBar ($percentage, $theme, $inline);
}

function getProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
{
	$done = ((int) ($percentage * 100));
	if (! $inline)
		$src = "?module=progressbar&done=$done" . (empty ($theme) ? '' : "&theme=${theme}");
	else
	{
		$bk_request = $_REQUEST;
		$_REQUEST['theme'] = $theme;
		$src = 'data:image/png;base64,' . chunk_split (base64_encode (getOutputOf ('renderProgressBarImage', $done)));
		$_REQUEST = $bk_request;
	}
	$ret = "<img width=100 height=10 border=0 title='${done}%' src='$src'>";
	return $ret;
}

function renderNetVLAN ($cell)
{
	echo getRenderedNetVLAN ($cell);
}

function getRenderedNetVLAN ($cell)
{
	if (empty ($cell['8021q']))
		return;
	$links = array();
	foreach ($cell['8021q'] as $vi)
	{
		$vlan_info = getVlanRow ("${vi['domain_id']}-${vi['vlan_id']}");
		$links[] = formatVLANAsShortLink ($vlan_info);
	}
	$noun = count ($cell['8021q']) > 1 ? 'VLANs' : 'VLAN';
	return "<div class='vlan'><strong><small>${noun}</small> " . implode (', ', $links) . '</strong></div>';
}

// DEPRECATED and will be removed in 0.22.0
function includeJQueryUI ($do_css = TRUE)
{
	includeJQueryUIJS();
	if ($do_css)
		includeJQueryUICSS();
}

function includeJQueryUIJS()
{
	addJSInternal ('js/jquery-ui-1.8.21.min.js');
}

function includeJQueryUICSS()
{
	addCSSInternal ('css/jquery-ui-1.8.22.redmond.css');
}

function getRenderedIPPortPair ($ip, $port = NULL)
{
	return "<a href=\"" .
		makeHref (array ('page' => 'ipaddress',  'tab'=>'default', 'ip' => $ip)) .
		"\">" . $ip . "</a>" .
		(isset ($port) ? ":" . $port : "");
}

// Print common operation form prologue, include bypass argument, if
// appropriate, and some extra hidden inputs, if requested.
// Use special encoding for upload forms
function printOpFormIntro ($opname, $extra = array(), $upload = FALSE)
{
	global $pageno, $tabno, $page;

	echo "<form method=post id=${opname} name=${opname} action='?module=redirect&page=${pageno}&tab=${tabno}&op=${opname}'";
	if ($upload)
		echo " enctype='multipart/form-data'";
	echo ">";
	fillBypassValues ($pageno, $extra);
	foreach ($extra as $inputname => $inputvalue)
		printf ('<input type=hidden name="%s" value="%s">', htmlspecialchars ($inputname, ENT_QUOTES), htmlspecialchars ($inputvalue, ENT_QUOTES));
}

// Display hrefs for all of a file's parents. If scissors are requested,
// prepend cutting button to each of them.
function serializeFileLinks ($links, $scissors = FALSE)
{
	$comma = '';
	$ret = '';
	foreach ($links as $link_id => $li)
	{
		$cell = spotEntity ($li['entity_type'], $li['entity_id']);
		$ret .= $comma;
		if ($scissors)
			$ret .= getOpLink (array('op'=>'unlinkFile', 'link_id'=>$link_id), '', 'cut', 'Unlink file') . ' ';
		$ret .= mkCellA ($cell);
		$comma = '<br>';
	}
	return $ret;
}

function makeFileDownloadButton ($file_id, $imgname = 'download')
{
	$href = makeHref (array ('module' => 'download', 'file_id' => $file_id));
	$img = getImageHREF ($imgname, 'download file');
	return "<a href='${href}'>${img}</a>";
}

// This function is DEPRECATED and will be removed in version 0.22.0.
// Instead of it please use one of the stringFor...() functions below.
function niftyString ($string, $maxlen = 30, $usetags = TRUE)
{
	$cutind = '&hellip;'; // length is 1
	if ($string == '')
		return '&nbsp;';
	// a tab counts for a space
	$string = preg_replace ("/\t/", ' ', $string);
	if (! $maxlen || mb_strlen ($string) <= $maxlen)
		return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
	return
		($usetags ? ("<span title='" . htmlspecialchars ($string, ENT_QUOTES, 'UTF-8') . "'>") : '') .
		str_replace (' ', '&nbsp;', htmlspecialchars (mb_substr ($string, 0, $maxlen - 1), ENT_QUOTES, 'UTF-8')) .
		$cutind .
		($usetags ? '</span>' : '');
}

// "Some text, %s, some more text."
function stringForLabel ($string, $maxlen = 30)
{
	// A tab counts for a space.
	$string = preg_replace ("/\t/", ' ', $string);
	$full = htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
	if ($maxlen == 0 || mb_strlen ($string) <= $maxlen)
		return $full;
	$trimmed = mb_substr ($string, 0, $maxlen - 1);
	$trimmed = htmlspecialchars ($trimmed, ENT_QUOTES, 'UTF-8');
	$trimmed = str_replace (' ', '&nbsp;', $trimmed) . '&hellip;';
	return "<span title='${full}'>${trimmed}</span>";
}

// "<TD>%s</TD>"
function stringForTD ($string, $maxlen = 30)
{
	// The non-breaking space helps the TD to render properly.
	return $string == '' ? '&nbsp;' : stringForLabel ($string, $maxlen);
}

// "<INPUT type=text value='%s'>"
function stringForTextInputValue ($string, $maxlen = 30)
{
	if ($maxlen != 0)
		$string = mb_substr ($string, 0, $maxlen);
	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
}

// "<TEXTAREA>%s</TEXTAREA>"
function stringForTextarea ($string)
{
	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
}

// <OPTION>%s</OPTION>
function stringForOption ($string, $maxlen = 80)
{
	$string = preg_replace ("/\t/", ' ', $string);
	if ($maxlen == 0 || mb_strlen ($string) <= $maxlen)
		return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
	$string = mb_substr ($string, 0, $maxlen - 1);
	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8') . '&hellip;';
}

function printTagsPicker ($preselect=NULL)
{
	global $taglist;
	if (! count ($taglist))
	{
		printf ('(None exist yet, %s?)', mkA ('configure', 'tagtree', NULL, 'edit'));
		return;
	}
	printTagsPickerInput ('taglist');
	printTagsPickerUl ('taglist', $preselect);
	enableTagsPicker ();
}

function printTagsPickerInput ($input_name)
{
	# use data-attribute as identifier for tagit
	echo "<input type='text' data-tagit-valuename='" . $input_name . "' data-tagit='yes' placeholder='new tags here...' class='ui-autocomplete-input' autocomplete='off' role='textbox' aria-autocomplete='list' aria-haspopup='true'>";
	echo "<span title='show tag tree' class='icon-folder-open tagit_input_" . $input_name . "'></span>";
}

function printTagsPickerUl ($input_name, $preselect = NULL)
{
	global $target_given_tags;
	if ($preselect === NULL)
		$preselect = $target_given_tags;
	foreach (array_keys ($preselect) as $key)
	{
		$preselect[$key]['time_parsed'] = formatAge ($preselect[$key]['time']); # readable time format
		$preselect[$key]['description'] = stringForTextarea ($preselect[$key]['description']);
	}
	usort ($preselect, 'cmpTags');
	$preselect_hidden = "";
	foreach ($preselect as $value)
		$preselect_hidden .= "<input type=hidden name=" . $input_name . "[] value=" . $value['id'] . ">";
	echo $preselect_hidden; # print preselected tags id that used in case javascript problems
	echo "<ul data-tagit='yes' data-tagit-valuename='" . $input_name . "' data-tagit-preselect='" . json_encode($preselect) . "' class='tagit-vertical'></ul>";
}

function enableTagsPicker ()
{
	global $taglist;
	static $taglist_inserted;
	includeJQueryUIJS();
	includeJQueryUICSS();
	addCSSInternal ('css/tagit.css');
	addJSInternal ('js/tag-it.js');
	addJSInternal ('js/tag-it-local.js');
	if (! $taglist_inserted)
	{
		$taglist_filtered = array();
		foreach ($taglist as $key => $taginfo) # remove unused fields
		{
			$taglist_filtered[$key] = array_sub ($taginfo, array("tag", "is_assignable", "trace"));
			if ($taginfo['color'] != NULL)
				$taglist_filtered[$key]['tagclass'] = getTagClass ($taginfo);
		}
		addJSText ('var taglist = ' . json_encode ($taglist_filtered) . ';');
		$taglist_inserted = TRUE;
	}
}

function makeIPAllocLink ($ip_bin, $alloc, $display_ifname = FALSE)
{
	$object_name = ! isset ($object_name) || $object_name == '' ?
		formatEntityName (spotEntity ('object', $alloc['object_id'])) :
		$alloc['object_name'];
	$title = $display_ifname ?
		'' :
		"{$alloc['name']} @ {$object_name}";
	return
		'<a href="' . makeHref (array ('page' => 'object', 'tab' => 'default', 'object_id' => $alloc['object_id'], 'hl_ip' => ip_format ($ip_bin))) . '"' .
		' title="' . htmlspecialchars ($title, ENT_QUOTES) . '"' .
		">" . ($display_ifname ? $alloc['name'] . '@' : '') . $object_name . "</a>";
}

function makeHtmlTag ($tagname, $attributes = array())
{
	$ret = '<' . $tagname;
	foreach ($attributes as $key => $value)
		$ret .= " $key=\"" . htmlspecialchars($value, ENT_QUOTES) . '"';
	$ret .= '>';
	return $ret;
}

function getCellClass ($cell, $context)
{
	$ctxmap = array
	(
		'atom_plain' => 'background:white;',
		'atom_selected' => 'border:3px solid #80ffff !important;background:white;',
		'list_plain' => '',
		'list_selected' => 'outline: 3px solid #0aff0a;',
	);
	if (! array_key_exists ($context, $ctxmap))
		throw new InvalidArgException ('context', $context, 'unknown value');
	if (! array_key_exists ('colors', $cell) || ! count ($cell['colors']))
		return '';
	$style = $ctxmap[$context];
	$step = intval (round (100 / count ($cell['colors'])));
	$percent = 0;
	$gradient = '';
	foreach ($cell['colors'] as $color)
	{
		$rgb = colorHex2Rgb ($color);
		$gradient .= "rgba(${rgb},0.2) ${percent}%, rgba(${rgb},0.3) " . ($percent + $step) . "%,";
		$percent += $step;
	}
	$style .= "background-image:linear-gradient(135deg," . trim ($gradient, ',') . ") !important;";
	$cell_id = $cell[$cell['realm'] == 'user' ? 'user_id' : 'id'];
	return getCachedCSSClassForStyle ("cellcolor-${cell_id}", $style);
}

function getTagClass ($taginfo)
{
	if (! array_key_exists ('color', $taginfo) || $taginfo['color'] === NULL)
		return '';
	$rgb = colorHex2Rgb ($taginfo['color'], TRUE);
	return getCachedCSSClassForStyle ("tagcolor-${taginfo['id']}", "background: rgb($rgb);");
}

// This function has a side effect: it adds inline CSS.
function getCachedCSSClassForStyle ($class, $style)
{
	static $cache = array();
	$cachedclass = array_search ($style, $cache);
	if ($cachedclass !== FALSE)
		return " $cachedclass";
	addCSSText (".{$class} {{$style}}");
	$cache[$class] = $style;
	return " $class";
}

function colorHex2Rgb($color, $pastel = FALSE)
{
	$color = trim ($color, '#');

	if ($pastel)
	{
		$rgb = intval (round ((hexdec (substr ($color, 0, 2)) + 255) / 2)) . ',';
		$rgb .= intval (round ((hexdec (substr ($color, 2, 2)) + 255) / 2)) . ',';
		$rgb .= intval (round ((hexdec (substr ($color, 4, 2)) + 255) / 2));
	}
	else
		$rgb = hexdec (substr ($color, 0, 2)) . ',' . hexdec (substr ($color, 2, 2)) . ',' . hexdec (substr ($color, 4, 2));

	return $rgb;
}

function setEntityColors(&$entity)
{
	$entity['colors'] = array();
	foreach ($entity['etags'] as $taginfo)
		if ($taginfo['color'] !== NULL && ! in_array ($taginfo['color'], $entity['colors']))
		{
			$entity['colors'][] = $taginfo['color'];
			getTagClass ($taginfo); // set tag CSS class
		}
}
