/** * 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<g_bu_list_state.length; j++){ curr_state_j = g_bu_list_state[j]; bFound = false; //console.log("looking for ID = " + curr_state_j[0] ); for( i=0; i<new_state.length; i++){ new_state_i = new_state[i]; //console.log("compare with", new_state_i ); if( new_state_i[0] == curr_state_j[0]){ bFound = (new_state_i[1] == curr_state_j[1]); break; } } if(!bFound){ //at least one ID or value does not match g_bu_list_state = new_state; set_main_content( false ); break; } } }, map_params ); } function set_main_content( bPushPage ){ //console.log("3"); map_params = new Map(); map_params.set('action', 'backup-list' ); postData( "post_handler.php", function( success, data, m ){ if(data[0]!="OK"){ console.log( data ); return; } let elem = document.getElementById('page_content'); //display page from server [ page_type, elem.innerHTML ] = data[1]; console.log('set_main_content page_type: ', page_type ); if(page_type=='login'){ //clear client side data clear_page_state(); g_svr_nonce = data[1][2]; return; } if(bPushPage) push_page('backup-list'); }, map_params ); } /** * Button clicked and we require a new page from the server to be placed in the page_content area. * instr(uction) is usually the page name except where a 'back' navigation is required. */ async function btn_clk_nav( elem, instr, id=0 ){ // instr can now also be a space separated list of instructions //we split them out instrArr = instr.split(" "); instr = instrArr[0]; //noTest map_params = new Map(); map_params.set('id', id ); let b_is_db_type = false; switch(instr){ case 'btn_login': if( !valid_btn_login() ) return; await set_btn_login( map_params ); map_params.set('svr_nonce', g_svr_nonce ); map_params.set('final-page', 'backup-list' ); navigate_page( instr, map_params, false ); return; case 'btn_run_bu': bIsBuTest = (instrArr.length==2 && instrArr[1]=='noTest'); if( bIsBuTest ){ map_params.set('isTest', 'false' ); }else{ map_params.set('isTest', 'true' ); console.log("running a test backup"); } exec_instruction( instr, map_params ); if(!bIsBuTest){ monitorBuOutput( true, id, -1 ); } return; case 'delete-bu': if( !confirm('Are you sure you wish to delete backup item ' + id ) ) return; map_params.set('final-page', 'backup-list' ); navigate_page( instr, map_params, false ); return; case 'btn_edit_shed_bu': if( !valid_btn_edit_shed_bu() ) return; set_btn_edit_shed_bu( map_params ); map_params.set('final-page', back_page() ); //server to return previous page (usually backup-list) after processing navigate_page( 'edit_shed_bu', map_params, false ); return; case 'btn_new_bu_db': b_is_db_type=true; //drop through case 'btn_new_bu_file': if( !valid_btn_new_bu( b_is_db_type ) ) return; set_btn_new_bu( b_is_db_type, map_params ); map_params.set('final-page', back_page() ); //server to return previous page (usually backup-list) after processing navigate_page( 'new-item-bu', map_params, false ); return; case 'btn_edit_bu_db': b_is_db_type=true; //drop through case 'btn_edit_bu_file': if( !valid_btn_new_bu( b_is_db_type ) ) return; set_btn_new_bu( b_is_db_type, map_params ); map_params.set('final-page', back_page() ); //server to return previous page (usually backup-list) after processing navigate_page( 'edit-item-bu', map_params, false ); return; case 'btn_cancel_new_bu': case 'btn_cancel': navigate_page( back_page(), map_params, false ); return; } navigate_page( instr, map_params, true ); } function navigate_page( goto_page, map_params, bPushPage ){ //console.log( 'navigate_page: ', goto_page ); map_params.set('action', goto_page ); //map_params.set('updateList', JSON.stringify( usrUpdates ) ); postData( "post_handler.php", function( success, data, m ){ if(data[0]!="OK"){ console.log( data[0], data[1] ); if(!bPushPage){ //we need to put back the page that we dropped off or maybe an error page //but use goto_page for now push_page( goto_page ); } return; } let elem = document.getElementById('page_content'); [ page_type, elem.innerHTML ] = data[1]; console.log('navigate_page page_type: ', page_type); if(page_type=='login'){ //clear client side data clear_page_state(); g_svr_nonce = data[1][2]; return; } if(page_type=='authenticated'){ elem = document.getElementById('anc_login'); elem.innerHTML="Logout"; push_page( map_params.get('final-page') ); return; } if(bPushPage) push_page( goto_page ); }, map_params ); } function setTextAreaFromArray( elem, arr ){ elem.value = arr.toString().replaceAll(',', "\r\n" ); } function exec_instruction( instr, map_params ){ console.log( 'exec_instruction: ', instr ); let id = map_params.get('id'); map_params.set('action', instr ); //map_params.set('updateList', JSON.stringify( usrUpdates ) ); postData( "post_handler.php", function( success, data, m ){ if(data[0]!="OK"){ console.log( data ); return; } bu_result = data[1][0]; //was it ok or err? switch(instr){ case 'btn_run_bu': if(bu_result!='ok:'){ //latest msg contains similar format as used by the server rpc layer console.log("odd message: " + bu_result ); console.log( data ); return; } bu_status = data[1][1]; //extra info if(bu_status=='running'){ // is it a test run? - by rights, I should send back the 'test' flag let elem = document.getElementById( "ta_testoutput" ); if(elem==null){ //not a test - set the tick let elem = document.getElementById( "td_run_" + id ); elem.innerHTML = "<img id=\"td_run_\"" + id + " src=\"img/green-tick.png\">"; //ensure the state map is also adjusted for( j=0; j<g_bu_list_state.length; j++){ if(g_bu_list_state[j][0]==id){ g_bu_list_state[j][1] = 1; } } }else{ //test run setTextAreaFromArray( elem, bu_result + " " + bu_status ); } }else{ console.log("unknown message: " + bu_status ); console.log( data ); } break; default: console.log( data[1] ); } }, map_params ); } function monitorBuOutput( bMonitor, buid, from_line_num ){ if(from_line_num==-1){ //first call, just set the timer and give some time for the test to kick off setTimeout( () => { monitorBuOutput(true, buid, 0); }, 1000); return ; } console.log("Fetch output from line: " + from_line_num ); let e_ta_testoutput = document.getElementById( "ta_testoutput" ); if(e_ta_testoutput==null){ // page gone (should terminate server open file?) return; } let map_params = new Map(); map_params.set('action', 'bu-test-file-lines' ); map_params.set('buid', buid ); map_params.set('from-line-num', from_line_num ); postData( "post_handler.php", function( success, data, m ){ //console.log( data ); let cnt=0; let rtm_msg = data[1][cnt++]; //'f:bu-test-file-lines' let line_count = data[1][cnt++]; let line_data = ''; if(line_count>0){ line_data = data[1][cnt++]; } e_ta_testoutput = document.getElementById( "ta_testoutput" ); if(e_ta_testoutput==null){ // page gone (should terminate server open file?) return; } if(line_count>0){ //setTextAreaFromArray( e_ta_testoutput, line_data ); e_ta_testoutput.value += "\n" + line_data; setTimeout( () => { monitorBuOutput(true, buid, line_count + from_line_num ); }, 1000); return ; }else{ console.log("No more lines to fetch" ); } }, map_params); } function valid_btn_login(){ let login_name = document.getElementById('login_name').value; if(login_name=='') { alert("Please enter your login name"); return false; } let login_pwd = document.getElementById('login_pwd').value; if(login_pwd=='') { alert("Please enter your password"); return false; } return true; } /** * Collect data from login form */ async function set_btn_login( map_params ){ let raw_pwd = document.getElementById('login_pwd').value; const pwd_hash = await getHash256(raw_pwd+g_svr_nonce); map_params.set( 'username', document.getElementById('login_name').value ); map_params.set( 'password', pwd_hash ); } async function getHash256(message) { //https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest const encoder = new TextEncoder(); const data = encoder.encode(message); const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array const hashHex = hashArray .map((b) => 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' ); } }