1<?php
2
3# This file is a part of RackTables, a datacenter and server room management
4# framework. See accompanying file "COPYING" for the full copyright and
5# licensing information.
6
7// Let's have it here, so extensions can add their own images.
8$image = array();
9$image['rackspace']['path'] = 'pix/racks.png';
10$image['rackspace']['width'] = 218;
11$image['rackspace']['height'] = 200;
12$image['objects']['path'] = 'pix/server.png';
13$image['objects']['width'] = 218;
14$image['objects']['height'] = 200;
15$image['depot']['path'] = 'pix/server.png';
16$image['depot']['width'] = 218;
17$image['depot']['height'] = 200;
18$image['files']['path'] = 'pix/files.png';
19$image['files']['width'] = 218;
20$image['files']['height'] = 200;
21$image['ipv4space']['path'] = 'pix/addressspace.png';
22$image['ipv4space']['width'] = 218;
23$image['ipv4space']['height'] = 200;
24$image['ipv6space']['path'] = 'pix/addressspacev6.png';
25$image['ipv6space']['width'] = 218;
26$image['ipv6space']['height'] = 200;
27$image['ipv4slb']['path'] = 'pix/slb.png';
28$image['ipv4slb']['width'] = 218;
29$image['ipv4slb']['height'] = 200;
30$image['config']['path'] = 'pix/configuration.png';
31$image['config']['width'] = 218;
32$image['config']['height'] = 200;
33$image['reports']['path'] = 'pix/report.png';
34$image['reports']['width'] = 218;
35$image['reports']['height'] = 200;
36$image['8021q']['path'] = 'pix/8021q.png';
37$image['8021q']['width'] = 218;
38$image['8021q']['height'] = 200;
39$image['objectlog']['path'] = 'pix/crystal-mimetypes-shellscript-218x200.png';
40$image['objectlog']['width'] = 218;
41$image['objectlog']['height'] = 200;
42$image['virtual']['path'] = 'pix/virtualresources.png';
43$image['virtual']['width'] = 218;
44$image['virtual']['height'] = 200;
45$image['cables']['path'] = 'pix/patch_cables.png';
46$image['cables']['width'] = 218;
47$image['cables']['height'] = 200;
48$image['download']['path'] = 'pix/download.png';
49$image['download']['width'] = 16;
50$image['download']['height'] = 16;
51$image['DOWNLOAD']['path'] = 'pix/download-big.png';
52$image['DOWNLOAD']['width'] = 32;
53$image['DOWNLOAD']['height'] = 32;
54$image['plug']['path'] = 'pix/tango-network-wired.png';
55$image['plug']['width'] = 16;
56$image['plug']['height'] = 16;
57$image['cut']['path'] = 'pix/tango-edit-cut-16x16.png';
58$image['cut']['width'] = 16;
59$image['cut']['height'] = 16;
60$image['Cut']['path'] = 'pix/tango-edit-cut-22x22.png';
61$image['Cut']['width'] = 22;
62$image['Cut']['height'] = 22;
63$image['Cut gray']['path'] = 'pix/tango-edit-cut-22x22-gray.png';
64$image['Cut gray']['width'] = 22;
65$image['Cut gray']['height'] = 22;
66$image['CUT']['path'] = 'pix/tango-edit-cut-32x32.png';
67$image['CUT']['width'] = 32;
68$image['CUT']['height'] = 32;
69$image['CUT gray']['path'] = 'pix/tango-edit-cut-32x32-gray.png';
70$image['CUT gray']['width'] = 32;
71$image['CUT gray']['height'] = 32;
72$image['add']['path'] = 'pix/tango-list-add.png';
73$image['add']['width'] = 16;
74$image['add']['height'] = 16;
75$image['ADD']['path'] = 'pix/tango-list-add-big.png';
76$image['ADD']['width'] = 32;
77$image['ADD']['height'] = 32;
78$image['delete']['path'] = 'pix/tango-list-remove.png';
79$image['delete']['width'] = 16;
80$image['delete']['height'] = 16;
81$image['DELETE']['path'] = 'pix/tango-list-remove-32x32.png';
82$image['DELETE']['width'] = 32;
83$image['DELETE']['height'] = 32;
84$image['destroy']['path'] = 'pix/tango-user-trash-16x16.png';
85$image['destroy']['width'] = 16;
86$image['destroy']['height'] = 16;
87$image['nodestroy']['path'] = 'pix/tango-user-trash-16x16-gray.png';
88$image['nodestroy']['width'] = 16;
89$image['nodestroy']['height'] = 16;
90$image['NODESTROY']['path'] = 'pix/tango-user-trash-32x32-gray.png';
91$image['NODESTROY']['width'] = 32;
92$image['NODESTROY']['height'] = 32;
93$image['DESTROY']['path'] = 'pix/tango-user-trash-32x32.png';
94$image['DESTROY']['width'] = 32;
95$image['DESTROY']['height'] = 32;
96$image['nodelete']['path'] = 'pix/tango-list-remove-shadow.png';
97$image['nodelete']['width'] = 16;
98$image['nodelete']['height'] = 16;
99$image['inservice']['path'] = 'pix/tango-emblem-system.png';
100$image['inservice']['width'] = 16;
101$image['inservice']['height'] = 16;
102$image['notinservice']['path'] = 'pix/tango-dialog-error.png';
103$image['notinservice']['width'] = 16;
104$image['notinservice']['height'] = 16;
105$image['find']['path'] = 'pix/tango-system-search.png';
106$image['find']['width'] = 16;
107$image['find']['height'] = 16;
108$image['next']['path'] = 'pix/tango-go-next.png';
109$image['next']['width'] = 32;
110$image['next']['height'] = 32;
111$image['prev']['path'] = 'pix/tango-go-previous.png';
112$image['prev']['width'] = 32;
113$image['prev']['height'] = 32;
114$image['COMMIT']['path'] = 'pix/tango-go-prev-next-32x32.png';
115$image['COMMIT']['width'] = 32;
116$image['COMMIT']['height'] = 32;
117$image['COMMIT gray']['path'] = 'pix/tango-go-prev-next-gray-32x32.png';
118$image['COMMIT gray']['width'] = 32;
119$image['COMMIT gray']['height'] = 32;
120$image['RECALC']['path'] = 'pix/tango-view-refresh-32x32.png';
121$image['RECALC']['width'] = 32;
122$image['RECALC']['height'] = 32;
123$image['recalc']['path'] = 'pix/tango-view-refresh-16x16.png';
124$image['recalc']['width'] = 16;
125$image['recalc']['height'] = 16;
126$image['clear']['path'] = 'pix/tango-edit-clear.png';
127$image['clear']['width'] = 16;
128$image['clear']['height'] = 16;
129$image['CLEAR']['path'] = 'pix/tango-edit-clear-big.png';
130$image['CLEAR']['width'] = 32;
131$image['CLEAR']['height'] = 32;
132$image['CLEAR gray']['path'] = 'pix/tango-edit-clear-gray-32x32.png';
133$image['CLEAR gray']['width'] = 32;
134$image['CLEAR gray']['height'] = 32;
135$image['save']['path'] = 'pix/tango-document-save-16x16.png';
136$image['save']['width'] = 16;
137$image['save']['height'] = 16;
138$image['SAVE']['path'] = 'pix/tango-document-save-32x32.png';
139$image['SAVE']['width'] = 32;
140$image['SAVE']['height'] = 32;
141$image['NOSAVE']['path'] = 'pix/tango-document-save-32x32-gray.png';
142$image['NOSAVE']['width'] = 32;
143$image['NOSAVE']['height'] = 32;
144$image['create']['path'] = 'pix/tango-document-new.png';
145$image['create']['width'] = 16;
146$image['create']['height'] = 16;
147$image['CREATE']['path'] = 'pix/tango-document-new-big.png';
148$image['CREATE']['width'] = 32;
149$image['CREATE']['height'] = 32;
150$image['DENIED']['path'] = 'pix/tango-dialog-error-big.png';
151$image['DENIED']['width'] = 32;
152$image['DENIED']['height'] = 32;
153$image['node-collapsed']['path'] = 'pix/node-collapsed.png';
154$image['node-collapsed']['width'] = 16;
155$image['node-collapsed']['height'] = 16;
156$image['node-expanded']['path'] = 'pix/node-expanded.png';
157$image['node-expanded']['width'] = 16;
158$image['node-expanded']['height'] = 16;
159$image['node-expanded-static']['path'] = 'pix/node-expanded-static.png';
160$image['node-expanded-static']['width'] = 16;
161$image['node-expanded-static']['height'] = 16;
162$image['dragons']['path'] = 'pix/mitsudragon.png';
163$image['dragons']['width'] = 195;
164$image['dragons']['height'] = 33;
165$image['LB']['path'] = 'pix/loadbalancer.png';
166$image['LB']['width'] = 32;
167$image['LB']['height'] = 32;
168$image['RS pool']['path'] = 'pix/serverpool.png';
169$image['RS pool']['width'] = 48;
170$image['RS pool']['height'] = 16;
171$image['VS']['path'] = 'pix/servicesign.png';
172$image['VS']['width'] = 39;
173$image['VS']['height'] = 62;
174$image['router']['path'] = 'pix/router.png';
175$image['router']['width'] = 32;
176$image['router']['height'] = 32;
177$image['object']['path'] = 'pix/bracket-16x16.png';
178$image['object']['width'] = 16;
179$image['object']['height'] = 16;
180$image['OBJECT']['path'] = 'pix/bracket-32x32.png';
181$image['OBJECT']['width'] = 32;
182$image['OBJECT']['height'] = 32;
183$image['LOCATION']['path'] = 'pix/tango-internet-32x32.png';
184$image['LOCATION']['width'] = 32;
185$image['LOCATION']['height'] = 32;
186$image['attach']['path'] = 'pix/tango-mail-attachment-16x16.png';
187$image['attach']['width'] = 16;
188$image['attach']['height'] = 16;
189$image['Attach']['path'] = 'pix/tango-mail-attachment-22x22.png';
190$image['Attach']['width'] = 22;
191$image['Attach']['height'] = 22;
192$image['ATTACH']['path'] = 'pix/tango-mail-attachment-32x32.png';
193$image['ATTACH']['width'] = 32;
194$image['ATTACH']['height'] = 32;
195$image['favorite']['path'] = 'pix/tango-emblem-favorite.png';
196$image['favorite']['width'] = 16;
197$image['favorite']['height'] = 16;
198$image['computer']['path'] = 'pix/tango-computer.png';
199$image['computer']['width'] = 16;
200$image['computer']['height'] = 16;
201$image['empty file']['path'] = 'pix/crystal-file-empty-32x32.png';
202$image['empty file']['width'] = 32;
203$image['empty file']['height'] = 32;
204$image['text file']['path'] = 'pix/crystal-file-text-32x32.png';
205$image['text file']['width'] = 32;
206$image['text file']['height'] = 32;
207$image['image file']['path'] = 'pix/crystal-file-image-32x32.png';
208$image['image file']['width'] = 32;
209$image['image file']['height'] = 32;
210$image['text']['path'] = 'pix/tango-text-x-generic-16x16.png';
211$image['text']['width'] = 16;
212$image['text']['height'] = 16;
213$image['NET']['path'] = 'pix/crystal-network_local-32x32.png';
214$image['NET']['width'] = 32;
215$image['NET']['height'] = 32;
216$image['net']['path'] = 'pix/crystal-network_local-16x16.png';
217$image['net']['width'] = 16;
218$image['net']['height'] = 16;
219$image['USER']['path'] = 'pix/crystal-edit-user-32x32.png';
220$image['USER']['width'] = 32;
221$image['USER']['height'] = 32;
222$image['setfilter']['path'] = 'pix/pgadmin3-viewfiltereddata.png';
223$image['setfilter']['width'] = 32;
224$image['setfilter']['height'] = 32;
225$image['setfilter gray']['path'] = 'pix/pgadmin3-viewfiltereddata-grayscale.png';
226$image['setfilter gray']['width'] = 32;
227$image['setfilter gray']['height'] = 32;
228$image['resetfilter']['path'] = 'pix/pgadmin3-viewdata.png';
229$image['resetfilter']['width'] = 32;
230$image['resetfilter']['height'] = 32;
231$image['resetfilter gray']['path'] = 'pix/pgadmin3-viewdata-grayscale.png';
232$image['resetfilter gray']['width'] = 32;
233$image['resetfilter gray']['height'] = 32;
234$image['knight']['path'] = 'pix/smiley_knight.png';
235$image['knight']['width'] = 72;
236$image['knight']['height'] = 33;
237$image['Zoom']['path'] = 'pix/tango-system-search-22x22.png';
238$image['Zoom']['width'] = 22;
239$image['Zoom']['height'] = 22;
240$image['Zooming']['path'] = 'pix/tango-view-fullscreen-22x22.png';
241$image['Zooming']['width'] = 22;
242$image['Zooming']['height'] = 22;
243$image['UNLOCK']['path'] = 'pix/crystal-actions-unlock-32x32.png';
244$image['UNLOCK']['width'] = 32;
245$image['UNLOCK']['height'] = 32;
246$image['CLOCK']['path'] = 'pix/tango-appointment-32x32.png';
247$image['CLOCK']['width'] = 32;
248$image['CLOCK']['height'] = 32;
249$image['DQUEUE done']['path'] = 'pix/crystal-ok-32x32.png';
250$image['DQUEUE done']['width'] = 32;
251$image['DQUEUE done']['height'] = 32;
252$image['DQUEUE sync_aging']['path'] = 'pix/tango-appointment-32x32.png';
253$image['DQUEUE sync_aging']['width'] = 32;
254$image['DQUEUE sync_aging']['height'] = 32;
255$image['DQUEUE resync_aging']['path'] = 'pix/tango-appointment-32x32.png';
256$image['DQUEUE resync_aging']['width'] = 32;
257$image['DQUEUE resync_aging']['height'] = 32;
258$image['DQUEUE sync_ready']['path'] = 'pix/tango-emblem-system-32x32.png';
259$image['DQUEUE sync_ready']['width'] = 32;
260$image['DQUEUE sync_ready']['height'] = 32;
261$image['DQUEUE resync_ready']['path'] = 'pix/tango-emblem-important-32x32.png';
262$image['DQUEUE resync_ready']['width'] = 32;
263$image['DQUEUE resync_ready']['height'] = 32;
264$image['DQUEUE failed']['path'] = 'pix/tango-emblem-unreadable-32x32.png';
265$image['DQUEUE failed']['width'] = 32;
266$image['DQUEUE failed']['height'] = 32;
267$image['DQUEUE disabled']['path'] = 'pix/tango-emblem-readonly-32x32.png';
268$image['DQUEUE disabled']['width'] = 32;
269$image['DQUEUE disabled']['height'] = 32;
270$image['copy']['path'] = 'pix/tango-edit-copy-16x16.png';
271$image['copy']['width'] = 16;
272$image['copy']['height'] = 16;
273$image['COPY']['path'] = 'pix/tango-edit-copy-32x32.png';
274$image['COPY']['width'] = 32;
275$image['COPY']['height'] = 32;
276$image['html']['path'] = 'pix/tango-text-html.png';
277$image['html']['width'] = 16;
278$image['html']['height'] = 16;
279$image['pencil']['path'] = 'pix/pencil-icon.png';
280$image['pencil']['width'] = 12;
281$image['pencil']['height'] = 12;
282$image['link up']['path'] = 'pix/link-up.png';
283$image['link up']['width'] = 16;
284$image['link up']['height'] = 16;
285$image['link down']['path'] = 'pix/link-down.png';
286$image['link down']['width'] = 16;
287$image['link down']['height'] = 16;
288$image['link disabled']['path'] = 'pix/link-disabled.png';
289$image['link disabled']['width'] = 16;
290$image['link disabled']['height'] = 16;
291$image['16x16t']['path'] = 'pix/1x1t.gif';
292$image['16x16t']['width'] = 16;
293$image['16x16t']['height'] = 16;
294$image['disable']['path'] = 'pix/link-disabled.png';
295$image['disable']['width'] = 16;
296$image['disable']['height'] = 16;
297$image['enable']['path'] = 'pix/link-up.png';
298$image['enable']['width'] = 16;
299$image['enable']['height'] = 16;
300$image['upgrade']['path'] = 'pix/tango-go-up.png';
301$image['upgrade']['width'] = 16;
302$image['upgrade']['height'] = 16;
303
304// Use the same workflow as for the array above:
305// 1. This file is required early, it assigns the default elements.
306// 2. Then plugin(s) load and can modify the array if necessary.
307// 3. Then interface-config.php, when conditionally required, can read it.
308$tag_palette = array
309(
310	'FFFFFF',
311	'C0C0C0',
312	'808080',
313	'000000',
314	'FF0000',
315	'800000',
316	'FF8000',
317	'FFFF00',
318	'808000',
319	'00FF00',
320	'008000',
321	'00FFFF',
322	'008080',
323	'0000FF',
324	'000080',
325	'FF00FF',
326	'800080',
327);
328
329$page_by_realm = array();
330$page_by_realm['object'] = 'depot';
331$page_by_realm['rack'] = 'rackspace';
332$page_by_realm['ipv4net'] = 'ipv4space';
333$page_by_realm['ipv6net'] = 'ipv6space';
334$page_by_realm['ipv4vs'] = 'ipv4slb';
335$page_by_realm['ipv4rspool'] = 'ipv4slb';
336$page_by_realm['file'] = 'files';
337$page_by_realm['user'] = 'userlist';
338
339function getSelectOptions ($options, $selected_id = NULL)
340{
341	$ret = '';
342	foreach ($options as $key => $value)
343	{
344		$selected = is_array ($selected_id) ? in_array ($key, $selected_id) : $key == $selected_id;
345		$ret .= "<option value='${key}'" . ($selected ? ' selected' : '') . '>';
346		$ret .= stringForOption ($value) . '</option>';
347	}
348	return $ret;
349}
350
351function printSelect ($optionList, $select_attrs = array(), $selected_id = NULL)
352{
353	echo getSelect ($optionList, $select_attrs, $selected_id);
354}
355
356// $selected_id can be an array if you want to use multiselect
357// in order to use multiselect $select_attrs should contain smth like this:
358// 		[ 'name' => 'some_name[]', 'multiple' => 'multiple', 'size' => 5 ]
359// Input array keys are OPTION VALUEs and input array values are OPTION text.
360function getSelect ($optionList, $select_attrs = array(), $selected_id = NULL, $treat_single_special = TRUE)
361{
362	$ret = '';
363	if (!array_key_exists ('name', $select_attrs))
364		throw new InvalidArgException ('select_attrs[\'name\']', '(not set)', 'must be set');
365	// handle two corner cases in a specific way
366	if (count ($optionList) == 0)
367		return '(none)';
368	if (count ($optionList) == 1 && $treat_single_special)
369	{
370		foreach ($optionList as $key => $value)
371			break;
372		return "<input type=hidden name=${select_attrs['name']} id=${select_attrs['name']} value=${key}>" .
373			stringForLabel ($value, 64);
374	}
375	if (!array_key_exists ('id', $select_attrs))
376		$select_attrs['id'] = $select_attrs['name'];
377	$ret .= '<select';
378	foreach ($select_attrs as $attr_name => $attr_value)
379		$ret .= " ${attr_name}=${attr_value}";
380	$ret .= '>' . getSelectOptions ($optionList, $selected_id) . '</select>';
381	return $ret;
382}
383
384function printNiftySelect ($groupList, $select_attrs = array(), $selected_id = NULL)
385{
386	echo getNiftySelect ($groupList, $select_attrs, $selected_id);
387}
388
389// Input is a cooked list of OPTGROUPs, each with own sub-list of OPTIONs in the same
390// format as printSelect() expects.
391// If tree is true, hierarchical drop-boxes are used, otherwise optgroups are used.
392function getNiftySelect ($groupList, $select_attrs, $selected_id = NULL)
393{
394	// special treatment for ungrouped data
395	if (count ($groupList) == 1 && isset ($groupList['other']))
396		return getSelect ($groupList['other'], $select_attrs, $selected_id);
397	if (!array_key_exists ('name', $select_attrs))
398		throw new InvalidArgException ('select_attrs[\'name\']', '(not set)', 'must be set');
399	if (!array_key_exists ('id', $select_attrs))
400		$select_attrs['id'] = $select_attrs['name'];
401
402	$ret = '<select';
403	foreach ($select_attrs as $attr_name => $attr_value)
404		$ret .= " ${attr_name}=${attr_value}";
405	$ret .= ">\n";
406	foreach ($groupList as $groupname => $groupdata)
407	{
408		$ret .= "<optgroup label='${groupname}'>\n";
409		foreach ($groupdata as $dict_key => $dict_value)
410		{
411			if (is_array ($selected_id))
412				$is_selected = in_array ($dict_key, $selected_id);
413			else
414				$is_selected = $dict_key == $selected_id;
415			$ret .= "<option value='${dict_key}'" . ($is_selected ? ' selected' : '') . ">${dict_value}</option>\n";
416		}
417		$ret .= "</optgroup>\n";
418	}
419	$ret .= "</select>\n";
420	return $ret;
421}
422
423function getOptionTree ($tree_name, $tree_options, $tree_config = array())
424{
425	$default_config = array
426	(
427		'choose' => 'select...',
428		'empty_value' => '',
429		'indexed' => TRUE,
430	);
431	addJSInternal ('js/jquery.optionTree.js');
432	addJSText ("
433$(function() {
434	var option_tree = " . json_encode ($tree_options) . ";
435	var options = " . json_encode ($tree_config + $default_config) . ";
436	$('input[name=${tree_name}]').optionTree(option_tree, options);
437});"
438	); // addJSText()
439
440	return "<input type=hidden name=${tree_name}>";
441}
442
443function printImageHREF ($tag, $title = '', $do_input = FALSE)
444{
445	echo getImageHREF ($tag, $title, $do_input);
446}
447
448// this would be better called mkIMG(), make "IMG" HTML element
449function getImageHREF ($tag, $title = '', $do_input = FALSE)
450{
451	global $image;
452	$attrs = array
453	(
454		'src' => array_key_exists ($tag, $image) ?
455			'?module=chrome&uri=' . $image[$tag]['path'] :
456			'?module=image&img=error',
457		'border' => 0,
458	);
459	if ($title != '')
460		$attrs['title'] = $title;
461	if ($do_input)
462	{
463		$element = 'input';
464		$attrs['type'] = 'image';
465		$attrs['name'] = 'submit';
466		$attrs['class'] = 'icon';
467		// Width and height for INPUT only appear in HTML 5.
468	}
469	else
470	{
471		$element = 'img';
472		if (array_key_exists ($tag, $image))
473		{
474			$attrs['width'] = $image[$tag]['width'];
475			$attrs['height'] = $image[$tag]['height'];
476		}
477	}
478	return makeHtmlTag ($element, $attrs);
479}
480
481// This function is DEPRECATED and will be removed in 0.22.0. See the set
482// of stringForXXXXX() functions for a proper replacement.
483function escapeString ($value, $do_db_escape = FALSE)
484{
485	$ret = htmlspecialchars ($value, ENT_QUOTES, 'UTF-8');
486	if ($do_db_escape)
487	{
488		global $dbxlink;
489		$ret = substr ($dbxlink->quote ($ret), 1, -1);
490	}
491	return $ret;
492}
493
494function transformRequestData()
495{
496	global $sic;
497	$seen_keys = array();
498
499	// Escape all globals before using and keep a copy of the original values.
500	$sic = array();
501	// walk through merged GET and POST instead of REQUEST array because it
502	// can contain cookies with data that could not be decoded from UTF-8
503	foreach (($_POST + $_GET) as $key => $value)
504	{
505		if (is_array ($value))
506			$_REQUEST[$key] = $value;
507		else
508		{
509			$value = dos2unix ($value);
510			$_REQUEST[$key] = htmlspecialchars ($value, ENT_QUOTES, 'UTF-8');
511		}
512		$sic[$key] = $value;
513		$seen_keys[$key] = 1;
514	}
515
516	// delete cookie information from the $_REQUEST array
517	foreach (array_keys ($_REQUEST) as $key)
518		if (! isset ($seen_keys[$key]))
519			unset ($_REQUEST[$key]);
520
521	if (isset ($_SERVER['PHP_AUTH_USER']))
522		$_SERVER['PHP_AUTH_USER'] = htmlspecialchars ($_SERVER['PHP_AUTH_USER'], ENT_QUOTES, 'UTF-8');
523	if (isset ($_SERVER['REMOTE_USER']))
524		$_SERVER['REMOTE_USER'] = htmlspecialchars ($_SERVER['REMOTE_USER'], ENT_QUOTES, 'UTF-8');
525}
526
527// Return whether value passed is likely to be a URI.
528function isUri ($uri)
529{
530	return preg_match (RE_STATIC_URI, $uri);
531}
532
533// Return whether value passed is likely to be a URL.
534function isUrl ($url)
535{
536	return preg_match ('~^[\w]+:\/\/[\w \-\.\_\/]+$~', $url);
537}
538
539// JS scripts should be included through the addJS* functions. They will automatically appear in the <head> of your page.
540// the addJS function has been replaced by addJSInternal, addJSExternal and addJSText() functions.  This function will be
541// removed in 2.22.0
542function addJS ($data, $inline = FALSE, $group = 'default')
543{
544	if ($inline)
545		addJSText ($data, $group);
546	else if (isURL ($data))
547		addJSExternal ($data, $group);
548	else
549		addJSInternal ($data, $group);
550}
551
552// addJSExternal links to external scripts via URL and must be prefixed with
553// either http:// or https://
554function addJSExternal ($url, $group = 'default')
555{
556	if (! isUrl ($url))
557		throw new InvalidArgException ('url', $url, 'Value passed is not a URL');
558
559	addPageHeader ("<script type='text/javascript' src='${url}'></script>\n", $group);
560}
561
562// addJSInternal adds links that go through the Chrome module of index.php
563function addJSInternal ($uri, $group = 'default')
564{
565	global $html_headers;
566
567	// Add jquery.js and racktables.js the first time a Javascript file is added.
568	// FIXME: Would it be better to do this initialization elsewhere?
569	if (! array_key_exists ('a_core', $html_headers))
570	{
571		// Prevent infinite recursion.
572		$html_headers['a_core'] = array();
573
574		addJSInternal('js/jquery-1.4.4.min.js', 'a_core');
575		addJSInternal('js/racktables.js',       'a_core');
576	}
577
578	if (! isUri ($uri))
579		throw new InvalidArgException ('uri', $uri, 'Value passed is not a valid URI');
580
581	addPageHeader ("<script type='text/javascript' src='?module=chrome&uri=${uri}'></script>\n", $group);
582}
583
584// This function adds script blocks that automatically appear in the <head> of your page.
585// Scripts are included in the order of adding within the same group, and groups are sorted alphabetically.
586function addJSText ($text, $group = 'default')
587{
588	if (isUrl ($text))
589		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URL and should use addJSExternal()');
590
591	if (isUri ($text))
592		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URI and should use addJSInternal()');
593
594	addPageHeader ('<script type="text/javascript">' . "\n" . trim ($text, "\r\n") . "\n</script>\n", $group);
595}
596
597// CSS styles should be included through this function.
598// They automatically appear in the <head> of your page.
599// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
600// Styles are included in the order of adding.
601function addCSS ($data, $inline = FALSE, $group = 'default')
602{
603	if ($inline)
604		addCSSText ($data, $group);
605	else if (isURL ($data))
606		addCSSExternal ($data, $group);
607	else
608		addCSSInternal ($data, $group);
609}
610
611// CSS styles should be included through this function.
612// They automatically appear in the <head> of your page.
613// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
614// Styles are included in the order of adding.
615function addCSSExternal ($url, $group = 'default')
616{
617	if (! isUrl ($url))
618		throw new InvalidArgException ('url', $url, 'Value passed is not a valid URL');
619
620	addPageHeader ("<link rel=stylesheet type='text/css' href='$url' />\n", $group);
621}
622
623// CSS styles should be included through this function.
624// They automatically appear in the <head> of your page.
625// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
626// Styles are included in the order of adding.
627function addCSSInternal ($uri, $group = 'default')
628{
629	if (! isUri ($uri))
630		throw new InvalidArgException ('uri', $uri, 'Value passed is not a valid URI');
631
632	addPageHeader ("<link rel=stylesheet type='text/css' href='?module=chrome&uri=$uri' />\n", $group);
633}
634
635// CSS styles should be included through this function.
636// They automatically appear in the <head> of your page.
637// $data is a CSS filename, or CSS code w/o tags around, if $inline = TRUE
638// Styles are included in the order of adding.
639function addCSSText ($text, $group = 'default')
640{
641	if (isUrl ($text))
642		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URL and should use addCSSExternal()');
643
644	if (isUri ($text))
645		throw new InvalidArgException ('text', $text, 'Value passed is most likely a URI and should use addCSSInternal()');
646
647	addPageHeader ('<style type="text/css">' . "\n" . trim ($text, "\r\n") . "\n</style>\n", $group);
648}
649
650function getRenderedIPNetCapacity ($range)
651{
652	switch (strlen ($range['ip_bin']))
653	{
654		case 4:  return getRenderedIPv4NetCapacity ($range);
655		case 16: return getRenderedIPv6NetCapacity ($range);
656		default: throw new InvalidArgException ('range["ip_bin"]', $range['ip_bin'], "Invalid binary IP");
657	}
658}
659
660function getRenderedIPv4NetCapacity ($range)
661{
662	$class = 'net-usage';
663	if (isset ($range['own_addrlist']))
664	{
665		// full mode
666		// $a is "aquamarine zone", $b is "gray zone"
667		$total = ip4_range_size ($range);
668
669		// compute $a_total: own range size, without subranges
670		if (! isset ($range['kidc']) || $range['kidc'] == 0)
671			$a_total = $total;
672		else
673		{
674			$a_total = 0;
675			foreach ($range['spare_ranges'] as $mask => $spare_list)
676				$a_total = bcadd ($a_total, bcmul (count ($spare_list), ip4_mask_size ($mask)), 0);
677		}
678		$a_used = markupIPAddrList ($range['own_addrlist']);
679		$b_total = bcsub ($total, $a_total, 0);
680		$b_used = markupIPAddrList ($range['addrlist']) - $a_used;
681
682		// generate link to progress bar image
683		$width = 100;
684		if ($total != 0)
685		{
686			$px_a = round (bcdiv ($a_total, $total, 4) * $width);
687			$px1 = round (bcdiv ($a_used, $total, 4) * $width);
688			$px2 = $px_a - $px1;
689			$px3 = round (bcdiv ($b_used, $total, 4) * $width);
690			if ($px3 + $px1 + $px2 > $width)
691				$px3 = $width - $px1 - $px2;
692		}
693		else
694			$px1 = $px2 = $px3 = 0;
695
696		$title_items = array();
697		$title2_items = array();
698		if ($a_total != 0)
699		{
700			$title_items[] = "$a_used / $a_total";
701			$title2_items[] = sprintf ("%d%% used", bcdiv ($a_used, $a_total, 4) * 100);
702		}
703		if ($b_total != 0)
704		{
705			$title_items[] = ($b_used ? "$b_used / " : "") . $b_total;
706			$title2_items[] = sprintf ("%d%% sub-allocated", bcdiv ($b_total, $total, 4) * 100);
707		}
708		$title = implode (', ', $title_items);
709		$title2 = implode (', ', $title2_items);
710		$text = "<img width='$width' height=10 border=0 title='$title2' src='?module=progressbar4&px1=$px1&px2=$px2&px3=$px3'>" .
711			" <small class='title'>$title</small>";
712	}
713	else
714	{
715		// fast mode
716		$class .= ' pending';
717		addJSInternal ('js/net-usage.js');
718
719		$free_text = '';
720		if (isset ($range['kidc']) && $range['kidc'] > 0)
721		{
722			$free_masks = array_keys ($range['spare_ranges']);
723			sort ($free_masks, SORT_NUMERIC);
724			if ($mask = array_shift ($free_masks))
725			{
726				$cnt = count ($range['spare_ranges'][$mask]);
727				$free_text = ', ' . ($cnt > 1 ? "<small>${cnt}&times;</small>" : "") . "/$mask free";
728			}
729		}
730		$text = ip4_range_size ($range) . $free_text;
731	}
732
733	$div_id = $range['ip'] . '/' . $range['mask'];
734
735	return "<div class=\"$class\" id=\"$div_id\">" . $text . "</div>";
736}
737
738function getRenderedIPv6NetCapacity ($range)
739{
740	$div_id = $range['ip'] . '/' . $range['mask'];
741	$class = 'net-usage';
742	if (isset ($range['addrlist']))
743		$used = markupIPAddrList ($range['addrlist']);
744	else
745	{
746		$used = NULL;
747		$class .= ' pending';
748		addJSInternal ('js/net-usage.js');
749	}
750
751	static $prefixes = array
752	(
753		0 =>  '',
754		3 =>  'k',
755		6 =>  'M',
756		9 =>  'G',
757		12 => 'T',
758		15 => 'P',
759		18 => 'E',
760		21 => 'Z',
761		24 => 'Y',
762	);
763
764	if ($range['mask'] <= 64)
765	{
766		$what = 'net';
767		$preposition = 'in';
768		$range['mask'] += 64;
769	}
770	else
771	{
772		$what = 'IP';
773		$preposition = 'of';
774	}
775	$what .= (0 == $range['mask'] % 64 ? '' : 's');
776	$addrc = isset ($used) ? "$used $preposition " : '';
777
778	$dec_order = intval ((128 - $range['mask']) / 10) * 3;
779	$mult = isset ($prefixes[$dec_order]) ? $prefixes[$dec_order] : '??';
780
781	$cnt = 1 << ((128 - $range['mask']) % 10);
782	if ($cnt == 1 && $mult == '')
783		$cnt = '1';
784
785	return "<div class=\"$class\" id=\"$div_id\">" . "{$addrc}${cnt}${mult} ${what}" . "</div>";
786}
787
788// Buffer the header only once. Disregard subsequent calls even if they
789// are made for a different group.
790function addPageHeader ($header, $group)
791{
792	global $html_headers;
793
794	foreach ($html_headers as $group_contents)
795		if (in_array ($header, $group_contents))
796			return;
797	$html_headers[$group][] = $header;
798}
799
800// print part of HTML HEAD block
801function printPageHeaders ()
802{
803	global $pageheaders;
804	ksort ($pageheaders);
805	foreach ($pageheaders as $s)
806		echo $s . "\n";
807	// add tabindex to all input forms
808	addJSInternal ('js/tabindex_auto.js');
809
810	// add JS/CSS headers
811	global $html_headers;
812	ksort($html_headers);
813
814	foreach ($html_headers as $group_name => $list)
815		foreach ($list as $index => $item)
816			echo "<!-- $group_name:$index -->\n" . $item;
817}
818
819function cmpTags ($a, $b)
820{
821	global $taglist;
822	if (isset ($a['id']) && isset ($b['id']))
823	{
824		$a_root = array_first ($taglist[$a['id']]['trace']);
825		$b_root = array_first ($taglist[$b['id']]['trace']);
826		if ($a_root < $b_root)
827			return -1;
828		elseif ($a_root > $b_root)
829			return 1;
830	}
831	elseif (isset ($a['id']))
832		return -1;
833	elseif (isset ($b['id']))
834		return 1;
835
836	return strcmp ($a['tag'], $b['tag']);
837}
838
839function getTagClassName ($tagid)
840{
841	global $taglist;
842
843	$class = '';
844	foreach ($taglist[$tagid]['trace'] as $parent)
845		$class .= 'tag-' . $parent . ' ';
846	$class .= 'tag-' . $tagid . ' etag-' . $tagid;
847
848	$class .= getTagClass ($taglist[$tagid]);
849
850	return $class;
851}
852
853function serializeTags ($chain, $baseurl = '')
854{
855	global $taglist;
856	$tmp = array();
857	usort ($chain, 'cmpTags');
858	foreach ($chain as $taginfo)
859	{
860		$title = '';
861		if (isset ($taginfo['user']) && isset ($taginfo['time']))
862			$title = htmlspecialchars ($taginfo['user'] . ', ' . formatAge ($taginfo['time']), ENT_QUOTES);
863		if (isset($taginfo['parent_id']))
864		{
865			$parent_info = array();
866			foreach ($taglist[$taginfo['id']]['trace'] as $tag_id)
867				$parent_info[] = $taglist[$tag_id]['tag'];
868			$parent_info[] = $taginfo['tag'];
869			if ($title != '')
870				$title .= "\n";
871			$title .= implode (" &rarr;  ", $parent_info);
872		}
873		$has_descr = array_key_exists ('description', $taginfo) && $taginfo['description'] !== NULL;
874		if ($has_descr)
875			$title .= ($title == '' ? '' : "\n\n") . stringForOption ($taginfo['description'], 0);
876		if ($title != '')
877			$title = "title='$title'";
878
879		if (! array_key_exists ('id', $taginfo))
880			$class = '';
881		else
882		{
883			$class = $has_descr ? 'tag-descr ' : '';
884			$class .= getTagClassName ($taginfo['id']);
885			$class = "class='${class}'";
886		}
887
888		$href = '';
889		if ($baseurl == '')
890			$tag = 'span';
891		else
892		{
893			$tag = 'a';
894			$href = "href='${baseurl}cft[]=${taginfo['id']}'";
895		}
896		$tmp[] = "<$tag $href $title $class>" . $taginfo['tag'] . "</$tag>";
897	}
898	return implode (', ', $tmp);
899}
900
901function startPortlet ($title = '')
902{
903	echo "<div class=portlet><h2>${title}</h2>";
904}
905
906function finishPortlet ()
907{
908	echo "</div>\n";
909}
910
911function getPageName ($page_code)
912{
913	global $page;
914	$title = isset ($page[$page_code]['title']) ? $page[$page_code]['title'] : callHook ('dynamic_title_decoder', $page_code);
915	if (is_array ($title))
916		$title = $title['name'];
917	return $title;
918}
919
920function printTagTRs ($cell, $baseurl = '')
921{
922	if (getConfigVar ('SHOW_EXPLICIT_TAGS') == 'yes' && count ($cell['etags']))
923	{
924		echo "<tr><th width='50%' class=tagchain>Explicit tags:</th><td class=tagchain>";
925		echo serializeTags ($cell['etags'], $baseurl) . "</td></tr>\n";
926	}
927	if (getConfigVar ('SHOW_IMPLICIT_TAGS') == 'yes' && count ($cell['itags']))
928	{
929		echo "<tr><th width='50%' class=tagchain>Implicit tags:</th><td class=tagchain>";
930		echo serializeTags ($cell['itags'], $baseurl) . "</td></tr>\n";
931	}
932	if (getConfigVar ('SHOW_AUTOMATIC_TAGS') == 'yes' && count ($cell['atags']))
933	{
934		echo "<tr><th width='50%' class=tagchain>Automatic tags:</th><td class=tagchain>";
935		echo serializeTags ($cell['atags']) . "</td></tr>\n";
936	}
937}
938
939// stub function to override it by chain-connected hooks
940function modifyEntitySummary ($cell, $summary)
941{
942	return $summary;
943}
944
945// renders 'summary' portlet, which persist on default tab of every realm page.
946// $values is a tricky array.
947// 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.
948// 'tags' key has a special meaning: instead of value, the result of printTagTRs call is appended to output
949// if the value is a single-element array, its value rendered as-is instead of <tr> tag and all its contents.
950// 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
951function renderEntitySummary ($cell, $title, $values = array())
952{
953	global $page_by_realm;
954	// allow plugins to override summary table
955	$values = callHook ('modifyEntitySummary', $cell, $values);
956
957	startPortlet ($title);
958	echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
959	foreach ($values as $name => $value)
960	{
961		if (is_array ($value) && count ($value) == 1)
962		{
963			$value = array_shift ($value);
964			echo $value;
965			continue;
966		}
967		if (is_array ($value))
968		{
969			$name = array_shift ($value);
970			$value = array_shift ($value);
971		}
972		elseif (! is_array ($value))
973			$name .= ':';
974		$class = 'tdright';
975		$m = array();
976		if (preg_match('/^\{(.*?)\}(.*)/', $name, $m))
977		{
978			$class .= ' ' . $m[1];
979			$name = $m[2];
980		}
981		if ($name == 'tags:')
982		{
983			$baseurl = '';
984			if (isset ($page_by_realm[$cell['realm']]))
985				$baseurl = makeHref(array('page'=>$page_by_realm[$cell['realm']], 'tab'=>'default'))."&";
986			printTagTRs ($cell, $baseurl);
987		}
988		else
989			echo "<tr><th width='50%' class='$class'>$name</th><td class=tdleft>$value</td></tr>";
990	}
991	echo "</table>\n";
992	finishPortlet();
993}
994
995function getOpLink ($params, $title,  $img_name = '', $comment = '', $class = '')
996{
997	if (isset ($params))
998	{
999		$ret = '<a href="' . makeHrefProcess ($params) . '"';
1000		$class .= ' input';
1001	}
1002	else
1003	{
1004		$ret = '<a href="#" onclick="return false;"';
1005		$class .= ' noclick';
1006	}
1007	if ($comment != '')
1008		$ret .= ' title="' . htmlspecialchars ($comment, ENT_QUOTES) . '"';
1009	$class = trim ($class);
1010	if ($class != '')
1011		$ret .= ' class="' . htmlspecialchars ($class, ENT_QUOTES) . '"';
1012	$ret .= '>';
1013	if ($img_name != '')
1014	{
1015		$ret .= getImageHREF ($img_name, $comment);
1016		if ($title != '')
1017			$ret .= ' ';
1018	}
1019	if (FALSE !== strpos ($class, 'need-confirmation'))
1020		addJSInternal ('js/racktables.js');
1021	$ret .= $title . '</a>';
1022	return $ret;
1023}
1024
1025function getPopupLink ($helper, $params, $window_name = '', $img_name = '', $title = '', $comment = '', $class = '')
1026{
1027	$ret = '';
1028	$popup_args = 'height=700, width=700, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
1029	$ret .= '<a href="#"';
1030	$class = trim ($class);
1031	if ($class != '')
1032		$ret .= ' class="' . htmlspecialchars ($class, ENT_QUOTES) . '"';
1033	if ($comment != '')
1034		$ret .= 'title="' . htmlspecialchars ($comment, ENT_QUOTES) . '"';
1035	$href = makeHref (array ('module' => 'popup', 'helper' => $helper) + makePageParams ($params));
1036	$ret .= " onclick=\"window.open('$href', '$window_name', '$popup_args'); return false\">";
1037
1038	if ($img_name != '')
1039	{
1040		$ret .= getImageHREF ($img_name, $comment);
1041		if ($title != '')
1042			$ret .= ' ';
1043	}
1044	$ret .= $title;
1045	$ret .= '</a>';
1046	return $ret;
1047}
1048
1049function renderProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
1050{
1051	echo getProgressBar ($percentage, $theme, $inline);
1052}
1053
1054function getProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
1055{
1056	$done = ((int) ($percentage * 100));
1057	if (! $inline)
1058		$src = "?module=progressbar&done=$done" . (empty ($theme) ? '' : "&theme=${theme}");
1059	else
1060	{
1061		$bk_request = $_REQUEST;
1062		$_REQUEST['theme'] = $theme;
1063		$src = 'data:image/png;base64,' . chunk_split (base64_encode (getOutputOf ('renderProgressBarImage', $done)));
1064		$_REQUEST = $bk_request;
1065	}
1066	$ret = "<img width=100 height=10 border=0 title='${done}%' src='$src'>";
1067	return $ret;
1068}
1069
1070function renderNetVLAN ($cell)
1071{
1072	echo getRenderedNetVLAN ($cell);
1073}
1074
1075function getRenderedNetVLAN ($cell)
1076{
1077	if (empty ($cell['8021q']))
1078		return;
1079	$links = array();
1080	foreach ($cell['8021q'] as $vi)
1081	{
1082		$vlan_info = getVlanRow ("${vi['domain_id']}-${vi['vlan_id']}");
1083		$links[] = formatVLANAsShortLink ($vlan_info);
1084	}
1085	$noun = count ($cell['8021q']) > 1 ? 'VLANs' : 'VLAN';
1086	return "<div class='vlan'><strong><small>${noun}</small> " . implode (', ', $links) . '</strong></div>';
1087}
1088
1089// DEPRECATED and will be removed in 0.22.0
1090function includeJQueryUI ($do_css = TRUE)
1091{
1092	includeJQueryUIJS();
1093	if ($do_css)
1094		includeJQueryUICSS();
1095}
1096
1097function includeJQueryUIJS()
1098{
1099	addJSInternal ('js/jquery-ui-1.8.21.min.js');
1100}
1101
1102function includeJQueryUICSS()
1103{
1104	addCSSInternal ('css/jquery-ui-1.8.22.redmond.css');
1105}
1106
1107function getRenderedIPPortPair ($ip, $port = NULL)
1108{
1109	return "<a href=\"" .
1110		makeHref (array ('page' => 'ipaddress',  'tab'=>'default', 'ip' => $ip)) .
1111		"\">" . $ip . "</a>" .
1112		(isset ($port) ? ":" . $port : "");
1113}
1114
1115// Print common operation form prologue, include bypass argument, if
1116// appropriate, and some extra hidden inputs, if requested.
1117// Use special encoding for upload forms
1118function printOpFormIntro ($opname, $extra = array(), $upload = FALSE)
1119{
1120	global $pageno, $tabno, $page;
1121
1122	echo "<form method=post id=${opname} name=${opname} action='?module=redirect&page=${pageno}&tab=${tabno}&op=${opname}'";
1123	if ($upload)
1124		echo " enctype='multipart/form-data'";
1125	echo ">";
1126	fillBypassValues ($pageno, $extra);
1127	foreach ($extra as $inputname => $inputvalue)
1128		printf ('<input type=hidden name="%s" value="%s">', htmlspecialchars ($inputname, ENT_QUOTES), htmlspecialchars ($inputvalue, ENT_QUOTES));
1129}
1130
1131// Display hrefs for all of a file's parents. If scissors are requested,
1132// prepend cutting button to each of them.
1133function serializeFileLinks ($links, $scissors = FALSE)
1134{
1135	$comma = '';
1136	$ret = '';
1137	foreach ($links as $link_id => $li)
1138	{
1139		$cell = spotEntity ($li['entity_type'], $li['entity_id']);
1140		$ret .= $comma;
1141		if ($scissors)
1142			$ret .= getOpLink (array('op'=>'unlinkFile', 'link_id'=>$link_id), '', 'cut', 'Unlink file') . ' ';
1143		$ret .= mkCellA ($cell);
1144		$comma = '<br>';
1145	}
1146	return $ret;
1147}
1148
1149function makeFileDownloadButton ($file_id, $imgname = 'download')
1150{
1151	$href = makeHref (array ('module' => 'download', 'file_id' => $file_id));
1152	$img = getImageHREF ($imgname, 'download file');
1153	return "<a href='${href}'>${img}</a>";
1154}
1155
1156// This function is DEPRECATED and will be removed in version 0.22.0.
1157// Instead of it please use one of the stringFor...() functions below.
1158function niftyString ($string, $maxlen = 30, $usetags = TRUE)
1159{
1160	$cutind = '&hellip;'; // length is 1
1161	if ($string == '')
1162		return '&nbsp;';
1163	// a tab counts for a space
1164	$string = preg_replace ("/\t/", ' ', $string);
1165	if (! $maxlen || mb_strlen ($string) <= $maxlen)
1166		return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
1167	return
1168		($usetags ? ("<span title='" . htmlspecialchars ($string, ENT_QUOTES, 'UTF-8') . "'>") : '') .
1169		str_replace (' ', '&nbsp;', htmlspecialchars (mb_substr ($string, 0, $maxlen - 1), ENT_QUOTES, 'UTF-8')) .
1170		$cutind .
1171		($usetags ? '</span>' : '');
1172}
1173
1174// "Some text, %s, some more text."
1175function stringForLabel ($string, $maxlen = 30)
1176{
1177	// A tab counts for a space.
1178	$string = preg_replace ("/\t/", ' ', $string);
1179	$full = htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
1180	if ($maxlen == 0 || mb_strlen ($string) <= $maxlen)
1181		return $full;
1182	$trimmed = mb_substr ($string, 0, $maxlen - 1);
1183	$trimmed = htmlspecialchars ($trimmed, ENT_QUOTES, 'UTF-8');
1184	$trimmed = str_replace (' ', '&nbsp;', $trimmed) . '&hellip;';
1185	return "<span title='${full}'>${trimmed}</span>";
1186}
1187
1188// "<TD>%s</TD>"
1189function stringForTD ($string, $maxlen = 30)
1190{
1191	// The non-breaking space helps the TD to render properly.
1192	return $string == '' ? '&nbsp;' : stringForLabel ($string, $maxlen);
1193}
1194
1195// "<INPUT type=text value='%s'>"
1196function stringForTextInputValue ($string, $maxlen = 30)
1197{
1198	if ($maxlen != 0)
1199		$string = mb_substr ($string, 0, $maxlen);
1200	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
1201}
1202
1203// "<TEXTAREA>%s</TEXTAREA>"
1204function stringForTextarea ($string)
1205{
1206	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
1207}
1208
1209// <OPTION>%s</OPTION>
1210function stringForOption ($string, $maxlen = 80)
1211{
1212	$string = preg_replace ("/\t/", ' ', $string);
1213	if ($maxlen == 0 || mb_strlen ($string) <= $maxlen)
1214		return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
1215	$string = mb_substr ($string, 0, $maxlen - 1);
1216	return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8') . '&hellip;';
1217}
1218
1219function printTagsPicker ($preselect=NULL)
1220{
1221	global $taglist;
1222	if (! count ($taglist))
1223	{
1224		printf ('(None exist yet, %s?)', mkA ('configure', 'tagtree', NULL, 'edit'));
1225		return;
1226	}
1227	printTagsPickerInput ('taglist');
1228	printTagsPickerUl ('taglist', $preselect);
1229	enableTagsPicker ();
1230}
1231
1232function printTagsPickerInput ($input_name)
1233{
1234	# use data-attribute as identifier for tagit
1235	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'>";
1236	echo "<span title='show tag tree' class='icon-folder-open tagit_input_" . $input_name . "'></span>";
1237}
1238
1239function printTagsPickerUl ($input_name, $preselect = NULL)
1240{
1241	global $target_given_tags;
1242	if ($preselect === NULL)
1243		$preselect = $target_given_tags;
1244	foreach (array_keys ($preselect) as $key)
1245	{
1246		$preselect[$key]['time_parsed'] = formatAge ($preselect[$key]['time']); # readable time format
1247		$preselect[$key]['description'] = stringForTextarea ($preselect[$key]['description']);
1248	}
1249	usort ($preselect, 'cmpTags');
1250	$preselect_hidden = "";
1251	foreach ($preselect as $value)
1252		$preselect_hidden .= "<input type=hidden name=" . $input_name . "[] value=" . $value['id'] . ">";
1253	echo $preselect_hidden; # print preselected tags id that used in case javascript problems
1254	echo "<ul data-tagit='yes' data-tagit-valuename='" . $input_name . "' data-tagit-preselect='" . json_encode($preselect) . "' class='tagit-vertical'></ul>";
1255}
1256
1257function enableTagsPicker ()
1258{
1259	global $taglist;
1260	static $taglist_inserted;
1261	includeJQueryUIJS();
1262	includeJQueryUICSS();
1263	addCSSInternal ('css/tagit.css');
1264	addJSInternal ('js/tag-it.js');
1265	addJSInternal ('js/tag-it-local.js');
1266	if (! $taglist_inserted)
1267	{
1268		$taglist_filtered = array();
1269		foreach ($taglist as $key => $taginfo) # remove unused fields
1270		{
1271			$taglist_filtered[$key] = array_sub ($taginfo, array("tag", "is_assignable", "trace"));
1272			if ($taginfo['color'] != NULL)
1273				$taglist_filtered[$key]['tagclass'] = getTagClass ($taginfo);
1274		}
1275		addJSText ('var taglist = ' . json_encode ($taglist_filtered) . ';');
1276		$taglist_inserted = TRUE;
1277	}
1278}
1279
1280function makeIPAllocLink ($ip_bin, $alloc, $display_ifname = FALSE)
1281{
1282	$object_name = ! isset ($object_name) || $object_name == '' ?
1283		formatEntityName (spotEntity ('object', $alloc['object_id'])) :
1284		$alloc['object_name'];
1285	$title = $display_ifname ?
1286		'' :
1287		"{$alloc['name']} @ {$object_name}";
1288	return
1289		'<a href="' . makeHref (array ('page' => 'object', 'tab' => 'default', 'object_id' => $alloc['object_id'], 'hl_ip' => ip_format ($ip_bin))) . '"' .
1290		' title="' . htmlspecialchars ($title, ENT_QUOTES) . '"' .
1291		">" . ($display_ifname ? $alloc['name'] . '@' : '') . $object_name . "</a>";
1292}
1293
1294function makeHtmlTag ($tagname, $attributes = array())
1295{
1296	$ret = '<' . $tagname;
1297	foreach ($attributes as $key => $value)
1298		$ret .= " $key=\"" . htmlspecialchars($value, ENT_QUOTES) . '"';
1299	$ret .= '>';
1300	return $ret;
1301}
1302
1303function getCellClass ($cell, $context)
1304{
1305	$ctxmap = array
1306	(
1307		'atom_plain' => 'background:white;',
1308		'atom_selected' => 'border:3px solid #80ffff !important;background:white;',
1309		'list_plain' => '',
1310		'list_selected' => 'outline: 3px solid #0aff0a;',
1311	);
1312	if (! array_key_exists ($context, $ctxmap))
1313		throw new InvalidArgException ('context', $context, 'unknown value');
1314	if (! array_key_exists ('colors', $cell) || ! count ($cell['colors']))
1315		return '';
1316	$style = $ctxmap[$context];
1317	$step = intval (round (100 / count ($cell['colors'])));
1318	$percent = 0;
1319	$gradient = '';
1320	foreach ($cell['colors'] as $color)
1321	{
1322		$rgb = colorHex2Rgb ($color);
1323		$gradient .= "rgba(${rgb},0.2) ${percent}%, rgba(${rgb},0.3) " . ($percent + $step) . "%,";
1324		$percent += $step;
1325	}
1326	$style .= "background-image:linear-gradient(135deg," . trim ($gradient, ',') . ") !important;";
1327	$cell_id = $cell[$cell['realm'] == 'user' ? 'user_id' : 'id'];
1328	return getCachedCSSClassForStyle ("cellcolor-${cell_id}", $style);
1329}
1330
1331function getTagClass ($taginfo)
1332{
1333	if (! array_key_exists ('color', $taginfo) || $taginfo['color'] === NULL)
1334		return '';
1335	$rgb = colorHex2Rgb ($taginfo['color'], TRUE);
1336	return getCachedCSSClassForStyle ("tagcolor-${taginfo['id']}", "background: rgb($rgb);");
1337}
1338
1339// This function has a side effect: it adds inline CSS.
1340function getCachedCSSClassForStyle ($class, $style)
1341{
1342	static $cache = array();
1343	$cachedclass = array_search ($style, $cache);
1344	if ($cachedclass !== FALSE)
1345		return " $cachedclass";
1346	addCSSText (".{$class} {{$style}}");
1347	$cache[$class] = $style;
1348	return " $class";
1349}
1350
1351function colorHex2Rgb($color, $pastel = FALSE)
1352{
1353	$color = trim ($color, '#');
1354
1355	if ($pastel)
1356	{
1357		$rgb = intval (round ((hexdec (substr ($color, 0, 2)) + 255) / 2)) . ',';
1358		$rgb .= intval (round ((hexdec (substr ($color, 2, 2)) + 255) / 2)) . ',';
1359		$rgb .= intval (round ((hexdec (substr ($color, 4, 2)) + 255) / 2));
1360	}
1361	else
1362		$rgb = hexdec (substr ($color, 0, 2)) . ',' . hexdec (substr ($color, 2, 2)) . ',' . hexdec (substr ($color, 4, 2));
1363
1364	return $rgb;
1365}
1366
1367function setEntityColors(&$entity)
1368{
1369	$entity['colors'] = array();
1370	foreach ($entity['etags'] as $taginfo)
1371		if ($taginfo['color'] !== NULL && ! in_array ($taginfo['color'], $entity['colors']))
1372		{
1373			$entity['colors'][] = $taginfo['color'];
1374			getTagClass ($taginfo); // set tag CSS class
1375		}
1376}
1377