Project

General

Profile

Download (22.4 KB) Statistics
| Branch: | Tag: | Revision:
1
/*
2
 * pfSenseHelpers.js
3
 *
4
 * part of pfSense (https://www.pfsense.org)
5
 * Copyright (c) 2004-2016 Rubicon Communications, LLC (Netgate)
6
 * All rights reserved.
7
 *
8
 * Redistribution and use in source and binary forms, with or without
9
 * modification, are permitted provided that the following conditions are met:
10
 *
11
 * 1. Redistributions of source code must retain the above copyright notice,
12
 *    this list of conditions and the following disclaimer.
13
 *
14
 * 2. Redistributions in binary form must reproduce the above copyright
15
 *    notice, this list of conditions and the following disclaimer in
16
 *    the documentation and/or other materials provided with the
17
 *    distribution.
18
 *
19
 * 3. All advertising materials mentioning features or use of this software
20
 *    must display the following acknowledgment:
21
 *    "This product includes software developed by the pfSense Project
22
 *    for use in the pfSense® software distribution. (http://www.pfsense.org/).
23
 *
24
 * 4. The names "pfSense" and "pfSense Project" must not be used to
25
 *    endorse or promote products derived from this software without
26
 *    prior written permission. For written permission, please contact
27
 *    coreteam@pfsense.org.
28
 *
29
 * 5. Products derived from this software may not be called "pfSense"
30
 *    nor may "pfSense" appear in their names without prior written
31
 *    permission of the Electric Sheep Fencing, LLC.
32
 *
33
 * 6. Redistributions of any form whatsoever must retain the following
34
 *    acknowledgment:
35
 *
36
 * "This product includes software developed by the pfSense Project
37
 * for use in the pfSense software distribution (http://www.pfsense.org/).
38
 *
39
 * THIS SOFTWARE IS PROVIDED BY THE pfSense PROJECT ``AS IS'' AND ANY
40
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
41
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
42
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE pfSense PROJECT OR
43
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
44
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
45
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
46
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
47
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
48
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
49
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
50
 * OF THE POSSIBILITY OF SUCH DAMAGE.
51
 */
