diff --git a/README.md b/README.md
index 468f046..0528c25 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1 @@
-backup-commander
-===============
-
-Simple web-based backup GUI
\ No newline at end of file
+Add this line in the sudoers file (use visudo) and it must go AFTER the %sudo line www-data ALL=(ALL) NOPASSWD: /usr/bin/php /var/www/html/bin/run-bu-by-id.php 3rd-party backup-commander dom_persist eclipse-gwt help_dlang help.txt html_backup_UI html_stanhope res_builder stanhope_d tmp Test by switching user to www-data: su -s /bin/bash www-data cd /var/www/html php bin/run-bu-by-id.php 19 0 where 19 is the backup id and 0/1 is not-test/test Comment out /inc/secure.php in post_handler.php if the login is not required TODO: create an install script: -- currently all manually done First version on Las-server Pre-requesite to push onto pearcey.net Generate a database schema with default login add the sudo entry for www-data (or other webserver user) kick off the schedule runner at the install stage create the schedule runner in php. simple polling of the DB get passwords from BD create a user credential page: new user+password change own password but only if the login is enabled (in post_handler.php)
diff --git a/html/bin/bu-n-err-18.txt b/html/bin/bu-n-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/bu-n-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/bu-n-err-19.txt b/html/bin/bu-n-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/bu-n-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/bu-n-op-19.txt b/html/bin/bu-n-op-19.txt
new file mode 100644
index 0000000..f566b18
--- /dev/null
+++ b/html/bin/bu-n-op-19.txt
@@ -0,0 +1,12 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/bu-n-err-19.txt
+html/bin/bu-n-op-19.txt
+html/bin/bu-x-op-19.txt
+html/bin/data.db
+html/js/
+html/js/main.js
+
+sent 33,631 bytes received 127 bytes 67,516.00 bytes/sec
+total size is 226,992 speedup is 6.72
diff --git a/html/bin/bu-t-err-18.txt b/html/bin/bu-t-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/bu-t-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/bu-t-op-18.txt b/html/bin/bu-t-op-18.txt
new file mode 100644
index 0000000..8a74739
--- /dev/null
+++ b/html/bin/bu-t-op-18.txt
@@ -0,0 +1,51 @@
+/*!999999\- enable the sandbox mode */
+-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: localhost Database: johntest
+-- ------------------------------------------------------
+-- Server version 10.6.18-MariaDB-0ubuntu0.22.04.1
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `BULIST`
+--
+
+DROP TABLE IF EXISTS `BULIST`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `BULIST` (
+ `BUID` int(11) NOT NULL AUTO_INCREMENT,
+ `BUName` varchar(9) DEFAULT NULL,
+ PRIMARY KEY (`BUID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `BULIST`
+--
+
+LOCK TABLES `BULIST` WRITE;
+/*!40000 ALTER TABLE `BULIST` DISABLE KEYS */;
+/*!40000 ALTER TABLE `BULIST` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2024-10-03 16:13:05
diff --git a/html/bin/bu-x-op-19.txt b/html/bin/bu-x-op-19.txt
new file mode 100644
index 0000000..9fcf8b5
--- /dev/null
+++ b/html/bin/bu-x-op-19.txt
@@ -0,0 +1 @@
+html/bin/rsync-n-op-19.txt
diff --git a/html/bin/data.db b/html/bin/data.db
new file mode 100644
index 0000000..e829a56
--- /dev/null
+++ b/html/bin/data.db
Binary files differ
diff --git a/html/bin/rsync-n-err-18.txt b/html/bin/rsync-n-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/rsync-n-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/rsync-n-err-19.txt b/html/bin/rsync-n-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/rsync-n-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/rsync-n-op-19.txt b/html/bin/rsync-n-op-19.txt
new file mode 100644
index 0000000..20ee8de
--- /dev/null
+++ b/html/bin/rsync-n-op-19.txt
@@ -0,0 +1,13 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/data.db
+html/bin/rsync-n-err-19.txt
+html/bin/rsync-t-err-19.txt
+html/bin/rsync-t-op-19.txt
+html/bin/rsync-x-op-19.txt
+html/inc/
+html/inc/bu-common.php
+
+sent 27,025 bytes received 146 bytes 54,342.00 bytes/sec
+total size is 217,616 speedup is 8.01
diff --git a/html/bin/rsync-t-err-18.txt b/html/bin/rsync-t-err-18.txt
new file mode 100644
index 0000000..3cfa03a
--- /dev/null
+++ b/html/bin/rsync-t-err-18.txt
@@ -0,0 +1 @@
+ID: 18
diff --git a/html/bin/rsync-t-err-19.txt b/html/bin/rsync-t-err-19.txt
new file mode 100644
index 0000000..062fc5d
--- /dev/null
+++ b/html/bin/rsync-t-err-19.txt
@@ -0,0 +1 @@
+ID: 19
diff --git a/html/bin/rsync-t-op-18.txt b/html/bin/rsync-t-op-18.txt
new file mode 100644
index 0000000..42f1c56
--- /dev/null
+++ b/html/bin/rsync-t-op-18.txt
@@ -0,0 +1,51 @@
+/*!999999\- enable the sandbox mode */
+-- MariaDB dump 10.19 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: localhost Database: johntest
+-- ------------------------------------------------------
+-- Server version 10.6.18-MariaDB-0ubuntu0.22.04.1
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `BULIST`
+--
+
+DROP TABLE IF EXISTS `BULIST`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `BULIST` (
+ `BUID` int(11) NOT NULL AUTO_INCREMENT,
+ `BUName` varchar(9) DEFAULT NULL,
+ PRIMARY KEY (`BUID`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `BULIST`
+--
+
+LOCK TABLES `BULIST` WRITE;
+/*!40000 ALTER TABLE `BULIST` DISABLE KEYS */;
+/*!40000 ALTER TABLE `BULIST` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2024-10-03 15:59:07
diff --git a/html/bin/rsync-t-op-19.txt b/html/bin/rsync-t-op-19.txt
new file mode 100644
index 0000000..6f9197e
--- /dev/null
+++ b/html/bin/rsync-t-op-19.txt
@@ -0,0 +1,12 @@
+ID: 19
+sending incremental file list
+html/bin/
+html/bin/data.db
+html/bin/rsync-t-err-19.txt
+html/bin/rsync-t-op-19.txt
+html/bin/rsync-x-op-19.txt
+html/inc/
+html/inc/bu-common.php
+
+sent 1,238 bytes received 43 bytes 2,562.00 bytes/sec
+total size is 217,366 speedup is 169.68 (DRY RUN)
diff --git a/html/bin/rsync-x-op-19.txt b/html/bin/rsync-x-op-19.txt
new file mode 100644
index 0000000..9fcf8b5
--- /dev/null
+++ b/html/bin/rsync-x-op-19.txt
@@ -0,0 +1 @@
+html/bin/rsync-n-op-19.txt
diff --git a/html/bin/run-bu-by-id.php b/html/bin/run-bu-by-id.php
new file mode 100644
index 0000000..7401295
--- /dev/null
+++ b/html/bin/run-bu-by-id.php
@@ -0,0 +1,25 @@
+ the name/path of this script
+$id = $argv[1];
+$isTest = $argv[2];
+
+$str_err = run_backup_block( $id, $isTest=='1' );
+
+/*
+ * Since this script is used from within the shell, we loose return values. Therefore, any error
+ * recieved from the previous call will be echoed to the console. It shall have the string
+ * "Error:" prepended.
+ *
+ * The shell exec will pickup the echoed output and test for the "Error" string.
+ *
+ * The output is silent if all went well.
+ */
+if($str_err!='') echo "Error: $str_err";
+
+?>
diff --git a/html/css/main.css b/html/css/main.css
new file mode 100644
index 0000000..9c6b9e2
--- /dev/null
+++ b/html/css/main.css
@@ -0,0 +1,188 @@
+* {
+ margin: 0px 0px 0px 0px;
+ padding: 0px 0px 0px 0px;
+ }
+
+ body, html {
+ padding: 3px 3px 3px 3px;
+ background-color: #D8DBE2;
+ font-family: Ubuntu, Verdana, sans-serif;
+ font-size: 11pt;
+ text-align: center;
+ }
+
+ div.main_page {
+ position: relative;
+ display: table;
+
+ width: 1000px;
+
+ margin-bottom: 3px;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 0px 0px 0px 0px;
+
+ border-width: 2px;
+ border-color: #212738;
+ border-style: solid;
+
+ background-color: #FFFFFF;
+
+ text-align: center;
+ }
+
+ div.content_section_text {
+ padding: 4px 8px 4px 8px;
+
+ color: #000000;
+ font-size: 100%;
+ }
+
+ div.content_section_text pre {
+ margin: 8px 0px 8px 0px;
+ padding: 8px 8px 8px 8px;
+
+ border-width: 1px;
+ border-style: dotted;
+ border-color: #000000;
+
+ background-color: #F5F6F7;
+
+ font-style: italic;
+ }
+
+ div.content_section_text p {
+ margin-bottom: 6px;
+ }
+
+ div.content_section_text ul, div.content_section_text li {
+ padding: 4px 8px 4px 16px;
+ }
+
+ div.section_header {
+ padding: 3px 6px 3px 6px;
+
+ background-color: #8E9CB2;
+
+ color: #FFFFFF;
+ font-weight: bold;
+ font-size: 112%;
+ text-align: center;
+ margin-top: 10px;
+ }
+
+ div.content_section_text a {
+ text-decoration: none;
+ font-weight: bold;
+ }
+
+
+ div.content_section_text a:link,
+ div.content_section_text a:visited,
+ div.content_section_text a:active {
+ background-color: #DCDFE6;
+ color: #0e3f2b;
+ }
+
+ div.content_section_text a:hover {
+ background-color: #0e3f2b;
+ color: #DCDFE6;
+ }
+
+.tblcenter {
+ width: 100%;
+ margin-left: 10px;
+ margin-right: 10px;
+ text-align: left;
+}
+
+.flex_header{
+ display: flex;
+ justify-content: space-between;
+ align-items: stretch;
+ background-color: #0e3f2b;
+ border: solid lightgray 1px;
+ border-bottom: solid gray 1px;
+ min-height: 50px;
+ margin-bottom: 10px;
+}
+
+.shb_Title img {
+ margin: 3px 3px 3px 5px;
+}
+
+.flex_header .shb_Title {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ color: #cfa949;
+ font-size: 18px;
+ font-weight: bold;
+ font-family: Helvetica, Arial, sans-serif;
+ margin-left: 0px;
+}
+
+.flex_header .shb_TitleRHS {
+ display: flex;
+ flex-direction: row;
+ align-items: center; /*align-items is perpendicular to the flex-direction*/
+ color: #c9bda2;
+}
+
+.page_inner{
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.btn_mse:hover {
+ cursor: pointer;
+}
+
+.btn_exec{
+ background-color: #04AA6D;
+ border: none;
+ color: white;
+ padding: 5px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 12px;
+ border-radius: 8px;
+}
+.btn_small {
+ font-size: 10px;
+ border-radius: 6px;
+ padding: 2px 5px 2px 5px;
+}
+
+.td_label_bkup{
+ text-align: right;
+ padding-right: 15px;
+}
+
+table tr.high-row-style td {
+ padding-top: 15px;
+}
+
+table tr td.td_icon {
+ width: 35px;
+}
+
+.two-col-panel {
+ padding-top: 15px;
+ text-align: left;
+ display: grid;
+ grid-template-columns: 200px auto;
+}
+
+.two-col-panel > div {
+ margin-top: 5px;
+}
+
+.two-col-panel input {
+ width: 300px;
+}
+
+.two-col-panel textarea {
+ width: 300px;
+}
diff --git a/html/img/edit-icon-24x24.png b/html/img/edit-icon-24x24.png
new file mode 100644
index 0000000..7777001
--- /dev/null
+++ b/html/img/edit-icon-24x24.png
Binary files differ
diff --git a/html/img/edit-icon-32x32.png b/html/img/edit-icon-32x32.png
new file mode 100644
index 0000000..cc0151c
--- /dev/null
+++ b/html/img/edit-icon-32x32.png
Binary files differ
diff --git a/html/img/edit-icon-L.png b/html/img/edit-icon-L.png
new file mode 100644
index 0000000..b2ed893
--- /dev/null
+++ b/html/img/edit-icon-L.png
Binary files differ
diff --git a/html/img/green-tick.png b/html/img/green-tick.png
new file mode 100644
index 0000000..b86cdd8
--- /dev/null
+++ b/html/img/green-tick.png
Binary files differ
diff --git a/html/img/red-cross.png b/html/img/red-cross.png
new file mode 100644
index 0000000..b965b00
--- /dev/null
+++ b/html/img/red-cross.png
Binary files differ
diff --git a/html/img/trash-bin-red-24x24.jpg b/html/img/trash-bin-red-24x24.jpg
new file mode 100644
index 0000000..0dd4304
--- /dev/null
+++ b/html/img/trash-bin-red-24x24.jpg
Binary files differ
diff --git a/html/img/trash-bin-red-24x24.png b/html/img/trash-bin-red-24x24.png
new file mode 100644
index 0000000..bdec232
--- /dev/null
+++ b/html/img/trash-bin-red-24x24.png
Binary files differ
diff --git a/html/img/uk-green.png b/html/img/uk-green.png
new file mode 100644
index 0000000..dab2252
--- /dev/null
+++ b/html/img/uk-green.png
Binary files differ
diff --git a/html/inc/.htaccess b/html/inc/.htaccess
new file mode 100644
index 0000000..15f487c
--- /dev/null
+++ b/html/inc/.htaccess
@@ -0,0 +1,4 @@
+
+Order Allow,Deny
+Deny from all
+
diff --git a/html/inc/btn_run_bu.php b/html/inc/btn_run_bu.php
new file mode 100644
index 0000000..170ef10
--- /dev/null
+++ b/html/inc/btn_run_bu.php
@@ -0,0 +1,52 @@
+0 ){
+ if( substr( $output[0], 0, 6) == 'Error:' ){
+ error_log( $output[0] );
+ return array('error', $output[0] );
+ }
+ }
+
+ if($testrun!='false'){
+ //a test run waits for the command to finish, so the output can be collected and sent back now
+ for($i=0;$i<2;$i++){
+ $fn = __DIR__."/../bin/".getFilename_out( $buid, $bIsTest, $i==0 );
+ $output[] = file_get_contents($fn);
+ }
+
+ return array_merge( array('done'), $output );
+ }
+
+ //error_log( "Backup kicked off for ID($buid): isTest=$isTest");
+
+ return array_merge( array('running'), $output );
+}
+
+
+?>
diff --git a/html/inc/bu-common.php b/html/inc/bu-common.php
new file mode 100644
index 0000000..508fa44
--- /dev/null
+++ b/html/inc/bu-common.php
@@ -0,0 +1,234 @@
+query("SELECT * FROM BULIST where BUID=$id");
+ $row = $results->fetchArray();
+
+ if($row){
+ return array( $row['BUName'], $row['Dir_Src'], $row['Dir_Dest'], $row['Files_Ex'], $row['BuType'], $row['BuRunning'], $row['BuError'], $row['LastRunDt'] );
+ }
+
+}
+
+/**
+ * Set (unset) the running datbase flag. Return true on success. The flag is
+ * unaltered if running in test mode.
+ */
+function set_bu_running( $id, bool $setit, bool $bu_test ){
+
+ if($bu_test) return true;
+
+ $db = new SQLite3( __DIR__.'/../bin/data.db' );
+
+ $setit_int = $setit?1:0;
+ return $db->exec("update BULIST set BuRunning=$setit_int where BUID=$id");
+}
+
+/**
+ * Set the error string in the database.
+ */
+function set_bu_error( $buid, $str_errs ){
+ $db = new SQLite3( __DIR__.'/../bin/data.db' );
+ if($str_errs==''){
+ return $db->exec("update BULIST set BuError = NULL where BUID=$buid");
+ }
+ $p = $db->prepare("update BULIST set BuError=:str_errs where BUID=:buid");
+ $p->bindValue(':str_errs', $str_errs );
+ $p->bindValue(':buid', $buid );
+ $p->execute();
+
+ if( $db->lastErrorCode()!==0){
+ return $db->lastErrorCode();
+ }
+}
+
+/**
+ *
+ */
+function set_last_run_date( $buid, $str_dt ){
+ $db = new SQLite3( __DIR__.'/../bin/data.db' );
+ $p = $db->prepare("update BULIST set LastRunDt=:str_dt where BUID=:buid");
+ $p->bindValue(':str_dt', $str_dt );
+ $p->bindValue(':buid', $buid );
+ $p->execute();
+
+ if( $db->lastErrorCode()!==0){
+ return $db->lastErrorCode();
+ }
+}
+
+
+/**
+ * Run the backup process for the given backup ID.
+ *
+ * Returns an error string on failure.
+ */
+function run_backup_block( $buid, $bu_test=true ){
+
+ $row = get_bu_item( $buid );
+
+ if( !$bu_test && $row[5]==1){
+ return "Backup for item($buid) is already running\n";
+ }
+
+ $bu_name = $row[0];
+ $bu_src = $row[1];
+ $bu_dest = $row[2];
+ $exFiles = $row[3];
+ $bu_type = $row[4];
+ $bu_rnng = $row[5];
+
+ // NOTE:
+ // using pcntl_fork() was a waste of time because the parent always waited for the child to finish.
+ // so we rely on bash &
+
+ if($bu_type==1 ){
+ return run_backup_DB( $bu_test, $buid, $bu_src, $bu_dest );
+ }
+
+ return run_backup_F( $bu_test, $buid, $bu_src, $bu_dest, $exFiles );
+}
+
+function run_backup_F( $bu_test, $buid, $bu_src, $bu_dest, $exFiles ){
+
+ if(!set_bu_running( $buid, true, $bu_test )){
+ return "Could not set the DB flag";
+ }
+
+ if($bu_test){
+ $flags = '-anv'; // n is the dry run indicator
+ }else{
+ $flags = '-av';
+ set_bu_error( $buid, '' ); //clear any previous error
+ set_last_run_date( $buid, gmdate("Y-m-d H:i:s") );
+ }
+
+ $fn_o = getFilename_out( $buid, $bu_test, false );
+ $fn_e = getFilename_out( $buid, $bu_test, true );
+
+ // create the output files with the ID in the first line
+ exec("echo 'ID: $buid' > bin/$fn_o");
+ exec("echo 'ID: $buid' > bin/$fn_e");
+
+ $str_exPaths = '';
+ if(trim( $exFiles )!=''){
+ $fn_x = getFilename_tmp( $buid, 'x' );
+ file_put_contents( "bin/$fn_x", $exFiles );
+ $str_exPaths = "--exclude-from=bin/$fn_x";
+ }
+
+ // bash command
+ //rsync -a --exclude-from=to-exclude.txt $PATH_SRC $BKUP_DEST
+ //rsync -anv --exclude-from=to-exclude.txt $PATH_SRC $BKUP_DEST <-- for testing
+ $command = "rsync $flags $str_exPaths $bu_src $bu_dest 2>> bin/$fn_e 1>> bin/$fn_o";
+
+ $output = array();
+ $result_code = null;
+ if( exec( $command, $output, $result_code )===FALSE){
+ set_bu_running( $buid, false, $bu_test );
+ return "rsync failed";
+ }
+ set_bu_running( $buid, false, $bu_test );
+
+ // can we find any error strings in the error file?
+ $str_errs = file_get_contents( "bin/$fn_e" );
+ $pos = strpos($str_errs, 'error' );
+ if($pos===FALSE){
+ $pos = strpos($str_errs, 'failed' );
+ }
+ if($pos===FALSE) return;
+
+ //place it in the database
+ set_bu_error( $buid, $str_errs );
+
+}
+
+
+/**
+ * Very similar to the file version. The common code is running the bach script.
+ */
+function run_backup_DB( $bu_test, $buid, $bu_src, $bu_dest ){
+
+ if(!set_bu_running( $buid, true, $bu_test )){
+ return "Could not set the DB flag";
+ }
+
+ if(!is_dir($bu_dest) ){
+ return "Destination ($bu_dest) is not a directory";
+ }
+
+ if($bu_test){
+ $fn_o = getFilename_out( $buid, $bu_test, false );
+ $bu_dest = "bin";
+ }else{
+ $fn_o = "$bu_src.sql";
+ set_bu_error( $buid, '' ); //clear any previous error
+ set_last_run_date( $buid, gmdate("Y-m-d H:i:s") );
+ }
+
+ // create the error files with the ID in the first line
+ $fn_e = getFilename_out( $buid, $bu_test, true );
+ exec("echo 'ID: $buid' > bin/$fn_e");
+
+ // bash command
+ //mysqldump johntest > /home/johnp/tmp/dbs/johntest.sql
+
+ $command = "mysqldump $bu_src 2>> bin/$fn_e 1>> $bu_dest/$fn_o";
+
+ $output = array();
+ $result_code = null;
+ if( exec( $command, $output, $result_code )===FALSE){
+ set_bu_running( $buid, false, $bu_test );
+ return "mysqldump failed";
+ }
+ set_bu_running( $buid, false, $bu_test );
+
+ // can we find any error strings in the error file?
+ $str_errs = file_get_contents( "bin/$fn_e" );
+ $pos = strpos($str_errs, 'error' );
+ if($pos===FALSE){
+ $pos = strpos($str_errs, 'failed' );
+ }
+ if($pos===FALSE) return;
+
+ //place it in the database
+ set_bu_error( $buid, $str_errs );
+
+}
+
+function getFilename_out( $buid, bool $bu_test, bool $isErr=false ){
+ return getFilename_tmp( $buid, $bu_test?'t':'n', $isErr? 'err': 'op' );
+}
+
+/**
+ * Create an output file for the backup (forked) process to use. These are essentually
+ * temporary files which may be deleted after the back process completes.
+ *
+ * buid: backup record id
+ *
+ * fn_type:
+ * t: for test files
+ * n: for non-test files
+ * x: for file exclusion list
+ *
+ * fn_stream:
+ * op: output stream
+ * err: error stream
+ */
+function getFilename_tmp( $buid, $fn_type='t', $fn_stream='op' ){
+ return "bu-$fn_type-$fn_stream-$buid.txt";
+}
+
+?>
diff --git a/html/inc/bu_list_content.php b/html/inc/bu_list_content.php
new file mode 100644
index 0000000..b5f9a2e
--- /dev/null
+++ b/html/inc/bu_list_content.php
@@ -0,0 +1,111 @@
+query('SELECT BUID, BuRunning FROM BULIST');
+ while ($row = $results->fetchArray()) {
+ array_push( $bu_state, array( $row['BUID'], $row['BuRunning'] ) );
+ }
+ return $bu_state;
+}
+
+
+/**
+ * Returns the main backup list page.
+ */
+function getbu_list_content(){
+
+ $db = new SQLite3('bin/data.db', SQLITE3_OPEN_READONLY );
+
+ $html_rows='';
+ $results = $db->query('SELECT * FROM BULIST');
+ while ($row = $results->fetchArray()) {
+ //var_dump($row);
+
+ $view_type = 'edit-file-bu';
+ if($row['BuType']==1){
+ $view_type = 'edit-db-bu';
+ }
+
+ $html_row = '
';
+
+ $html_row .= ''.$row['BUID'].' | ';
+ $html_row .= " | "; // edit icon
+ $html_row .= " | "; // trash icon
+ $html_row .= "{$row['BUName']} | ";
+ $but = $row['BuType']==1?'DB':'F';
+ $html_row .= "$but | ";
+
+ if($row['BuError']!=''){
+ $html_row .= "!! {$row['LastRunDt']} | ";
+ }else{
+ $html_row .= "{$row['LastRunDt']} | ";
+ }
+
+ if( $row['BU_REPEAT']=='N' ){
+ $html_row .= " | ";
+ }else{
+ $html_row .= " {$row['BU_REPEAT']} | ";
+ }
+
+ if($row['BuRunning']){
+ $html_row .= ' | ';
+ }else{
+ $html_row .= " | ";
+ }
+
+ $html_row .= '
';
+
+ $html_rows .= $html_row;
+ }
+
+$content = <<<'EOD'
+
+
+
+
+
+
+
+
+ ID |
+ |
+ |
+ Name |
+ Type |
+ Last Run (UTC) |
+ Scheduled |
+ Running |
+
+ --INSERT-ROWS--
+
+ |
+ |
+ |
+ Database backup |
+ |
+
+
+
+ |
+ |
+ |
+ Files backup |
+ |
+
+
+
+
+EOD;
+
+ return str_replace('--INSERT-ROWS--', $html_rows, $content);
+
+}
+
+?>
diff --git a/html/inc/common.php b/html/inc/common.php
new file mode 100644
index 0000000..240ad08
--- /dev/null
+++ b/html/inc/common.php
@@ -0,0 +1,352 @@
+getTraceAsString();
+ }
+}
+
+/**
+ * e.g. split
+ * ../db/stanhopetest/img/img_52548
+ * into
+ * [ ../db/stanhopetest/img/ , img_52548 ]
+ *
+ */
+function split_file_path( $ffn ){
+ $fn = basename( $ffn ); // img_52548
+ //$fp = substr( $ffn, 0, strlen($ffn) - strlen($fn) );
+ $fp = dirname( $ffn ) . '/';
+ return [ $fp, $fn ];
+}
+
+/**
+ * Returns true if it is possible to create this directory on the file system.
+ * Includes the possibility of multiple directories.
+ */
+function is_creatable_dir( $dirs ){
+ $dir = $dirs;
+ while(!file_exists($dir)){
+ $dir = dirname( $dir );
+ }
+ return is_writable($dir);
+}
+
+/**
+ * Function to make directories and actually SET the permissions
+ * https://www.php.net/manual/en/function.chmod.php
+ *
+ * $perms is an octal number
+ * e.g. mkdirs( $dir, 02770 ); //770 and g+s
+ */
+function mkdirs( $dirs, $perms ){
+ $dir = $dirs;
+ while(!file_exists($dir)){
+ $dir = dirname( $dir );
+ }
+ $rootDir = $dir;
+ if(!file_exists($dirs)) mkdir( $dirs, 0750, true ); //perms not always set
+ $dir = $dirs;
+ while( $dir!=$rootDir) {
+ chmod( $dir, $perms );
+ $dir = dirname( $dir );
+ }
+}
+
+/**
+ * Return the contents of a url as a string using the curl functions.
+ */
+function getContentsFromUrl( $url ){
+
+ $ch = curl_init( $url );
+ //$fp = fopen("example_homepage.txt", "w");
+ $fp = tmpfile();
+
+ curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+
+ curl_exec($ch);
+ $c_err = curl_error($ch);
+ curl_close($ch);
+ if($c_err) {
+ error_log( $c_err );
+ fclose($fp);
+ return;
+ }
+
+ $f_path = stream_get_meta_data($fp)['uri'];
+ $rtn = file_get_contents( $f_path );
+ fclose($fp);
+
+ return $rtn;
+}
+
+/**
+ * Set the given key-value pair.
+ *
+ * Returns the record ID.
+ */
+function params_set( $key, $val ) {
+
+ $rec = R::findOne( 'params', 'pkey=?', [$key] );
+ if($rec) {
+ $rec->pval = $val;
+ R::store( $rec );
+ return $rec->id;
+ }
+
+ $rec = R::dispense( 'params' );
+ $rec->pkey = $key;
+ $rec->pval = $val;
+ return R::store( $rec );
+}
+
+function params_get_date( $key ){
+ return strtotime( params_get($key) );
+}
+
+/**
+ * Returns the value for the given key. Returns null if no such key exists.
+ */
+function params_get( $key ){
+
+ $rec = R::findOne( 'params', 'pkey=?', [$key] );
+ if(!$rec) return null;
+ return $rec->pval;
+}
+
+/**
+ * A novel way of creating a unique ID each time a call is made. The new ID is
+ * returned.
+ *
+ * e.g.
+ * This can be used to give a client which might want to create temporary unique IDs. It is
+ * called once per client refresh and used to ensure uniqueness of certain CSS data points.
+ */
+function params_unique( ) {
+
+ $rec = R::dispense( 'params' );
+ $rec->pkey = 'unique';
+ $id = R::store( $rec );
+ R::exec( "delete from params where id<$id and pkey='unique'" );
+ return $id;
+}
+
+/**
+ * Return a string representation for the given dom node suitable for debugging.
+ */
+function getNodeString( $rmElem ){
+
+ if($rmElem==null) return "Element is null";
+ $str="";
+ if( $rmElem->nodeType==XML_ELEMENT_NODE){
+ $str .= $rmElem->nodeName;
+ $a = $rmElem->getAttribute("shb_compid");
+ if($a){
+ $str .= " shb_compid=$a";
+ }
+ }else{
+ $str .= 'type = '.$rmElem->nodeType;
+ }
+ return $str;
+}
+
+/**
+ * Simple logger to write to a file. Using php error_log distorts all crlf pairs. This
+ * is a pain when you're trying to preserve them in html.
+ */
+function mylog( $msg ){
+
+ global $logging_on;
+
+ if(!$logging_on) return;
+ $p = __DIR__ . "/../../shb.log";
+ $h = fopen( $p, "a");
+ if($h===false) return;
+ fwrite( $h, "$msg\n");
+ fclose($h);
+
+}
+
+function generateNonce( $digitCount = 12 ){
+ // e.g. 422466a05f24b09c978fa1f6
+ return bin2hex( random_bytes($digitCount) );
+}
+
+/**
+ * Generate a randon username starting with a letter and containing only letters and numbers.
+ */
+function generateRndUserName( $digitCount = 10 ){
+ $l1 = generateRndStrUsing( 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 1 );
+ return $l1 . generateRndStrUsing( BASE64_DIGITS, $digitCount-1 );
+}
+
+/**
+ * Generate a randon password with letters and numbers and some funny stuff.
+ */
+function generateRndPwd( $digitCount = 10 ){
+ return generateRndStrUsing( '0123456789_!"£$%^&*()_-|\?/<>#~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', $digitCount );
+}
+
+/**
+ * Generate a randon string of asc char. Default consisting of only [0-9][a-z][A-Z]
+ */
+function generateRndStrUsing( $using=BASE64_DIGITS, $digitCount = 8 ){
+
+ $str = '';
+ $maxlen = strlen($using);
+ for($i=0; $i<$digitCount; $i++){
+ $iv = random_int(0, $maxlen-1);
+ $str .= mb_substr( $using, $iv, 1); //use mb_substr because £ is not and asc char!
+ }
+ return $str;
+}
+
+
+/**
+ * Remove all html from the given string returning just the text content.
+ */
+function removeHtml( $str ){
+ //txt = txt.replace(//gi, '\n');
+ return preg_replace( '/<\/?[^>]+>/i', '', $str );
+}
+
+function getHtmlOkAsString( $data ){
+ $data = json_encode( $data );
+ return "[\"OK\", $data ]";
+}
+
+function sendHtmlOk( $data="" ){
+ echo getHtmlOkAsString( $data );
+}
+
+/**
+ * An effort to rectify my previously ill-thought out return encoding. You do not
+ * need to encode the data before calling this method. Any arrays will be json encoded.
+ */
+function sendHtmlOk_WithData( $data ){
+ $data = json_encode( $data );
+ echo "[\"OK\", $data ]";
+}
+
+function getHtmlErrorAsString( $strErrDesc = "--blank--" ){
+ $data = json_encode( $strErrDesc );
+ return "[\"Error\", $data]";
+}
+
+function sendHtmlError( $strErrDesc = "--blank--" ){
+ $rtn = getHtmlErrorAsString( $strErrDesc );
+ mylog( $rtn );
+ echo $rtn;
+}
+
+/**
+ *
+ */
+function getAttForPath( $name, $prefPath, $path){
+ return "$name = \"$prefPath/$path\"";
+}
+
+global $g_pub_path;
+
+/**
+ * If the browser=true, the file returned will be correct for adding to an href or src attribute so that the
+ * browser will find the link.
+ */
+function getDirForType( $filetype='html', $browser=false, $userid=-1 ){
+
+ global $g_pub_path;
+
+ if($g_pub_path==null){
+ $g_pub_path = params_get('pub_path');
+ if($g_pub_path==null){
+ throw new Exception("Publishing now requires a valid key('pub_path') in the params table for the publishing output path");
+ }
+
+ if(str_ends_with( $g_pub_path, '/') ) $g_pub_path = substr($g_pub_path, 0, strlen($g_pub_path)-1 );
+
+ //ensure all the directories exist
+ createUserDir( 'img', $userid );
+ createUserDir( 'css', $userid );
+ createUserDir( 'html', $userid );
+ createUserDir( 'js', $userid );
+
+ }
+
+ return getDirForType_( $filetype, $browser, $userid );
+
+}
+
+function createUserDir( $dName, $userid ){
+
+ if(!file_exists(getDirForType_( $dName, false, $userid ))) {
+ $dir = getDirForType_( $dName, false, $userid );
+ error_log("common.php: createUserDir: $dir");
+ mkdir( $dir, 0770, true);
+ }
+}
+
+/**
+ * Part of the new path handling system is to sepatate out path names from real files
+*/
+function mkdirIfNotExists( $d ){
+ if(!file_exists( $d ) ) {
+ error_log("common.php: createUserDir: $d");
+ mkdir( $d, 0770, true );
+ }
+}
+
+/**
+ * For the moment, call getDirForType and not this function. In time, the setup routine will make the directories
+ * and eliminate the need for the check during a user session.
+ *
+ * If the browser flag is set, we assuming a document root sub-dir. But in time, this will need to also cater for
+ * a site serving the g_pub_path as root (wrt the browser).
+ *
+ */
+function getDirForType_( $filetype, $browser, $userid ){
+
+ global $g_pub_path;
+ $strPath = $g_pub_path;
+
+
+ if($userid>0){
+ $strPath .= "_dbg_$userid";
+ }
+
+ switch($filetype){
+
+ case 'img':
+ if($browser) return "/$strPath/img/";
+ return fxdPth( "$strPath/img/" );
+
+ case 'js':
+ if($browser) return "/$strPath/js/";
+ return fxdPth( "$strPath/js/" );
+
+ case 'css':
+ if($browser) return "/$strPath/css/";
+ return fxdPth( "$strPath/css/" );
+
+ case 'html':
+ default:
+ if($browser) return "/$strPath/";
+ return fxdPth( "$strPath/" );
+
+ }
+}
+
+
+//NO white-space
diff --git a/html/inc/delete-file-bu.php b/html/inc/delete-file-bu.php
new file mode 100644
index 0000000..b24d99c
--- /dev/null
+++ b/html/inc/delete-file-bu.php
@@ -0,0 +1,17 @@
+prepare('delete from BULIST where BUID=:buid');
+ $db_st->bindValue(':buid', $id );
+ $db_st->execute();
+
+ if( $db->lastErrorCode()!==0){
+ return $db->lastErrorCode();
+ }
+
+}
+
+?>
diff --git a/html/inc/edit-sh-bu.php b/html/inc/edit-sh-bu.php
new file mode 100644
index 0000000..920a1c7
--- /dev/null
+++ b/html/inc/edit-sh-bu.php
@@ -0,0 +1,111 @@
+prepare('SELECT * FROM BULIST where BUID=:buid');
+ $db_st->bindValue(':buid', $db_id);
+ $results = $db_st->execute();
+ $row = $results->fetchArray();
+ if ( $row===FALSE ) {
+ return "Item $db_id not found";
+ }
+
+ $bun = $row['BUName'];
+ $bus = $row['Dir_Src'];
+ $bu_dt = $row['BU_DATE'];
+ if($bu_dt==''){
+ $bu_dt = date("Y-m-d");
+ }
+ $bu_t = $row['BU_TIME'];
+ if($bu_t==''){
+ $bu_t = '00:00';
+ }
+ $bu_r = $row['BU_REPEAT'];
+
+ switch($bu_r){
+ case 'D':
+ $id_checked = 'id="sched_ip_daily"';
+ break;
+ case 'W':
+ $id_checked = 'id="sched_ip_weekly"';
+ break;
+ case 'M':
+ $id_checked = 'id="sched_ip_monthly"';
+ break;
+ case 'N':
+ default:
+ $id_checked = 'id="sched_ip_none"';
+ }
+
+$content = <<<'EOD'
+
+
+
+
+
+
+
+
ID
+
$db_id
+
+
Backup Name
+
$bun
+
+
Data Source
+
$bus
+
+
Next Run Date/Time
+
+
+
+
+
+
Repeat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+EOD;
+
+ $content = str_replace($id_checked, $id_checked.' checked ', $content );
+ $content = str_replace('$bus', $bus, $content);
+ $content = str_replace('$bun', $bun, $content);
+ $content = str_replace('$bu_dt', $bu_dt, $content);
+ $content = str_replace('$bu_t', $bu_t, $content);
+ return str_replace('$db_id', $db_id, $content);
+}
+
+function save_bu_schedule( $postVars ){
+
+ //error_log( $postVars['id'] );
+
+ $db = new SQLite3('bin/data.db' );
+
+ $db_st = $db->prepare('UPDATE BULIST SET BU_DATE=:bud, BU_TIME=:but, BU_REPEAT=:bur where BUID=:buid');
+ $db_st->bindValue(':buid', $postVars['id'], SQLITE3_INTEGER );
+ $db_st->bindValue(':bud', $postVars['sched_date'] );
+ $db_st->bindValue(':but', $postVars['sched_time'] );
+ $db_st->bindValue(':bur', $postVars['sched_period'] );
+ $db_st->execute();
+
+ if( $db->lastErrorCode()!==0){
+ return $db->lastErrorCode();
+ }
+
+}
+
+?>
diff --git a/html/inc/login.php b/html/inc/login.php
new file mode 100644
index 0000000..9304f5c
--- /dev/null
+++ b/html/inc/login.php
@@ -0,0 +1,45 @@
+Login
+
+
+
+
+
+
Login Name
+
+
+
Password
+
+
+
+
+
+
+
+
+
+
+
+EOD;
+
+ //$content = str_replace('$header_cont', $header_cont, $content);
+ return $content;
+}
+
+function authenticate( $username, $hashed_pwd_client, $svr_nonce ){
+
+ //TODO:
+ // get the username and password from somewhere.
+ $hashed_pwd_correct = hash('sha256', 'biggles'.$svr_nonce );
+
+ return $hashed_pwd_correct == $hashed_pwd_client;
+}
+
+
+?>
diff --git a/html/inc/new-file-bu.php b/html/inc/new-file-bu.php
new file mode 100644
index 0000000..0c6951d
--- /dev/null
+++ b/html/inc/new-file-bu.php
@@ -0,0 +1,172 @@
+0){
+ $id_line = "ID
$db_id
";
+ $item_data = get_bu_item( $db_id ); // [ BUName, Dir_Src, Dir_Dest, Files_Ex, Bu_Type, BuRunning, BuError, LastRunDt ]
+
+ $bu_name_value = "value='{$item_data[0]}'";
+ $bu_src_value = "value='{$item_data[1]}'";
+ $bu_dest_value = "value='{$item_data[2]}'";
+ $bu_ex_files = $item_data[3];
+
+ $bu_error_list = str_replace( "\n", '
', $item_data[6]);
+ $bu_error_list = "Errors
$bu_error_list
";
+
+ $lastRunTime = "Last Run (UTC)
{$item_data[7]}
";
+
+ }
+
+ if($bDbStyle){
+ if($db_id>0){
+ $header_cont = '';
+ $btn_new_bu = "btn_edit_bu_db','$db_id";
+
+ }else{
+ $header_cont = '';
+ $btn_new_bu = 'btn_new_bu_db';
+
+ }
+ $lab1_cont = 'Database name
';
+
+ }else{
+ if($db_id>0){
+ $header_cont = '';
+ $btn_new_bu = "btn_edit_bu_file','$db_id";
+
+ }else{
+ $header_cont = '';
+ $btn_new_bu = 'btn_new_bu_file';
+
+ }
+
+ $lab1_cont = 'Source File/Dir
';
+ $exfiles_cont = "Exclude Files
";
+ }
+
+$content = <<<'EOD'
+
+ $header_cont
+
+
+
+
+
+ $id_line
+
+
Backup Name
+
(optional)
+
+ $lab1_cont
+
+
+
Destination Dir
+
+
+ $exfiles_cont
+
+
+
+
+
+
+
+
+
Test output
+
+
+ $lastRunTime
+ $bu_error_list
+
+
+
+
+EOD;
+
+ $content = str_replace('$bu_error_list', $bu_error_list, $content);
+ $content = str_replace('$lastRunTime', $lastRunTime, $content);
+ $content = str_replace('btn_run_bu\'', "btn_run_bu', $db_id", $content);
+ $content = str_replace('$bu_name_value', $bu_name_value, $content);
+ $content = str_replace('$bu_src_value', $bu_src_value, $content);
+ $content = str_replace('$bu_dest_value', $bu_dest_value, $content);
+
+ $content = str_replace('$id_line', $id_line, $content);
+ $content = str_replace('$btn_new_bu', $btn_new_bu, $content);
+ $content = str_replace('$exfiles_cont', $exfiles_cont, $content);
+ $content = str_replace('$lab1_cont', $lab1_cont, $content);
+ $content = str_replace('$header_cont', $header_cont, $content);
+ return $content;
+}
+
+/**
+ * Both inserts as well as edits a single backup item.
+ */
+function save_bu_item( $postVars, $bu_id=0 ){
+
+ //error_log( "save_bu_item id = $bu_id, new_bu_dest = {$postVars['new_bu_dest']}" );
+
+ $db = new SQLite3('bin/data.db' );
+ $db_st = null;
+ if( $postVars['new_bu_is_dbType'] ){
+ if($bu_id==0){
+ $db_st = $db->prepare('INSERT INTO BULIST ( BUName, Dir_Src, Dir_Dest, BuType ) values( :bun, :bus, :bud, 1 )');
+ }else{
+ $db_st = $db->prepare('update BULIST set BUName=:bun, Dir_Src=:bus, Dir_Dest=:bud where BUID=:buid');
+ }
+ }else{
+ if($bu_id==0){
+ $db_st = $db->prepare('INSERT INTO BULIST ( BUName, Dir_Src, Dir_Dest, Files_Ex, BuType ) values( :bun, :bus, :bud, :buex, 0 )');
+ }else{
+ $db_st = $db->prepare('update BULIST set BUName=:bun, Dir_Src=:bus, Dir_Dest=:bud, Files_Ex=:buex where BUID=:buid');
+ }
+ }
+
+ if($bu_id>0){
+ $db_st->bindValue(':buid', $bu_id );
+ }
+
+ $db_st->bindValue(':bun', $postVars['new_bu_name'] );
+ $db_st->bindValue(':bus', $postVars['new_bu_source'] );
+ $db_st->bindValue(':bud', $postVars['new_bu_dest'] );
+ if( !$postVars['new_bu_is_dbType'] ){
+ $db_st->bindValue(':buex', $postVars['new_bu_exf'] );
+ }
+
+ $db_st->execute();
+
+ if( $db->lastErrorCode()!==0){
+ return $db->lastErrorCode();
+ }
+
+}
+
+
+?>
diff --git a/html/inc/pagemap.php b/html/inc/pagemap.php
new file mode 100644
index 0000000..11801de
--- /dev/null
+++ b/html/inc/pagemap.php
@@ -0,0 +1,156 @@
+0){
+ sendHtmlError( "DB error: $db_err" );
+ return true;
+ }
+
+ if($postvars['final-page'] == '' ){
+ error_log( "Error: Expecting a final-page for: ". $cmd );
+ sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+ return true;
+ }
+ return invokeCommand( $postvars['final-page'], $postvars );
+ }
+
+
+ if( $cmd=='new-item-bu' ){
+
+ include_once __DIR__ . "/new-file-bu.php";
+ $db_err = save_bu_item( $postvars );
+ if($db_err>0){
+ sendHtmlError( "DB error: $db_err" );
+ return true;
+ }
+
+ if($postvars['final-page'] == '' ){
+ error_log( "Error: Expecting a final-page for: ". $cmd );
+ sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+ return true;
+ }
+ return invokeCommand( $postvars['final-page'], $postvars );
+
+ }
+
+ if( $cmd=='edit-item-bu' ){
+
+ include_once __DIR__ . "/new-file-bu.php";
+ $db_err = save_bu_item( $postvars, $postvars['id'] );
+ if($db_err>0){
+ sendHtmlError( "DB error: $db_err" );
+ return true;
+ }
+
+ if($postvars['final-page'] == '' ){
+ error_log( "Error: Expecting a final-page for: ". $cmd );
+ sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+ return true;
+ }
+ return invokeCommand( $postvars['final-page'], $postvars );
+
+ }
+
+ if( $cmd=='delete-bu' ){
+
+ include_once __DIR__ . "/delete-file-bu.php";
+ $db_err = delete_bu_item( $postvars['id'] );
+ if($db_err>0){
+ sendHtmlError( "DB error: $db_err" );
+ return true;
+ }
+
+ if($postvars['final-page'] == '' ){
+ error_log( "Error: Expecting a final-page for: ". $cmd );
+ sendHtmlError( "Error: Expecting a final-page for: $cmd in ". __FILE__ );
+ return true;
+ }
+ return invokeCommand( $postvars['final-page'], $postvars );
+
+ }
+
+}
+
+?>
diff --git a/html/inc/secure.php b/html/inc/secure.php
new file mode 100644
index 0000000..0e1450a
--- /dev/null
+++ b/html/inc/secure.php
@@ -0,0 +1,77 @@
+=4 ){
+ //the user is logged in so nothing further to do in this script
+ return;
+}
+
+//is there a login attempt
+if( isset($_POST['username']) && isset($_POST['password']) ){
+
+ //any request must have the 'action' set to btn_login
+ if( !isset($_POST['action']) || $_POST['action']!='btn_login' ) {
+ error_log("Login Error: Missing or incorrect value for 'action'");
+ exit;
+ }
+
+ if( !isset($_POST['svr_nonce']) || $_POST['svr_nonce']=='' ) {
+ error_log("Login Error: Missing or blank value for 'svr_nonce'");
+ exit;
+ }
+
+ //has the correct nonce been returned?
+ if( $_POST['svr_nonce']!=$_SESSION['svr_nonce'] ){
+ $_SESSION['svr_nonce'] = ''; // invalidate nonce
+ error_log("Login Error: Incorrect value for 'svr_nonce'");
+ exit;
+ }
+
+ include_once "common.php";
+ include_once "login.php";
+ if(authenticate( $_POST['username'], $_POST['password'], $_SESSION['svr_nonce'] ) ){
+
+ error_log("Login Success: ".$_POST['username']);
+ $_SESSION['username'] = $_POST['username'];
+
+ //return the backup list page
+ include_once __DIR__ . "/bu_list_content.php";
+ sendHtmlOk_WithData( [ 'authenticated', getbu_list_content() ] );
+
+ }else{
+ error_log("Login Failure: ".$_POST['username']);
+ // failed authentication
+ // --- Send back the login page ---
+ $_SESSION['svr_nonce'] = generateNonce();
+ sendHtmlOk_WithData( [ 'login', getLogin_content(), $_SESSION['svr_nonce'] ] );
+ }
+
+ exit;
+}
+
+// no session is set
+// no attempt to login was made
+// likely that the page was just refreshed
+// --- Send back the login page ---
+
+include_once "common.php";
+$_SESSION['svr_nonce'] = generateNonce();
+
+include_once "login.php";
+sendHtmlOk_WithData( [ 'login', getLogin_content(), $_SESSION['svr_nonce'] ] );
+exit; //end the script to stop further processing by calling files
+
+//$svr_username = $_SESSION['username'];
+
+?>
diff --git a/html/index.php b/html/index.php
new file mode 100644
index 0000000..3232869
--- /dev/null
+++ b/html/index.php
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Pearcey: Excecutive Backup Panel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
For help using this panel, please email john@pearcey.net
+
+
This backup utility is open source and can be downloaded from pearcey.net
+
+
Please report bugs to john@pearcey.net
+
+
+
+
+
+
+
diff --git a/html/js/common.js b/html/js/common.js
new file mode 100644
index 0000000..dc25eab
--- /dev/null
+++ b/html/js/common.js
@@ -0,0 +1,101 @@
+
+/**
+ * COPYRIGHT © 2022 JOHN PEARCEY
+ * All rights reserved
+*/
+
+/*
+ * pagename:
+ * The name of the server-side script to handle this post.
+ * fnCallback:
+ * The javascript callback function, called after the AJAX call completes.
+ * map_params:
+ * A map containing name value pairs required by the script.
+ * e.g.
+ * map_params = new Map();
+ map_params.set('username', document.getElementById('username').value );
+ map_params.set('pwdhash', document.getElementById('pwd').value );
+ */
+function postData( pagename, fnCallback, map_params ){
+
+ //console.log( 'postData called' );
+
+ let formData = new FormData();
+ map_params.forEach((value, key, map) => {
+ //if(key=='action') console.log("postData: ", pagename, value );
+ formData.append( key, value );
+ });
+
+ //https://www.digitalocean.com/community/tutorials/how-to-use-the-javascript-fetch-api-to-get-data
+ //https://jsonplaceholder.typicode.com/users
+ //https://javascript.info/fetch
+ fetch( pagename, {
+ method: 'POST',
+ body: formData,
+
+ })
+ /*.then((response) => {
+ return response.json(); */
+
+ .then(response => response.text())
+ .then((dataStr) => {
+
+ //console.log(dataStr);
+
+ //there's a whole bunch of warnings and errors that might be sent by PHP. It is not possible
+ //to catch them in the serverside PHP code. So here we search for our json encoded return string which
+ //must be either Error or OK.
+ let pos = dataStr.indexOf("[\"OK\"");
+ let additional=null;
+ if(pos==-1){
+ pos = dataStr.indexOf("[\"Error\"");
+ if(pos==-1){
+ //the PHP is pretty fucked up if you get here!
+ console.warn( dataStr );
+ let encodedStr = JSON.stringify( ["Illegal return from server. Did not find Error or OK.", dataStr ] );
+ return JSON.parse( encodedStr );
+ }
+ }
+ //console.log(dataStr);
+ return JSON.parse(dataStr);
+
+ }).then((data) => {
+ fnCallback( true, data, map_params );
+
+ }).catch(function(error) {
+ console.log( error );
+ fnCallback( false, error, map_params );
+ });
+
+}
+
+function checkReturn( success, data, bGetErr=false ){
+
+ if(!success){
+ console.warn( "callback error: " + data );
+ return false;
+ }
+
+ if(data[0]=="Error"){
+ if(bGetErr) return data;
+ alert( "Error: " + data[1] );
+ for( i=2; i/gi, '\n');
+ txt = txt.replace(/<\/?[^>]+>/gi, ''); //strip HTML
+ return txt;
+}
diff --git a/html/js/main.js b/html/js/main.js
new file mode 100644
index 0000000..78fb441
--- /dev/null
+++ b/html/js/main.js
@@ -0,0 +1,454 @@
+
+/**
+ * COPYRIGHT © 2022 JOHN PEARCEY
+ * All rights reserved
+*/
+
+/**
+ * The last nonce we just got from the server
+ */
+var g_svr_nonce = [];
+
+/**
+ * Every 10secs, we get the state of the backup list from the server. This is so that we can keep the front end up-to-date
+ * for the user. I'm sure that there are better ways with modern HTML5 but polling is easy and the application is not
+ * time critical. This is sufficient.
+ */
+var g_bu_list_state = [];
+
+/**
+ * This var g_stack_pages contains the app navigation pages in order. The top most page being the
+ * current page. So, to go back, you would remove the top most page and navigate to the new top page.
+*/
+var g_stack_pages = [];
+
+function clear_page_state(){
+ g_bu_list_state = [];
+ g_stack_pages = [];
+}
+
+function push_page( pagename ){
+ g_stack_pages.push( pagename );
+}
+
+/**
+ * Pops the current page off the stack and returns the penultimate one.
+ */
+function back_page( ){
+ g_stack_pages.pop( );
+ return g_stack_pages[g_stack_pages.length - 1];
+}
+
+function index_onload(){
+ update_bu_state( false );
+ set_main_content( true );
+}
+
+function update_bu_state( bCheckPage = true ){
+
+ setTimeout( () => {
+ update_bu_state();
+ }, 10000);
+
+ if(bCheckPage){
+ //console.log( "update_bu_state pages:", g_stack_pages );
+ if( g_stack_pages[g_stack_pages.length - 1] != 'backup-list' ) return;
+ }
+
+ //console.log( "update_bu_state polling" );
+
+ map_params = new Map();
+ map_params.set('action', 'backup-list-state' );
+
+ postData( "post_handler.php", function( success, data, m ){
+
+ if(data[1][0]=='login'){
+ console.log( 'Cannot get backup-list-state: Not logged in' );
+ return;
+ }
+
+ let new_state = data[1][1];
+
+ //console.log( 'backup-list-state' + new_state );
+
+ if( g_bu_list_state.length == 0 ){
+ //just store the new state
+ g_bu_list_state = new_state;
+ return;
+ }
+
+ //compare the two arrays, if different length, then refresh the UI.
+ if( g_bu_list_state.length != new_state.length ){
+ g_bu_list_state = new_state;
+ set_main_content( false );
+ return;
+ }
+
+ //check each row in turn
+ let bFound = false;
+ for( j=0; j";
+
+ //ensure the state map is also adjusted
+ for( j=0; j b.toString(16).padStart(2, "0"))
+ .join(""); // convert bytes to hex string
+ return hashHex;
+}
+
+/**
+ * Validate the form for a new backup entry
+ * b_is_db_type is true for DB entry and false for file entry.
+ */
+function valid_btn_new_bu( b_is_db_type ){
+ return true;
+}
+
+/**
+ * Collect the form data together.
+ * b_is_db_type is true for DB entry and false for file entry.
+ */
+function set_btn_new_bu( b_is_db_type, map_params ){
+
+ map_params.set( 'new_bu_is_dbType', b_is_db_type?1:0 );
+ map_params.set( 'new_bu_name', document.getElementById('new_bu_name').value );
+ map_params.set( 'new_bu_source', document.getElementById('new_bu_source').value );
+ map_params.set( 'new_bu_dest', document.getElementById('new_bu_dest').value );
+ if(!b_is_db_type){
+ map_params.set( 'new_bu_exf', document.getElementById('new_bu_exf').value );
+ }
+}
+
+/**
+ * Validate the form for the backup scheduling page
+ */
+function valid_btn_edit_shed_bu(){
+
+ sched_date = document.getElementById('sched_date').value;
+ if(sched_date=='') {
+ alert("Please choose a date");
+ return false;
+ }
+
+ sched_time = document.getElementById('sched_time').value;
+ if(sched_time=='') {
+ alert("Please choose a time");
+ return false;
+ }
+
+ let dt_chosen = new Date( sched_date + 'T' + sched_time + ':00Z');
+ let now = new Date();
+
+ if(dt_chosen<=now){
+ alert("Please choose a time in the future, not the past: ", dt_chosen, now );
+ return false;
+ }
+
+ console.log("dates: ", dt_chosen, now );
+
+ return true;
+}
+
+/**
+ * Collect the form data together for the backup scheduling page
+ */
+function set_btn_edit_shed_bu( map_params ){
+
+ map_params.set( 'sched_date', document.getElementById('sched_date').value );
+ map_params.set( 'sched_time', document.getElementById('sched_time').value );
+
+ switch(document.querySelector('input[name="sched_period"]:checked').id){
+ case 'sched_ip_daily':
+ map_params.set( 'sched_period', 'D' );
+ break;
+
+ case 'sched_ip_weekly':
+ map_params.set( 'sched_period', 'W' );
+ break;
+
+ case 'sched_ip_monthly':
+ map_params.set( 'sched_period', 'M' );
+ break;
+
+ case 'sched_ip_none':
+ default:
+ map_params.set( 'sched_period', 'N' );
+ }
+
+
+}
+
diff --git a/html/phpinfo.php b/html/phpinfo.php
new file mode 100644
index 0000000..cf60860
--- /dev/null
+++ b/html/phpinfo.php
@@ -0,0 +1,3 @@
+
diff --git a/html/post_handler.php b/html/post_handler.php
new file mode 100644
index 0000000..bce003d
--- /dev/null
+++ b/html/post_handler.php
@@ -0,0 +1,25 @@
+