diff --git a/app/admin/getUsers.php b/app/admin/getUsers.php index 5c08c45..498a952 100644 --- a/app/admin/getUsers.php +++ b/app/admin/getUsers.php @@ -1,5 +1,5 @@ escaped_str */ + if(!strlen($string)) return ''; + if(!db_link()) sql("SELECT 1+1", $eo); // if this is a previously escaped string, return from cached @@ -297,115 +299,191 @@ function checkPermissionVal($pvn) { } } ######################################################################## - if(!function_exists('sql')) { - function sql($statment, &$o) { + function dieErrorPage($error) { + global $Translation; - /* - Supported options that can be passed in $o options array (as array keys): - 'silentErrors': If true, errors will be returned in $o['error'] rather than displaying them on screen and exiting. - */ + $header = (defined('ADMIN_AREA') ? __DIR__ . '/incHeader.php' : __DIR__ . '/../header.php'); + $footer = (defined('ADMIN_AREA') ? __DIR__ . '/incFooter.php' : __DIR__ . '/../footer.php'); - global $Translation; - static $connected = false, $db_link; + ob_start(); - $dbServer = config('dbServer'); - $dbUsername = config('dbUsername'); - $dbPassword = config('dbPassword'); - $dbDatabase = config('dbDatabase'); + @include_once($header); + echo Notification::placeholder(); + echo Notification::show([ + 'message' => $error, + 'class' => 'danger', + 'dismiss_seconds' => 7200 + ]); + @include_once($footer); - $admin_dir = dirname(__FILE__); - $header = (defined('ADMIN_AREA') ? "{$admin_dir}/incHeader.php" : "{$admin_dir}/../header.php"); - $footer = (defined('ADMIN_AREA') ? "{$admin_dir}/incFooter.php" : "{$admin_dir}/../footer.php"); + echo ob_get_clean(); + exit; + } + ######################################################################## + function openDBConnection(&$o) { + static $connected = false, $db_link; - ob_start(); + $dbServer = config('dbServer'); + $dbUsername = config('dbUsername'); + $dbPassword = config('dbPassword'); + $dbDatabase = config('dbDatabase'); - if(!$connected) { - /****** Connect to MySQL ******/ - if(!extension_loaded('mysql') && !extension_loaded('mysqli')) { - $o['error'] = 'PHP is not configured to connect to MySQL on this machine. Please see this page for help on how to configure MySQL.'; - if($o['silentErrors']) return false; - - @include_once($header); - echo Notification::placeholder(); - echo Notification::show(array( - 'message' => $o['error'], - 'class' => 'danger', - 'dismiss_seconds' => 7200 - )); - @include_once($footer); - echo ob_get_clean(); - exit; - } - - if(!($db_link = @db_connect($dbServer, $dbUsername, $dbPassword))) { - $o['error'] = db_error($db_link, true); - if($o['silentErrors']) return false; - - @include_once($header); - echo Notification::placeholder(); - echo Notification::show(array( - 'message' => $o['error'], - 'class' => 'danger', - 'dismiss_seconds' => 7200 - )); - @include_once($footer); - echo ob_get_clean(); - exit; - } - - /****** Select DB ********/ - if(!db_select_db($dbDatabase, $db_link)) { - $o['error'] = db_error($db_link); - if($o['silentErrors']) return false; - - @include_once($header); - echo Notification::placeholder(); - echo Notification::show(array( - 'message' => $o['error'], - 'class' => 'danger', - 'dismiss_seconds' => 7200 - )); - @include_once($footer); - echo ob_get_clean(); - exit; - } - - $connected = true; - } + if($connected) return $db_link; - if(!$result = @db_query($statment, $db_link)) { - if(!stristr($statment, "show columns")) { - // retrieve error codes - $errorNum = db_errno($db_link); - $errorMsg = htmlspecialchars(db_error($db_link)); - - if(getLoggedAdmin()) $errorMsg .= "
{$Translation['query:']}\n" . htmlspecialchars($statment) . "

{$Translation['admin-only info']}

{$Translation['try rebuild fields']}

"; - - if($o['silentErrors']) { $o['error'] = $errorMsg; return false; } - - @include_once($header); - echo Notification::placeholder(); - echo Notification::show(array( - 'message' => $errorMsg, - 'class' => 'danger', - 'dismiss_seconds' => 7200 - )); - @include_once($footer); - echo ob_get_clean(); - exit; - } - } + /****** Check that MySQL module is enabled ******/ + if(!extension_loaded('mysql') && !extension_loaded('mysqli')) { + $o['error'] = 'PHP is not configured to connect to MySQL on this machine. Please see this page for help on how to configure MySQL.'; + if($o['silentErrors']) return false; - ob_end_clean(); - return $result; + dieErrorPage($o['error']); + } + + /****** Connect to MySQL ******/ + if(!($db_link = @db_connect($dbServer, $dbUsername, $dbPassword))) { + $o['error'] = db_error($db_link, true); + if($o['silentErrors']) return false; + + dieErrorPage($o['error']); + } + + /****** Select DB ********/ + if(!db_select_db($dbDatabase, $db_link)) { + $o['error'] = db_error($db_link); + if($o['silentErrors']) return false; + + dieErrorPage($o['error']); } + + $connected = true; + return $db_link; + } + ######################################################################## + function sql($statement, &$o) { + + /* + Supported options that can be passed in $o options array (as array keys): + 'silentErrors': If true, errors will be returned in $o['error'] rather than displaying them on screen and exiting. + 'noSlowQueryLog': don't log slow query if true + 'noErrorQueryLog': don't log error query if true + */ + + global $Translation; + + $db_link = openDBConnection($o); + + /* + if openDBConnection() fails, it would abort execution unless 'silentErrors' is true, + in which case, we should return false from sql() without further action since + $o['error'] would be already set by openDBConnection() + */ + if(!$db_link) return false; + + $t0 = microtime(true); + + if(!$result = @db_query($statement, $db_link)) { + if(!stristr($statement, "show columns")) { + // retrieve error codes + $errorNum = db_errno($db_link); + $o['error'] = htmlspecialchars(db_error($db_link)); + + if(empty($o['noErrorQueryLog'])) + logErrorQuery($statement, $o['error']); + + if(getLoggedAdmin()) + $o['error'] .= "
{$Translation['query:']}\n" . htmlspecialchars($statement) . "

{$Translation['admin-only info']}

{$Translation['try rebuild fields']}

