commit a1aa91dec887ba929be08c993170803396a669b2
Author: Steve Beaver <sbeaver@netgate.com>
Date:   Wed Sep 26 11:27:11 2018 -0400

    Added scheduled config backup
    Fixed: #8947

diff --git a/src/etc/inc/config.lib.inc b/src/etc/inc/config.lib.inc
index 17b4f0f244..67ba403643 100644
--- a/src/etc/inc/config.lib.inc
+++ b/src/etc/inc/config.lib.inc
@@ -545,9 +545,13 @@ function write_config($desc="Unknown", $backup = true, $write_config_only = fals
 	}
 
 	// Try the core AutoConfigBackup system
-	if (is_array($config['system']['acb']) && $config['system']['acb']['enable'] == "yes") {
+	if (is_array($config['system']['acb']) && $config['system']['acb']['enable'] == "yes" &&
+	    (!isset($config['system']['acb']['frequency']) || $config['system']['acb']['frequency'] == "every") || file_exists("/tmp/forceacb")) {
 		require_once("acb.inc");
 		upload_config();
+		if (file_exists("/tmp/forceacb")) {
+			unlink("/tmp/forceacb");
+		}
 	}
 
 	return $config;
diff --git a/src/usr/local/sbin/execacb.php b/src/usr/local/sbin/execacb.php
new file mode 100755
index 0000000000..ac3f929c08
--- /dev/null
+++ b/src/usr/local/sbin/execacb.php
@@ -0,0 +1,38 @@
+<?php
+/*
+ * execacb.php
+ *
+ * part of pfSense (https://www.pfsense.org)
+ * Copyright (c) 2004-2018 Rubicon Communications, LLC (Netgate)
+ * Copyright (c) 2009 Erik Kristensen
+ * All rights reserved.
+ *
+ * originally part of m0n0wall (http://m0n0.ch/wall)
+ * Copyright (c) 2003-2004 Manuel Kasper <mk@neon1.net>.
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ // Called from cron, this script makes a remote backup of the current configuration
+ // via the ACB system
+
+ require_once("config.inc");
+ require_once("acb.inc");
+
+ if ($config['system']['acb']['enable'] == "yes") {
+ 	write_config("Scheduled backup");
+ 	upload_config();
+ }
+
+ ?>
diff --git a/src/usr/local/www/js/pfSense.js b/src/usr/local/www/js/pfSense.js
index f5829e046a..f80e89f88f 100644
--- a/src/usr/local/www/js/pfSense.js
+++ b/src/usr/local/www/js/pfSense.js
@@ -221,7 +221,9 @@ $(function() {
 			all.prop('checked', (all.length != checked.length));
 		});
 
-		a.appendTo($(this));
+		if ( ! $(this).parent().hasClass("notoggleall")) {
+			a.appendTo($(this));
+		}
 	});
 
 	// The need to NOT hide the advanced options if the elements therein are not set to the system
diff --git a/src/usr/local/www/services_acb_backup.php b/src/usr/local/www/services_acb_backup.php
index b11a570288..085218b823 100644
--- a/src/usr/local/www/services_acb_backup.php
+++ b/src/usr/local/www/services_acb_backup.php
@@ -18,9 +18,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-require("globals.inc");
-require("guiconfig.inc");
-require("acb.inc");
+require_once("globals.inc");
+require_once("guiconfig.inc");
+require_once("acb.inc");
 
 if ($_POST) {
 
@@ -28,6 +28,8 @@ if ($_POST) {
 		touch("/tmp/acb_nooverwrite");
 	}
 
+	touch("/tmp/forceacb");
+
 	if ($_REQUEST['reason']) {
 		if (write_config($_REQUEST['reason'])) {
 			$savemsg = "Backup completed successfully.";
diff --git a/src/usr/local/www/services_acb_settings.php b/src/usr/local/www/services_acb_settings.php
index 855067eb08..66b61f4208 100644
--- a/src/usr/local/www/services_acb_settings.php
+++ b/src/usr/local/www/services_acb_settings.php
@@ -32,7 +32,31 @@
 
 require_once("guiconfig.inc");
 require_once("functions.inc");
+require_once("pfsense-utils.inc");
+require_once("services.inc");
 
+function index_of_command() {
+	global $croncmd, $a_cron;
+
+	$i = 0;
+	$rv = -1;
+
+	if (count($a_cron) > 0) {
+		foreach ($a_cron as $ent) {
+			if ($ent['command'] === $croncmd) {
+				return $i;
+			}
+
+		$i++;
+		}
+	}
+
+	return $rv;
+
+}
+
+$croncmd = "/usr/bin/nice -n20 /usr/local/bin/php /usr/local/sbin/execacb.php";
+$a_cron = &$config['cron']['item'];
 $pconfig = $config['system']['acb'];
 
 
@@ -63,6 +87,24 @@ if (isset($_POST['save'])) {
 		}
 	}
 
+	if ($_POST['frequency'] === 'cron') {
+		if(!preg_match('/^[0-9\*\/]+$/', $_POST['hours'])) {
+			$input_errors[] = gettext("Schedule hours may only contin numbers '*' or '/'");
+		}
+
+		if(!preg_match('/^[0-9\*\/]+$/', $_POST['day'])) {
+			$input_errors[] = gettext("Schedule days may only contin numbers '*' or '/'");
+		}
+
+		if(!preg_match('/^[0-9\*\/]+$/', $_POST['month'])) {
+			$input_errors[] = gettext("Schedule month may only contin numbers '*' or '/'");
+		}
+
+		if(!preg_match('/^[0-9\*\/]+$/', $_POST['dow'])) {
+			$input_errors[] = gettext("Schedule day of week may only contin numbers '*' or '/'");
+		}
+	}
+
 	// Validate legacy settings
 	// All blank is allowed, otherwise they must be valid
 	if ($pconfig['legacy'] == 'yes') {
@@ -118,8 +160,35 @@ if (isset($_POST['save'])) {
 
 		$config['system']['acb']['gold_username'] = $pconfig['gold_username'];
 		$config['system']['acb']['hint'] = $pconfig['hint'];
+		$config['system']['acb']['frequency'] = $pconfig['frequency'];
+		$config['system']['acb']['hours'] = $pconfig['hours'];
+		$config['system']['acb']['month'] = $pconfig['month'];
+		$config['system']['acb']['day'] = $pconfig['day'];
+		$config['system']['acb']['dow'] = $pconfig['dow'];
+
+		// Remove any existing cron jobs
+		$cronid = index_of_command();
+
+		if ($cronid >= 0) {
+			unset($a_cron[$cronid]);
+		}
+
+		if ($pconfig['frequency'] === "cron") {
+			$ent = array();
+			$ent['minute'] = '0';
+			$ent['hour'] = $pconfig['hours'];
+			$ent['mday'] = $pconfig['day'];
+			$ent['month'] = $pconfig['month'];
+			$ent['wday'] = $pconfig['dow'];
+			$ent['who'] = 'root';
+			$ent['command'] = $croncmd;
+
+			$a_cron[] = $ent;
+
+		}
 
 		write_config("AutoConfigBackup settings updated");
+		configure_cron();
 	}
 }
 
@@ -153,6 +222,71 @@ $section->addInput(new Form_Checkbox(
 	($pconfig['enable'] == "yes")
 ));
 
+$group = new Form_MultiCheckboxGroup('Backup Frequency');
+
+$group->add(new Form_MultiCheckbox(
+	'frequency',
+	'',
+	'Automatically backup on every configuration change',
+	(!isset($pconfig['frequency']) || $pconfig['frequency'] === 'every'),
+	'every'
+))->displayasRadio();
+/*
+$group->add(new Form_MultiCheckbox(
+	'frequency',
+	'',
+	'Backup manually only',
+	($pconfig['frequency'] === 'manual'),
+	'manual'
+))->displayasRadio();
+*/
+$group->add(new Form_MultiCheckbox(
+	'frequency',
+	'',
+	'Automatically backup on a regular schedule',
+	($pconfig['frequency'] === 'cron'),
+	'cron'
+))->displayasRadio();
+
+$group->addClass("notoggleall");
+$section->add($group);
+
+
+$group = new Form_Group("Schedule");
+
+$group->add(new Form_Input(
+	'hours',
+	'Hour',
+	'text',
+	(isset($pconfig['hours']) ? $pconfig['hours']:'*')
+))->setHelp("Hours (0-23)");
+
+$group->add(new Form_Input(
+	'day',
+	'Day of month',
+	'text',
+	(isset($pconfig['day']) ? $pconfig['day']:'*')
+))->setHelp("Day (1-31)");
+
+$group->add(new Form_Input(
+	'month',
+	'Month',
+	'text',
+	(isset($pconfig['month']) ? $pconfig['month']:'*')
+))->setHelp("Month (1-12)");
+
+$group->add(new Form_Input(
+	'dow',
+	'Day of week',
+	'text',
+	(isset($pconfig['dow']) ? $pconfig['dow']:'*')
+))->setHelp("Day of week (0-6)");
+
+$group->addClass("cronsched");
+$group->setHelp(sprintf('Use * ("every"), divisor or exact value.  Minutes are fixed at 0. See %s for more information.',
+	'<a href="https://www.freebsd.org/cgi/man.cgi?crontab(5)" target="_blank">Cron format</a>'));
+$section->add($group);
+
 $section->addPassword(new Form_Input(
 	'encryption_password',
 	'*Encryption Password',
@@ -227,6 +361,12 @@ print $form;
 				$('#legacy').val('no');
 			}
 		});
+
+		$('input:radio[name=frequency]').click(function() {
+			hideClass("cronsched", ($(this).val() != 'cron'));
+		});
+
+		hideClass("cronsched", ("<?=$pconfig['frequency']?>" != 'cron'));
 	});
 //]]>
 </script>