52

    
53
// These helper functions are used on many/most UI pages to hide/show/disable/enable form elements where required
54

    
55
// Cause the input to be displayed as a required field by adding the element-required class to the label
56
function setRequired(id, req) {
57
	if (req)
58
		$('#' + id).parent().parent('div').find('span:first').addClass('element-required');
59
	else
60
		$('#' + id).parent().parent('div').find('span:first').removeClass('element-required');
61
}
62

    
63
// Hides the <div> in which the specified input element lives so that the input, its label and help text are hidden
64
function hideInput(id, hide) {
65
	if (hide)
66
		$('#' + id).parent().parent('div').addClass('hidden');
67
	else
68
		$('#' + id).parent().parent('div').removeClass('hidden');
69
}
70

    
71
// Hides the <div> in which the specified group input element lives so that the input,
72
// its label and help text are hidden
73
function hideGroupInput(id, hide) {
74
	if (hide)
75
		$('#' + id).parent('div').addClass('hidden');
76
	else
77
		$('#' + id).parent('div').removeClass('hidden');
78
}
79

    
80
// Hides the <div> in which the specified checkbox lives so that the checkbox, its label and help text are hidden
81
function hideCheckbox(id, hide) {
82
	if (hide)
83
		$('#' + id).parent().parent().parent('div').addClass('hidden');
84
	else
85
		$('#' + id).parent().parent().parent('div').removeClass('hidden');
86
}
87

    
88
// Disables the specified input element
89
function disableInput(id, disable) {
90
	$('#' + id).prop("disabled", disable);
91
}
92

    
93
// Hides all elements of the specified class. This will usually be a section
94
function hideClass(s_class, hide) {
95
	if (hide)
96
		$('.' + s_class).hide();
97
	else
98
		$('.' + s_class).show();
99
}
100

    
101
function hideSelect(id, hide) {
102
	if (hide)
103
		$('#' + id).parent('div').parent('div').addClass('hidden');
104
	else
105
		$('#' + id).parent('div').parent('div').removeClass('hidden');
106
}
107

    
108
function hideMultiCheckbox(id, hide) {
109
	if (hide)
110
		$("[name=" + id + "]").parent().addClass('hidden');
111
	else
112
		$("[name=" + id + "]").parent().removeClass('hidden');
113
}
114

    
115
// Hides the <div> in which the specified IP address element lives so that the input, any mask selector, its label and help text are hidden
116
function hideIpAddress(id, hide) {
117
	if (hide)
118
		$('#' + id).parent().parent().parent('div').addClass('hidden');
119
	else
120
		$('#' + id).parent().parent().parent('div').removeClass('hidden');
121
}
122

    
123
// Hides all elements of the specified class belonging to a multiselect.
124
function hideMultiClass(s_class, hide) {
125
	if (hide)
126
		$('.' + s_class).parent().parent().hide();
127
	else
128
		$('.' + s_class).parent().parent().show();
129
}
130

    
131
// Hides div whose label contains the specified text. (Good for StaticText)
132
function hideLabel(text, hide) {
133

    
134
	var element = $('label:contains(' + text + ')');
135

    
136
	if (hide)
137
		element.parent('div').addClass('hidden');
138
	else
139
		element.parent('div').removeClass('hidden');
140
}
141

    
142
// Hides the '/' and the subnet mask of an Ip_Address/subnet_mask group
143
function hideMask(name, hide) {
144
	if (hide) {
145
		$('[id^=' + name + ']').hide();
146
		$('[id^=' + name + ']').prev('span').hide();
147
		$('[id^=' + name + ']').parent('div').removeClass('input-group');
148
	} else {
149
		$('[id^=' + name + ']').show();
150
		$('[id^=' + name + ']').prev('span').show();
151
		$('[id^=' + name + ']').parent('div').addClass('input-group');
152
	}
153
}
154

    
155
// Set the help text for a given input
156
function setHelpText(id, text) {
157
	$('#' + id).parent().parent('div').find('span:nth-child(2)').html(text);
158
}
159

    
160
// Toggle table row checkboxes and background colors on the pages that use sortable tables:
161
//	/usr/local/www/firewall_nat.php
162
//	/usr/local/www/firewall_nat_1to1.php
163
//	/usr/local/www/firewall_nat_out.php
164
//	/usr/local/www/firewall_rules.php
165
//	/usr/local/www/vpn_ipsec.php
166
// Striping of the tables is handled here, NOT with the Bootstrap table-striped class because it would
167
// get confused when rows are sorted or deleted.
168

    
169
function fr_toggle(id, prefix) {
170
	if (!prefix)
171
		prefix = 'fr';
172

    
173
	var checkbox = document.getElementById(prefix + 'c' + id);
174
	checkbox.checked = !checkbox.checked;
175
	fr_bgcolor(id, prefix);
176
}
177

    
178
// Change background color of selected row based on state of checkbox
179
function fr_bgcolor(id, prefix) {
180
	if (!prefix)
181
		prefix = 'fr';
182

    
183
	var row = $('#' + prefix + id);
184

    
185
	if ($('#' + prefix + 'c' + id).prop('checked') ) {
186
		row.addClass('active');
187
	} else {
188
		row.removeClass('active');
189
	}
190
}
191

    
192
// The following functions are used by Form_Groups assigned a class of "repeatable" and provide the ability
193
// to add/delete rows of sequentially numbered elements, their labels and their help text
194
// See firewall_aliases_edit.php for an example
195

    
196
// NOTE: retainhelp is a global var that when defined prevents any help text from being deleted as lines are inserted.
197
// IOW it causes every row to have help text, not just the last row
198

    
199
function setMasks() {
200
	// Find all ipaddress masks and make dynamic based on address family of input
201
	$('span.pfIpMask + select').each(function (idx, select){
202
		var input = $(select).prevAll('input[type=text]');
203

    
204
		input.on('change', function(e){
205
			var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
206
			if (!isV6)
207
				max = 32;
208

    
209
			if (input.val() == "")
210
				return;
211

    
212
			while (select.options.length > max)
213
				select.remove(0);
214

    
215
			if (select.options.length < max) {
216
				for (var i=select.options.length; i<=max; i++)
217
					select.options.add(new Option(i, i), 0);
218
			}
219
		});
220

    
221
		// Fire immediately
222
		input.change();
223
	});
224
}
225

    
226
// Complicated function to move all help text associated with this input id to the same id
227
// on the row above. That way if you delete the last row, you don't lose the help
228
function moveHelpText(id) {
229

    
230
	$('#' + id).parent('div').parent('div').find('input, select, checkbox, button').each(function() {	 // For each <span></span>
231
		var fromId = this.id;
232
		var toId = decrStringInt(fromId);
233
		var helpSpan;
234

    
235

    
236
		if (!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
237
			if ($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group')) {
238
				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
239
			} else {
240
				helpSpan = $('#' + fromId).parent('div').find('span:last').clone();
241
			}
242

    
243
			if ($(helpSpan).hasClass('help-block')) {
244
				if ($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group')) {
245
					$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
246
				} else {
247
					$('#' + decrStringInt(fromId)).after(helpSpan);
248
				}
249
			}
250
		}
251
	});
252
}
253

    
254
// Increment the number at the end of the string
255
function bumpStringInt( str )	{
256
  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
257

    
258
  if (data)
259
	newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
260

    
261
  return newStr || str;
262
}
263

    
264
// Decrement the number at the end of the string
265
function decrStringInt( str )	{
266
  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
267

    
268
  if (data)
269
	newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
270

    
271
  return newStr || str;
272
}
273

    
274
// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
275
// gaps, but I do :)
276
function renumber() {
277
	var idx = 0;
278

    
279
	$('.repeatable').each(function() {
280

    
281
		$(this).find('input').each(function() {
282
			$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
283
			$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
284
		});
285

    
286
		$(this).find('select').each(function() {
287
			$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
288
			$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
289
		});
290

    
291
		$(this).find('button').each(function() {
292
			$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
293
			$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
294
		});
295

    
296
//		$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
297

    
298
		idx++;
299
	});
300
}
301

    
302
function delete_row(rowDelBtn) {
303
	var rowLabel;
304

    
305
	// If we are deleting row zero, we need to save/restore the label
306
	if ( (rowDelBtn == "deleterow0") && ((typeof retainhelp) == "undefined")) {
307
		rowLabel = $('#' + rowDelBtn).parent('div').parent('div').find('label').text();
308
	}
309

    
310
	$('#' + rowDelBtn).parent('div').parent('div').remove();
311

    
312
	renumber();
313
	checkLastRow();
314

    
315
	if (rowDelBtn == "deleterow0") {
316
		$('#' + rowDelBtn).parent('div').parent('div').find('label').text(rowLabel);
317
	}
318
}
319

    
320
function checkLastRow() {
321
	if ($('.repeatable').length <= 1) {
322
		$('#deleterow0').hide();
323
	} else {
324
		$('[id^=deleterow]').show();
325
	}
326
}
327

    
328
function add_row() {
329
	// Find the last repeatable group
330
	var lastRepeatableGroup = $('.repeatable:last');
331

    
332
	// If the number of repeats exceeds the maximum, do not add another clone
333
	if ($('.repeatable').length >= lastRepeatableGroup.attr('max_repeats')) {
334
		// Alert user if alert message is specified
335
		if (typeof lastRepeatableGroup.attr('max_repeats_alert') !== 'undefined') {
336
			alert(lastRepeatableGroup.attr('max_repeats_alert'));
337
		}
338
		return;
339
	}
340

    
341
	// Clone it
342
	var newGroup = lastRepeatableGroup.clone();
343

    
344
	// Increment the suffix number for each input element in the new group
345
	$(newGroup).find('input').each(function() {
346
		$(this).prop("id", bumpStringInt(this.id));
347
		$(this).prop("name", bumpStringInt(this.name));
348
		if (!$(this).is('[id^=delete]'))
349
			$(this).val('');
350
	});
351

    
352
	// Increment the suffix number for the deleterow button element in the new group
353
	$(newGroup).find('[id^=deleterow]').each(function() {
354
		$(this).prop("id", bumpStringInt(this.id));
355
		$(this).prop("name", bumpStringInt(this.name));
356
	});
357

    
358
	// Do the same for selectors
359
	$(newGroup).find('select').each(function() {
360
		$(this).prop("id", bumpStringInt(this.id));
361
		$(this).prop("name", bumpStringInt(this.name));
362
		// If this selector lists mask bits, we need it to be reset to all 128 options
363
		// and no items selected, so that automatic v4/v6 selection still works
364
		if ($(this).is('[id^=address_subnet]')) {
365
			$(this).empty();
366
			for (idx=128; idx>0; idx--) {
367
				$(this).append($('<option>', {
368
					value: idx,
369
					text: idx
370
				}));
371
			}
372
		}
373
	});
374

    
375
	// And for "for" tags
376
//	$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
377

    
378
	$(newGroup).find('label:first').text(""); // Clear the label. We only want it on the very first row
379

    
380
	// Insert the updated/cloned row
381
	$(lastRepeatableGroup).after(newGroup);
382

    
383
	// Delete any help text from the group we have cloned
384
	$(lastRepeatableGroup).find('.help-block').each(function() {
385
		if ((typeof retainhelp) == "undefined")
386
			$(this).remove();
387
	});
388

    
389
	setMasks();
390

    
391
	checkLastRow();
392

    
393
	// Autocomplete
394
	if ( typeof addressarray !== 'undefined') {
395
		$('[id^=address]').each(function() {
396
			if (this.id.substring(0, 8) != "address_") {
397
				$(this).autocomplete({
398
					source: addressarray
399
				});
400
			}
401
		});
402
	}
403

    
404
	// Now that we are no longer cloning the event handlers, we need to remove and re-add after a new row
405
	// has been added to the table
406
	$('[id^=delete]').unbind();
407
	$('[id^=delete]').click(function(event) {
408
		if ($('.repeatable').length > 1) {
409
			if ((typeof retainhelp) == "undefined")
410
				moveHelpText(event.target.id);
411

    
412
			delete_row(event.target.id);
413
		} else {
414
			alert('The last row may not be deleted.');
415
		}
416
	});
417

    
418
}
419

    
420
// These are action buttons, not submit buttons
421
$('[id^=addrow]').prop('type','button');
422
$('[id^=delete]').prop('type','button');
423

    
424
// on click . .
425
$('[id^=addrow]').click(function() {
426
	add_row();
427
});
428

    
429
$('[id^=delete]').click(function(event) {
430
	if ($('.repeatable').length > 1) {
431
		if ((typeof retainhelp) == "undefined")
432
			moveHelpText($(this).attr("id"));
433

    
434
		delete_row($(this).attr("id"));
435
	} else {
436
		alert('The last row may not be deleted.');
437
	}
438
});
439

    
440
// "More information" handlers --------------------------------------------------------------------
441

    
442
// If there is an infoblock, automatically add an info icon that toggles its display
443

    
444
var sfx = 0;
445

    
446
$('.infoblock').each(function() {
447
	// If the block has the class "blockopen" it is initially open
448
	if (! $(this).hasClass("blockopen")) {
449
		$(this).hide();
450
	} else {
451
		$(this).removeClass("blockopen");
452
	}
453

    
454
	// Add the "i" icon before the infoblock, incrementing the icon id for each block (in case there are multiple infoblocks on a page)
455
	$(this).before('<i class="fa fa-info-circle icon-pointer" style="color: #337AB7; font-size:20px; margin-left: 10px; margin-bottom: 10px;" id="showinfo' + sfx.toString() + '" title="More information"></i>');
456
	$(this).removeClass("infoblock");
457
	$(this).addClass("infoblock" + sfx.toString());
458
	sfx++;
459
});
460

    
461
// Show the help on clicking the info icon
462
$('[id^="showinfo"]').click(function() {
463
	var id = $(this).attr("id");
464

    
465
	$('.' + "infoblock" + id.substr(8)).toggle();
466
	document.getSelection().removeAllRanges();		// Ensure the text is un-selected (Chrome browser quirk)
467
});
468
// ------------------------------------------------------------------------------------------------
469

    
470
// Put a dummy row into any empty table to keep IE happy
471
$('tbody').each(function(){
472
	$(this).html($.trim($(this).html()))
473
});
474

    
475
$('tbody:empty').html("<tr><td></td></tr>");
476

    
477
// Hide configuration button for panels without configuration
478
$('.container .panel-heading a.config').each(function (idx, el){
479
	var config = $(el).parents('.panel').children('.panel-footer');
480
	if (config.length == 1)
481
		$(el).removeClass('hidden');
482
});
483

    
484
// Initial state & toggle icons of collapsed panel
485
$('.container .panel-heading a[data-toggle="collapse"]').each(function (idx, el){
486
	var body = $(el).parents('.panel').children('.panel-body')
487
	var isOpen = body.hasClass('in');
488

    
489
	$(el).children('i').toggleClass('fa-plus-circle', !isOpen);
490
	$(el).children('i').toggleClass('fa-minus-circle', isOpen);
491

    
492
	body.on('shown.bs.collapse', function(){
493
		$(el).children('i').toggleClass('fa-minus-circle', true);
494
		$(el).children('i').toggleClass('fa-plus-circle', false);
495
	});
496

    
497
	body.on('hidden.bs.collapse', function(){
498
		$(el).children('i').toggleClass('fa-minus-circle', false);
499
		$(el).children('i').toggleClass('fa-plus-circle', true);
500
	});
501
});
502

    
503
// Separator bar stuff ------------------------------------------------------------------------
504

    
505
// Globals
506
gColor = 'bg-info';
507
newSeparator = false;
508
saving = false;
509
dirty = false;
510

    
511
$("#addsep").prop('type' ,'button');
512

    
513
$("#addsep").click(function() {
514
	if (newSeparator) {
515
		return(false);
516
	}
517

    
518
	gColor = 'bg-info';
519
	// Insert a temporary bar in which the user can enter some optional text
520
	sepcols = $( "#ruletable tr th" ).length - 2;
521

    
522
	$('#ruletable > tbody:last').append('<tr>' +
523
		'<td class="' + gColor + '" colspan="' + sepcols + '"><input id="newsep" placeholder="' + svbtnplaceholder + '" class="col-md-12" type="text" /></td>' +
524
		'<td class="' + gColor + '" colspan="2"><button class="btn btn-primary btn-sm" id="btnnewsep"><i class="fa fa-save icon-embed-btn"></i>' + svtxt + '</button>' +
525
		'<button class="btn btn-info btn-sm" id="btncncsep"><i class="fa fa-undo icon-embed-btn"></i>' + cncltxt + '</button>' +
526
		'&nbsp;&nbsp;&nbsp;&nbsp;' +
527
		'&nbsp;&nbsp;<a id="sepclrblue" value="bg-info"><i class="fa fa-circle text-info icon-pointer"></i></a>' +
528
		'&nbsp;&nbsp;<a id="sepclrred" value="bg-danger"><i class="fa fa-circle text-danger icon-pointer"></i></a>' +
529
		'&nbsp;&nbsp;<a id="sepclrgreen" value="bg-success"><i class="fa fa-circle text-success icon-pointer"></i></a>' +
530
		'&nbsp;&nbsp;<a id="sepclrorange" value="bg-warning"><i class="fa fa-circle text-warning icon-pointer"></i></button>' +
531
		'</td></tr>');
532

    
533
	$('#newsep').focus();
534
	newSeparator = true;
535

    
536
	$("#btnnewsep").prop('type' ,'button');
537

    
538
	// Watch escape and enter keys
539
	$('#newsep').keyup(function(e) {
540
		if (e.which == 27) {
541
			$('#btncncsep').trigger('click');
542
		}
543
	});
544

    
545
	$('#newsep').keypress(function(e) {
546
		if (e.which == 13) {
547
			$('#btnnewsep').trigger('click');
548
		}
549
	});
550

    
551
	handle_colors();
552

    
553
	// Replace the temporary separator bar with the final version containing the
554
	// user's text and a delete icon
555
	$("#btnnewsep").click(function() {
556
		var septext = escapeHtml($('#newsep').val());
557
		sepcols = $( "#ruletable tr th" ).length - 1;
558

    
559
		$(this).parents('tr').replaceWith('<tr class="ui-sortable-handle separator">' +
560
			'<td class="' + gColor + '" colspan="' + sepcols + '">' + '<span class="' + gColor + '">' + septext + '</span></td>' +
561
			'<td class="' + gColor + '"><a href="#"><i class="fa fa-trash sepdel"></i></a>' +
562
			'</td></tr>');
563

    
564
		$('#order-store').removeAttr('disabled');
565
		newSeparator = false;
566
		dirty = true;
567
	});
568

    
569
	// Cancel button
570
	$('#btncncsep').click(function(e) {
571
		e.preventDefault();
572
		$(this).parents('tr').remove();
573
		newSeparator = false;
574
	});
575
});
576

    
577
// Delete a separator row
578
$(function(){
579
	$('table').on('click','tr a .sepdel',function(e){
580
		e.preventDefault();
581
		$(this).parents('tr').remove();
582
		$('#order-store').removeAttr('disabled');
583
		dirty = true;
584
	});
585
});
586

    
587
// Compose an input array containing the row #, color and text for each separator
588
function save_separators() {
589
	var row = 0;
590
	var sepinput;
591
	var sepnum = 0;
592

    
593
	$('#ruletable > tbody > tr').each(function() {
594
		if ($(this).hasClass('separator')) {
595
			seprow = $(this).next('tr').attr("id");
596
			if (seprow == undefined) {
597
				seprow = "fr" + row;
598
			}
599

    
600
			sepinput = '<input type="hidden" name="separator[' + sepnum + '][row]" value="' + seprow + '"></input>';
601
			$('form').append(sepinput);
602
			sepinput = '<input type="hidden" name="separator[' + sepnum + '][text]" value="' + escapeHtml($(this).find('td').text()) + '"></input>';
603
			$('form').append(sepinput);
604
			sepinput = '<input type="hidden" name="separator[' + sepnum + '][color]" value="' + $(this).find('td').prop('class') + '"></input>';
605
			$('form').append(sepinput);
606
			sepinput = '<input type="hidden" name="separator[' + sepnum + '][if]" value="' + iface + '"></input>';
607
			$('form').append(sepinput);
608
			sepnum++;
609
		} else {
610
			if ($(this).parent('tbody').hasClass('user-entries')) {
611
				row++;
612
			}
613
		}
614
	});
615
}
616

    
617
function reindex_rules(section) {
618
	var row = 0;
619

    
620
	section.find('tr').each(function() {
621
		if (this.id) {
622
			$(this).attr("id", "fr" + row);
623
			$(this).attr("onclick", "fr_toggle(" + row + ")")
624
			$(this).find('input:checkbox:first').each(function() {
625
				$(this).attr("id", "frc" + row);
626
				$(this).attr("onclick", "fr_toggle(" + row + ")");
627
			});
628

    
629
			row++;
630
		}
631
	});
632
}
633

    
634
function handle_colors() {
635
	$('[id^=sepclr]').prop("type", "button");
636

    
637
	$('[id^=sepclr]').click(function () {
638
		var color =	 $(this).attr('value');
639
		// Clear all the color classes
640
		$(this).parent('td').prop('class', '');
641
		$(this).parent('td').prev('td').prop('class', '');
642
		// Install our new color class
643
		$(this).parent('td').addClass(color);
644
		$(this).parent('td').prev('td').addClass(color);
645
		// Set the global color
646
		gColor = color;
647
	});
648
}
649

    
650
//JS equivalent to PHP htmlspecialchars()
651
function escapeHtml(text) {
652
	var map = {
653
		'&': '&amp;',
654
		'<': '&lt;',
655
		'>': '&gt;',
656
		'"': '&quot;',
657
		"'": '&#039;'
658
	};
659

    
660
	return text.replace(/[&<>"']/g, function(m) { return map[m]; });
661
}
662
// --------------------------------------------------------------------------------------------
663

    
664
// Select every option in the specified multiselect
665
function AllServers(id, selectAll) {
666
   for (i = 0; i < id.length; i++)	   {
667
	   id.eq(i).prop('selected', selectAll);
668
   }
669
}
670

    
671
// Move all selected options from one multiselect to another
672
function moveOptions(From, To)	{
673
	var len = From.length;
674
	var option;
675

    
676
	if (len > 0) {
677
		for (i=0; i<len; i++) {
678
			if (From.eq(i).is(':selected')) {
679
				option = From.eq(i).val();
680
				value  = From.eq(i).text();
681
				To.append(new Option(value, option));
682
				From.eq(i).remove();
683
			}
684
		}
685
	}
686
}
687

    
688

    
689
// ------------- Service start/stop/restart functions.
690
// If a start/stop/restart button is clicked, parse the button name and make a POST via AJAX
691
$('[id*=restartservice-], [id*=stopservice-], [id*=startservice-]').click(function(event) {
692
	var args = this.id.split('-');
693
	var action, name, mode_zone, id;
694

    
695
	if (args[0] == "openvpn") {
696
		action = args[1];
697
		name = args[0];
698
		mode_zone = args[2];
699
		id = args[3];
700
	} else if (args[0] == "captiveportal") {
701
		action = args[1];
702
		name = args[0];
703
		mode_zone = args[2];
704
		id = args[3];
705
	} else {
706
		action = args[0];
707
		args.shift();
708
		name = args.join('-');
709
	}
710

    
711
	$(this).children('i').removeClass().addClass('fa fa-cog fa-spin text-success');
712
	this.blur();
713

    
714
	ajaxRequest = $.ajax(
715
		{
716
			url: "/status_services.php",
717
			type: "post",
718
			data: {
719
				ajax: 		"ajax",
720
				mode: 		action,
721
				service: 	name,
722
				vpnmode: 	mode_zone,
723
				zone: 		mode_zone,
724
				id: 		id
725
			}
726
		}
727
	);
728

    
729
	// Once the AJAX call has returned, refresh the page to show the new service
730
	ajaxRequest.done(function (response, textStatus, jqXHR) {
731
		location.reload(true);
732
	});
733
});
(2-2/4)