"; + + if($o['silentErrors']) return false; + + dieErrorPage($o['error']); + } + } + + /* log slow queries that take more than 1 sec */ + $t1 = microtime(true); + if($t1 - $t0 > 1.0 && empty($o['noSlowQueryLog'])) + logSlowQuery($statement, $t1 - $t0); + + return $result; + } + ######################################################################## + function logSlowQuery($statement, $duration) { + if(!createQueryLogTable()) return; + + $o = [ + 'silentErrors' => true, + 'noSlowQueryLog' => true, + 'noErrorQueryLog' => true + ]; + $statement = makeSafe($statement); + $duration = floatval($duration); + $memberID = makeSafe(getLoggedMemberID()); + $uri = makeSafe($_SERVER['REQUEST_URI']); + + sql("INSERT INTO `appgini_query_log` SET + `statement`='$statement', + `duration`=$duration, + `memberID`='$memberID', + `uri`='$uri' + ", $o); + } + ######################################################################## + function logErrorQuery($statement, $error) { + if(!createQueryLogTable()) return; + + $o = [ + 'silentErrors' => true, + 'noSlowQueryLog' => true, + 'noErrorQueryLog' => true + ]; + $statement = makeSafe($statement); + $error = makeSafe($error); + $memberID = makeSafe(getLoggedMemberID()); + $uri = makeSafe($_SERVER['REQUEST_URI']); + + sql("INSERT INTO `appgini_query_log` SET + `statement`='$statement', + `error`='$error', + `memberID`='$memberID', + `uri`='$uri' + ", $o); + } + + ######################################################################## + function createQueryLogTable() { + static $created = false; + if($created) return true; + + $o = [ + 'silentErrors' => true, + 'noSlowQueryLog' => true, + 'noErrorQueryLog' => true + ]; + + sql("CREATE TABLE IF NOT EXISTS `appgini_query_log` ( + `datetime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `statement` LONGTEXT, + `duration` DECIMAL(10,2) UNSIGNED DEFAULT 0.0, + `error` TEXT, + `memberID` VARCHAR(200), + `uri` VARCHAR(200) + ) CHARSET " . mysql_charset, $o); + + // check if table created + //$o2 = $o; + //$o2['error'] = ''; + //sql("SELECT COUNT(1) FROM 'appgini_query_log'", $o2); + + //$created = empty($o2['error']); + + $created = true; + return $created; } ######################################################################## - function sqlValue($statment, &$error = NULL) { - // executes a statment that retreives a single data value and returns the value retrieved + function sqlValue($statement, &$error = NULL) { + // executes a statement that retreives a single data value and returns the value retrieved $eo = ['silentErrors' => true]; - if(!$res = sql($statment, $eo)) { $error = $eo['error']; return false; } + if(!$res = sql($statement, $eo)) { $error = $eo['error']; return false; } if(!$row = db_fetch_row($res)) return false; return $row[0]; } diff --git a/app/admin/incHeader.php b/app/admin/incHeader.php index 3f65631..1c3d45a 100644 --- a/app/admin/incHeader.php +++ b/app/admin/incHeader.php @@ -200,13 +200,15 @@ function hideDialogs() { diff --git a/app/admin/pageBackupRestore.php b/app/admin/pageBackupRestore.php index afc5955..900b727 100644 --- a/app/admin/pageBackupRestore.php +++ b/app/admin/pageBackupRestore.php @@ -129,7 +129,7 @@ protected function utf8ize($mixed) { */ protected function get_specified_backup_file() { $md5_hash = $this->request['md5_hash']; - if(!preg_match('/^[a-f0-9]{32}$/i', $md5_hash)) return false; + if(!preg_match('/^[a-f0-9]{17,32}$/i', $md5_hash)) return false; $bfile = "{$this->curr_dir}/backups/{$md5_hash}.sql"; if(!is_file($bfile)) return false; @@ -361,10 +361,10 @@ public function get_backup_files() { $list = []; while(false !== ($entry = $d->read())) { - if(!preg_match('/^[a-f0-9]{32}\.sql$/i', $entry)) continue; + if(!preg_match('/^[a-f0-9]{17,32}\.sql$/i', $entry)) continue; $fts = @filemtime("{$bdir}/{$entry}"); $list[$fts] = array( - 'md5_hash' => substr($entry, 0, 32), + 'md5_hash' => substr($entry, 0, -4), 'datetime' => date($dtf, $fts), 'size' => number_format(@filesize("{$bdir}/{$entry}") / 1024) ); @@ -388,7 +388,7 @@ public function create_backup() { $config = ['dbServer' => '', 'dbUsername' => '', 'dbPassword' => '', 'dbDatabase' => '']; foreach($config as $k => $v) $config[$k] = escapeshellarg(config($k)); - $dump_file = escapeshellarg(normalize_path($this->curr_dir)) . '/backups/' . md5(microtime()) . '.sql'; + $dump_file = escapeshellarg(normalize_path($this->curr_dir)) . '/backups/' . substr(md5(microtime() . rand(0, 100000)), -17) . '.sql'; $pass_param = ($config['dbPassword'] ? "-p{$config['dbPassword']}" : ''); $this->cmd = "(mysqldump --no-tablespaces -u{$config['dbUsername']} {$pass_param} -h{$config['dbServer']} {$config['dbDatabase']} -r {$dump_file}) 2>&1"; diff --git a/app/admin/pageEditMember.php b/app/admin/pageEditMember.php index 943d676..c29d45f 100644 --- a/app/admin/pageEditMember.php +++ b/app/admin/pageEditMember.php @@ -383,7 +383,7 @@ .parents('.form-group').removeClass('has-error has-success'); }, success: function(resp) { - if(resp.match(/\ +
', '' . html_attr($_REQUEST['memberID']) . '', $Translation['username invalid']); ?> - +
diff --git a/app/cli-update-calculated-fields.php b/app/cli-update-calculated-fields.php index 85ad3da..9c287f5 100644 --- a/app/cli-update-calculated-fields.php +++ b/app/cli-update-calculated-fields.php @@ -43,7 +43,7 @@ $args = []; for($i = 0; $i < count($argv); $i += 2) { if(!in_array($argv[$i], $allowed_args)) continue; - $args[$argv[$i]] = array_map(trim, explode(',', $argv[$i + 1])); + $args[$argv[$i]] = array_map('trim', explode(',', $argv[$i + 1])); } $calc = calculated_fields(); diff --git a/app/clients_autofill.php b/app/clients_autofill.php index 825b2af..33b1c52 100644 --- a/app/clients_autofill.php +++ b/app/clients_autofill.php @@ -1,5 +1,5 @@ ' + (jQuery('#name').val() || '') + '');\n"; $jsReadOnly .= "\tjQuery('#contact').replaceWith('
' + (jQuery('#contact').val() || '') + '
');\n"; $jsReadOnly .= "\tjQuery('#title').replaceWith('
' + (jQuery('#title').val() || '') + '
');\n"; @@ -351,8 +352,8 @@ function clients_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $Al $noUploads = true; } elseif(($AllowInsert && !$selected_id) || ($AllowUpdate && $selected_id)) { - $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', true);"; // temporarily disable form change handler - $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', false);"; // re-enable form change handler + $jsEditable = "\tjQuery('form').eq(0).data('already_changed', true);"; // temporarily disable form change handler + $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', false);"; // re-enable form change handler } // process combos diff --git a/app/clients_view.php b/app/clients_view.php index 1a98baa..76f8d88 100644 --- a/app/clients_view.php +++ b/app/clients_view.php @@ -1,5 +1,5 @@ setTimeout(function() { window.location = "index.php?signOut=1"; }, 2000);'; + echo error_message($Translation['tableAccessDenied']); exit; } @@ -157,23 +156,6 @@ $x->AllowConsoleLog = false; $x->AllowDVNavigation = true; - // mm: build the query based on current member's permissions - $DisplayRecords = $_REQUEST['DisplayRecords']; - if(!in_array($DisplayRecords, ['user', 'group'])) { $DisplayRecords = 'all'; } - if($perm['view'] == 1 || ($perm['view'] > 1 && $DisplayRecords == 'user' && !$_REQUEST['NoFilter_x'])) { // view owner only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `clients`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='clients' AND LCASE(`membership_userrecords`.`memberID`)='" . getLoggedMemberID() . "'"; - } elseif($perm['view'] == 2 || ($perm['view'] > 2 && $DisplayRecords == 'group' && !$_REQUEST['NoFilter_x'])) { // view group only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `clients`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='clients' AND `membership_userrecords`.`groupID`='" . getLoggedGroupID() . "'"; - } elseif($perm['view'] == 3) { // view all - // no further action - } elseif($perm['view'] == 0) { // view none - $x->QueryFields = ['Not enough permissions' => 'NEP']; - $x->QueryFrom = '`clients`'; - $x->QueryWhere = ''; - $x->DefaultSortField = ''; - } // hook: clients_init $render = true; if(function_exists('clients_init')) { diff --git a/app/common.js b/app/common.js index 1b7826e..22a3879 100644 --- a/app/common.js +++ b/app/common.js @@ -194,15 +194,13 @@ jQuery(function() { // in table view, hide unnecessary page elements if no records are displayed if($j('.table_view').length) { + var tvHasWarning = $j('.table_view tfoot .alert-warning').length > 0; + setInterval(function() { - if($j('tfoot .alert-warning').length) { - $j('#Print, #CSV, #tv-tools, thead, tr.success').addClass('hidden'); - $j('.tv-toggle').parent().addClass('hidden'); - return; - } + $j('#Print, #CSV, #tv-tools, .table_view thead, .table_view tr.success') + .toggleClass('hidden', tvHasWarning); - $j('#Print, #CSV, #tv-tools, thead, tr.success').removeClass('hidden'); - $j('.tv-toggle').parent().removeClass('hidden'); + $j('.tv-toggle').parent().toggleClass('hidden', tvHasWarning); }, 100); } diff --git a/app/datalist.php b/app/datalist.php index 5170d9e..318b26e 100644 --- a/app/datalist.php +++ b/app/datalist.php @@ -157,6 +157,8 @@ function Render() { if(isset($_REQUEST['record_selector']) && is_array($_REQUEST['record_selector'])) $record_selector = $_REQUEST['record_selector']; + $this->applyPermissionsToQuery($DisplayRecords); + if($SelectedID && !$Embedded && $this->AllowDVNavigation) { $setSelectedIDPreviousPage = !empty($_REQUEST['setSelectedIDPreviousPage']); $setSelectedIDNextPage = !empty($_REQUEST['setSelectedIDNextPage']) && !$setSelectedIDPreviousPage; @@ -461,7 +463,6 @@ function Render() { // TV code, only if user has view permission if($this->Permissions['view']) { - $QueryHasCustomWhere = (strlen($this->QueryWhere) > 0); // apply lookup filterers to the query foreach($this->filterers as $filterer => $caption) { @@ -477,26 +478,27 @@ function Render() { // apply quick search to the query if($SearchString != '') { - if($Search_x != '') { $FirstRecord = 1; } - - if($this->QueryWhere == '') - $this->QueryWhere = "where "; - else - $this->QueryWhere .= " and "; + if($Search_x != '') $FirstRecord = 1; foreach($this->QueryFieldsQS as $fName => $fCaption) - if(strpos($fName, 'QuerySearchableFields[$fName] = $fCaption; - $this->QueryWhere .= '(' . implode(" LIKE '%" . makeSafe($SearchString) . "%' or ", array_keys($this->QuerySearchableFields)) . " LIKE '%" . makeSafe($SearchString) . "%')"; + $sss = makeSafe($SearchString); // safe search string + + if(count($this->QuerySearchableFields)) + $this->QueryWhere .= ' AND (' . + implode( + " LIKE '%{$sss}%' OR ", + array_keys($this->QuerySearchableFields) + ) . " LIKE '%{$sss}%'" . + ')'; } // set query filters - // $this->QueryWhere might be empty or might contain a clause (starting with WHERE) to retrieve only user/group data - $QueryHasWhere = preg_match('/^WHERE\s+/i', $this->QueryWhere); - $WhereNeedsClosing = 0; + $filterGroups = []; for($i = 1; $i <= (datalist_filters_count * $FiltersPerGroup); $i += $FiltersPerGroup) { // Number of filters allowed // test current filter group $GroupHasFilters = 0; @@ -515,63 +517,78 @@ function Render() { } } - if($GroupHasFilters) { - if(stripos($this->QueryWhere, 'where ') === false) - $this->QueryWhere = 'WHERE ('; - elseif($QueryHasWhere) { - $this->QueryWhere .= ' and ('; - $QueryHasWhere = 0; - } + if(!$GroupHasFilters) continue; - $this->QueryWhere .= " " . $FilterAnd[$i] . " ("; + $filterGroups[] = [ + 'join' => '', + 'filters' => [ /* one or more strings, each describing a filter */ ] + ]; + $currentGroup =& $filterGroups[count($filterGroups) - 1]; - for($j = 0; $j < $FiltersPerGroup; $j++) { - $ij = $i + $j; - if($FilterField[$ij] != '' && $this->QueryFieldsIndexed[($FilterField[$ij])] != '' && $FilterOperator[$ij] != '' && ($FilterValue[$ij] != '' || strpos($FilterOperator[$ij], 'empty'))) { - if($FilterAnd[$ij] == '') { - $FilterAnd[$ij] = 'and'; - } - // test for date/time fields - $tries = 0; $isDateTime = $isDate = false; - $fieldName = str_replace('`', '', $this->QueryFieldsIndexed[($FilterField[$ij])]); - list($tn, $fn) = explode('.', $fieldName); - while(!($res = sql("SHOW COLUMNS FROM `{$tn}` LIKE '{$fn}'", $eo)) && $tries < 2) { - $tn = substr($tn, 0, -1); - $tries++; - } - if($res !== false && $row = @db_fetch_array($res)) { - $isDateTime = in_array($row['Type'], array('date', 'time', 'datetime')); - $isDate = in_array($row['Type'], ['date', 'datetime']); - } - // end of test - if($FilterOperator[$ij] == 'is-empty' && !$isDateTime) { - $this->QueryWhere .= ' ' . $FilterAnd[$ij] . ' (' . $this->QueryFieldsIndexed[($FilterField[$ij])] . "='' or " . $this->QueryFieldsIndexed[($FilterField[$ij])] . ' is NULL) '; - } elseif($FilterOperator[$ij] == 'is-not-empty' && !$isDateTime) { - $this->QueryWhere .= ' ' . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . "!='' "; - } elseif($FilterOperator[$ij] == 'is-empty' && $isDateTime) { - $this->QueryWhere .= " " . $FilterAnd[$ij] . " (" . $this->QueryFieldsIndexed[($FilterField[$ij])] . "=0 or " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " is NULL) "; - } elseif($FilterOperator[$ij] == 'is-not-empty' && $isDateTime) { - $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . "!=0 "; - } elseif($FilterOperator[$ij] == 'like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { - $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " like '%" . makeSafe($FilterValue[$ij]) . "%' "; - } elseif($FilterOperator[$ij] == 'not-like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) { - $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " not like '%" . makeSafe($FilterValue[$ij]) . "%' "; - } elseif($isDate) { - $dateValue = mysql_datetime($FilterValue[$ij]); - $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " " . $GLOBALS['filter_operators'][$FilterOperator[$ij]] . " '$dateValue' "; - } else { - $this->QueryWhere .= " " . $FilterAnd[$ij] . " " . $this->QueryFieldsIndexed[($FilterField[$ij])] . " " . $GLOBALS['filter_operators'][$FilterOperator[$ij]] . " '" . makeSafe($FilterValue[$ij]) . "' "; - } - } - } + for($j = 0; $j < $FiltersPerGroup; $j++) { + $ij = $i + $j; + + // not a valid filter? + if( + !$FilterField[$ij] || + !$this->QueryFieldsIndexed[($FilterField[$ij])] || + !$FilterOperator[$ij] || + (!$FilterValue[$ij] && strpos($FilterOperator[$ij], 'empty') === false) + ) continue; + + if($FilterAnd[$ij] == '') $FilterAnd[$ij] = 'and'; + $currentGroup['filters'][] = ''; + $currentFilter =& $currentGroup['filters'][count($currentGroup['filters']) - 1]; + + // always use the 1st FilterAnd of the group as the group's join + if(empty($currentGroup['join'])) $currentGroup['join'] = thisOr($FilterAnd[$i], 'and'); + + // if this is NOT the first filter in the group, add its FilterAnd, else ignore + if(count($currentGroup['filters']) > 1) + $currentFilter = $FilterAnd[$ij] . ' '; + + list($isDate, $isDateTime) = $this->fieldIsDateTime($FilterField[$ij]); + + if($FilterOperator[$ij] == 'is-empty' && !$isDateTime) + $currentFilter .= '(' . $this->QueryFieldsIndexed[($FilterField[$ij])] . "='' OR " . $this->QueryFieldsIndexed[($FilterField[$ij])] . ' IS NULL)'; + + elseif($FilterOperator[$ij] == 'is-not-empty' && !$isDateTime) + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . "!=''"; + + elseif($FilterOperator[$ij] == 'is-empty' && $isDateTime) + $currentFilter .= '(' . $this->QueryFieldsIndexed[($FilterField[$ij])] . "=0 OR " . $this->QueryFieldsIndexed[($FilterField[$ij])] . ' IS NULL)'; + + elseif($FilterOperator[$ij] == 'is-not-empty' && $isDateTime) + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . "!=0"; + + elseif($FilterOperator[$ij] == 'like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . " LIKE '%" . makeSafe($FilterValue[$ij]) . "%'"; + + elseif($FilterOperator[$ij] == 'not-like' && !strstr($FilterValue[$ij], "%") && !strstr($FilterValue[$ij], "_")) + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . " NOT LIKE '%" . makeSafe($FilterValue[$ij]) . "%'"; + + elseif($isDate) { + $dateValue = mysql_datetime($FilterValue[$ij]); + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . ' ' . $GLOBALS['filter_operators'][$FilterOperator[$ij]] . " '$dateValue'"; + + } else + $currentFilter .= $this->QueryFieldsIndexed[($FilterField[$ij])] . ' ' . $GLOBALS['filter_operators'][$FilterOperator[$ij]] . " '" . makeSafe($FilterValue[$ij]) . "'"; - $this->QueryWhere .= ") "; - $WhereNeedsClosing = 1; } } - if($WhereNeedsClosing && !$QueryHasCustomWhere) - $this->QueryWhere .= ")"; + // construct filters from $filterGroups + $filtersWhere = ''; + foreach($filterGroups as $fg) { + if(empty($fg['filters'])) continue; + + // ignore 1st join (i.e. use it only if filtersWhere already populated) + if($filtersWhere) $filtersWhere .= " {$fg['join']} "; + + $filtersWhere .= '(' . implode(' ', $fg['filters']) . ')'; + } + + if($filtersWhere) $this->QueryWhere .= " AND ($filtersWhere)"; // set query sort if(!stristr($this->QueryOrder, "order by ") && $SortField != '' && $this->AllowSorting) { @@ -580,22 +597,12 @@ function Render() { $actualSortField = str_replace(" $fieldNum ", " $fieldSort ", " $actualSortField "); $actualSortField = str_replace(",$fieldNum ", ",$fieldSort ", " $actualSortField "); } - $this->QueryOrder = "order by $actualSortField $SortDirection"; + $this->QueryOrder = "ORDER BY $actualSortField $SortDirection"; } - // clean up query - $this->QueryWhere = str_replace('( and ', '( ', $this->QueryWhere); - $this->QueryWhere = str_replace('( or ', '( ', $this->QueryWhere); - $this->QueryWhere = str_replace('( and ', '( ', $this->QueryWhere); - $this->QueryWhere = str_replace('( or ', '( ', $this->QueryWhere); - $this->QueryWhere = str_replace('', '', $this->QueryWhere); - $this->QueryWhere = str_replace('', '', $this->QueryWhere); - $this->QueryWhere = str_replace('', '', $this->QueryWhere); - $this->QueryWhere = str_replace('', '', $this->QueryWhere); - // if no 'order by' clause found, apply default sorting if specified if($this->DefaultSortField != '' && $this->QueryOrder == '') { - $this->QueryOrder = "order by {$this->DefaultSortField} {$this->DefaultSortDirection}"; + $this->QueryOrder = "ORDER BY {$this->DefaultSortField} {$this->DefaultSortDirection}"; } // Output CSV on request @@ -1187,7 +1194,12 @@ function Render() { ); if($dvCode) { - $this->HTML .= "\n\t
TableName}\" class=\"col-xs-12 table-{$this->TableName} detail_view {$this->DVClasses}\">{$tv_dv_separator}
{$dvCode}
"; + $this->HTML .= sprintf( + '
%s
%s
', + $this->TableName, $this->TableName, $this->DVClasses, $tv_dv_separator, + $dvCode == $this->translation['tableAccessDenied'] ? 'alert alert-danger' : 'panel panel-default', + $dvCode + ); $this->ContentType = 'detailview'; $dvShown = true; } @@ -1210,7 +1222,7 @@ function Render() { // handle the case were user has no view access and has just inserted a record // by redirecting to tablename_view.php (which should redirect them to insert form) - if(!$this->Permissions['view'] && !$dvCode && $SelectedID && isset($_REQUEST['record-added-ok'])) { + if(!$this->Permissions['view'] && (!$dvCode || $dvCode == $this->translation['tableAccessDenied']) && $SelectedID && isset($_REQUEST['record-added-ok'])) { ob_start(); ?>'; + echo error_message($Translation['tableAccessDenied']); exit; } @@ -127,23 +126,6 @@ $x->AllowConsoleLog = false; $x->AllowDVNavigation = true; - // mm: build the query based on current member's permissions - $DisplayRecords = $_REQUEST['DisplayRecords']; - if(!in_array($DisplayRecords, ['user', 'group'])) { $DisplayRecords = 'all'; } - if($perm['view'] == 1 || ($perm['view'] > 1 && $DisplayRecords == 'user' && !$_REQUEST['NoFilter_x'])) { // view owner only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `invoice_items`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='invoice_items' AND LCASE(`membership_userrecords`.`memberID`)='" . getLoggedMemberID() . "'"; - } elseif($perm['view'] == 2 || ($perm['view'] > 2 && $DisplayRecords == 'group' && !$_REQUEST['NoFilter_x'])) { // view group only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `invoice_items`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='invoice_items' AND `membership_userrecords`.`groupID`='" . getLoggedGroupID() . "'"; - } elseif($perm['view'] == 3) { // view all - // no further action - } elseif($perm['view'] == 0) { // view none - $x->QueryFields = ['Not enough permissions' => 'NEP']; - $x->QueryFrom = '`invoice_items`'; - $x->QueryWhere = ''; - $x->DefaultSortField = ''; - } // hook: invoice_items_init $render = true; if(function_exists('invoice_items_init')) { diff --git a/app/invoices_autofill.php b/app/invoices_autofill.php index c9ef280..49f7800 100644 --- a/app/invoices_autofill.php +++ b/app/invoices_autofill.php @@ -1,5 +1,5 @@ Request::val('code', ''), 'status' => Request::val('status', 'Unpaid'), 'date_due' => Request::dateComponents('date_due', '1'), - 'client' => Request::val('client', ''), + 'client' => Request::lookup('client', ''), 'client_contact' => Request::lookup('client'), 'client_address' => Request::lookup('client'), 'client_phone' => Request::lookup('client'), @@ -164,7 +164,7 @@ function invoices_update(&$selected_id, &$error_message = '') { 'code' => Request::val('code', ''), 'status' => Request::val('status', ''), 'date_due' => Request::dateComponents('date_due', ''), - 'client' => Request::val('client', ''), + 'client' => Request::lookup('client', ''), 'client_contact' => Request::lookup('client'), 'client_address' => Request::lookup('client'), 'client_phone' => Request::lookup('client'), @@ -246,7 +246,7 @@ function invoices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $A // mm: get table permissions $arrPerm = getTablePermissions('invoices'); - if(!$arrPerm['insert'] && $selected_id=='') { return ''; } + if(!$arrPerm['insert'] && $selected_id=='') return $Translation['tableAccessDenied']; $AllowInsert = ($arrPerm['insert'] ? true : false); // print preview? $dvprint = false; @@ -304,14 +304,14 @@ function invoices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $A if($selected_id) { // mm: check member permissions - if(!$arrPerm['view']) return ''; + if(!$arrPerm['view']) return $Translation['tableAccessDenied']; // mm: who is the owner? $ownerGroupID = sqlValue("SELECT `groupID` FROM `membership_userrecords` WHERE `tableName`='invoices' AND `pkValue`='" . makeSafe($selected_id) . "'"); $ownerMemberID = sqlValue("SELECT LCASE(`memberID`) FROM `membership_userrecords` WHERE `tableName`='invoices' AND `pkValue`='" . makeSafe($selected_id) . "'"); - if($arrPerm['view'] == 1 && getLoggedMemberID() != $ownerMemberID) return ''; - if($arrPerm['view'] == 2 && getLoggedGroupID() != $ownerGroupID) return ''; + if($arrPerm['view'] == 1 && getLoggedMemberID() != $ownerMemberID) return $Translation['tableAccessDenied']; + if($arrPerm['view'] == 2 && getLoggedGroupID() != $ownerGroupID) return $Translation['tableAccessDenied']; // can edit? $AllowUpdate = 0; @@ -344,7 +344,7 @@ function invoices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $A '; + echo error_message($Translation['tableAccessDenied']); exit; } @@ -182,23 +181,6 @@ $x->AllowConsoleLog = false; $x->AllowDVNavigation = true; - // mm: build the query based on current member's permissions - $DisplayRecords = $_REQUEST['DisplayRecords']; - if(!in_array($DisplayRecords, ['user', 'group'])) { $DisplayRecords = 'all'; } - if($perm['view'] == 1 || ($perm['view'] > 1 && $DisplayRecords == 'user' && !$_REQUEST['NoFilter_x'])) { // view owner only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `invoices`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='invoices' AND LCASE(`membership_userrecords`.`memberID`)='" . getLoggedMemberID() . "'"; - } elseif($perm['view'] == 2 || ($perm['view'] > 2 && $DisplayRecords == 'group' && !$_REQUEST['NoFilter_x'])) { // view group only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `invoices`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='invoices' AND `membership_userrecords`.`groupID`='" . getLoggedGroupID() . "'"; - } elseif($perm['view'] == 3) { // view all - // no further action - } elseif($perm['view'] == 0) { // view none - $x->QueryFields = ['Not enough permissions' => 'NEP']; - $x->QueryFrom = '`invoices`'; - $x->QueryWhere = ''; - $x->DefaultSortField = ''; - } // hook: invoices_init $render = true; if(function_exists('invoices_init')) { diff --git a/app/item_prices_autofill.php b/app/item_prices_autofill.php index 02ad2bf..43b8fe0 100644 --- a/app/item_prices_autofill.php +++ b/app/item_prices_autofill.php @@ -1,5 +1,5 @@ Request::val('item', ''), + 'item' => Request::lookup('item', ''), 'price' => Request::val('price', '0.00'), 'date' => Request::dateComponents('date', '1'), ]; @@ -109,7 +109,7 @@ function item_prices_update(&$selected_id, &$error_message = '') { if(!check_record_permission('item_prices', $selected_id, 'edit')) return false; $data = [ - 'item' => Request::val('item', ''), + 'item' => Request::lookup('item', ''), 'price' => Request::val('price', ''), 'date' => Request::dateComponents('date', ''), ]; @@ -177,7 +177,7 @@ function item_prices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, // mm: get table permissions $arrPerm = getTablePermissions('item_prices'); - if(!$arrPerm['insert'] && $selected_id=='') { return ''; } + if(!$arrPerm['insert'] && $selected_id=='') return $Translation['tableAccessDenied']; $AllowInsert = ($arrPerm['insert'] ? true : false); // print preview? $dvprint = false; @@ -204,14 +204,14 @@ function item_prices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, if($selected_id) { // mm: check member permissions - if(!$arrPerm['view']) return ''; + if(!$arrPerm['view']) return $Translation['tableAccessDenied']; // mm: who is the owner? $ownerGroupID = sqlValue("SELECT `groupID` FROM `membership_userrecords` WHERE `tableName`='item_prices' AND `pkValue`='" . makeSafe($selected_id) . "'"); $ownerMemberID = sqlValue("SELECT LCASE(`memberID`) FROM `membership_userrecords` WHERE `tableName`='item_prices' AND `pkValue`='" . makeSafe($selected_id) . "'"); - if($arrPerm['view'] == 1 && getLoggedMemberID() != $ownerMemberID) return ''; - if($arrPerm['view'] == 2 && getLoggedGroupID() != $ownerGroupID) return ''; + if($arrPerm['view'] == 1 && getLoggedMemberID() != $ownerMemberID) return $Translation['tableAccessDenied']; + if($arrPerm['view'] == 2 && getLoggedGroupID() != $ownerGroupID) return $Translation['tableAccessDenied']; // can edit? $AllowUpdate = 0; @@ -238,7 +238,7 @@ function item_prices_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, '; + echo error_message($Translation['tableAccessDenied']); exit; } @@ -107,23 +106,6 @@ $x->AllowConsoleLog = false; $x->AllowDVNavigation = true; - // mm: build the query based on current member's permissions - $DisplayRecords = $_REQUEST['DisplayRecords']; - if(!in_array($DisplayRecords, ['user', 'group'])) { $DisplayRecords = 'all'; } - if($perm['view'] == 1 || ($perm['view'] > 1 && $DisplayRecords == 'user' && !$_REQUEST['NoFilter_x'])) { // view owner only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `item_prices`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='item_prices' AND LCASE(`membership_userrecords`.`memberID`)='" . getLoggedMemberID() . "'"; - } elseif($perm['view'] == 2 || ($perm['view'] > 2 && $DisplayRecords == 'group' && !$_REQUEST['NoFilter_x'])) { // view group only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `item_prices`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='item_prices' AND `membership_userrecords`.`groupID`='" . getLoggedGroupID() . "'"; - } elseif($perm['view'] == 3) { // view all - // no further action - } elseif($perm['view'] == 0) { // view none - $x->QueryFields = ['Not enough permissions' => 'NEP']; - $x->QueryFrom = '`item_prices`'; - $x->QueryWhere = ''; - $x->DefaultSortField = ''; - } // hook: item_prices_init $render = true; if(function_exists('item_prices_init')) { diff --git a/app/items_autofill.php b/app/items_autofill.php index ddd7b47..07fccf1 100644 --- a/app/items_autofill.php +++ b/app/items_autofill.php @@ -1,5 +1,5 @@ ' + (jQuery('#item_description').val() || '') + '');\n"; $jsReadOnly .= "\tjQuery('.select2-container').hide();\n"; $noUploads = true; } elseif($AllowInsert) { - $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', true);"; // temporarily disable form change handler - $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', false);"; // re-enable form change handler + $jsEditable = "\tjQuery('form').eq(0).data('already_changed', true);"; // temporarily disable form change handler + $jsEditable .= "\tjQuery('form').eq(0).data('already_changed', false);"; // re-enable form change handler } // process combos diff --git a/app/items_view.php b/app/items_view.php index 4c89b7c..aa46c4c 100644 --- a/app/items_view.php +++ b/app/items_view.php @@ -1,5 +1,5 @@ setTimeout(function() { window.location = "index.php?signOut=1"; }, 2000);'; + echo error_message($Translation['tableAccessDenied']); exit; } @@ -102,23 +101,6 @@ $x->AllowConsoleLog = false; $x->AllowDVNavigation = true; - // mm: build the query based on current member's permissions - $DisplayRecords = $_REQUEST['DisplayRecords']; - if(!in_array($DisplayRecords, ['user', 'group'])) { $DisplayRecords = 'all'; } - if($perm['view'] == 1 || ($perm['view'] > 1 && $DisplayRecords == 'user' && !$_REQUEST['NoFilter_x'])) { // view owner only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `items`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='items' AND LCASE(`membership_userrecords`.`memberID`)='" . getLoggedMemberID() . "'"; - } elseif($perm['view'] == 2 || ($perm['view'] > 2 && $DisplayRecords == 'group' && !$_REQUEST['NoFilter_x'])) { // view group only - $x->QueryFrom .= ', `membership_userrecords`'; - $x->QueryWhere = "WHERE `items`.`id`=`membership_userrecords`.`pkValue` AND `membership_userrecords`.`tableName`='items' AND `membership_userrecords`.`groupID`='" . getLoggedGroupID() . "'"; - } elseif($perm['view'] == 3) { // view all - // no further action - } elseif($perm['view'] == 0) { // view none - $x->QueryFields = ['Not enough permissions' => 'NEP']; - $x->QueryFrom = '`items`'; - $x->QueryWhere = ''; - $x->DefaultSortField = ''; - } // hook: items_init $render = true; if(function_exists('items_init')) { diff --git a/app/language.php b/app/language.php index e974782..0e781a8 100644 --- a/app/language.php +++ b/app/language.php @@ -1,43 +1,43 @@ -). - -Do NOT translate the strings between square brackets ([]) -Also, leave the text between < and > untranslated. -And also, leave the special string %s as-is, untranslated. - -===================================================== -PLEASE NOTE: -============ -When a new version of AppGini is released, new strings -might be added to the "defaultLang.php" file. To translate -them, simply copy them to this file ("language.php") and -translate them here. Do NOT translate them directly in -the "defaultLang.php" file. -===================================================== - -To avoid any error messages, please make sure to: - -1. Add a comma at the end of each line if it's not already there. -2. Leave the last line in this file like this: -]; - -*/ - -$Translation = [ - 'language' => 'english', +). + +Do NOT translate the strings between square brackets ([]) +Also, leave the text between < and > untranslated. +And also, leave the special string %s as-is, untranslated. + +===================================================== +PLEASE NOTE: +============ +When a new version of AppGini is released, new strings +might be added to the "defaultLang.php" file. To translate +them, simply copy them to this file ("language.php") and +translate them here. Do NOT translate them directly in +the "defaultLang.php" file. +===================================================== + +To avoid any error messages, please make sure to: + +1. Add a comma at the end of each line if it's not already there. +2. Leave the last line in this file like this: +]; + +*/ + +$Translation = [ + 'language' => 'english', 'membership management' => 'Membership Management', 'password mismatch' => "Password doesn't match.", 'error' => 'Error', @@ -797,4 +797,16 @@ 'Mail' => 'Mail', 'Preconfigured users and groups' => 'Preconfigured users and groups', 'Application' => 'Application', + + // Added in 5.97 + 'Query logs' => 'Query logs', + 'Query log table does not exist' => 'There was an error while attempting to create appgini_query_log table. Please make sure the database user has CREATE TABLE privilege.', + 'slow queries' => 'Slow queries', + 'error queries' => 'Error queries', + 'date/time' => 'Date/time', + 'duration (sec)' => 'Duration (sec)', + 'page address' => 'Page address', + 'query' => 'Query', + 'page x of y' => 'Page of ', + 'total # queries' => 'Total # queries.', ]; \ No newline at end of file diff --git a/app/lib.php b/app/lib.php index 221a51a..4a01dd8 100644 --- a/app/lib.php +++ b/app/lib.php @@ -1,5 +1,5 @@ /)) { + if(resp.indexOf('username-available') > -1) { reset_username_status('success'); } else { reset_username_status('error'); diff --git a/app/parent-children.php b/app/parent-children.php index 7f95743..06787b8 100644 --- a/app/parent-children.php +++ b/app/parent-children.php @@ -1,5 +1,5 @@ +
diff --git a/app/templates/invoice_items_templateDVP.html b/app/templates/invoice_items_templateDVP.html index 70deab4..0d1996c 100644 --- a/app/templates/invoice_items_templateDVP.html +++ b/app/templates/invoice_items_templateDVP.html @@ -72,3 +72,14 @@
+ diff --git a/app/templates/invoices_templateDVP.html b/app/templates/invoices_templateDVP.html index 82dc409..7f537b1 100644 --- a/app/templates/invoices_templateDVP.html +++ b/app/templates/invoices_templateDVP.html @@ -149,6 +149,17 @@ +
diff --git a/app/templates/item_prices_templateDVP.html b/app/templates/item_prices_templateDVP.html index f5bd35f..78be282 100644 --- a/app/templates/item_prices_templateDVP.html +++ b/app/templates/item_prices_templateDVP.html @@ -44,3 +44,14 @@
+ diff --git a/app/templates/items_templateDVP.html b/app/templates/items_templateDVP.html index 90a2892..4cf4f31 100644 --- a/app/templates/items_templateDVP.html +++ b/app/templates/items_templateDVP.html @@ -37,6 +37,17 @@ +
diff --git a/online-invoicing-system.axp b/online-invoicing-system.axp index 548341d..ac88339 100644 --- a/online-invoicing-system.axp +++ b/online-invoicing-system.axp @@ -1,4 +1,4 @@ -online_inovicing_systemlocalhostUTF-8False12FalseEurope/LondonFalseFalseTrueFalsebootstrap.cssFalseTrue0245.952021-03-28 18:44:41C:\xampp\htdocs\open-source-apps\online-invoicing-system\app0False1004False{"events":{"unpaid-invoice":{"type":"unpaid-invoice","color":"danger","textColor":"danger","table":"invoices","customWhere":"`invoices`.`status` = 'Unpaid'","title":"{5}<br>Invoice# {2}<br>{3}","allDay":true,"startDateField":"date_due","startTimeField":"","endDateField":"","endTimeField":""}},"calendars":{"unpaid-invoices":{"id":"unpaid-invoices","title":"Unpaid invoices","initial-view":"dayGridMonth","initial-date":"[last-month]","events":["unpaid-invoice"],"locale":"","groups":["Admins"],"links-home":"1","links-navmenu":"1"}}}3:4:2:12:14:9001:[{"report_hash":"pprh7b8b2dazav20cln9","title":"Client sales over time","table":"invoices","table_index":0,"label":"client","caption1":"Client","caption2":"Sum of Invoices","group_function":"sum","group_function_field":"total","group_array":[],"look_up_table":"clients","look_up_value":"name","label_field_index":"5","date_field":"date_due","date_field_index":"4","report_header_url":"","report_footer_url":"","data_table_section":1,"barchart_section":0,"piechart_section":0,"override_permissions":0,"custom_where":"","date_separator":"\/"}][{"label":"Mark as paid","icon":"ok","field":"status","value":"fixedValue","fixedValue":"Paid","confirmation":1,"groups":[],"hash":"1nvkk0q0ckqc7b8migay"},{"label":"Mark as cancelled","icon":"remove","field":"status","value":"fixedValue","fixedValue":"Cancelled","confirmation":1,"groups":[],"hash":"xe0xlisfn56ps9sp3p76"}]invoicesFalseFalseTrueFalseFalseTrueFalseTrueTrueTrueTrueTrueTrueFalseTrueTrue15010invoice_items;TrueFalseFalse2descattributes_display.png0True0TruehorizontalFalseFalse25%id400TrueFalseFalseTrueFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseFalse0False00FalseFalseright0000Falsecode15200FalseFalseFalseFalseTrueFalseFalseFalse60False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01FalseFalseFalseFalse10False00FalseFalseleft0000Falsestatus15200FalseFalseTrueFalseFalseFalseFalseFalse70False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse21FalseFalseFalseFalse12False00FalseFalseleft0000Falsedate_due900FalseFalseFalseFalseFalseFalseFalseFalse100False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01FalseFalseFalseFalse1False00FalseFalseleft0000Falseclient400FalseFalseFalseFalseFalseTrueFalseFalse250False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsenameidclientsTrueTrueFalse01FalseFalseFalseFalse2False00FalseFalseleft0000Falseclient_contact400FalseFalseFalseFalseFalseTrueFalseFalse200False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsecontactidclientsTrueFalseTrue01FalseFalseFalseFalse3False00FalseFalseleft0000Falseclient_address400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseaddressidclientsTrueFalseTrue01TrueFalseFalseFalse4False00FalseFalseleft0000Falseclient_phone400FalseFalseFalseFalseFalseTrueFalseFalse100False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsephoneidclientsTrueFalseTrue01FalseFalseFalseFalse5False00FalseFalseleft0000Falseclient_email400FalseFalseFalseFalseFalseTrueFalseFalse50False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseemailidclientsTrueFalseTrue01TrueFalseFalseFalse6False00FalseFalseleft0000Falseclient_website400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsewebsiteidclientsTrueFalseTrue01TrueFalseFalseFalse8False00FalseFalseleft0000Falseclient_comments400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsecommentsidclientsTrueFalseTrue01TrueFalseFalseFalse7False00FalseFalseleft0000Falsesubtotal892FalseFalseFalseFalseFalseFalseFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseTrue14False00FalseFalseright0000Trueonline_inovicing_systemlocalhostUTF-8False12FalseEurope/LondonFalseFalseTrueFalsebootstrap.cssFalseTrue0245.972021-06-14 16:19:49C:\xampp\htdocs\open-source-apps\online-invoicing-system\app0False1004False{"events":{"unpaid-invoice":{"type":"unpaid-invoice","color":"danger","textColor":"danger","table":"invoices","customWhere":"`invoices`.`status` = 'Unpaid'","title":"{5}<br>Invoice# {2}<br>{3}","allDay":true,"startDateField":"date_due","startTimeField":"","endDateField":"","endTimeField":""}},"calendars":{"unpaid-invoices":{"id":"unpaid-invoices","title":"Unpaid invoices","initial-view":"dayGridMonth","initial-date":"[last-month]","events":["unpaid-invoice"],"locale":"","groups":["Admins"],"links-home":"1","links-navmenu":"1"}}}
3:4:2:12:14:9001:[{"report_hash":"pprh7b8b2dazav20cln9","title":"Client sales over time","table":"invoices","table_index":0,"label":"client","caption1":"Client","caption2":"Sum of Invoices","group_function":"sum","group_function_field":"total","group_array":[],"look_up_table":"clients","look_up_value":"name","label_field_index":"5","date_field":"date_due","date_field_index":"4","report_header_url":"","report_footer_url":"","data_table_section":1,"barchart_section":0,"piechart_section":0,"override_permissions":0,"custom_where":"","date_separator":"\/"}][{"label":"Mark as paid","icon":"ok","field":"status","value":"fixedValue","fixedValue":"Paid","confirmation":1,"groups":[],"hash":"1nvkk0q0ckqc7b8migay"},{"label":"Mark as cancelled","icon":"remove","field":"status","value":"fixedValue","fixedValue":"Cancelled","confirmation":1,"groups":[],"hash":"xe0xlisfn56ps9sp3p76"}]invoicesFalseFalseTrueFalseFalseTrueFalseTrueTrueTrueTrueTrueTrueFalseTrueTrue15010invoice_items;TrueFalseFalse2descattributes_display.png0True0TruehorizontalFalseFalse25%id400TrueFalseFalseTrueFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseFalse0False00FalseFalseright0000Falsecode15200FalseFalseFalseFalseTrueFalseFalseFalse60False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01FalseFalseFalseFalse10False00FalseFalseleft0000Falsestatus15200FalseFalseTrueFalseFalseFalseFalseFalse70False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse21FalseFalseFalseFalse12False00FalseFalseleft0000Falsedate_due900FalseFalseFalseFalseFalseFalseFalseFalse100False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01FalseFalseFalseFalse1False00FalseFalseleft0000Falseclient400FalseFalseFalseFalseFalseTrueFalseFalse250False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsenameidclientsTrueTrueFalse01FalseFalseFalseFalse2False00FalseFalseleft0000Falseclient_contact400FalseFalseFalseFalseFalseTrueFalseFalse200False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsecontactidclientsTrueFalseTrue01FalseFalseFalseFalse3False00FalseFalseleft0000Falseclient_address400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseaddressidclientsTrueFalseTrue01TrueFalseFalseFalse4False00FalseFalseleft0000Falseclient_phone400FalseFalseFalseFalseFalseTrueFalseFalse100False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsephoneidclientsTrueFalseTrue01FalseFalseFalseFalse5False00FalseFalseleft0000Falseclient_email400FalseFalseFalseFalseFalseTrueFalseFalse50False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseemailidclientsTrueFalseTrue01TrueFalseFalseFalse6False00FalseFalseleft0000Falseclient_website400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsewebsiteidclientsTrueFalseTrue01TrueFalseFalseFalse8False00FalseFalseleft0000Falseclient_comments400FalseFalseFalseFalseFalseTrueFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalsecommentsidclientsTrueFalseTrue01TrueFalseFalseFalse7False00FalseFalseleft0000Falsesubtotal892FalseFalseFalseFalseFalseFalseFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseTrue14False00FalseFalseright0000Truediscount842FalseFalseFalseFalseFalseFalseFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseFalse13False00FalseFalseright0000Falsetax892FalseFalseFalseFalseFalseFalseFalseFalse150False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01TrueFalseFalseFalse15False00FalseFalseright0000Falsetotal892FalseFalseFalseFalseFalseFalseTrueFalse70False0FalseFalseFalseFalse00FalseFalseFalse00FalseFalse0FalseFalseFalseFalseFalse01FalseFalseFalseTrue9False00FalseFalseright0000True