diff --git a/app/admin/getUsers.php b/app/admin/getUsers.php index 2ea6847..9242fca 100644 --- a/app/admin/getUsers.php +++ b/app/admin/getUsers.php @@ -1,5 +1,5 @@ $respBody, ]; } + + /** + * @brief Retrieve owner username of the record with the given primary key value + * + * @param $tn string table name + * @param $pkValue string primary key value + * @return string|null username of the record owner, or null if not found + */ + function getRecordOwner($tn, $pkValue) { + $tn = makeSafe($tn); + $pkValue = makeSafe($pkValue); + $owner = sqlValue("SELECT `memberID` FROM `membership_userrecords` WHERE `tableName`='{$tn}' AND `pkValue`='$pkValue'"); + + if(!strlen($owner)) return null; + return $owner; + } + + /** + * @brief Retrieve lookup field name that determines record owner of the given table + * + * @param $tn string table name + * @return string|null lookup field name, or null if default (record owner is user that creates the record) + */ + function tableRecordOwner($tn) { + $owners = [ + ]; + + return $owners[$tn] ?? null; + } + diff --git a/app/admin/incHeader.php b/app/admin/incHeader.php index 012a29c..2751f68 100644 --- a/app/admin/incHeader.php +++ b/app/admin/incHeader.php @@ -205,10 +205,10 @@ function hideDialogs() {
  • -
  • +
  • -
  • +
  • diff --git a/app/admin/pageFixRecordOwners.php b/app/admin/pageFixRecordOwners.php new file mode 100644 index 0000000..364e34b --- /dev/null +++ b/app/admin/pageFixRecordOwners.php @@ -0,0 +1,357 @@ + $tc) { + if(!tableRecordOwner($tn)) unset($tables[$tn]); + } + + // clear session variables from previous runs + foreach($tables as $tn => $tc) { + unset($_SESSION['fixRecordOwners_' . $tn]); + } + + $GLOBALS['page_title'] = $Translation['fix record owners']; + include(__DIR__ . '/incHeader.php'); + ?> + + + +

    + + +
    + + + +
    + +
    + +
    +
    + +
    +
    0 /
    +
    +
    + +
    + + + + + + + + + + + + [lookup1, lookup2, ...], pt2 => [...], ...] + foreach($pts as $pt => $lookups) { + if(in_array($lookupField, $lookups)) return $pt; + } + return false; + } + + function cleanupSessionData($tn) { + unset($_SESSION['fixRecordOwners_' . $tn]); + } + + function storeRecordsInfoToSession($tn, $lookupField) { + $processId = 'fixRecordOwners_' . $tn; + $_SESSION[$processId] = $_SESSION[$processId] ?? []; + $store =& $_SESSION[$processId]; + + if(!empty($store['lookupOwnersDone']) && !empty($store['recordsDone'])) return; // already done + + // parent table name + $ptn = parentTable($tn, $lookupField); + + $eo = ['silentErrors' => true]; + + // init + $store += [ + 'lookupOwners' => [], + 'records' => [], + 'lookupOwnersDone' => false, + 'recordsDone' => false, + 'totalRecords' => INF, // assume infinite number of records initially + ]; + + // get all distinct lookup values as keys and their corresponding record owners as values, [lookupValue => recordOwner] + while(withinTimeLimit() && !$store['lookupOwnersDone']) { + $start = count($store['lookupOwners']); + $res = sql(" + SELECT `pkValue` AS `lookupValue`, `memberID` AS `recordOwner` + FROM `membership_userrecords` + WHERE `tableName` = '{$ptn}' + ORDER BY `pkValue` + LIMIT {$start}, " . BATCH_READ_SIZE, $eo + ); + + // no more records to read? + if(!db_num_rows($res)) { + $store['lookupOwnersDone'] = true; + break; + } + + while($row = db_fetch_assoc($res)) { + $store['lookupOwners'][$row['lookupValue']] = $row['recordOwner']; + } + } + + // get all records of the table [id => lookupValue] + $pk = getPKFieldName($tn); + while(withinTimeLimit() && !$store['recordsDone']) { + $start = count($store['records']); + $res = sql(" + SELECT `{$pk}` AS `id`, `{$lookupField}` AS `lookupValue` + FROM `{$tn}` + WHERE `{$lookupField}` IS NOT NULL + ORDER BY `{$lookupField}` + LIMIT {$start}, " . BATCH_READ_SIZE, $eo + ); + + // no more records to read? + if(!db_num_rows($res)) { + $store['recordsDone'] = true; + $store['totalRecords'] = count($store['records']); + break; + } + + while($row = db_fetch_assoc($res)) { + $store['records'][$row['id']] = $row['lookupValue']; + } + } + } + + function updateRecordOwners($tn) { + $store =& $_SESSION['fixRecordOwners_' . $tn]; + $affectedRecords = 0; + + while(withinTimeLimit() && count($store['records'])) { + // get a single record [id => lookupValue] off the array + $record = array_slice($store['records'], 0, 1, true); // [id => lookupValue] + $id = array_keys($record)[0]; + $owner = $store['lookupOwners'][$record[$id]]; + + $insertSet = prepare_sql_set([ + 'tableName' => $tn, + 'pkValue' => $id, + 'memberID' => $owner, + 'groupID' => get_group_id($owner), + 'dateAdded' => time(), + ]); + $updateSet = prepare_sql_set([ + 'memberID' => $owner, + 'groupID' => get_group_id($owner), + ]); + + // insert or update record owner + $eo = ['silentErrors' => true]; + sql("INSERT INTO `membership_userrecords` SET $insertSet ON DUPLICATE KEY UPDATE $updateSet", $eo); + + // get affected rows + $affectedRecords += db_affected_rows(); + + // remove record from array + unset($store['records'][$id]); + } + + return $affectedRecords; + } + + function batchProcess() { + global $Translation; + + $tn = Request::val('tn'); + $run = intval(Request::val('run', 0)); + if(!csrf_token(true)) json_response($Translation['invalid security token'], true); + + $lookupField = tableRecordOwner($tn); + if(!$lookupField) json_response($Translation['record owner not configured for this table'], true); + + storeRecordsInfoToSession($tn, $lookupField); + $affectedRecords = updateRecordOwners($tn); + + $store =& $_SESSION['fixRecordOwners_' . $tn]; + + if($store['totalRecords'] === INF) { + $progress = 0; + } elseif(count($store['records']) === 0) { + $progress = 100; + cleanupSessionData($tn); + } else { + $progress = min(100, round((1 - count($store['records']) / $store['totalRecords']) * 100)); + } + + json_response([ + 'tn' => $tn, + 'progress' => $progress, + 'totalRecords' => $store['totalRecords'], + 'affectedRecords' => $affectedRecords, + 'run' => $run, + ]); + } diff --git a/app/admin/pageInstallPlugin.php b/app/admin/pageInstallPlugin.php index 5cb50b2..83ec16a 100644 --- a/app/admin/pageInstallPlugin.php +++ b/app/admin/pageInstallPlugin.php @@ -455,8 +455,8 @@ function handlePluginInstall() { $pluginSlug = Request::val('installPlugin'); $pluginUrl = 'https://bigprof.com' . Request::val('pluginUrl'); - // validate the plugin name (alpha-numeric and hyphens only, 5-20 characters) - if(!preg_match('/^[a-z0-9\-_]{5,20}$/i', $pluginSlug)) + // validate the plugin name (alpha-numeric and hyphens only, 3-30 characters) + if(!preg_match('/^[a-z0-9\-_]{3,30}$/i', $pluginSlug)) die(json_response($Translation['plugin install failed'] . ' LINE# ' . __LINE__, true)); // validate the plugin url diff --git a/app/admin/pageServerStatus.php b/app/admin/pageServerStatus.php index 27befa9..51e22dd 100644 --- a/app/admin/pageServerStatus.php +++ b/app/admin/pageServerStatus.php @@ -1,6 +1,6 @@ formatUri(preg_replace('/admin$/', '', dirname($_SERVER['SCRIPT_NAME']))), 'host' => config('host'), - 'adminConfig' => [ + 'adminConfig' => array_merge([ 'adminUsername' => strtolower($post['adminUsername']), 'adminPassword' => $adminPassword, 'notifyAdminNewMembers' => intval($post['notifyAdminNewMembers']), @@ -115,7 +115,9 @@ 'smtp_pass' => $post['smtp_pass'], 'googleAPIKey' => $post['googleAPIKey'], 'baseUploadPath' => ($post['baseUploadPath'] ? $post['baseUploadPath'] : 'images'), - ] + ], + LDAP::postToSettings($post) + ), ]; // save changes @@ -295,6 +297,7 @@ function settings_checkbox($name, $label, $value, $set_value, $hint = '') {
  • + @@ -409,6 +412,8 @@ function settings_checkbox($name, $label, $value, $set_value, $hint = '') { '' ); ?> + + diff --git a/app/ajax_combo.php b/app/ajax_combo.php index 82d539d..59b7a51 100644 --- a/app/ajax_combo.php +++ b/app/ajax_combo.php @@ -1,5 +1,5 @@ Query)) { - // do we have an order by? - if(preg_match('/\border by\b/i', $combo->Query)) - $combo->Query = preg_replace('/\b(order by)\b/i', ' GROUP BY 2 $1', $combo->Query); - else - $combo->Query .= ' GROUP BY 2 '; - } - if(!preg_match('/ limit .+/i', $combo->Query)) { if(!$search_id) $combo->Query .= " LIMIT {$skip}, {$results_per_page}"; if($search_id) $combo->Query .= " LIMIT 1"; @@ -376,12 +367,25 @@ $eo = ['silentErrors' => true]; + /* + if unique text (ut=1), we don't care about IDs and can group by text. + initiate array to hold unique texts. + */ + if($uniqueText) $uniqueArr = []; + $res = sql($combo->Query, $eo); if($res) while($row = db_fetch_row($res)) { + // Add initial empty value if field is not required and this is the first page of results if(empty($prepared_data) && $page == 1 && !$search_id && !$field['not_null']) { $prepared_data[] = ['id' => empty_lookup_value, 'text' => to_utf8("<{$Translation['none']}>")]; } + // if unique text, add to uniqueArr and skip if already exists + if($uniqueText) { + if(in_array($row[1], $uniqueArr)) continue; + $uniqueArr[] = $row[1]; + } + $prepared_data[] = ['id' => to_utf8($row[0]), 'text' => to_utf8($xss->xss_clean($row[1]))]; } } diff --git a/app/applicants_and_tenants_autofill.php b/app/applicants_and_tenants_autofill.php index 06c5dbc..e2eb790 100644 --- a/app/applicants_and_tenants_autofill.php +++ b/app/applicants_and_tenants_autofill.php @@ -1,5 +1,5 @@ SelectedText = (isset($filterField[1]) && $filterField[1] == '8' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); - $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '13' && $filterOperator[1] == '<=>' ? $filterValue[1] : 'Applicant'); + $combo_driver_license_state->SelectedText = (isset($filterField[1]) && $filterField[1] == '8' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); + $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '13' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('Applicant')); } $combo_driver_license_state->Render(); $combo_status->Render(); diff --git a/app/applicants_and_tenants_view.php b/app/applicants_and_tenants_view.php index 24dfc0e..0ed7ac4 100644 --- a/app/applicants_and_tenants_view.php +++ b/app/applicants_and_tenants_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/applications_leases_autofill.php b/app/applications_leases_autofill.php index 2505100..39d5257 100644 --- a/app/applications_leases_autofill.php +++ b/app/applications_leases_autofill.php @@ -1,5 +1,5 @@ SelectedData = $filterer_tenants; - $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '3' && $filterOperator[1] == '<=>' ? $filterValue[1] : 'Application'); + $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '3' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('Application')); $combo_property->SelectedData = $filterer_property; $combo_unit->SelectedData = $filterer_unit; - $combo_type->SelectedText = (isset($filterField[1]) && $filterField[1] == '6' && $filterOperator[1] == '<=>' ? $filterValue[1] : 'Fixed'); - $combo_recurring_charges_frequency->SelectedText = (isset($filterField[1]) && $filterField[1] == '10' && $filterOperator[1] == '<=>' ? $filterValue[1] : 'Monthly'); + $combo_type->SelectedText = (isset($filterField[1]) && $filterField[1] == '6' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('Fixed')); + $combo_recurring_charges_frequency->SelectedText = (isset($filterField[1]) && $filterField[1] == '10' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('Monthly')); } $combo_tenants->HTML = ''; $combo_tenants->MatchText = ''; diff --git a/app/applications_leases_view.php b/app/applications_leases_view.php index c3c19e5..cc901f8 100644 --- a/app/applications_leases_view.php +++ b/app/applications_leases_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/common.js b/app/common.js index e90f60f..fa3b1a8 100644 --- a/app/common.js +++ b/app/common.js @@ -1,6 +1,6 @@ var AppGini = AppGini || {}; -AppGini.version = 23.17; +AppGini.version = 24.11; /* initials and fixes */ jQuery(function() { @@ -313,6 +313,22 @@ jQuery(function() { // if upload toolbox is empty, hide it $j('.upload-toolbox').toggleClass('hidden', !$j('.upload-toolbox').children().not('.hidden').length) + + // on clicking .sql-query-copier or .sql-query-container, copy the query to clipboard + // and set .sql-query-copier to 'copied' for 1 second + $j(document).on('click', '.sql-query-copier', function() { + const query = $j(this).siblings('.sql-query-container').text().trim(); + if(!query) return; + + AppGini.copyToClipboard(query); + + $j(this).text(AppGini.Translate._map['copied']); + setTimeout(() => $j(this).text(AppGini.Translate._map['click to copy']), 1000); + }) + $j(document).on('click', '.sql-query-container', function() { + $j(this).siblings('.sql-query-copier').click(); + }) + }); /* show/hide TV action buttons based on whether records are selected or not */ @@ -1904,6 +1920,11 @@ AppGini.sortSelect2ByRelevence = function(res, cont, qry) { if(aStart && !bStart) return false; if(!aStart && bStart) return true; } + + // if trimmed item is empty, always return it first + if(a.text.trim() == '') return false; + if(b.text.trim() == '') return true; + return a.text > b.text; }); } @@ -1924,6 +1945,12 @@ AppGini.alterDVTitleLinkToBack = function() { }) } +AppGini.isRecordUpdated = () => { + var url = new URL(window.location.href); + var params = new URLSearchParams(url.search); + return params.has('record-updated-ok') || params.has('record-added-ok'); +} + AppGini.lockUpdatesOnUserRequest = function() { // if this is not DV of existing record where editing and saving a copy are both enabled, skip if(!$j('#update').length || !$j('#insert').length || !$j('input[name=SelectedID]').val().length) return; @@ -1955,6 +1982,9 @@ AppGini.lockUpdatesOnUserRequest = function() { locker.toggleClass('active'); locker.prop('title', AppGini.Translate._map[locker.hasClass('active') ? 'Enable' : 'Disable']); }) + + // if record has just been added/updated, lock updates + if(AppGini.isRecordUpdated()) $j('.btn-update-locker').trigger('click'); } /* function to focus a specific element of a form, given field name */ @@ -2547,3 +2577,23 @@ AppGini.updateChildrenCount = (scheduleNextCall = true) => { setTimeout(AppGini.updateChildrenCount, elapsed > 2000 ? 60000 : 10000); }); } + +AppGini.copyToClipboard = (text) => { + if(navigator.clipboard) { + navigator.clipboard.writeText(text); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); +} + +AppGini.htmlEntitiesToText = (html) => { + const txt = document.createElement('textarea'); + txt.innerHTML = html; + return txt.value; +} diff --git a/app/datalist.php b/app/datalist.php index 204387c..4b602f0 100644 --- a/app/datalist.php +++ b/app/datalist.php @@ -48,6 +48,7 @@ class DataList { $AllowPrintingDV, $HideTableView, $AllowCSV, + $AllowAdminShowSQL, $CSVSeparator, $QuickSearch, // 0 to 3 @@ -96,6 +97,7 @@ function DataList() { // Constructor function $this->HideTableView = 0; $this->QuickSearch = 0; $this->AllowCSV = 0; + $this->AllowAdminShowSQL = 0; $this->CSVSeparator = ','; $this->AllowDVNavigation = true; @@ -853,6 +855,11 @@ function Render() { // begin table and display table title if(!$this->HideTableView && !($dvprint_x && $this->AllowSelection && $SelectedID) && !$PrintDV && !$Embedded) { + if(getLoggedAdmin() && $this->AllowAdminShowSQL) { + $this->HTML .= ''; + $this->HTML .= ''; + } + $this->HTML .= '
    '; $this->HTML .= ''; @@ -917,7 +924,7 @@ function Render() { /* handle child info column header */ if($this->ColNumber[$i] == -1) { - [$childTable, $lookupField] = @explode('.', trim($this->ColFieldName[$i], '%')); + list($childTable, $lookupField) = @explode('.', trim($this->ColFieldName[$i], '%')); $extraClasses = "child-{$childTable}-{$lookupField} child-records-info"; $extraAttributes = " data-table=\"{$childTable}\" data-lookup-field=\"{$lookupField}\""; $extraHint = ' '; @@ -1146,7 +1153,9 @@ function Render() { if($Print_x == '' && $i) { // TV $this->HTML .= '
    '; $this->HTML .= '
    '; - if($FirstRecord > 1) $this->HTML .= ''; + if($FirstRecord > 1) + $this->HTML .= '
     
    ' . + ''; $this->HTML .= '
    '; $this->HTML .= '
    '; @@ -1154,7 +1163,9 @@ function Render() { $this->HTML .= '
    '; $this->HTML .= '
    '; - if($i < $RecordCount) $this->HTML .= ''; + if($i < $RecordCount) + $this->HTML .= '
     
    ' . + ''; $this->HTML .= '
    '; $this->HTML .= '
    '; } @@ -1590,6 +1601,10 @@ function getTVButtons($print = false) { if($this->AllowCSV) $buttons .= ''; + // if admin and AllowAdminShowSQL, display SQL icon + if(getLoggedAdmin() && $this->AllowAdminShowSQL) + $buttons .= ''; + // display Filter icon if($this->AllowFilters) $buttons .= ''; @@ -1634,7 +1649,7 @@ function buildQuery($fieldsArray = false, $start = false, $length = false) { ]); } - function getTVRevords($first) { + function getTVQuery($first) { // TV/TVP query $tvFields = $this->QueryFieldsTV; @@ -1642,9 +1657,12 @@ function getTVRevords($first) { if($this->PrimaryKey) $tvFields["COALESCE($this->PrimaryKey)"] = str_replace('`', '', $this->PrimaryKey); - $tvQuery = $this->buildQuery($tvFields, $first - 1, $this->RecordsPerPage); + return $this->buildQuery($tvFields, $first - 1, $this->RecordsPerPage); + } + + function getTVRevords($first) { //$eo = ['silentErrors' => true]; - $result = sql($tvQuery, $eo); + $result = sql($this->getTVQuery($first), $eo); $tvRecords = []; if(!$result) return $tvRecords; while($row = db_fetch_array($result)) $tvRecords[] = $row; @@ -1864,6 +1882,7 @@ private function isValidFilter($index) { && ( !empty($this->FilterValue[$index]) || strpos($this->FilterOperator[$index], 'empty') !== false + || $this->FilterValue[$index] === '0' ) ); } diff --git a/app/db.php b/app/db.php index 317243d..92a34f3 100644 --- a/app/db.php +++ b/app/db.php @@ -34,6 +34,8 @@ function db_select_db($dbname, $link = NULL) { } function db_fetch_array($res) { + if(!$res) return false; + switch(DATABASE) { case 'mysql': return @mysql_fetch_array($res); @@ -43,6 +45,8 @@ function db_fetch_array($res) { } function db_fetch_assoc($res) { + if(!$res) return false; + switch(DATABASE) { case 'mysql': return @mysql_fetch_assoc($res); @@ -52,6 +56,8 @@ function db_fetch_assoc($res) { } function db_fetch_row($res) { + if(!$res) return false; + switch(DATABASE) { case 'mysql': return @mysql_fetch_row($res); diff --git a/app/defaultLang.php b/app/defaultLang.php index b0f43c6..d834d97 100644 --- a/app/defaultLang.php +++ b/app/defaultLang.php @@ -847,4 +847,32 @@ 'plugin install failed' => 'Plugin installation failed!', 'plugin installed successfully' => 'Plugin installed successfully! Click to launch.', 'missing dependencies' => 'Errors or missing dependecies found', + + // Added in 24.10 + 'fix record owners' => 'Fix record owners', + 'record owner not configured for this table' => 'Record owner is not configured for this table.', + 'start' => 'Start', + 'fixing record owners' => 'Fixing record owners ...', + 'number of runs' => 'Number of runs', + 'no tables to fix record owners' => 'No tables have record owners configured. Nothing to fix.', + 'fix record owners description' => 'This tool enables you to update the record owners for pre-existing records in tables that have record owners configured. This is useful when you have recently configured record owners for one or more tables and wish to apply the updated configuration to the existing records.', + 'records updated:' => 'Records updated: ', + 'ldap disable user signup' => 'Disable (only admins can add users)', + 'ldap settings' => 'LDAP settings', + 'login method' => 'Login method', + 'default' => 'Default', + 'ldap' => 'LDAP', + 'ldap server' => 'LDAP server', + 'ldap version' => 'LDAP version', + 'ldap username prefix' => 'LDAP username prefix', + 'ldap username suffix' => 'LDAP username suffix', + 'ldap default user group' => 'Default user group for new LDAP users', + 'Examples: ' => 'Examples: ', + 'Example: ' => 'Example: ', + 'ldap admin user warning' => 'Before enabling LDAP login, make sure the admin username %s exists in the LDAP server. Otherwise, you will not be able to sign in as admin.', + + // Added in 24.11 + 'SQL' => 'SQL', + 'click to copy' => 'Click to copy', + 'copied' => 'Copied!', ]; diff --git a/app/definitions.php b/app/definitions.php index ac5127f..0f783f1 100644 --- a/app/definitions.php +++ b/app/definitions.php @@ -3,7 +3,7 @@ @define('SESSION_NAME', 'Rental_property_manager'); @define('APP_TITLE', 'Rental Property Manager'); @define('APP_DIR', __DIR__); - @define('APP_VERSION', '23.17'); + @define('APP_VERSION', '24.11'); @define('maxSortBy', 4); @define('empty_lookup_value', '{empty_value}'); @define('MULTIPLE_SUPER_ADMINS', false); diff --git a/app/dynamic.css b/app/dynamic.css index 9ffde58..00c464a 100644 --- a/app/dynamic.css +++ b/app/dynamic.css @@ -507,3 +507,465 @@ dd.child-records-info > a.children-count { direction: ltr; text-align: right; } +.sql-query-container { + white-space: pre-wrap; + word-break: break-word; + direction: ltr; + cursor: pointer; + padding-top: 2em; +} +.sql-query-copier { + position: absolute !important; + top: 0 !important; + right: 1em !important; +} +.theme-yeti { + .bg-primary, .bg-primary a, + .bg-primary.text-primary, .bg-primary .text-primary, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #222; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #008cba; + } + + .progress{ + height: 2.5em; + } + + .progress .progress-bar { + line-height: 2; + font-size: 1em; + } +} + +.theme-united { + .bg-primary, .bg-primary a, + .bg-primary.text-primary, .bg-primary .text-primary, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #333; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #dd4814; + } + +} + +.theme-superhero { + .bg-info, .bg-info a, + .bg-warning, .bg-warning a, + .bg-danger, .bg-danger a, + .bg-success, .bg-success a, + .bg-primary, .bg-primary a, + + .bg-info.text-info, .bg-info .text-info, + .bg-warning.text-warning, .bg-warning .text-warning, + .bg-danger.text-danger, .bg-danger .text-danger, + .bg-success.text-success, .bg-success .text-success, + .bg-primary.text-primary, .bg-primary .text-primary, + + .light-text { + color: #ebebeb; + } + + .dark-text { + color: #ebebeb; + } + + .dark-link { + color: #df691a; + } + +} + +.theme-spacelab { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #666; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #3399f3; + } + +} + +.theme-slate { + .bg-danger, .bg-danger a, + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .dark-text { + color: #c8c8c8; + } + + .dark-link { + color: #fff; + } + +} + +.theme-simplex { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #777; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #d9230f; + } + +} + +.theme-sandstone { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #3e3f3a; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #93c54b; + } + +} + +.theme-readable { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #333; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #4582ec; + } + +} + +.theme-paper { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #666; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #2196f3; + } + + .progress{ + height: 2em; + } + + .progress .progress-bar { + line-height: 2; + font-size: 1em; + } +} + +.theme-journal { + .bg-primary, .bg-primary a, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #777; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #eb6864; + } + +} + +.theme-flatly { + + .bg-info, .bg-info a, + .bg-warning, .bg-warning a, + .bg-danger, .bg-danger a, + .bg-success, .bg-success a, + .bg-primary, .bg-primary a, + + .bg-info.text-info, .bg-info .text-info, + .bg-warning.text-warning, .bg-warning .text-warning, + .bg-danger.text-danger, .bg-danger .text-danger, + .bg-success.text-success, .bg-success .text-success, + .bg-primary.text-primary, .bg-primary .text-primary, + + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-hover>tbody>tr.bg-success:hover, .table-hover>tbody>tr:hover .bg-success, + .table-hover>tbody>tr.bg-warning:hover, .table-hover>tbody>tr:hover .bg-warning, + .table-hover>tbody>tr.bg-info:hover, .table-hover>tbody>tr:hover .bg-info, + .table-hover>tbody>tr.bg-danger:hover, .table-hover>tbody>tr:hover .bg-danger, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger, + + .dark-text { + color: #3c3c3c; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-hover>tbody>tr.bg-success:hover a, .table-hover>tbody>tr:hover .bg-success a, + .table-hover>tbody>tr.bg-warning:hover a, .table-hover>tbody>tr:hover .bg-warning a, + .table-hover>tbody>tr.bg-info:hover a, .table-hover>tbody>tr:hover .bg-info a, + .table-hover>tbody>tr.bg-danger:hover a, .table-hover>tbody>tr:hover .bg-danger a, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success a, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning a, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info a, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger a, + + .dark-link { + color: #18bc9c; + } + + .progress{ + height: 2em; + } + + .progress .progress-bar { + line-height: 2; + font-size: 1em; + } +} + +.theme-darkly { + + .bg-info, .bg-info a, + .bg-warning, .bg-warning a, + .bg-danger, .bg-danger a, + .bg-success, .bg-success a, + .bg-primary, .bg-primary a, + + .bg-info.text-info, .bg-info .text-info, + .bg-warning.text-warning, .bg-warning .text-warning, + .bg-danger.text-danger, .bg-danger .text-danger, + .bg-success.text-success, .bg-success .text-success, + .bg-primary.text-primary, .bg-primary .text-primary, + + .light-text { + color: #fff; + } + + .progress{ + height: 2em; + } + + .progress .progress-bar { + line-height: 2; + font-size: 1em; + } + +} + +.theme-cyborg { + .bg-info, .bg-info a, + .bg-warning, .bg-warning a, + .bg-danger, .bg-danger a, + .bg-success, .bg-success a, + .bg-primary, .bg-primary a, + + .bg-info.text-info, .bg-info .text-info, + .bg-warning.text-warning, .bg-warning .text-warning, + .bg-danger.text-danger, .bg-danger .text-danger, + .bg-success.text-success, .bg-success .text-success, + .bg-primary.text-primary, .bg-primary .text-primary, + + .light-text { + color: #fff; + } + +} + +.theme-cosmo { + .bg-info, .bg-info a, + .bg-warning, .bg-warning a, + .bg-danger, .bg-danger a, + .bg-success, .bg-success a, + .bg-primary, .bg-primary a, + + .bg-info.text-info, .bg-info .text-info, + .bg-warning.text-warning, .bg-warning .text-warning, + .bg-danger.text-danger, .bg-danger .text-danger, + .bg-success.text-success, .bg-success .text-success, + .bg-primary.text-primary, .bg-primary .text-primary, + + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-hover>tbody>tr.bg-success:hover, .table-hover>tbody>tr:hover .bg-success, + .table-hover>tbody>tr.bg-warning:hover, .table-hover>tbody>tr:hover .bg-warning, + .table-hover>tbody>tr.bg-info:hover, .table-hover>tbody>tr:hover .bg-info, + .table-hover>tbody>tr.bg-danger:hover, .table-hover>tbody>tr:hover .bg-danger, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger, + + .dark-text { + color: #333; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-hover>tbody>tr.bg-success:hover a, .table-hover>tbody>tr:hover .bg-success a, + .table-hover>tbody>tr.bg-warning:hover a, .table-hover>tbody>tr:hover .bg-warning a, + .table-hover>tbody>tr.bg-info:hover a, .table-hover>tbody>tr:hover .bg-info a, + .table-hover>tbody>tr.bg-danger:hover a, .table-hover>tbody>tr:hover .bg-danger a, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success a, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning a, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info a, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger a, + + .dark-link { + color: #2780e3; + } + + .progress{ + height: 2em; + } + + .progress .progress-bar { + line-height: 2; + font-size: 1em; + } +} + +.theme-cerulean { + .bg-primary, .bg-primary a, + .bg-primary.text-primary, .bg-primary .text-primary, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-hover>tbody>tr.bg-success:hover, .table-hover>tbody>tr:hover .bg-success, + .table-hover>tbody>tr.bg-warning:hover, .table-hover>tbody>tr:hover .bg-warning, + .table-hover>tbody>tr.bg-info:hover, .table-hover>tbody>tr:hover .bg-info, + .table-hover>tbody>tr.bg-danger:hover, .table-hover>tbody>tr:hover .bg-danger, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger, + + .dark-text { + color: #555; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-hover>tbody>tr.bg-success:hover a, .table-hover>tbody>tr:hover .bg-success a, + .table-hover>tbody>tr.bg-warning:hover a, .table-hover>tbody>tr:hover .bg-warning a, + .table-hover>tbody>tr.bg-info:hover a, .table-hover>tbody>tr:hover .bg-info a, + .table-hover>tbody>tr.bg-danger:hover a, .table-hover>tbody>tr:hover .bg-danger a, + + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .table-striped>tbody>tr.bg-success:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-success a, + .table-striped>tbody>tr.bg-warning:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-warning a, + .table-striped>tbody>tr.bg-info:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-info a, + .table-striped>tbody>tr.bg-danger:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-danger a, + + .dark-link { + color: #2fa4e7; + } + +} + +.theme-bootstrap { + .bg-primary, .bg-primary a, + .bg-primary.text-primary, .bg-primary .text-primary, + .light-text { + color: #fff; + } + + .table-hover>tbody>tr.bg-primary:hover, .table-hover>tbody>tr:hover .bg-primary, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1), .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary, + .dark-text { + color: #333; + } + + .table-hover>tbody>tr.bg-primary:hover a, .table-hover>tbody>tr:hover .bg-primary a, + .table-striped>tbody>tr.bg-primary:nth-of-type(2n+1) a, .table-striped>tbody>tr:nth-of-type(2n+1) .bg-primary a, + .dark-link { + color: #337ab7; + } + +} diff --git a/app/employment_and_income_history_autofill.php b/app/employment_and_income_history_autofill.php index 1838980..d0ab562 100644 --- a/app/employment_and_income_history_autofill.php +++ b/app/employment_and_income_history_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/footer.php b/app/footer.php index d01e5c3..6165a5b 100644 --- a/app/footer.php +++ b/app/footer.php @@ -9,7 +9,7 @@
    diff --git a/app/language.php b/app/language.php index e4d9552..63dc558 100644 --- a/app/language.php +++ b/app/language.php @@ -874,4 +874,32 @@ 'plugin install failed' => 'Plugin installation failed!', 'plugin installed successfully' => 'Plugin installed successfully! Click to launch.', 'missing dependencies' => 'Errors or missing dependecies found', + + // Added in 24.10 + 'fix record owners' => 'Fix record owners', + 'record owner not configured for this table' => 'Record owner is not configured for this table.', + 'start' => 'Start', + 'fixing record owners' => 'Fixing record owners ...', + 'number of runs' => 'Number of runs', + 'no tables to fix record owners' => 'No tables have record owners configured. Nothing to fix.', + 'fix record owners description' => 'This tool enables you to update the record owners for pre-existing records in tables that have record owners configured. This is useful when you have recently configured record owners for one or more tables and wish to apply the updated configuration to the existing records.', + 'records updated:' => 'Records updated: ', + 'ldap disable user signup' => 'Disable (only admins can add users)', + 'ldap settings' => 'LDAP settings', + 'login method' => 'Login method', + 'default' => 'Default', + 'ldap' => 'LDAP', + 'ldap server' => 'LDAP server', + 'ldap version' => 'LDAP version', + 'ldap username prefix' => 'LDAP username prefix', + 'ldap username suffix' => 'LDAP username suffix', + 'ldap default user group' => 'Default user group for new LDAP users', + 'Examples: ' => 'Examples: ', + 'Example: ' => 'Example: ', + 'ldap admin user warning' => 'Before enabling LDAP login, make sure the admin username %s exists in the LDAP server. Otherwise, you will not be able to sign in as admin.', + + // Added in 24.11 + 'SQL' => 'SQL', + 'click to copy' => 'Click to copy', + 'copied' => 'Copied!', ]; \ No newline at end of file diff --git a/app/lib.php b/app/lib.php index 04fd1a5..cc71ecd 100644 --- a/app/lib.php +++ b/app/lib.php @@ -1,5 +1,5 @@ SelectedText = (isset($filterField[1]) && $filterField[1] == '4' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); + $combo_type->SelectedText = (isset($filterField[1]) && $filterField[1] == '4' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); $combo_owner->SelectedData = $filterer_owner; - $combo_operating_account->SelectedText = (isset($filterField[1]) && $filterField[1] == '7' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); - $combo_country->SelectedText = (isset($filterField[1]) && $filterField[1] == '10' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); - $combo_State->SelectedText = (isset($filterField[1]) && $filterField[1] == '13' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); + $combo_operating_account->SelectedText = (isset($filterField[1]) && $filterField[1] == '7' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); + $combo_country->SelectedText = (isset($filterField[1]) && $filterField[1] == '10' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); + $combo_State->SelectedText = (isset($filterField[1]) && $filterField[1] == '13' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); } $combo_type->Render(); $combo_owner->HTML = ''; diff --git a/app/properties_view.php b/app/properties_view.php index 1cde885..b076be8 100644 --- a/app/properties_view.php +++ b/app/properties_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/property_photos_autofill.php b/app/property_photos_autofill.php index be087e7..7fb92c0 100644 --- a/app/property_photos_autofill.php +++ b/app/property_photos_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/references_autofill.php b/app/references_autofill.php index b49e645..cc0ed66 100644 --- a/app/references_autofill.php +++ b/app/references_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/rental_owners_autofill.php b/app/rental_owners_autofill.php index 4e62239..dd720fb 100644 --- a/app/rental_owners_autofill.php +++ b/app/rental_owners_autofill.php @@ -1,5 +1,5 @@ SelectedText = (isset($filterField[1]) && $filterField[1] == '9' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); - $combo_state->SelectedText = (isset($filterField[1]) && $filterField[1] == '12' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); + $combo_country->SelectedText = (isset($filterField[1]) && $filterField[1] == '9' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); + $combo_state->SelectedText = (isset($filterField[1]) && $filterField[1] == '12' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); } $combo_country->Render(); $combo_state->Render(); diff --git a/app/rental_owners_view.php b/app/rental_owners_view.php index 4e5ac03..b6412ef 100644 --- a/app/rental_owners_view.php +++ b/app/rental_owners_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/residence_and_rental_history_autofill.php b/app/residence_and_rental_history_autofill.php index 3aacad2..578ab85 100644 --- a/app/residence_and_rental_history_autofill.php +++ b/app/residence_and_rental_history_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/resources/lib/Authentication.php b/app/resources/lib/Authentication.php index 819fd64..d3d63ea 100644 --- a/app/resources/lib/Authentication.php +++ b/app/resources/lib/Authentication.php @@ -204,17 +204,29 @@ private static function processPostSignIn() { // fail if no user/pass provided if(!$username || !$password) self::exitFailedLogin(); - // fail if password incorrect/user not found - $hash = self::passwordHash($username); - if(!password_match($password, $hash)) self::exitFailedLogin(); + $loginMethod = config('adminConfig')['loginMethod'] ?? 'default'; - // prepare session - self::signInAs($username); + switch($loginMethod) { + case 'ldap': + if(!LDAP::authenticate($username, $password)) + self::exitFailedLogin(); + break; - Request::val('rememberMe') == 1 ? RememberMe::login($username) : RememberMe::delete(); + case 'default': + default: + // fail if password incorrect/user not found + $hash = self::passwordHash($username); + if(!password_match($password, $hash)) self::exitFailedLogin(); - // harden user's password hash - password_harden($username, $password, $hash); + // prepare session + self::signInAs($username); + + Request::val('rememberMe') == 1 ? RememberMe::login($username) : RememberMe::delete(); + + // harden user's password hash + password_harden($username, $password, $hash); + break; + } // hook: login_ok if(function_exists('login_ok')) { diff --git a/app/resources/lib/Combo.php b/app/resources/lib/Combo.php index df13df5..a5e81cf 100644 --- a/app/resources/lib/Combo.php +++ b/app/resources/lib/Combo.php @@ -44,6 +44,7 @@ function Render() { global $Translation; $this->HTML = ''; + if(!is_array($this->ListData)) $this->ListData = []; if(!is_array($this->ListItem)) $this->ListItem = $this->ListData; $ArrayCount = count($this->ListItem); diff --git a/app/resources/lib/LDAP.php b/app/resources/lib/LDAP.php new file mode 100644 index 0000000..e466e14 --- /dev/null +++ b/app/resources/lib/LDAP.php @@ -0,0 +1,201 @@ + 'ldap', +// 'ldapServer' => 'ldap.example.com', // or 'ldaps://ldap.example.com' for SSL + // to include port: 'ldap.example.com:389' or 'ldaps://ldap.example.com:636' +// 'ldapVersion' => 3, // optional, default is 3 +// 'ldapUsernamePrefix' => 'FDC\\', // or whatever prefix is needed to login to LDAP, if any +// 'ldapUsernameSuffix' => ',ou=people,dc=ldapmock,dc=local', // or whatever suffix is needed to login to LDAP, if any +// 'ldapDefaultUserGroup' => false // if ldap user not found in db, add to this group ID. false to reject login + +class LDAP { + private static $error = ''; + + private static function setError($msg) { + self::$error = $msg; + return false; + } + + public static function getError() { + return self::$error; + } + + public static function authenticate($username, $password) { + if (!self::enabled()) { + return self::setError('LDAP login not enabled'); + } + + $ldap = self::connect(); + if (!$ldap) { + return self::setError('Invalid LDAP server address'); + } + + if (!self::bind($ldap, $username, $password)) { + return self::setError(ldap_error($ldap)); + } + + $memberInfo = Authentication::getMemberInfo($username); + if (!$memberInfo) { + if(!self::handleUserNotFound($username)) { + return false; // Error message already set by handleUserNotFound + } + + $memberInfo = Authentication::getMemberInfo($username, true); // prevent caching + } + + return self::finalizeAuthentication($memberInfo, $username); + } + + public static function enabled() { + $conf = config('adminConfig'); + return ($conf['loginMethod'] ?? '') == 'ldap' && self::supported(); + } + + public static function supported() { + return function_exists('ldap_connect'); + } + + private static function connect() { + $conf = config('adminConfig'); + $ldap = @ldap_connect($conf['ldapServer'] ?? ''); + if ($ldap) { + @ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $conf['ldapVersion'] ?? 3); + } + return $ldap; + } + + private static function bind($ldap, $username, $password) { + $conf = config('adminConfig'); + $ldapUser = ($conf['ldapUsernamePrefix'] ?? '') . $username . ($conf['ldapUsernameSuffix'] ?? ''); + return @ldap_bind($ldap, $ldapUser, $password); + } + + private static function handleUserNotFound($username) { + $conf = config('adminConfig'); + if (!($conf['ldapDefaultUserGroup'] ?? false)) { + return self::setError('User not found in database'); + } + + return self::createNewUser($username); + } + + private static function createNewUser($username) { + // Add user to default group + $conf = config('adminConfig'); + $groupID = intval($conf['ldapDefaultUserGroup']); + if (!$groupID) { + return self::setError('Trying to add new user to non-existent group'); + } + + self::insertNewUser($username, $groupID); + + // Check if user was successfully added + $memberInfo = Authentication::getMemberInfo($username, true); // prevent caching + if (!$memberInfo) { + return self::setError('Failed to add new user to database'); + } + + return true; + } + + private static function insertNewUser($username, $groupID) { + insert('membership_users', [ + 'groupID' => $groupID, + 'memberID' => strtolower($username), + 'passMD5' => password_hash(random_bytes(20), PASSWORD_DEFAULT), + 'email' => '', + 'signupDate' => @date('Y-m-d'), + 'comments' => 'LDAP user added automatically when logging in for the first time', + 'isBanned' => 0, + 'isApproved' => 1, + ]); + } + + private static function finalizeAuthentication($memberInfo, $username) { + if ($memberInfo['banned']) { + return self::setError('User is banned'); + } + + if (!$memberInfo['approved']) { + return self::setError('User is not approved'); + } + + // Prepare session + Authentication::signInAs($username); + + return true; + } + + private static function groups() { + global $Translation; + $adminConfig = config('adminConfig'); + + $groups = ['0' => $Translation['ldap disable user signup']]; + $eo = ['silentErrors' => true]; + $res = sql("SELECT `groupID`, `name` FROM `membership_groups` ORDER BY `name`", $eo); + while ($row = db_fetch_row($res)) { + if($row[1] == $adminConfig['anonymousGroup']) continue; // skip anonymous group + if($row[1] == 'Admins') continue; // skip admins group + + $groups[$row[0]] = $row[1]; + } + + return $groups; + } + + public static function settingsTab() { + if(!self::supported()) return ''; // LDAP not supported + + global $Translation; + ob_start(); ?> +
  • + +
    +
    + +
    + {$adminConfig['adminUsername']}"); ?> +
    + + $Translation['default'], + 'ldap' => $Translation['ldap'], + ]); ?> + ldap.example.com, ldaps://ldap.example.com, ldaps://ldap.example.com:636, ...'); ?> + 2, 3', 'number'); ?> + FDC\\\\, cn=, uid=, ...'); ?> + ,ou=people,dc=ldap,dc=example,dc=com'); ?> +
    + +
    +
    + + $post['loginMethod'] ?? '', + 'ldapServer' => $post['ldapServer'] ?? '', + 'ldapVersion' => $post['ldapVersion'] ?? '', + 'ldapUsernamePrefix' => $post['ldapUsernamePrefix'] ?? '', + 'ldapUsernameSuffix' => $post['ldapUsernameSuffix'] ?? '', + 'ldapDefaultUserGroup' => $post['ldapDefaultUserGroup'] ?? '', + ]; + } +} \ No newline at end of file diff --git a/app/unit_photos_autofill.php b/app/unit_photos_autofill.php index 21f6e40..4bbe1a8 100644 --- a/app/unit_photos_autofill.php +++ b/app/unit_photos_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/units_autofill.php b/app/units_autofill.php index 43d2850..16b915a 100644 --- a/app/units_autofill.php +++ b/app/units_autofill.php @@ -1,5 +1,5 @@ SelectedData = $filterer_property; - $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '5' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); + $combo_status->SelectedText = (isset($filterField[1]) && $filterField[1] == '5' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); } $combo_property->HTML = ''; $combo_property->MatchText = ''; diff --git a/app/units_view.php b/app/units_view.php index 62f891a..c8fec97 100644 --- a/app/units_view.php +++ b/app/units_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search'];