diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 48fa262f..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - 'env': { - 'browser': true, - 'es6': true, - 'commonjs': true, - 'jquery': true - }, - 'extends': 'eslint:recommended', - 'parserOptions': { - 'ecmaVersion': 6 - }, - 'rules': { - 'indent': [ - 'error', - 'tab', - { - 'SwitchCase': 1 - } - ], - 'linebreak-style': [ - 'error', - 'unix' - ], - 'quotes': [ - 'error', - 'single' - ], - 'semi': [ - 'error', - 'always' - ], - 'no-console': 'off', - 'no-unused-vars': [ - 'error', { - 'vars': 'all', - 'args': 'after-used', - 'ignoreRestSiblings': false - } - ], - // Requires eslint >= v8.14.0 - 'no-constant-binary-expression': 'error' - } -}; diff --git a/.gitignore b/.gitignore index dd606ef4..35a8acbc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ vendor/ tools/ www/composer.lock www/vendor +**/.env +**/.target diff --git a/.phive/phars.xml b/.phive/phars.xml index e1f29fd0..57263c64 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,10 +1,10 @@ - + - - - + + + diff --git a/4dev/checking/phan.sh b/4dev/checking/phan.sh index 5c788c16..856a20c3 100755 --- a/4dev/checking/phan.sh +++ b/4dev/checking/phan.sh @@ -1,5 +1,5 @@ base="/storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/"; # must be run in ${base} -cd $base; +cd $base || exit; ${base}tools/phan --progress-bar -C --analyze-twice; -cd ~; +cd ~ || exit; diff --git a/4dev/checking/phpstan.sh b/4dev/checking/phpstan.sh index 079bbf6c..00481ad8 100755 --- a/4dev/checking/phpstan.sh +++ b/4dev/checking/phpstan.sh @@ -1,5 +1,5 @@ base="/storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/"; # must be run in ${base} -cd $base; +cd $base || exit; ${base}tools/phpstan; -cd ~; +cd ~ || exit; diff --git a/4dev/checking/phpunit.sh b/4dev/checking/phpunit.sh index 0fbf93d8..40271ec8 100755 --- a/4dev/checking/phpunit.sh +++ b/4dev/checking/phpunit.sh @@ -1,49 +1,96 @@ #!/bin/env bash -base="/storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/"; +function error() { + if [ -t 1 ]; then echo "[MAK] ERROR: $*" >&2; fi; exit 0; +} + +usage() { + cat < to force a certain php version opt_testdox=""; -if [ "${1}" = "t" ] || [ "${2}" = "t" ]; then - opt_testdox="--testdox"; -fi; -php_bin=""; -if [ -n "${1}" ]; then +opt_verbose=""; +php_version=""; +no_php_version=0; +while [ -n "${1-}" ]; do case "${1}" in - # "7.3") php_bin="/usr/bin/php7.3 "; ;; - # "7.4") php_bin="/usr/bin/php7.4 "; ;; - # "8.0") php_bin="/usr/bin/php8.0 "; ;; - # "8.1") php_bin="/usr/bin/php8.1 "; ;; - "8.2") php_bin="/usr/bin/php8.2 "; ;; - "8.3") php_bin="/usr/bin/php8.4 "; ;; - *) echo "Not support PHP: ${1}"; exit; ;; - esac; + -t | --testdox) + opt_testdox="--testdox"; + ;; + -v | --verbose) + opt_verbose="--verbose"; + ;; + -p | --php) + php_version="${2-}"; + shift + ;; + -h | --help) + usage + ;; + # invalid option + -?*) + error "[!] Unknown option: '$1'." + ;; + esac + shift; +done; + +if [ -z "${php_version}" ]; then + php_version="${DEFAULT_PHP_VERSION}"; + no_php_version=1; fi; -if [ -n "${2}" ] && [ -z "${php_bin}" ]; then - case "${2}" in - # "7.3") php_bin="/usr/bin/php7.3 "; ;; - # "7.4") php_bin="/usr/bin/php7.4 "; ;; - # "8.0") php_bin="/usr/bin/php8.0 "; ;; - # "8.1") php_bin="/usr/bin/php8.1 "; ;; - "8.2") php_bin="/usr/bin/php8.2 "; ;; - "8.3") php_bin="/usr/bin/php8.3 "; ;; - *) echo "Not support PHP: ${1}"; exit; ;; - esac; +php_bin="${PHP_BIN_PATH}${php_version}"; +echo "Use PHP Version: ${php_version}"; + +if [ ! -f "${php_bin}" ]; then + echo "Set php ${php_bin} does not exist"; + exit; fi; +php_bin="${php_bin} "; # Note 4dev/tests/bootstrap.php has to be set as bootstrap file in phpunit.xml -phpunit_call="${php_bin}${base}vendor/bin/phpunit ${opt_testdox} -c ${base}phpunit.xml ${base}4dev/tests/"; +phpunit_call="${php_bin}${BASE_PATH}vendor/bin/phpunit ${opt_testdox} ${opt_verbose} -c ${PHPUNIT_CONFIG} ${BASE_PATH}4dev/tests/"; ${phpunit_call}; -if [ ! -z "${php_bin}" ]; then - echo "CALLED WITH PHP: ${php_bin}"$(${php_bin} --version); +echo -e "\nPHPUnit Config: ${PHPUNIT_CONFIG}"; +if [ "${no_php_version}" -eq 0 ]; then + echo "CALLED WITH PHP: ${php_bin}$(${php_bin} --version)"; else - echo "Default PHP used: "$(php --version); + echo "Default PHP used: $(php --version)"; fi; # __END__ diff --git a/4dev/composer/sync-to-composer-all-folder.sh b/4dev/composer/sync-to-composer-all-folder.sh index d717bfcb..07e57098 100755 --- a/4dev/composer/sync-to-composer-all-folder.sh +++ b/4dev/composer/sync-to-composer-all-folder.sh @@ -13,7 +13,7 @@ if [ "${GO}" != "go" ]; then fi; BASE="/storage/var/www/html/developers/clemens/core_data/"; -SOURCE="${BASE}php_libraries/trunk/" +SOURCE="${BASE}php_libraries/master/" TARGET="${BASE}composer-packages/CoreLibs-Composer-All/" rsync ${DRY_RUN}-Plzvrupt --stats --delete ${SOURCE}4dev/tests/ ${TARGET}test/phpunit/ diff --git a/4dev/database/function/set_date.sql b/4dev/database/function/set_date.sql index 408688a1..13a51743 100644 --- a/4dev/database/function/set_date.sql +++ b/4dev/database/function/set_date.sql @@ -5,9 +5,9 @@ RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; diff --git a/4dev/database/function/set_edit_generic.sql b/4dev/database/function/set_edit_generic.sql index 4ec68683..be7519b9 100644 --- a/4dev/database/function/set_edit_generic.sql +++ b/4dev/database/function/set_edit_generic.sql @@ -7,11 +7,11 @@ DECLARE random_length INT = 25; -- that should be long enough BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); NEW.cuid := random_string(random_length); NEW.cuuid := gen_random_uuid(); ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; diff --git a/4dev/database/function/set_generic_uid.sql b/4dev/database/function/set_generic_uid.sql index 71c27275..8ad24467 100644 --- a/4dev/database/function/set_generic_uid.sql +++ b/4dev/database/function/set_generic_uid.sql @@ -8,12 +8,12 @@ DECLARE random_length INT = 32; -- long for massive data BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); IF NEW.uid IS NULL THEN NEW.uid := random_string(random_length); END IF; ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; diff --git a/4dev/database/function/update_function.sql b/4dev/database/function/update_function.sql deleted file mode 100644 index 80a3dccf..00000000 --- a/4dev/database/function/update_function.sql +++ /dev/null @@ -1,19 +0,0 @@ --- adds the created or updated date tags - --- OLD, DEPRECATED, use set_generic.sql - --- CREATE OR REPLACE FUNCTION set_generic() --- RETURNS TRIGGER AS --- $$ --- BEGIN --- IF TG_OP = 'INSERT' THEN --- NEW.date_created := clock_timestamp(); --- NEW.user_created := current_user; --- ELSIF TG_OP = 'UPDATE' THEN --- NEW.date_updated := clock_timestamp(); --- NEW.user_updated := current_user; --- END IF; --- RETURN NEW; --- END; --- $$ --- LANGUAGE 'plpgsql'; diff --git a/4dev/database/table/edit_log.sql b/4dev/database/table/edit_log.sql index 0ebf2599..7edd8da5 100644 --- a/4dev/database/table/edit_log.sql +++ b/4dev/database/table/edit_log.sql @@ -10,35 +10,51 @@ CREATE TABLE edit_log ( edit_log_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, euid INT, -- this is a foreign key, but I don't nedd to reference to it FOREIGN KEY (euid) REFERENCES edit_user (edit_user_id) MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL, - username VARCHAR, - password VARCHAR, - ecuid VARCHAR, - ecuuid UUID, + eucuid VARCHAR, + eucuuid UUID, -- this is the one we want to use, full UUIDv4 from the edit user table + -- date_created equal, but can be overridden event_date TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, - ip VARCHAR, + -- session ID if set + session_id VARCHAR, + -- username + username VARCHAR, + -- DEPRECATED [password] + password VARCHAR, + ip_address JSONB, -- REMOTE_IP and all other IPs (X_FORWARD, etc) as JSON block + -- DEPRECATED [ip] + ip VARCHAR, -- just the REMOTE_IP, full set see ip_address + -- string blocks, general error TEXT, event TEXT, + -- bytea or string type storage of any data data_binary BYTEA, data TEXT, + -- set page name only page VARCHAR, - action VARCHAR, - action_id VARCHAR, - action_sub_id VARCHAR, - action_yes VARCHAR, - action_flag VARCHAR, - action_menu VARCHAR, - action_loaded VARCHAR, - action_value VARCHAR, - action_type VARCHAR, - action_error VARCHAR, + -- various info data sets user_agent VARCHAR, referer VARCHAR, script_name VARCHAR, query_string VARCHAR, + request_scheme VARCHAR, -- http or https server_name VARCHAR, http_host VARCHAR, - http_accept VARCHAR, - http_accept_charset VARCHAR, - http_accept_encoding VARCHAR, - session_id VARCHAR + http_data JSONB, + -- DEPRECATED [http*] + http_accept VARCHAR, -- in http_data + http_accept_charset VARCHAR, -- in http_data + http_accept_encoding VARCHAR, -- in http_data + -- any action var, -> same set in action_data as JSON + action_data JSONB, + -- DEPRECATED [action*] + action VARCHAR, -- in action_data + action_id VARCHAR, -- in action_data + action_sub_id VARCHAR, -- in action_data + action_yes VARCHAR, -- in action_data + action_flag VARCHAR, -- in action_data + action_menu VARCHAR, -- in action_data + action_loaded VARCHAR, -- in action_data + action_value VARCHAR, -- in action_data + action_type VARCHAR, -- in action_data + action_error VARCHAR -- in action_data ) INHERITS (edit_generic) WITHOUT OIDS; diff --git a/4dev/database/table/edit_user.sql b/4dev/database/table/edit_user.sql index abae29c6..47c4914c 100644 --- a/4dev/database/table/edit_user.sql +++ b/4dev/database/table/edit_user.sql @@ -35,11 +35,10 @@ CREATE TABLE edit_user ( strict SMALLINT DEFAULT 0, locked SMALLINT DEFAULT 0, protected SMALLINT NOT NULL DEFAULT 0, - -- legacy, debug flags - debug SMALLINT NOT NULL DEFAULT 0, - db_debug SMALLINT NOT NULL DEFAULT 0, -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- force lgout counter + force_logout INT DEFAULT 0, -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, -- login error @@ -76,9 +75,8 @@ COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overri COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; -COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; -COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; +COMMENT ON COLUMN edit_user.force_logout IS 'Counter for forced log out, if this one is higher than the session set one the session gets terminated'; COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date'; diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php index 912d715f..9523a60d 100644 --- a/4dev/tests/AAASetupData/requests/http_requests.php +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -48,7 +48,7 @@ header("Content-Type: application/json; charset=UTF-8"); if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) { header("HTTP/1.1 401 Unauthorized"); print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}'); - exit; + exit(1); } // if server request type is get set file_get to null -> no body @@ -57,7 +57,7 @@ if ($_SERVER['REQUEST_METHOD'] == "GET") { } elseif (($file_get = file_get_contents('php://input')) === false) { header("HTTP/1.1 404 Not Found"); print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}'); - exit; + exit(1); } print buildContent($http_headers, $file_get); diff --git a/4dev/tests/ACL/CoreLibsACLLoginTest.php b/4dev/tests/ACL/CoreLibsACLLoginTest.php index 92d3d978..f29f02d7 100644 --- a/4dev/tests/ACL/CoreLibsACLLoginTest.php +++ b/4dev/tests/ACL/CoreLibsACLLoginTest.php @@ -12,6 +12,8 @@ Not yet covered tests: - loginGetLocale - loginGetHeaderColor - loginGetPages +- loginGetPageLookupList +- loginPageAccessAllowed - loginGetEuid */ @@ -22,8 +24,12 @@ Not yet covered tests: */ final class CoreLibsACLLoginTest extends TestCase { - private static $db; - private static $log; + private static \CoreLibs\DB\IO $db; + private static \CoreLibs\Logging\Logging $log; + + private static string $edit_access_cuid; + private static string $edit_user_cuid; + private static string $edit_user_cuuid; /** * start DB conneciton, setup DB, etc @@ -108,21 +114,46 @@ final class CoreLibsACLLoginTest extends TestCase self::$db->dbSetMaxQueryCall(-1); // insert additional content for testing (locked user, etc) $queries = [ - "INSERT INTO edit_access_data " - . "(edit_access_id, name, value, enabled) VALUES " - . "((SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'), " - . "'test', 'value', 1)" + <<dbExec($query); } + // read edit access cuid, edit user cuid and edit user cuuid + $row = self::$db->dbReturnRowParams( + "SELECT cuid FROM edit_access WHERE uid = $1", + ["AdminAccess"] + ); + self::$edit_access_cuid = $row['cuid'] ?? ''; + if (empty(self::$edit_access_cuid)) { + self::markTestIncomplete( + 'Cannot read edit access cuid for "AdminAccess".' + ); + } + $row = self::$db->dbReturnRowParams( + "SELECT cuid, cuuid FROM edit_user WHERE username = $1", + ["admin"] + ); + self::$edit_user_cuid = $row['cuid'] ?? ''; + self::$edit_user_cuuid = $row['cuuid'] ?? ''; + if (empty(self::$edit_user_cuid) || empty(self::$edit_user_cuuid)) { + self::markTestIncomplete( + 'Cannot read edit user cuid or cuuid for "admin".' + ); + } // define mandatory constant // must set // TARGET define('TARGET', 'test'); // LOGIN DB SCHEMA - // define('LOGIN_DB_SCHEMA', ''); // SHOULD SET // DEFAULT_ACL_LEVEL (d80) @@ -235,24 +266,25 @@ final class CoreLibsACLLoginTest extends TestCase 'ajax_post_action' => 'login', ], ], - 'load, session euid set only, php error' => [ + 'load, session eucuuid set only, php error' => [ [ 'page_name' => 'edit_users.php', ], [], [], [ - 'EUID' => 1, - 'ECUID' => 'abc', - 'ECUUID' => '1233456-1234-1234-1234-123456789012', + 'LOGIN_EUID' => 1, + 'LOGIN_EUCUID' => 'abc', + 'LOGIN_EUCUUID' => '1233456-1234-1234-1234-123456789012', ], 2, [], ], - 'load, session euid set, all set' => [ + 'load, session eucuuid set, all set' => [ [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -261,22 +293,23 @@ final class CoreLibsACLLoginTest extends TestCase [], [], [ - 'EUID' => 1, - 'ECUID' => 'abc', - 'ECUUID' => '1233456-1234-1234-1234-123456789012', - 'USER_NAME' => '', - 'GROUP_NAME' => '', - 'ADMIN' => 1, - 'GROUP_ACL_LEVEL' => -1, - 'PAGES_ACL_LEVEL' => [], - 'USER_ACL_LEVEL' => -1, - 'USER_ADDITIONAL_ACL' => [], - 'GROUP_ADDITIONAL_ACL' => [], - 'UNIT_UID' => [ - 'AdminAccess' => 1, + 'LOGIN_EUID' => 1, + 'LOGIN_EUCUID' => 'abc', + 'LOGIN_EUCUUID' => 'SET_EUCUUID_IN_TEST', + 'LOGIN_USER_NAME' => '', + 'LOGIN_GROUP_NAME' => '', + 'LOGIN_ADMIN' => 1, + 'LOGIN_GROUP_ACL_LEVEL' => -1, + 'LOGIN_PAGES_ACL_LEVEL' => [], + 'LOGIN_USER_ACL_LEVEL' => -1, + 'LOGIN_USER_ADDITIONAL_ACL' => [], + 'LOGIN_GROUP_ADDITIONAL_ACL' => [], + 'LOGIN_UNIT_UID' => [ + 'AdminAccess' => '123456789012', ], - 'UNIT' => [ - 1 => [ + 'LOGIN_UNIT' => [ + '123456789012' => [ + 'id' => 1, 'acl_level' => 80, 'name' => 'Admin Access', 'uid' => 'AdminAccess', @@ -288,8 +321,8 @@ final class CoreLibsACLLoginTest extends TestCase 'additional_acl' => [] ], ], - // 'UNIT_DEFAULT' => '', - // 'DEFAULT_ACL_LIST' => [], + // 'LOGIN_UNIT_DEFAULT' => '', + // 'LOGIN_DEFAULT_ACL_LIST' => [], ], 0, [ @@ -297,6 +330,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -416,6 +450,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_deleted' => true @@ -441,6 +476,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_enabled' => true @@ -466,6 +502,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked' => true @@ -491,6 +528,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_get_locked' => true, @@ -515,6 +553,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_until' => 'on' @@ -540,6 +579,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -559,6 +599,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -569,6 +610,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_after' => 'on' @@ -594,6 +636,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_until' => 'on', @@ -620,6 +663,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_locked' => true @@ -645,6 +689,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -663,6 +708,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -673,6 +719,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -692,6 +739,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -702,6 +750,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -721,6 +770,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -731,6 +781,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -750,6 +801,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -781,6 +833,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -804,6 +857,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -814,6 +868,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -837,6 +892,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -847,6 +903,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_revalidate_after' => 'on', @@ -873,6 +930,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -893,6 +951,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -903,6 +962,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_from' => 'on', @@ -929,6 +989,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -949,6 +1010,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -959,6 +1021,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_until' => 'on', @@ -985,6 +1048,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_from' => 'on', @@ -1012,6 +1076,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -1042,6 +1107,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -1111,11 +1177,15 @@ final class CoreLibsACLLoginTest extends TestCase $_POST[$post_var] = $post_value; } + // set ingoing session cuuid if requested + if (isset($session['LOGIN_EUCUUID']) && $session['LOGIN_EUCUUID'] == 'SET_EUCUUID_IN_TEST') { + $session['LOGIN_EUCUUID'] = self::$edit_user_cuuid; + } + // set _SESSION data foreach ($session as $session_var => $session_value) { $_SESSION[$session_var] = $session_value; } - /** @var \CoreLibs\ACL\Login&MockObject */ $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) ->setConstructorArgs([ @@ -1134,7 +1204,7 @@ final class CoreLibsACLLoginTest extends TestCase . 'locale' . DIRECTORY_SEPARATOR, ] ]) - ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin']) + ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity']) ->getMock(); $login_mock->expects($this->any()) ->method('loginTerminate') @@ -1152,6 +1222,10 @@ final class CoreLibsACLLoginTest extends TestCase ->method('loginPrintLogin') ->willReturnCallback(function () { }); + $login_mock->expects($this->any()) + ->method('loginEnhanceHttpSecurity') + ->willReturnCallback(function () { + }); // if mock_settings: enabled OFF // run DB update and set off @@ -1369,6 +1443,19 @@ final class CoreLibsACLLoginTest extends TestCase // run test try { + // preset, we cannot set that in the provider + if ( + isset($expected['check_access_cuid']) && + $expected['check_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST' + ) { + $expected['check_access_cuid'] = self::$edit_access_cuid; + } + if ( + isset($mock_settings['edit_access_cuid']) && + $mock_settings['edit_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST' + ) { + $mock_settings['edit_access_cuid'] = self::$edit_access_cuid; + } // if ajax call // check if parameter, or globals (old type) // else normal call @@ -1427,6 +1514,31 @@ final class CoreLibsACLLoginTest extends TestCase $login_mock->loginCheckAccessPage($mock_settings['page_access']), 'Assert page access' ); + // - loginCheckEditAccessCuid + $this->assertEquals( + $expected['check_access'], + $login_mock->loginCheckEditAccessCuid($mock_settings['edit_access_cuid']), + 'Assert check access' + ); + // - loginCheckEditAccessValidCuid + $this->assertEquals( + $expected['check_access_cuid'], + $login_mock->loginCheckEditAccessValidCuid($mock_settings['edit_access_cuid']), + 'Assert check access cuid valid' + ); + // - loginGetEditAccessCuidFromUid + $this->assertEquals( + $expected['check_access_cuid'], + $login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_uid']), + 'Assert check access uid to cuid valid' + ); + // - loginGetEditAccessCuidFromId + $this->assertEquals( + $expected['check_access_cuid'], + $login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_id']), + 'Assert check access id to cuid valid' + ); + // Deprecated // - loginCheckEditAccess $this->assertEquals( $expected['check_access'], @@ -1449,7 +1561,7 @@ final class CoreLibsACLLoginTest extends TestCase $this->assertEquals( $expected['check_access_data'], $login_mock->loginGetEditAccessData( - $mock_settings['edit_access_id'], + $mock_settings['edit_access_uid'], $mock_settings['edit_access_data'] ), 'Assert check access id data value valid' @@ -1480,11 +1592,12 @@ final class CoreLibsACLLoginTest extends TestCase // - loginCheckPermissions // - loginGetPermissionOkay } catch (\Exception $e) { - // print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" - // . ($expected['login_error'] ?? 0) . "\n"; - // print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n"; - // print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n"; - // print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n"; + /* print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" + . ($expected['login_error'] ?? 0) . "\n"; + print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n"; + print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n"; + print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n"; + print "POST exit: " . ($_POST['login_exit'] ?? '{0}') . "\n"; */ // if this is 100, then we do further error checks if ( $e->getCode() == 100 || diff --git a/4dev/tests/ACL/database/CoreLibsACLLogin_database_create_data.sql b/4dev/tests/ACL/database/CoreLibsACLLogin_database_create_data.sql index d3a8d0ea..1b6855b5 100644 --- a/4dev/tests/ACL/database/CoreLibsACLLogin_database_create_data.sql +++ b/4dev/tests/ACL/database/CoreLibsACLLogin_database_create_data.sql @@ -30,11 +30,11 @@ DECLARE random_length INT = 12; -- that should be long enough BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); NEW.cuid := random_string(random_length); NEW.cuuid := gen_random_uuid(); ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; @@ -321,7 +321,7 @@ CREATE TABLE edit_generic ( -- DROP TABLE edit_visible_group; CREATE TABLE edit_visible_group ( - edit_visible_group_id SERIAL PRIMARY KEY, + edit_visible_group_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR, flag VARCHAR ) INHERITS (edit_generic) WITHOUT OIDS; @@ -336,7 +336,7 @@ CREATE TABLE edit_visible_group ( -- DROP TABLE edit_menu_group; CREATE TABLE edit_menu_group ( - edit_menu_group_id SERIAL PRIMARY KEY, + edit_menu_group_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR, flag VARCHAR, order_number INT NOT NULL @@ -354,7 +354,7 @@ CREATE TABLE edit_menu_group ( -- DROP TABLE edit_page; CREATE TABLE edit_page ( - edit_page_id SERIAL PRIMARY KEY, + edit_page_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, content_alias_edit_page_id INT, -- alias for page content, if the page content is defined on a different page, ege for ajax backend pages FOREIGN KEY (content_alias_edit_page_id) REFERENCES edit_page (edit_page_id) MATCH FULL ON DELETE RESTRICT ON UPDATE CASCADE, filename VARCHAR, @@ -378,7 +378,7 @@ CREATE TABLE edit_page ( -- DROP TABLE edit_query_string; CREATE TABLE edit_query_string ( - edit_query_string_id SERIAL PRIMARY KEY, + edit_query_string_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_page_id INT NOT NULL, FOREIGN KEY (edit_page_id) REFERENCES edit_page (edit_page_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, enabled SMALLINT NOT NULL DEFAULT 0, @@ -430,7 +430,7 @@ CREATE TABLE edit_page_menu_group ( -- DROP TABLE edit_access_right; CREATE TABLE edit_access_right ( - edit_access_right_id SERIAL PRIMARY KEY, + edit_access_right_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR, level SMALLINT, type VARCHAR, @@ -447,7 +447,7 @@ CREATE TABLE edit_access_right ( -- DROP TABLE edit_scheme; CREATE TABLE edit_scheme ( - edit_scheme_id SERIAL PRIMARY KEY, + edit_scheme_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, enabled SMALLINT NOT NULL DEFAULT 0, name VARCHAR, header_color VARCHAR, @@ -466,7 +466,7 @@ CREATE TABLE edit_scheme ( -- DROP TABLE edit_language; CREATE TABLE edit_language ( - edit_language_id SERIAL PRIMARY KEY, + edit_language_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, enabled SMALLINT NOT NULL DEFAULT 0, lang_default SMALLINT NOT NULL DEFAULT 0, long_name VARCHAR, @@ -485,7 +485,7 @@ CREATE TABLE edit_language ( -- DROP TABLE edit_group; CREATE TABLE edit_group ( - edit_group_id SERIAL PRIMARY KEY, + edit_group_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_scheme_id INT, FOREIGN KEY (edit_scheme_id) REFERENCES edit_scheme (edit_scheme_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_access_right_id INT NOT NULL, @@ -507,7 +507,7 @@ CREATE TABLE edit_group ( -- DROP TABLE edit_page_access; CREATE TABLE edit_page_access ( - edit_page_access_id SERIAL PRIMARY KEY, + edit_page_access_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_group_id INT NOT NULL, FOREIGN KEY (edit_group_id) REFERENCES edit_group (edit_group_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_page_id INT NOT NULL, @@ -530,7 +530,7 @@ CREATE TABLE edit_page_access ( -- DROP TABLE edit_page_content; CREATE TABLE edit_page_content ( - edit_page_content_id SERIAL PRIMARY KEY, + edit_page_content_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_page_id INT NOT NULL, FOREIGN KEY (edit_page_id) REFERENCES edit_page (edit_page_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_access_right_id INT NOT NULL, @@ -551,7 +551,7 @@ CREATE TABLE edit_page_content ( -- DROP TABLE edit_user; CREATE TABLE edit_user ( - edit_user_id SERIAL PRIMARY KEY, + edit_user_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, connect_edit_user_id INT, -- possible reference to other user FOREIGN KEY (connect_edit_user_id) REFERENCES edit_user (edit_user_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_language_id INT NOT NULL, @@ -579,11 +579,10 @@ CREATE TABLE edit_user ( strict SMALLINT DEFAULT 0, locked SMALLINT DEFAULT 0, protected SMALLINT NOT NULL DEFAULT 0, - -- legacy, debug flags - debug SMALLINT NOT NULL DEFAULT 0, - db_debug SMALLINT NOT NULL DEFAULT 0, -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- forced logout counter + force_logout INT DEFAULT 0, -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, -- login error @@ -620,8 +619,6 @@ COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overri COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; -COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; -COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; @@ -652,40 +649,56 @@ COMMENT ON COLUMN edit_user.additional_acl IS 'Additional Access Control List st -- DROP TABLE edit_log; CREATE TABLE edit_log ( - edit_log_id SERIAL PRIMARY KEY, + edit_log_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, euid INT, -- this is a foreign key, but I don't nedd to reference to it - ecuid VARCHAR, - ecuuid UUID, FOREIGN KEY (euid) REFERENCES edit_user (edit_user_id) MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL, - username VARCHAR, - password VARCHAR, + eucuid VARCHAR, + eucuuid UUID, -- this is the one we want to use, full UUIDv4 from the edit user table + -- date_created equal, but can be overridden event_date TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, - ip VARCHAR, + -- session ID if set + session_id VARCHAR, + -- username + username VARCHAR, + -- DEPRECATED [password] + password VARCHAR, + ip_address JSONB, -- REMOTE_IP and all other IPs (X_FORWARD, etc) as JSON block + -- DEPRECATED [ip] + ip VARCHAR, -- just the REMOTE_IP, full set see ip_address + -- string blocks, general error TEXT, event TEXT, + -- bytea or string type storage of any data data_binary BYTEA, data TEXT, + -- set page name only page VARCHAR, - action VARCHAR, - action_id VARCHAR, - action_sub_id VARCHAR, - action_yes VARCHAR, - action_flag VARCHAR, - action_menu VARCHAR, - action_loaded VARCHAR, - action_value VARCHAR, - action_type VARCHAR, - action_error VARCHAR, + -- various info data sets user_agent VARCHAR, referer VARCHAR, script_name VARCHAR, query_string VARCHAR, + request_scheme VARCHAR, -- http or https server_name VARCHAR, http_host VARCHAR, - http_accept VARCHAR, - http_accept_charset VARCHAR, - http_accept_encoding VARCHAR, - session_id VARCHAR + http_data JSONB, + -- DEPRECATED [http*] + http_accept VARCHAR, -- in http_data + http_accept_charset VARCHAR, -- in http_data + http_accept_encoding VARCHAR, -- in http_data + -- any action var, -> same set in action_data as JSON + action_data JSONB, + -- DEPRECATED [action*] + action VARCHAR, -- in action_data + action_id VARCHAR, -- in action_data + action_sub_id VARCHAR, -- in action_data + action_yes VARCHAR, -- in action_data + action_flag VARCHAR, -- in action_data + action_menu VARCHAR, -- in action_data + action_loaded VARCHAR, -- in action_data + action_value VARCHAR, -- in action_data + action_type VARCHAR, -- in action_data + action_error VARCHAR -- in action_data ) INHERITS (edit_generic) WITHOUT OIDS; -- END: table/edit_log.sql -- START: table/edit_log_overflow.sql @@ -712,7 +725,7 @@ ALTER TABLE edit_log_overflow ADD CONSTRAINT edit_log_overflow_euid_fkey FOREIGN -- DROP TABLE edit_access; CREATE TABLE edit_access ( - edit_access_id SERIAL PRIMARY KEY, + edit_access_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, enabled SMALLINT NOT NULL DEFAULT 0, protected SMALLINT DEFAULT 0, deleted SMALLINT DEFAULT 0, @@ -733,7 +746,7 @@ CREATE TABLE edit_access ( -- DROP TABLE edit_access_user; CREATE TABLE edit_access_user ( - edit_access_user_id SERIAL PRIMARY KEY, + edit_access_user_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_access_id INT NOT NULL, FOREIGN KEY (edit_access_id) REFERENCES edit_access (edit_access_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_user_id INT NOT NULL, @@ -754,7 +767,7 @@ CREATE TABLE edit_access_user ( -- DROP TABLE edit_access_data; CREATE TABLE edit_access_data ( - edit_access_data_id SERIAL PRIMARY KEY, + edit_access_data_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, edit_access_id INT NOT NULL, FOREIGN KEY (edit_access_id) REFERENCES edit_access (edit_access_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, enabled SMALLINT NOT NULL DEFAULT 0, @@ -1015,7 +1028,7 @@ INSERT INTO edit_page_access (enabled, edit_group_id, edit_page_id, edit_access_ -- edit user -- inserts admin user so basic users can be created DELETE FROM edit_user; -INSERT INTO edit_user (username, password, enabled, debug, db_debug, email, protected, admin, edit_language_id, edit_group_id, edit_scheme_id, edit_access_right_id) VALUES ('admin', 'admin', 1, 1, 1, '', 1, 1, +INSERT INTO edit_user (username, password, enabled, email, protected, admin, edit_language_id, edit_group_id, edit_scheme_id, edit_access_right_id) VALUES ('admin', 'admin', 1, 'test@tequila.jp', 1, 1, (SELECT edit_language_id FROM edit_language WHERE short_name = 'en_US'), (SELECT edit_group_id FROM edit_group WHERE name = 'Admin'), (SELECT edit_scheme_id FROM edit_scheme WHERE name = 'Admin'), diff --git a/4dev/tests/Combined/CoreLibsCombinedArrayHandlerTest.php b/4dev/tests/Combined/CoreLibsCombinedArrayHandlerTest.php index 8dc5729a..e11aab8e 100644 --- a/4dev/tests/Combined/CoreLibsCombinedArrayHandlerTest.php +++ b/4dev/tests/Combined/CoreLibsCombinedArrayHandlerTest.php @@ -1201,6 +1201,203 @@ final class CoreLibsCombinedArrayHandlerTest extends TestCase 'Find next key in array' ); } + + public function providerReturnMatchingKeyOnley(): array + { + return [ + 'limited entries' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'a', 'b' + ], + [ + 'a' => 'foo', + 'b' => 'bar', + ], + ], + 'limited entries, with one wrong key' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'a', 'b', 'f' + ], + [ + 'a' => 'foo', + 'b' => 'bar', + ], + ], + 'wrong keys only' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'f', 'f' + ], + [ + ], + ], + 'empty keys' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [], + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::arrayReturnMatchingKeyOnly + * @dataProvider providerReturnMatchingKeyOnley + * @testdox arrayReturnMatchingKeyOnly get only selected key entries from array [$_dataName] + * + * @param array $input + * @param array $key_list + * @param array $expected + * @return void + */ + public function testArrayReturnMatchingKeyOnly( + array $input, + array $key_list, + array $expected + ): void { + $this->assertEquals( + $expected, + \CoreLibs\Combined\ArrayHandler::arrayReturnMatchingKeyOnly( + $input, + $key_list + ) + ); + } + + /** + * provider for arrayModifyKey + * + * @return array> + */ + public function providerArrayModifyKey(): array + { + return [ + 'prefix and suffix add' => [ + 'array' => [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar', + ], + 'prefix' => 'Prefix: ', + 'suffix' => '.suffix', + 'expected' => [ + 'Prefix: a.suffix' => 'foo', + 'Prefix: b.suffix' => 'bar', + 'Prefix: c.suffix' => 'foobar', + ], + ], + 'prefix add only' => [ + 'array' => [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar', + ], + 'prefix' => 'Prefix: ', + 'suffix' => '', + 'expected' => [ + 'Prefix: a' => 'foo', + 'Prefix: b' => 'bar', + 'Prefix: c' => 'foobar', + ], + ], + 'suffix add only' => [ + 'array' => [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar', + ], + 'prefix' => '', + 'suffix' => '.suffix', + 'expected' => [ + 'a.suffix' => 'foo', + 'b.suffix' => 'bar', + 'c.suffix' => 'foobar', + ], + ], + 'empty array' => [ + 'array' => [], + 'prefix' => '', + 'suffix' => '.suffix', + 'expected' => [], + ], + 'no suffix or prefix' => [ + 'array' => [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar', + ], + 'prefix' => '', + 'suffix' => '', + 'expected' => [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar', + ], + ], + 'integer index mixed' => [ + 'array' => [ + 'a' => 'foo', + 'b' => 'bar', + 3 => 'foobar', + ], + 'prefix' => '', + 'suffix' => '.suffix', + 'expected' => [ + 'a.suffix' => 'foo', + 'b.suffix' => 'bar', + '3.suffix' => 'foobar', + ], + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::arrayModifyKey + * @dataProvider providerArrayModifyKey + * @testdox arrayModifyKey check that key is correctly modified with $key_mod_prefix and $key_mod_suffix [$_dataName] + * + * @param array $in_array + * @param string $key_mod_prefix + * @param string $key_mod_suffix + * @param array $expected + * @return void + */ + public function testArrayModifyKey( + array $in_array, + string $key_mod_prefix, + string $key_mod_suffix, + array $expected + ): void { + $this->assertEquals( + \CoreLibs\Combined\ArrayHandler::arrayModifyKey($in_array, $key_mod_prefix, $key_mod_suffix), + $expected + ); + } } // __END__ diff --git a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php index d7efa9b7..1833f708 100644 --- a/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php +++ b/4dev/tests/Combined/CoreLibsCombinedDateTimeTest.php @@ -926,48 +926,114 @@ final class CoreLibsCombinedDateTimeTest extends TestCase public function daysIntervalProvider(): array { return [ - 'valid interval /, not named array' => [ - '2020/1/1', - '2020/1/30', - false, - [29, 22, 8], + // normal and format tests + 'valid interval / not named array' => [ + 'input_a' => '2020/1/1', + 'input_b' => '2020/1/30', + 'return_named' => false, // return_named + 'include_end_date' => true, // include_end_date + 'exclude_start_date' => false, // exclude_start_date + 'expected' => [30, 22, 8, false], ], - 'valid interval /, named array' => [ - '2020/1/1', - '2020/1/30', - true, - ['overall' => 29, 'weekday' => 22, 'weekend' => 8], + 'valid interval / named array' => [ + 'input_a' => '2020/1/1', + 'input_b' => '2020/1/30', + 'return_named' => true, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => ['overall' => 30, 'weekday' => 22, 'weekend' => 8, 'reverse' => false], ], - 'valid interval -' => [ - '2020-1-1', - '2020-1-30', - false, - [29, 22, 8], - ], - 'valid interval switched' => [ - '2020/1/30', - '2020/1/1', - false, - [28, 0, 0], + 'valid interval with "-"' => [ + 'input_a' => '2020-1-1', + 'input_b' => '2020-1-30', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, false], ], 'valid interval with time' => [ - '2020/1/1 12:12:12', - '2020/1/30 13:13:13', - false, - [28, 21, 8], + 'input_a' => '2020/1/1 12:12:12', + 'input_b' => '2020/1/30 13:13:13', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, false], ], + // invalid 'invalid dates' => [ - 'abc', - 'xyz', - false, - [0, 0, 0] + 'input_a' => 'abc', + 'input_b' => 'xyz', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [0, 0, 0, false] ], - // this test will take a long imte + // this test will take a long time 'out of bound dates' => [ - '1900-1-1', - '9999-12-31', - false, - [2958463,2113189,845274], + 'input_a' => '1900-1-1', + 'input_b' => '9999-12-31', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [2958463, 2113189, 845274, false], + ], + // tests for include/exclude + 'exclude end date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => false, + 'expected' => [29, 21, 8, false], + ], + 'exclude start date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => true, + 'expected' => [29, 21, 8, false], + ], + 'exclude start and end date' => [ + 'input_b' => '2020/1/1', + 'input_a' => '2020/1/30', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => true, + 'expected' => [28, 20, 8, false], + ], + // reverse + 'reverse: valid interval' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => false, + 'expected' => [30, 22, 8, true], + ], + 'reverse: exclude end date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => false, + 'expected' => [29, 21, 8, true], + ], + 'reverse: exclude start date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => true, + 'exclude_start_date' => true, + 'expected' => [29, 21, 8, true], + ], + 'reverse: exclude start and end date' => [ + 'input_a' => '2020/1/30', + 'input_b' => '2020/1/1', + 'return_named' => false, + 'include_end_date' => false, + 'exclude_start_date' => true, + 'expected' => [28, 20, 8, true], ], ]; } @@ -982,20 +1048,52 @@ final class CoreLibsCombinedDateTimeTest extends TestCase * * @param string $input_a * @param string $input_b - * @param bool $flag - * @param array $expected + * @param bool $return_named + * @param array $expected * @return void */ public function testCalcDaysInterval( string $input_a, string $input_b, - bool $flag, + bool $return_named, + bool $include_end_date, + bool $exclude_start_date, $expected ): void { $this->assertEquals( $expected, - \CoreLibs\Combined\DateTime::calcDaysInterval($input_a, $input_b, $flag) + \CoreLibs\Combined\DateTime::calcDaysInterval( + $input_a, + $input_b, + return_named:$return_named, + include_end_date:$include_end_date, + exclude_start_date:$exclude_start_date + ), + 'call calcDaysInterval' ); + if ($return_named) { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::calcDaysIntervalNamedIndex( + $input_a, + $input_b, + include_end_date:$include_end_date, + exclude_start_date:$exclude_start_date + ), + 'call calcDaysIntervalNamedIndex' + ); + } else { + $this->assertEquals( + $expected, + \CoreLibs\Combined\DateTime::calcDaysIntervalNumIndex( + $input_a, + $input_b, + include_end_date:$include_end_date, + exclude_start_date:$exclude_start_date + ), + 'call calcDaysIntervalNamedIndex' + ); + } } /** @@ -1187,7 +1285,38 @@ final class CoreLibsCombinedDateTimeTest extends TestCase '2023-07-03', '2023-07-27', true - ] + ], + // reverse + 'reverse: no weekend' => [ + '2023-07-04', + '2023-07-03', + false + ], + 'reverse: start weekend sat' => [ + '2023-07-04', + '2023-07-01', + true + ], + 'reverse: start weekend sun' => [ + '2023-07-04', + '2023-07-02', + true + ], + 'reverse: end weekend sat' => [ + '2023-07-08', + '2023-07-03', + true + ], + 'reverse: end weekend sun' => [ + '2023-07-09', + '2023-07-03', + true + ], + 'reverse: long period > 6 days' => [ + '2023-07-27', + '2023-07-03', + true + ], ]; } diff --git a/4dev/tests/Convert/CoreLibsConvertByteTest.php b/4dev/tests/Convert/CoreLibsConvertByteTest.php index 95838889..0cd1682f 100644 --- a/4dev/tests/Convert/CoreLibsConvertByteTest.php +++ b/4dev/tests/Convert/CoreLibsConvertByteTest.php @@ -40,7 +40,7 @@ final class CoreLibsConvertByteTest extends TestCase 4 => '1.00 KB', 5 => '1.02KiB', ], - 'invalud string number' => [ + 'invalid string number' => [ 0 => '1024 MB', 1 => '1024 MB', 2 => '1024 MB', diff --git a/4dev/tests/Create/CoreLibsCreateHashTest.php b/4dev/tests/Create/CoreLibsCreateHashTest.php index fcab739e..9ce36b35 100644 --- a/4dev/tests/Create/CoreLibsCreateHashTest.php +++ b/4dev/tests/Create/CoreLibsCreateHashTest.php @@ -21,8 +21,10 @@ final class CoreLibsCreateHashTest extends TestCase public function hashData(): array { return [ - 'any string' => [ + 'hash tests' => [ + // this is the string 'text' => 'Some String Text', + // hash list special 'crc32b_reverse' => 'c5c21d91', // crc32b (in revere) 'sha1Short' => '4d2bc9ba0', // sha1Short // via hash @@ -31,6 +33,8 @@ final class CoreLibsCreateHashTest extends TestCase 'fnv132' => '9df444f9', // hash: fnv132 'fnv1a32' => '2c5f91b9', // hash: fnv1a32 'joaat' => '50dab846', // hash: joaat + 'ripemd160' => 'aeae3f041b20136451519edd9361570909300342', // hash: ripemd160, + 'sha256' => '9055080e022f224fa835929b80582b3c71c672206fa3a49a87412c25d9d42ceb', // hash: sha256 ] ]; } @@ -81,7 +85,7 @@ final class CoreLibsCreateHashTest extends TestCase { $list = []; foreach ($this->hashData() as $name => $values) { - foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat'] as $_hash_type) { + foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat', 'ripemd160', 'sha256'] as $_hash_type) { // default value test if ($_hash_type === null) { $hash_type = \CoreLibs\Create\Hash::STANDARD_HASH_SHORT; @@ -114,6 +118,22 @@ final class CoreLibsCreateHashTest extends TestCase ]; } + /** + * Undocumented function + * + * @return array + */ + public function hashStandardProvider(): array + { + $hash_source = 'Some String Text'; + return [ + 'Long Hash check: ' . \CoreLibs\Create\Hash::STANDARD_HASH => [ + $hash_source, + hash(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source) + ], + ]; + } + /** * Undocumented function * @@ -136,9 +156,13 @@ final class CoreLibsCreateHashTest extends TestCase /** * Undocumented function * + * phpcs:disable Generic.Files.LineLength * @covers ::__sha1Short + * @covers ::__crc32b + * @covers ::sha1Short * @dataProvider sha1ShortProvider - * @testdox __sha1Short $input will be $expected (crc32b) and $expected_sha1 (sha1 short) [$_dataName] + * @testdox __sha1Short/__crc32b/sha1short $input will be $expected (crc32b) and $expected_sha1 (sha1 short) [$_dataName] + * phpcs:enable Generic.Files.LineLength * * @param string $input * @param string $expected @@ -149,16 +173,29 @@ final class CoreLibsCreateHashTest extends TestCase // uses crc32b $this->assertEquals( $expected, - \CoreLibs\Create\Hash::__sha1Short($input) + \CoreLibs\Create\Hash::__sha1Short($input), + '__sha1Short depreacted' ); $this->assertEquals( $expected, - \CoreLibs\Create\Hash::__sha1Short($input, false) + \CoreLibs\Create\Hash::__sha1Short($input, false), + '__sha1Short (false) depreacted' + ); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::__crc32b($input), + '__crc32b' ); // sha1 type $this->assertEquals( $expected_sha1, - \CoreLibs\Create\Hash::__sha1Short($input, true) + \CoreLibs\Create\Hash::__sha1Short($input, true), + '__sha1Short (true) depreacted' + ); + $this->assertEquals( + $expected_sha1, + \CoreLibs\Create\Hash::sha1Short($input), + 'sha1Short' ); } @@ -166,8 +203,10 @@ final class CoreLibsCreateHashTest extends TestCase * Undocumented function * * @covers ::__hash + * @covers ::hashShort + * @covers ::hashShort * @dataProvider hashProvider - * @testdox __hash $input with $hash_type will be $expected [$_dataName] + * @testdox __hash/hashShort/hash $input with $hash_type will be $expected [$_dataName] * * @param string $input * @param string|null $hash_type @@ -179,12 +218,24 @@ final class CoreLibsCreateHashTest extends TestCase if ($hash_type === null) { $this->assertEquals( $expected, - \CoreLibs\Create\Hash::__hash($input) + \CoreLibs\Create\Hash::__hash($input), + '__hash' + ); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashShort($input), + 'hashShort' ); } else { $this->assertEquals( $expected, - \CoreLibs\Create\Hash::__hash($input, $hash_type) + \CoreLibs\Create\Hash::__hash($input, $hash_type), + '__hash with hash type' + ); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hash($input, $hash_type), + 'hash with hash type' ); } } @@ -193,8 +244,9 @@ final class CoreLibsCreateHashTest extends TestCase * Undocumented function * * @covers ::__hashLong + * @covers ::hashLong * @dataProvider hashLongProvider - * @testdox __hashLong $input will be $expected [$_dataName] + * @testdox __hashLong/hashLong $input will be $expected [$_dataName] * * @param string $input * @param string $expected @@ -206,6 +258,168 @@ final class CoreLibsCreateHashTest extends TestCase $expected, \CoreLibs\Create\Hash::__hashLong($input) ); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashLong($input) + ); + } + + /** + * Undocumented function + * + * @covers ::hash + * @covers ::hashStd + * @dataProvider hashStandardProvider + * @testdox hash/hashStd $input will be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testHashStandard(string $input, string $expected): void + { + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashStd($input) + ); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hash($input) + ); + } + + /** + * Undocumented function + * + * @covers ::hash + * @testdox hash with invalid type + * + * @return void + */ + public function testInvalidHashType(): void + { + $hash_source = 'Some String Text'; + $expected = hash(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hash($hash_source, 'DOES_NOT_EXIST') + ); + } + + /** + * Note: this only tests default sha256 + * + * @covers ::hashHmac + * @testdox hash hmac test + * + * @return void + */ + public function testHashMac(): void + { + $hash_key = 'FIX KEY'; + $hash_source = 'Some String Text'; + $expected = '16479b3ef6fa44e1cdd8b2dcfaadf314d1a7763635e8738f1e7996d714d9b6bf'; + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key) + ); + } + + /** + * Undocumented function + * + * @covers ::hashHmac + * @testdox hash hmac with invalid type + * + * @return void + */ + public function testInvalidHashMacType(): void + { + $hash_key = 'FIX KEY'; + $hash_source = 'Some String Text'; + $expected = hash_hmac(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source, $hash_key); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key, 'DOES_NOT_EXIST') + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function providerHashTypes(): array + { + return [ + 'Hash crc32b' => [ + 'crc32b', + true, + false, + ], + 'Hash adler32' => [ + 'adler32', + true, + false, + ], + 'HAsh fnv132' => [ + 'fnv132', + true, + false, + ], + 'Hash fnv1a32' => [ + 'fnv1a32', + true, + false, + ], + 'Hash: joaat' => [ + 'joaat', + true, + false, + ], + 'Hash: ripemd160' => [ + 'ripemd160', + true, + true, + ], + 'Hash: sha256' => [ + 'sha256', + true, + true, + ], + 'Hash: invalid' => [ + 'invalid', + false, + false + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::isValidHashType + * @covers ::isValidHashHmacType + * @dataProvider providerHashTypes + * @testdox check if $hash_type is valid for hash $hash_ok and hash hmac $hash_hmac_ok [$_dataName] + * + * @param string $hash_type + * @param bool $hash_ok + * @param bool $hash_hmac_ok + * @return void + */ + public function testIsValidHashAndHashHmacTypes(string $hash_type, bool $hash_ok, bool $hash_hmac_ok): void + { + $this->assertEquals( + $hash_ok, + \CoreLibs\Create\Hash::isValidHashType($hash_type), + 'hash valid' + ); + $this->assertEquals( + $hash_hmac_ok, + \CoreLibs\Create\Hash::isValidHashHmacType($hash_type), + 'hash hmac valid' + ); } } diff --git a/4dev/tests/Create/CoreLibsCreateSessionTest.php b/4dev/tests/Create/CoreLibsCreateSessionTest.php index 2ac833fd..411b610b 100644 --- a/4dev/tests/Create/CoreLibsCreateSessionTest.php +++ b/4dev/tests/Create/CoreLibsCreateSessionTest.php @@ -54,7 +54,9 @@ final class CoreLibsCreateSessionTest extends TestCase 'getSessionId' => '1234abcd4567' ], 'sessionNameGlobals', - false, + [ + 'auto_write_close' => false, + ], ], 'auto write close' => [ 'sessionNameAutoWriteClose', @@ -66,7 +68,9 @@ final class CoreLibsCreateSessionTest extends TestCase 'getSessionId' => '1234abcd4567' ], 'sessionNameAutoWriteClose', - true, + [ + 'auto_write_close' => true, + ], ], ]; } @@ -81,13 +85,14 @@ final class CoreLibsCreateSessionTest extends TestCase * @param string $input * @param array $mock_data * @param string $expected + * @param array $options * @return void */ public function testStartSession( string $input, array $mock_data, string $expected, - ?bool $auto_write_close, + ?array $options, ): void { /** @var \CoreLibs\Create\Session&MockObject $session_mock */ $session_mock = $this->createPartialMock( @@ -174,9 +179,14 @@ final class CoreLibsCreateSessionTest extends TestCase 4, '/^\[SESSION\] Failed to activate session/' ], + 'expired session' => [ + \RuntimeException::class, + 5, + '/^\[SESSION\] Expired session found/' + ], 'not a valid session id returned' => [ \UnexpectedValueException::class, - 5, + 6, '/^\[SESSION\] getSessionId did not return a session id/' ], */ ]; @@ -206,7 +216,8 @@ final class CoreLibsCreateSessionTest extends TestCase $this->expectException($exception); $this->expectExceptionCode($exception_code); $this->expectExceptionMessageMatches($expected_error); - new \CoreLibs\Create\Session($session_name); + // cannot set ini after header sent, plus we are on command line there are no headers + new \CoreLibs\Create\Session($session_name, ['session_strict' => false]); } /** diff --git a/4dev/tests/DB/CoreLibsDBIOTest.php b/4dev/tests/DB/CoreLibsDBIOTest.php index ec8a28e1..f9e66c6a 100644 --- a/4dev/tests/DB/CoreLibsDBIOTest.php +++ b/4dev/tests/DB/CoreLibsDBIOTest.php @@ -17,7 +17,7 @@ Table with Primary Key: table_with_primary_key Table without Primary Key: table_without_primary_key Table with primary key has additional row: -row_primary_key SERIAL PRIMARY KEY, +row_primary_key INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, Each table has the following rows row_int INT, row_numeric NUMERIC, @@ -135,6 +135,7 @@ final class CoreLibsDBIOTest extends TestCase } // check if they already exist, drop them if ($db->dbShowTableMetaData('table_with_primary_key') !== false) { + $db->dbExec("CREATE EXTENSION IF NOT EXISTS pgcrypto"); $db->dbExec("DROP TABLE table_with_primary_key"); $db->dbExec("DROP TABLE table_without_primary_key"); $db->dbExec("DROP TABLE test_meta"); @@ -160,7 +161,6 @@ final class CoreLibsDBIOTest extends TestCase // create the tables $db->dbExec( // primary key name is table + '_id' - // table_with_primary_key_id SERIAL PRIMARY KEY, << [ + 'query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'stm_name' => 'test_stm_a', + 'check_stm_name' => '', + 'check_query' => '', + 'expected' => false + ], + 'different stm_name' => [ + 'query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'stm_name' => 'test_stm_b', + 'check_stm_name' => 'other_name', + 'check_query' => '', + 'expected' => 0 + ], + 'same stm_name' => [ + 'query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'stm_name' => 'test_stm_c', + 'check_stm_name' => 'test_stm_c', + 'check_query' => '', + 'expected' => 1 + ], + 'same stm_name and query' => [ + 'query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'stm_name' => 'test_stm_d', + 'check_stm_name' => 'test_stm_d', + 'check_query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'expected' => 2 + ], + 'same stm_name and different query' => [ + 'query' => 'SELECT row_int, uid FROM table_with_primary_key', + 'stm_name' => 'test_stm_e', + 'check_stm_name' => 'test_stm_e', + 'check_query' => 'SELECT row_int, uid, row_int FROM table_with_primary_key', + 'expected' => 1 + ], + 'insert query test' => [ + 'query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES ($1, $2)', + 'stm_name' => 'test_stm_f', + 'check_stm_name' => 'test_stm_f', + 'check_query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES ($1, $2)', + 'expected' => 2 + ] + ]; + } + + /** + * test cursor status for prepared statement + * + * @covers ::dbPreparedCursorStatus + * @dataProvider providerDbPreparedCursorStatus + * @testdox Check prepared $stm_name ($check_stm_name) status is $expected [$_dataName] + * + * @param string $query + * @param string $stm_name + * @param string $check_stm_name + * @param string $check_query + * @param bool|int $expected + * @return void + */ + public function testDbPreparedCursorStatus( + string $query, + string $stm_name, + string $check_stm_name, + string $check_query, + bool|int $expected + ): void { + $db = new \CoreLibs\DB\IO( + self::$db_config['valid'], + self::$log + ); + $db->dbPrepare($stm_name, $query); + // $db->dbExecute($stm_name); + $this->assertEquals( + $expected, + $db->dbPreparedCursorStatus($check_stm_name, $check_query), + 'check prepared stement cursor status' + ); + unset($db); + } + // - schema set/get tests // dbGetSchema, dbSetSchema @@ -4657,7 +4745,7 @@ final class CoreLibsDBIOTest extends TestCase $res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']); // all hast to be string foreach ($res as $key => $value) { - $this->assertIsString($value, 'Aseert string for column: ' . $key); + $this->assertIsString($value, 'Assert string for column: ' . $key); } // convert base only $db->dbSetConvertFlag(Convert::on); @@ -4670,10 +4758,10 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4687,13 +4775,13 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4707,17 +4795,17 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; case 'json': case 'jsonb': - $this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); + $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4731,25 +4819,25 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; case 'json': case 'jsonb': - $this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); + $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name); break; case 'bytea': // for hex types it must not start with \x $this->assertStringStartsNotWith( '\x', $value, - 'Aseert bytes not starts with \x for column: ' . $key . '/' . $name + 'Assert bytes not starts with \x for column: ' . $key . '/' . $name ); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4921,8 +5009,8 @@ final class CoreLibsDBIOTest extends TestCase ) ), ($params === null ? - $db->dbGetQueryHash($query) : - $db->dbGetQueryHash($query, $params) + $db->dbBuildQueryHash($query) : + $db->dbBuildQueryHash($query, $params) ), 'Failed assertdbGetQueryHash ' ); @@ -5136,8 +5224,142 @@ final class CoreLibsDBIOTest extends TestCase SQL, 'count' => 6, 'convert' => false, + ], + 'comments in insert' => [ + 'query' => << 4, + 'convert' => false + ], + 'comment in update' => [ + 'query' => << 4, + 'convert' => false, + ], + // Note some are not set + 'a complete set of possible' => [ + 'query' => << $3 + AND row_varchar > $4 AND row_varchar < $5 + AND row_varchar >= $6 AND row_varchar <=$7 + AND row_jsonb->'a' = $8 AND row_jsonb->>$9 = 'a' + AND row_jsonb<@$10 AND row_jsonb@>$11 + AND row_varchar ^@ $12 + SQL, + 'count' => 12, + 'convert' => false, + ], + // all the same + 'all the same numbered' => [ + 'query' => << 1, + 'convert' => false, + ], + 'update with case' => [ + 'query' => << 3, + 'convert' => false, + ], + 'select with case' => [ + 'query' => << 2, + 'convert' => false, + ], + // special $$ string case + 'text string, with $ placehoders that could be seen as $$ string' => [ + 'query' => << 6, + 'convert' => false, + ], + // NOTE, in SQL heredoc we cannot write $$ strings parts + 'text string, with $ placehoders are in $$ strings' => [ + 'query' => ' + SELECT row_int + FROM table_with_primary_key + WHERE + row_varchar = $$some string$$ OR + row_varchar = $tag$some string$tag$ OR + row_varchar = $btag$some $1 string$btag$ OR + row_varchar = $btag$some $1 $subtag$ something $subtag$string$btag$ OR + row_varchar = $1 + ', + 'count' => 1, + 'convert' => false, + ], + // a text string with escaped quite + 'text string, with escaped quote' => [ + 'query' => << 2, + 'convert' => false, ] ]; + $string = <<assertTrue(true, 'Default fallback as true'); + break; } } diff --git a/4dev/tests/Language/CoreLibsLanguageGetLocaleTest.php b/4dev/tests/Language/CoreLibsLanguageGetLocaleTest.php index fa053413..afd71a4d 100644 --- a/4dev/tests/Language/CoreLibsLanguageGetLocaleTest.php +++ b/4dev/tests/Language/CoreLibsLanguageGetLocaleTest.php @@ -21,341 +21,6 @@ final class CoreLibsLanguageGetLocaleTest extends TestCase . 'includes' . DIRECTORY_SEPARATOR . 'locale' . DIRECTORY_SEPARATOR; - /** - * set all constant variables that must be set before call - * - * @return void - */ - public static function setUpBeforeClass(): void - { - // default web page encoding setting - /* if (!defined('DEFAULT_ENCODING')) { - define('DEFAULT_ENCODING', 'UTF-8'); - } - if (!defined('DEFAULT_LOCALE')) { - // default lang + encoding - define('DEFAULT_LOCALE', 'en_US.UTF-8'); - } - // site - if (!defined('SITE_ENCODING')) { - define('SITE_ENCODING', DEFAULT_ENCODING); - } - if (!defined('SITE_LOCALE')) { - define('SITE_LOCALE', DEFAULT_LOCALE); - } */ - // just set - /* if (!defined('BASE')) { - define('BASE', str_replace('/configs', '', __DIR__) . DIRECTORY_SEPARATOR); - } - if (!defined('INCLUDES')) { - define('INCLUDES', 'includes' . DIRECTORY_SEPARATOR); - } - if (!defined('LANG')) { - define('LANG', 'lang' . DIRECTORY_SEPARATOR); - } - if (!defined('LOCALE')) { - define('LOCALE', 'locale' . DIRECTORY_SEPARATOR); - } - if (!defined('CONTENT_PATH')) { - define('CONTENT_PATH', 'frontend' . DIRECTORY_SEPARATOR); - } */ - // array session - $_SESSION = []; - global $_SESSION; - } - - /** - * all the test data - * - * @return array - */ - /* public function setLocaleProvider(): array - { - return [ - // 0: locale - // 1: domain - // 2: encoding - // 3: path - // 4: SESSION: DEFAULT_LOCALE - // 5: SESSION: DEFAULT_CHARSET - // 6: expected array - // 7: deprecation message - 'no params, all default constants' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'en_US.UTF-8', - 'lang' => 'en_US', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $locale or unset SESSION locale is deprecated', - ], - 'no params, session charset and lang' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - 'ja_JP', 'UTF-8', - // return array - [ - 'locale' => 'ja_JP', - 'lang' => 'ja_JP', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated' - ], - 'no params, session charset and lang short' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - 'ja', 'UTF-8', - // return array - [ - 'locale' => 'ja', - 'lang' => 'ja', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // param lang (no sessions) - 'locale param only, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // different locale setting - 'locale complex param only, no sessions' => [ - // lang, domain, encoding, path - 'ja_JP.SJIS', null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja_JP.SJIS', - 'lang' => 'ja_JP', - 'domain' => 'frontend', - 'encoding' => 'SJIS', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // param lang and domain (no override) - 'locale, domain params, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated', - ], - // param lang and domain (no override) - 'locale, domain, encoding params, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', 'UTF-8', null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated' - ], - // lang, domain, path (no override) - 'locale, domain and path, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', '', __DIR__ . '/locale_other/', - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?locale_other\/$/", - ], - null - ], - // all params set (no override) - 'all parameter, no sessions' => [ - // lang, domain, encoding, path - 'ja', 'admin', 'UTF-8', __DIR__ . '/locale_other/', - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?locale_other\/$/", - ], - null - ], - // param lang and domain (no override) - 'long locale, domain, encoding params, no sessions' => [ - // lang, domain, encoding, path - 'de_CH.UTF-8@euro', 'admin', 'UTF-8', null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'de_CH.UTF-8@euro', - 'lang' => 'de_CH', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated', - ], - // TODO invalid params (bad path) (no override) - // TODO param calls, but with override set - ]; - } */ - - /** - * Undocumented function - * - * @covers ::setLocale - * @dataProvider setLocaleProvider - * @testdox lang settings lang $language, domain $domain, encoding $encoding, path $path; session lang: $SESSION_DEFAULT_LOCALE, session char: $SESSION_DEFAULT_CHARSET [$_dataName] - * - * @param string|null $language - * @param string|null $domain - * @param string|null $encoding - * @param string|null $path - * @param string|null $SESSION_DEFAULT_LOCALE - * @param string|null $SESSION_DEFAULT_CHARSET - * @param array $expected - * @param string|null $deprecation_message - * @return void - */ - /* public function testsetLocale( - ?string $language, - ?string $domain, - ?string $encoding, - ?string $path, - ?string $SESSION_DEFAULT_LOCALE, - ?string $SESSION_DEFAULT_CHARSET, - array $expected, - ?string $deprecation_message - ): void { - $return_lang_settings = []; - global $_SESSION; - // set override - if ($SESSION_DEFAULT_LOCALE !== null) { - $_SESSION['DEFAULT_LOCALE'] = $SESSION_DEFAULT_LOCALE; - } - if ($SESSION_DEFAULT_CHARSET !== null) { - $_SESSION['DEFAULT_CHARSET'] = $SESSION_DEFAULT_CHARSET; - } - if ($deprecation_message !== null) { - set_error_handler( - static function (int $errno, string $errstr): never { - throw new \Exception($errstr, $errno); - }, - E_USER_DEPRECATED - ); - // catch this with the message - $this->expectExceptionMessage($deprecation_message); - } - // function call - if ( - $language === null && $domain === null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale(); - } elseif ( - $language !== null && $domain === null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language - ); - } elseif ( - $language !== null && $domain !== null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain - ); - } elseif ( - $language !== null && $domain !== null && - $encoding !== null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain, - $encoding - ); - } else { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain, - $encoding, - $path - ); - } - restore_error_handler(); - // print "RETURN: " . print_r($return_lang_settings, true) . "\n"; - - foreach ( - [ - 'locale', 'lang', 'domain', 'encoding', 'path' - ] as $key - ) { - $value = $expected[$key]; - if (strpos($value, "/") === 0) { - // this is regex - $this->assertMatchesRegularExpression( - $value, - $return_lang_settings[$key], - 'assert regex failed for ' . $key - ); - } else { - // assert equal - $this->assertEquals( - $value, - $return_lang_settings[$key], - 'assert equal failed for ' . $key - ); - } - } - // unset all vars - $_SESSION = []; - unset($GLOBALS['OVERRIDE_LANG']); - } */ - /** * all the test data * diff --git a/4dev/tests/Language/locale_other/.gitignore b/4dev/tests/Language/locale_other/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/4dev/tests/Language/locale_other/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/4dev/tests/Logging/CoreLibsLoggingErrorMessagesTest.php b/4dev/tests/Logging/CoreLibsLoggingErrorMessagesTest.php index ad2d606c..5e9c3ac6 100644 --- a/4dev/tests/Logging/CoreLibsLoggingErrorMessagesTest.php +++ b/4dev/tests/Logging/CoreLibsLoggingErrorMessagesTest.php @@ -10,7 +10,7 @@ use CoreLibs\Logging\Logger\Level; /** * Test class for Logging * @coversDefaultClass \CoreLibs\Logging\ErrorMessages - * @testdox \CoreLibs\Logging\ErrorMEssages method tests + * @testdox \CoreLibs\Logging\ErrorMessages method tests */ final class CoreLibsLoggingErrorMessagesTest extends TestCase { diff --git a/4dev/tests/Logging/CoreLibsLoggingLoggingTest.php b/4dev/tests/Logging/CoreLibsLoggingLoggingTest.php index da99916d..3d2b3894 100644 --- a/4dev/tests/Logging/CoreLibsLoggingLoggingTest.php +++ b/4dev/tests/Logging/CoreLibsLoggingLoggingTest.php @@ -395,7 +395,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase } $per_run_id = $log->getLogUniqueId(); $this->assertMatchesRegularExpression( - "/^\d{4}-\d{2}-\d{2}_\d{6}_U_[a-z0-9]{8}$/", + "/^\d{4}-\d{2}-\d{2}_\d{6}\.U_[a-z0-9]{8}$/", $per_run_id, 'assert per log run id 1st' ); @@ -403,7 +403,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase $log->setLogUniqueId(true); $per_run_id_2nd = $log->getLogUniqueId(); $this->assertMatchesRegularExpression( - "/^\d{4}-\d{2}-\d{2}_\d{6}_U_[a-z0-9]{8}$/", + "/^\d{4}-\d{2}-\d{2}_\d{6}\.U_[a-z0-9]{8}$/", $per_run_id_2nd, 'assert per log run id 2nd' ); @@ -824,13 +824,13 @@ final class CoreLibsLoggingLoggingTest extends TestCase $this->assertTrue($log_ok, 'assert ::log (debug) OK'); $this->assertEquals( $log->getLogFile(), - $log->getLogFileId() . '_DEBUG.log' + $log->getLogFileId() . '.DEBUG.log' ); $log_ok = $log->log(Level::Info, 'INFO', group_id: 'GROUP_ID', prefix: 'PREFIX:'); $this->assertTrue($log_ok, 'assert ::log (info) OK'); $this->assertEquals( $log->getLogFile(), - $log->getLogFileId() . '_INFO.log' + $log->getLogFileId() . '.INFO.log' ); } diff --git a/4dev/tests/Security/CoreLibsSecurityAsymmetricAnonymousEncryptionTest.php b/4dev/tests/Security/CoreLibsSecurityAsymmetricAnonymousEncryptionTest.php new file mode 100644 index 00000000..d5d6b461 --- /dev/null +++ b/4dev/tests/Security/CoreLibsSecurityAsymmetricAnonymousEncryptionTest.php @@ -0,0 +1,838 @@ +assertTrue( + $crypt->compareKeyPair($key_pair), + 'set key pair not equal to original key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'automatic set public key not equal to original public key' + ); + $this->assertEquals( + $key_pair, + $crypt->getKeyPair(), + 'set key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'automatic set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @covers ::getKeyPair + * @covers ::compareKeyPair + * @covers ::getPublicKey + * @covers ::comparePublicKey + * @testdox Check if init class set key pair and public key matches to created key pair and public key + * + * @return void + */ + public function testKeyPairPublicKeyInitGetCompare(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption($key_pair, $public_key); + $this->assertTrue( + $crypt->compareKeyPair($key_pair), + 'set key pair not equal to original key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'set public key not equal to original public key' + ); + $this->assertEquals( + $key_pair, + $crypt->getKeyPair(), + 'set key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @covers ::getKeyPair + * @covers ::getPublicKey + * @covers ::comparePublicKey + * @testdox Check if init class set public key matches to created public key + * + * @return void + */ + public function testPublicKeyInitGetCompare(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption(public_key:$public_key); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'set public key not equal to original public key' + ); + $this->assertEquals( + null, + $crypt->getKeyPair(), + 'unset set key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @covers ::setKeyPair + * @covers ::getKeyPair + * @covers ::compareKeyPair + * @covers ::getPublicKey + * @covers ::comparePublicKey + * @testdox Check if set key pair after class init matches to created key pair and public key + * + * @return void + */ + public function testKeyPairSetGetCompare(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption(); + $crypt->setKeyPair($key_pair); + $this->assertTrue( + $crypt->compareKeyPair($key_pair), + 'post class init set key pair not equal to original key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'post class init automatic set public key not equal to original public key' + ); + $this->assertEquals( + $key_pair, + $crypt->getKeyPair(), + 'post class init set key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'post class init automatic set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @covers ::setKeyPair + * @covers ::setPublicKey + * @covers ::getKeyPair + * @covers ::compareKeyPair + * @covers ::getPublicKey + * @covers ::comparePublicKey + * @testdox Check if set key pair after class init matches to created key pair and public key + * + * @return void + */ + public function testKeyPairPublicKeySetGetCompare(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption(); + $crypt->setKeyPair($key_pair); + $crypt->setPublicKey($public_key); + $this->assertTrue( + $crypt->compareKeyPair($key_pair), + 'post class init set key pair not equal to original key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'post class init set public key not equal to original public key' + ); + $this->assertEquals( + $key_pair, + $crypt->getKeyPair(), + 'post class init set key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'post class init set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @covers ::setPublicKey + * @covers ::getKeyPair + * @covers ::compareKeyPair + * @covers ::getPublicKey + * @covers ::comparePublicKey + * @testdox Check if set key pair after class init matches to created key pair and public key + * + * @return void + */ + public function testPublicKeySetGetCompare(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption(); + $crypt->setPublicKey($public_key); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'post class init set public key not equal to original public key' + ); + $this->assertEquals( + null, + $crypt->getKeyPair(), + 'post class init unset key pair returned not equal to original key pair' + ); + $this->assertEquals( + $public_key, + $crypt->getPublicKey(), + 'post class init set public key returned not equal to original public key' + ); + } + + /** + * Undocumented function + * + * @testdox Check different key pair and public key set + * + * @return void + */ + public function testDifferentSetKeyPairPublicKey() + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $key_pair_2 = CreateKey::createKeyPair(); + $public_key_2 = CreateKey::getPublicKey($key_pair_2); + $crypt = new AsymmetricAnonymousEncryption($key_pair, $public_key_2); + $this->assertTrue( + $crypt->compareKeyPair($key_pair), + 'key pair set matches key pair created' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key_2), + 'alternate public key set matches alternate public key created' + ); + $this->assertFalse( + $crypt->comparePublicKey($public_key), + 'alternate public key set does not match key pair public key' + ); + } + + /** + * Undocumented function + * + * @testdox Check if new set privat key does not overwrite set public key + * + * @return void + */ + public function testUpdateKeyPairNotUpdatePublicKey(): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $crypt = new AsymmetricAnonymousEncryption($key_pair); + $this->assertTrue( + $crypt->compareKeyPair($key_pair), + 'set key pair not equal to original key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'set public key not equal to original public key' + ); + $key_pair_2 = CreateKey::createKeyPair(); + $public_key_2 = CreateKey::getPublicKey($key_pair_2); + $crypt->setKeyPair($key_pair_2); + $this->assertTrue( + $crypt->compareKeyPair($key_pair_2), + 'new set key pair not equal to original new key pair' + ); + $this->assertTrue( + $crypt->comparePublicKey($public_key), + 'original set public key not equal to original public key' + ); + $this->assertFalse( + $crypt->comparePublicKey($public_key_2), + 'new public key equal to original public key' + ); + } + + // MARK: empty encrytped string + + /** + * Undocumented function + * + * @covers ::decryptKey + * @covers ::decrypt + * @testdox Test empty encrypted string to decrypt + * + * @return void + */ + public function testEmptyDecryptionString(): void + { + $this->expectExceptionMessage('Encrypted string cannot be empty'); + AsymmetricAnonymousEncryption::decryptKey('', CreateKey::generateRandomKey()); + } + + // MARK: encrypt/decrypt + + /** + * Undocumented function + * + * @return array + */ + public function providerEncryptDecryptSuccess(): array + { + return [ + 'valid string' => [ + 'input' => 'I am a secret', + 'expected' => 'I am a secret', + ], + ]; + } + + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccess(string $input, string $expected): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + // test class + $crypt = new AsymmetricAnonymousEncryption($key_pair); + $encrypted = $crypt->encrypt($input); + $decrypted = $crypt->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class call', + ); + $crypt = new AsymmetricAnonymousEncryption($key_pair, $public_key); + $encrypted = $crypt->encrypt($input); + $decrypted = $crypt->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class call botjh set', + ); + } + + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt indirect $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccessIndirect(string $input, string $expected): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + // test indirect + $encrypted = AsymmetricAnonymousEncryption::getInstance(public_key:$public_key)->encrypt($input); + $decrypted = AsymmetricAnonymousEncryption::getInstance($key_pair)->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class Instance call', + ); + } + + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt indirect with public key $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccessIndirectPublicKey(string $input, string $expected): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + // test indirect + $encrypted = AsymmetricAnonymousEncryption::getInstance(public_key:$public_key)->encrypt($input); + $decrypted = AsymmetricAnonymousEncryption::getInstance($key_pair)->decrypt($encrypted); + $this->assertEquals( + $expected, + $decrypted, + 'Class Instance call public key', + ); + } + + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt static $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccessStatic(string $input, string $expected): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + // test static + $encrypted = AsymmetricAnonymousEncryption::encryptKey($input, $public_key); + $decrypted = AsymmetricAnonymousEncryption::decryptKey($encrypted, $key_pair); + + $this->assertEquals( + $expected, + $decrypted, + 'Static call', + ); + } + + // MARK: invalid decrypt key + + /** + * Undocumented function + * + * @return array + */ + public function providerEncryptFailed(): array + { + return [ + 'wrong decryption key' => [ + 'input' => 'I am a secret', + 'excpetion_message' => 'Invalid key pair' + ], + ]; + } + + /** + * Test decryption with wrong key + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptFailed + * @testdox decrypt with wrong key $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testEncryptFailed(string $input, string $exception_message): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $wrong_key_pair = CreateKey::createKeyPair(); + + // wrong key in class call + $crypt = new AsymmetricAnonymousEncryption(public_key:$public_key); + $encrypted = $crypt->encrypt($input); + $this->expectExceptionMessage($exception_message); + $crypt->setKeyPair($wrong_key_pair); + $crypt->decrypt($encrypted); + } + + /** + * Test decryption with wrong key + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptFailed + * @testdox decrypt indirect with wrong key $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testEncryptFailedIndirect(string $input, string $exception_message): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $wrong_key_pair = CreateKey::createKeyPair(); + + // class instance + $encrypted = AsymmetricAnonymousEncryption::getInstance(public_key:$public_key)->encrypt($input); + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::getInstance($wrong_key_pair)->decrypt($encrypted); + } + + /** + * Test decryption with wrong key + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptFailed + * @testdox decrypt static with wrong key $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testEncryptFailedStatic(string $input, string $exception_message): void + { + $key_pair = CreateKey::createKeyPair(); + $public_key = CreateKey::getPublicKey($key_pair); + $wrong_key_pair = CreateKey::createKeyPair(); + + // class static + $encrypted = AsymmetricAnonymousEncryption::encryptKey($input, $public_key); + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::decryptKey($encrypted, $wrong_key_pair); + } + + // MARK: invalid key pair + + /** + * Undocumented function + * + * @return array + */ + public function providerWrongKeyPair(): array + { + return [ + 'not hex key pair' => [ + 'key_pair' => 'not_a_hex_key_pair', + 'exception_message' => 'Invalid hex key pair' + ], + 'too short hex key pair' => [ + 'key_pair' => '1cabd5cba9e042f12522f4ff2de5c31d233b', + 'excpetion_message' => 'Key pair is not the correct size (must be ' + ], + 'empty key pair' => [ + 'key_pair' => '', + 'excpetion_message' => 'Key pair cannot be empty' + ] + ]; + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongKeyPair + * @testdox wrong key pair $key_pair throws $exception_message [$_dataName] + * + * @param string $key_pair + * @param string $exception_message + * @return void + */ + public function testWrongKeyPair(string $key_pair, string $exception_message): void + { + $enc_key_pair = CreateKey::createKeyPair(); + + // class + $this->expectExceptionMessage($exception_message); + $crypt = new AsymmetricAnonymousEncryption($key_pair); + $this->expectExceptionMessage($exception_message); + $crypt->encrypt('test'); + $crypt->setKeyPair($enc_key_pair); + $encrypted = $crypt->encrypt('test'); + $this->expectExceptionMessage($exception_message); + $crypt->setKeyPair($key_pair); + $crypt->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongKeyPair + * @testdox wrong key pair indirect $key_pair throws $exception_message [$_dataName] + * + * @param string $key_pair + * @param string $exception_message + * @return void + */ + public function testWrongKeyPairIndirect(string $key_pair, string $exception_message): void + { + $enc_key_pair = CreateKey::createKeyPair(); + + // set valid encryption + $encrypted = AsymmetricAnonymousEncryption::getInstance($enc_key_pair)->encrypt('test'); + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::getInstance($key_pair)->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongKeyPair + * @testdox wrong key pair static $key_pair throws $exception_message [$_dataName] + * + * @param string $key_pair + * @param string $exception_message + * @return void + */ + public function testWrongKeyPairStatic(string $key_pair, string $exception_message): void + { + $enc_key_pair = CreateKey::createKeyPair(); + + // set valid encryption + $encrypted = AsymmetricAnonymousEncryption::encryptKey('test', CreateKey::getPublicKey($enc_key_pair)); + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::decryptKey($encrypted, $key_pair); + } + + // MARK: invalid public key + + /** + * Undocumented function + * + * @return array + */ + public function providerWrongPublicKey(): array + { + return [ + 'not hex public key' => [ + 'public_key' => 'not_a_hex_public_key', + 'exception_message' => 'Invalid hex public key' + ], + 'too short hex public key' => [ + 'public_key' => '1cabd5cba9e042f12522f4ff2de5c31d233b', + 'excpetion_message' => 'Public key is not the correct size (must be ' + ], + 'empty public key' => [ + 'public_key' => '', + 'excpetion_message' => 'Public key cannot be empty' + ] + ]; + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongPublicKey + * @testdox wrong public key $public_key throws $exception_message [$_dataName] + * + * @param string $public_key + * @param string $exception_message + * @return void + */ + public function testWrongPublicKey(string $public_key, string $exception_message): void + { + $enc_key_pair = CreateKey::createKeyPair(); + // $enc_public_key = CreateKey::getPublicKey($enc_key_pair); + + // class + $this->expectExceptionMessage($exception_message); + $crypt = new AsymmetricAnonymousEncryption(public_key:$public_key); + $this->expectExceptionMessage($exception_message); + $crypt->decrypt('test'); + $crypt->setKeyPair($enc_key_pair); + $encrypted = $crypt->encrypt('test'); + $this->expectExceptionMessage($exception_message); + $crypt->setPublicKey($public_key); + $crypt->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongPublicKey + * @testdox wrong public key indirect $key throws $exception_message [$_dataName] + * + * @param string $key + * @param string $exception_message + * @return void + */ + public function testWrongPublicKeyIndirect(string $key, string $exception_message): void + { + $enc_key = CreateKey::createKeyPair(); + + // class instance + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::getInstance(public_key:$key)->encrypt('test'); + // we must encrypt valid thing first so we can fail with the wrong key + $encrypted = AsymmetricAnonymousEncryption::getInstance($enc_key)->encrypt('test'); + // $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::getInstance($key)->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongPublicKey + * @testdox wrong public key static $key throws $exception_message [$_dataName] + * + * @param string $key + * @param string $exception_message + * @return void + */ + public function testWrongPublicKeyStatic(string $key, string $exception_message): void + { + $enc_key = CreateKey::createKeyPair(); + + // class static + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::encryptKey('test', $key); + // we must encrypt valid thing first so we can fail with the wrong key + $encrypted = AsymmetricAnonymousEncryption::encryptKey('test', $enc_key); + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::decryptKey($encrypted, $key); + } + + // MARK: wrong cipher text + + /** + * Undocumented function + * + * @return array + */ + public function providerWrongCiphertext(): array + { + return [ + 'invalid cipher text' => [ + 'input' => 'short', + 'exception_message' => 'base642bin failed: ' + ], + 'cannot decrypt' => [ + // phpcs:disable Generic.Files.LineLength + 'input' => 'Um8tBGiVfFAOg2YoUgA5fTqK1wXPB1S7uxhPNE1lqDxgntkEhYJDOmjXa0DMpBlYHjab6sC4mgzwZSzGCUnXDAgsHckwYwfAzs/r', + // phpcs:enable Generic.Files.LineLength + 'exception_message' => 'Invalid key pair' + ], + 'invalid text' => [ + 'input' => 'U29tZSB0ZXh0IGhlcmU=', + 'exception_message' => 'Invalid key pair' + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::decrypt + * @dataProvider providerWrongCiphertext + * @testdox too short ciphertext $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testWrongCiphertext(string $input, string $exception_message): void + { + $key = CreateKey::createKeyPair(); + // class + $crypt = new AsymmetricAnonymousEncryption($key); + $this->expectExceptionMessage($exception_message); + $crypt->decrypt($input); + } + + /** + * Undocumented function + * + * @covers ::decryptKey + * @dataProvider providerWrongCiphertext + * @testdox too short ciphertext indirect $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testWrongCiphertextIndirect(string $input, string $exception_message): void + { + $key = CreateKey::createKeyPair(); + + // class instance + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::getInstance($key)->decrypt($input); + + // class static + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::decryptKey($input, $key); + } + + /** + * Undocumented function + * + * @covers ::decryptKey + * @dataProvider providerWrongCiphertext + * @testdox too short ciphertext static $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testWrongCiphertextStatic(string $input, string $exception_message): void + { + $key = CreateKey::createKeyPair(); + // class static + $this->expectExceptionMessage($exception_message); + AsymmetricAnonymousEncryption::decryptKey($input, $key); + } +} + +// __END__ diff --git a/4dev/tests/Security/CoreLibsSecurityPasswordTest.php b/4dev/tests/Security/CoreLibsSecurityPasswordTest.php index 80adec64..7904997a 100644 --- a/4dev/tests/Security/CoreLibsSecurityPasswordTest.php +++ b/4dev/tests/Security/CoreLibsSecurityPasswordTest.php @@ -13,6 +13,11 @@ use PHPUnit\Framework\TestCase; */ final class CoreLibsSecurityPasswordTest extends TestCase { + /** + * Undocumented function + * + * @return array + */ public function passwordProvider(): array { return [ @@ -21,6 +26,11 @@ final class CoreLibsSecurityPasswordTest extends TestCase ]; } + /** + * Note: we need different hash types for PHP versions + * + * @return array + */ public function passwordRehashProvider(): array { return [ @@ -63,6 +73,10 @@ final class CoreLibsSecurityPasswordTest extends TestCase */ public function testPasswordRehashCheck(string $input, bool $expected): void { + // in PHP 8.4 the length is $12 + if (PHP_VERSION_ID > 80400) { + $input = str_replace('$2y$10$', '$2y$12$', $input); + } $this->assertEquals( $expected, \CoreLibs\Security\Password::passwordRehashCheck($input) diff --git a/4dev/tests/Security/CoreLibsSecuritySymmetricEncryptionTest.php b/4dev/tests/Security/CoreLibsSecuritySymmetricEncryptionTest.php index d3f502af..1251a6da 100644 --- a/4dev/tests/Security/CoreLibsSecuritySymmetricEncryptionTest.php +++ b/4dev/tests/Security/CoreLibsSecuritySymmetricEncryptionTest.php @@ -15,6 +15,77 @@ use CoreLibs\Security\SymmetricEncryption; */ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase { + // MARK: key set compare + + /** + * Undocumented function + * + * @covers ::compareKey + * @covers ::getKey + * @testdox Check if init class set key matches to created key + * + * @return void + */ + public function testKeyInitGetCompare(): void + { + $key = CreateKey::generateRandomKey(); + $crypt = new SymmetricEncryption($key); + $this->assertTrue( + $crypt->compareKey($key), + 'set key not equal to original key' + ); + $this->assertEquals( + $key, + $crypt->getKey(), + 'set key returned not equal to original key' + ); + } + + /** + * Undocumented function + * + * @covers ::setKey + * @covers ::compareKey + * @covers ::getKey + * @testdox Check if set key after class init matches to created key + * + * @return void + */ + public function testKeySetGetCompare(): void + { + $key = CreateKey::generateRandomKey(); + $crypt = new SymmetricEncryption(); + $crypt->setKey($key); + $this->assertTrue( + $crypt->compareKey($key), + 'set key not equal to original key' + ); + $this->assertEquals( + $key, + $crypt->getKey(), + 'set key returned not equal to original key' + ); + } + + // MARK: empty encrypted string + + /** + * Undocumented function + * + * @covers ::decryptKey + * @covers ::decrypt + * @testdox Test empty encrypted string to decrypt + * + * @return void + */ + public function testEmptyDecryptionString(): void + { + $this->expectExceptionMessage('Encrypted string cannot be empty'); + SymmetricEncryption::decryptKey('', CreateKey::generateRandomKey()); + } + + // MARK: encrypt/decrypt compare + /** * Undocumented function * @@ -56,7 +127,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $decrypted, 'Class call', ); + } + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt indirect $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccessIndirect(string $input, string $expected): void + { + $key = CreateKey::generateRandomKey(); // test indirect $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); $decrypted = SymmetricEncryption::getInstance($key)->decrypt($encrypted); @@ -65,7 +153,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $decrypted, 'Class Instance call', ); + } + /** + * test encrypt/decrypt produce correct output + * + * @covers ::generateRandomKey + * @covers ::encryptKey + * @covers ::decryptKey + * @dataProvider providerEncryptDecryptSuccess + * @testdox encrypt/decrypt static $input must be $expected [$_dataName] + * + * @param string $input + * @param string $expected + * @return void + */ + public function testEncryptDecryptSuccessStatic(string $input, string $expected): void + { + $key = CreateKey::generateRandomKey(); // test static $encrypted = SymmetricEncryption::encryptKey($input, $key); $decrypted = SymmetricEncryption::decryptKey($encrypted, $key); @@ -77,6 +182,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase ); } + // MARK: invalid key + /** * Undocumented function * @@ -114,13 +221,51 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $crypt = new SymmetricEncryption($key); $encrypted = $crypt->encrypt($input); $this->expectExceptionMessage($exception_message); - $crypt->setKey($key); + $crypt->setKey($wrong_key); $crypt->decrypt($encrypted); + } + + /** + * Test decryption with wrong key + * + * @covers ::generateRandomKey + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerEncryptFailed + * @testdox decrypt indirect with wrong key $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testEncryptFailedIndirect(string $input, string $exception_message): void + { + $key = CreateKey::generateRandomKey(); + $wrong_key = CreateKey::generateRandomKey(); // class instance $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); $this->expectExceptionMessage($exception_message); SymmetricEncryption::getInstance($wrong_key)->decrypt($encrypted); + } + + /** + * Test decryption with wrong key + * + * @covers ::generateRandomKey + * @covers ::encryptKey + * @covers ::decryptKey + * @dataProvider providerEncryptFailed + * @testdox decrypt static with wrong key $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testEncryptFailedStatic(string $input, string $exception_message): void + { + $key = CreateKey::generateRandomKey(); + $wrong_key = CreateKey::generateRandomKey(); // class static $encrypted = SymmetricEncryption::encryptKey($input, $key); @@ -128,6 +273,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase SymmetricEncryption::decryptKey($encrypted, $wrong_key); } + // MARK: wrong key + /** * Undocumented function * @@ -144,6 +291,10 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase 'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b', 'excpetion_message' => 'Key is not the correct size (must be ' ], + 'empty key' => [ + 'key' => '', + 'excpetion_message' => 'Key cannot be empty' + ] ]; } @@ -164,6 +315,7 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $enc_key = CreateKey::generateRandomKey(); // class + $this->expectExceptionMessage($exception_message); $crypt = new SymmetricEncryption($key); $this->expectExceptionMessage($exception_message); $crypt->encrypt('test'); @@ -172,6 +324,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $this->expectExceptionMessage($exception_message); $crypt->setKey($key); $crypt->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encrypt + * @covers ::decrypt + * @dataProvider providerWrongKey + * @testdox wrong key indirect $key throws $exception_message [$_dataName] + * + * @param string $key + * @param string $exception_message + * @return void + */ + public function testWrongKeyIndirect(string $key, string $exception_message): void + { + $enc_key = CreateKey::generateRandomKey(); // class instance $this->expectExceptionMessage($exception_message); @@ -180,6 +349,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $encrypted = SymmetricEncryption::getInstance($enc_key)->encrypt('test'); $this->expectExceptionMessage($exception_message); SymmetricEncryption::getInstance($key)->decrypt($encrypted); + } + + /** + * test invalid key provided to decrypt or encrypt + * + * @covers ::encryptKey + * @covers ::decryptKey + * @dataProvider providerWrongKey + * @testdox wrong key static $key throws $exception_message [$_dataName] + * + * @param string $key + * @param string $exception_message + * @return void + */ + public function testWrongKeyStatic(string $key, string $exception_message): void + { + $enc_key = CreateKey::generateRandomKey(); // class static $this->expectExceptionMessage($exception_message); @@ -190,6 +376,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase SymmetricEncryption::decryptKey($encrypted, $key); } + // MARK: wrong input + /** * Undocumented function * @@ -232,6 +420,49 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase $this->expectExceptionMessage($exception_message); SymmetricEncryption::decryptKey($input, $key); } + + /** + * Undocumented function + * + * @covers ::decryptKey + * @dataProvider providerWrongCiphertext + * @testdox too short ciphertext indirect $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testWrongCiphertextIndirect(string $input, string $exception_message): void + { + $key = CreateKey::generateRandomKey(); + + // class instance + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::getInstance($key)->decrypt($input); + + // class static + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::decryptKey($input, $key); + } + + /** + * Undocumented function + * + * @covers ::decryptKey + * @dataProvider providerWrongCiphertext + * @testdox too short ciphertext static $input throws $exception_message [$_dataName] + * + * @param string $input + * @param string $exception_message + * @return void + */ + public function testWrongCiphertextStatic(string $input, string $exception_message): void + { + $key = CreateKey::generateRandomKey(); + // class static + $this->expectExceptionMessage($exception_message); + SymmetricEncryption::decryptKey($input, $key); + } } // __END__ diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 276b2ef6..e5a22024 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -969,44 +969,76 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase "query" => ["foo-get" => "bar"] ]); $this->assertEquals("200", $response["code"], "multi call: get response code not matching"); - $this->assertEquals( - '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' - . '"HTTP_FIRST_CALL":"get","HTTP_ACCEPT":"*\/*",' - . '"HTTP_HOST":"soba.egplusww.jp"},' - . '"REQUEST_TYPE":"GET",' - . '"PARAMS":{"foo-get":"bar"},"BODY":null}', - $response['content'], - 'multi call: get content not matching' - ); + if (PHP_VERSION_ID >= 80400) { + $this->assertEquals( + '{"HEADERS":{"HTTP_HOST":"soba.egplusww.jp",' + . '"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_FIRST_CALL":"get",' + . '"HTTP_ACCEPT":"*\/*"},"REQUEST_TYPE":"GET","PARAMS":{"foo-get":"bar"},"BODY":null}', + $response['content'], + 'multi call: get content not matching' + ); + } else { + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_FIRST_CALL":"get","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"GET",' + . '"PARAMS":{"foo-get":"bar"},"BODY":null}', + $response['content'], + 'multi call: get content not matching' + ); + } // post $response = $curl->post($this->url_basic, [ "headers" => ["second-call" => "post"], "body" => ["foo-post" => "baz"] ]); $this->assertEquals("200", $response["code"], "multi call: post response code not matching"); - $this->assertEquals( - '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' - . '"HTTP_SECOND_CALL":"post","HTTP_ACCEPT":"*\/*",' - . '"HTTP_HOST":"soba.egplusww.jp"},' - . '"REQUEST_TYPE":"POST",' - . '"PARAMS":[],"BODY":{"foo-post":"baz"}}', - $response['content'], - 'multi call: post content not matching' - ); + if (PHP_VERSION_ID >= 80400) { + $this->assertEquals( + '{"HEADERS":{"HTTP_HOST":"soba.egplusww.jp",' + . '"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_SECOND_CALL":"post","HTTP_ACCEPT":"*\/*"},' + . '"REQUEST_TYPE":"POST","PARAMS":[],"BODY":{"foo-post":"baz"}}', + $response['content'], + 'multi call: post content not matching' + ); + } else { + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_SECOND_CALL":"post","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"POST",' + . '"PARAMS":[],"BODY":{"foo-post":"baz"}}', + $response['content'], + 'multi call: post content not matching' + ); + } // delete $response = $curl->delete($this->url_basic, [ "headers" => ["third-call" => "delete"], ]); $this->assertEquals("200", $response["code"], "multi call: delete response code not matching"); - $this->assertEquals( - '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' - . '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*",' - . '"HTTP_HOST":"soba.egplusww.jp"},' - . '"REQUEST_TYPE":"DELETE",' - . '"PARAMS":[],"BODY":[]}', - $response['content'], - 'multi call: delete content not matching' - ); + if (PHP_VERSION_ID >= 80400) { + $this->assertEquals( + '{"HEADERS":{"HTTP_HOST":"soba.egplusww.jp",' + . '"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*"},' + . '"REQUEST_TYPE":"DELETE","PARAMS":[],"BODY":[]}', + $response['content'], + 'multi call: delete content not matching' + ); + } else { + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"DELETE",' + . '"PARAMS":[],"BODY":[]}', + $response['content'], + 'multi call: delete content not matching' + ); + } } // MARK: auth header set via config diff --git a/4dev/update/20241203_update_edit_tables/edit_tables_cuid_cuuid_update_add.sql b/4dev/update/20241203_update_edit_tables/edit_tables_cuid_cuuid_update_add.sql index f4e36ec3..5bbb9c49 100644 --- a/4dev/update/20241203_update_edit_tables/edit_tables_cuid_cuuid_update_add.sql +++ b/4dev/update/20241203_update_edit_tables/edit_tables_cuid_cuuid_update_add.sql @@ -1,8 +1,16 @@ -- 20241203: update edit tables ALTER TABLE edit_generic ADD cuuid UUID DEFAULT gen_random_uuid(); -ALTER TABLE edit_log ADD ecuid VARCHAR; -ALTER TABLE edit_log ADD ecuuid VARCHAR; +ALTER TABLE edit_log ADD eucuid VARCHAR; +ALTER TABLE edit_log ADD eucuuid VARCHAR; ALTER TABLE edit_log ADD action_sub_id VARCHAR; +ALTER TABLE edit_log ADD http_data JSONB; +ALTER TABLE edit_log ADD ip_address JSONB; +ALTER TABLE edit_log ADD action_data JSONB; +ALTER TABLE edit_log ADD request_scheme VARCHAR; +ALTER TABLE edit_user ADD force_logout INT DEFAULT 0; +COMMENT ON COLUMN edit_user.force_logout IS 'Counter for forced log out, if this one is higher than the session set one the session gets terminated'; +ALTER TABLE edit_user ADD last_login TIMESTAMP WITHOUT TIME ZONE; +COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; -- update set_edit_gneric -- adds the created or updated date tags @@ -14,13 +22,15 @@ DECLARE random_length INT = 25; -- that should be long enough BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); NEW.cuid := random_string(random_length); NEW.cuuid := gen_random_uuid(); ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; $$ LANGUAGE 'plpgsql'; + +-- END -- diff --git a/README.md b/README.md index dec1cb13..76278ef3 100644 --- a/README.md +++ b/README.md @@ -114,3 +114,11 @@ Add `.libs` to the master .gitingore ### Update phpunit On a version update the old phpunit folder in .libs has to be removed and the new version extracted again + +## Javascript + +The original edit.js javascript functions are now in utils.js or utils.min.js. + +The development for thos files is located in a different repository + +https://[service]/CodeBlocks/javascript-utils diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..ff91f7eb --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,59 @@ +import globals from 'globals'; +import pluginJs from '@eslint/js'; + +/* +module.exports = { + // in globals block + 'extends': 'eslint:recommended', + 'parserOptions': { + 'ecmaVersion': 6 + }, + // rules copied +}; +*/ + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + {languageOptions: { + globals: { + ...globals.browser, + ...globals.jquery + } + }}, + pluginJs.configs.recommended, + { + 'rules': { + 'indent': [ + 'error', + 'tab', + { + 'SwitchCase': 1 + } + ], + 'linebreak-style': [ + 'error', + 'unix' + ], + // 'quotes': [ + // 'error', + // 'single' + // ], + 'semi': [ + 'error', + 'always' + ], + 'no-console': 'off', + 'no-unused-vars': [ + 'error', { + 'vars': 'all', + 'args': 'after-used', + 'ignoreRestSiblings': false + } + ], + // Requires eslint >= v8.14.0 + 'no-constant-binary-expression': 'error' + } + } +]; + +// __END__ diff --git a/jsconfig.json b/jsconfig.json index 9a284483..9d08c675 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,11 @@ +// https://www.typescriptlang.org/tsconfig/#compilerOptions { "compilerOptions": { "module": "ESNext", "moduleResolution": "Node", "target": "ES2020", "jsx": "react", + "checkJs": true, "allowImportingTsExtensions": true, "strictNullChecks": true, "strictFunctionTypes": true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..5fdccbe6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1567 @@ +{ + "name": "core-libraries", + "version": "9.26.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "core-libraries", + "version": "9.26.8", + "devDependencies": { + "@eslint/js": "^9.20.0", + "esbuild": "^0.25.0", + "eslint": "^9.20.1", + "globals": "^15.15.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.11.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..af2d2ff1 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "core-libraries", + "version": "9.26.8", + "main": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Clemens Schwaighofer", + "license": "", + "description": "Core Libraries", + "devDependencies": { + "@eslint/js": "^9.20.0", + "esbuild": "^0.25.0", + "eslint": "^9.20.1", + "globals": "^15.15.0" + } +} diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 7011a7dd..77105949 100755 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -10,5 +10,6 @@ $_SERVER['HTTP_HOST'] = 'soba.tokyo.tequila.jp'; define('BASE_NAME', ''); define('SITE_DOMAIN', ''); define('HOST_NAME', 'soba.tokyo.tequila.jp'); +define('DEFAULT_ENCODING', 'en_US.UTF-8'); // __END__ diff --git a/phpstan.neon b/phpstan.neon index 1d6c4ff5..6d3c1508 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,7 +9,7 @@ parameters: #friendly: # lineBefore: 3 # lineAfter: 3 - level: 8 # max is now 9 + level: 8 # max is now 10 # strictRules: # allRules: false checkMissingCallableSignature: true diff --git a/phpunit.xml b/phpunit.xml index dcaf3022..dfc1669b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,8 +1,13 @@ + + + 4dev/tests + + diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php index 3d310ff4..d74e32e8 100644 --- a/www/admin/UrlRequests.target.php +++ b/www/admin/UrlRequests.target.php @@ -52,7 +52,7 @@ header("Content-Type: application/json; charset=UTF-8"); if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) { header("HTTP/1.1 401 Unauthorized"); print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}'); - exit; + exit(1); } // if server request type is get set file_get to null -> no body @@ -61,7 +61,7 @@ if ($_SERVER['REQUEST_METHOD'] == "GET") { } elseif (($file_get = file_get_contents('php://input')) === false) { header("HTTP/1.1 404 Not Found"); print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}'); - exit; + exit(1); } // str_replace('\"', '"', trim($file_get, '"')); diff --git a/www/admin/class_test.admin.backend.php b/www/admin/class_test.admin.backend.php index 492bbd8e..86402b18 100644 --- a/www/admin/class_test.admin.backend.php +++ b/www/admin/class_test.admin.backend.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.array.php b/www/admin/class_test.array.php index 85dbfc0a..2e826f4d 100644 --- a/www/admin/class_test.array.php +++ b/www/admin/class_test.array.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -250,6 +250,21 @@ foreach (array_keys($array) as $search) { } print "Key not exists: " . DgS::printAr(ArrayHandler::arrayGetNextKey($array, 'z')) . "
"; +print "
"; +$keys = ['b', 'c', 'f']; +print "Return only: " . DgS::printAr($keys) . ": " + . DgS::printAr(ArrayHandler::arrayReturnMatchingKeyOnly($array, $keys)) . "
"; + +$out = array_filter($array, fn($key) => in_array($key, $keys), ARRAY_FILTER_USE_KEY); +print "array filter: " . DgS::printAr($keys) . ": " . DgS::printAr($out) . "
"; +$out = array_intersect_key( + $array, + array_flip($keys) +); +print "array intersect key: " . DgS::printAr($keys) . ": " . DgS::printAr($out) . "
"; + +print "array + suffix: " . DgS::printAr(ArrayHandler::arrayModifyKey($array, key_mod_suffix:'_attached')) . "
"; + print ""; // __END__ diff --git a/www/admin/class_test.autoloader.php b/www/admin/class_test.autoloader.php index 173ce5af..97c53180 100644 --- a/www/admin/class_test.autoloader.php +++ b/www/admin/class_test.autoloader.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); // basic class test file diff --git a/www/admin/class_test.byte.php b/www/admin/class_test.byte.php index 3105c09f..cc17ce2f 100644 --- a/www/admin/class_test.byte.php +++ b/www/admin/class_test.byte.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.check.colors.php b/www/admin/class_test.check.colors.php index 705ea6bf..73fb8658 100644 --- a/www/admin/class_test.check.colors.php +++ b/www/admin/class_test.check.colors.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.class-calls.php b/www/admin/class_test.class-calls.php index b184069b..dd5d52ce 100644 --- a/www/admin/class_test.class-calls.php +++ b/www/admin/class_test.class-calls.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.config.direct.php b/www/admin/class_test.config.direct.php index 9ce49225..7e824bda 100644 --- a/www/admin/class_test.config.direct.php +++ b/www/admin/class_test.config.direct.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.config.link.php b/www/admin/class_test.config.link.php index f19acbfe..d980e56c 100644 --- a/www/admin/class_test.config.link.php +++ b/www/admin/class_test.config.link.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.convert.colors.php b/www/admin/class_test.convert.colors.php index 6754aa51..462c7ef1 100644 --- a/www/admin/class_test.convert.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -18,7 +18,7 @@ require 'config.php'; $LOG_FILE_ID = 'classTest-convert-colors'; ob_end_flush(); -use CoreLibs\Convert\Colors; +// use CoreLibs\Convert\Colors; use CoreLibs\Convert\Color\Color; use CoreLibs\Convert\Color\Coordinates; use CoreLibs\Debug\Support as DgS; @@ -29,7 +29,6 @@ $log = new CoreLibs\Logging\Logging([ 'log_file_id' => $LOG_FILE_ID, 'log_per_date' => true, ]); -$color_class = 'CoreLibs\Convert\Colors'; /** * print out a color block with info @@ -131,7 +130,8 @@ try { } catch (\LengthException $e) { print "*Exception: " . $e->getMessage() . "
" . print_r($e, true) . "

"; } -print "
"; + +/* print "
"; print "

LEGACY

"; // B(valid) $rgb = [50, 20, 30]; @@ -173,7 +173,7 @@ $hsb = [0, 0, 5]; print "S::COLOR hsb->rgb: $hsb[0], $hsb[1], $hsb[2]: " . DgS::printAr(SetVarType::setArray( Colors::hsb2rgb($hsb[0], $hsb[1], $hsb[2]) - )) . "
"; + )) . "
"; */ print "
"; diff --git a/www/admin/class_test.create_email.php b/www/admin/class_test.create_email.php index ece9c410..40643f50 100644 --- a/www/admin/class_test.create_email.php +++ b/www/admin/class_test.create_email.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.datetime.php b/www/admin/class_test.datetime.php index 18379ea8..54633004 100644 --- a/www/admin/class_test.datetime.php +++ b/www/admin/class_test.datetime.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -268,7 +268,9 @@ foreach ($compare_datetimes as $compare_datetime) { print "COMPAREDATE: $compare_datetime[0] = $compare_datetime[1]: " . (string)DateTime::compareDateTime($compare_datetime[0], $compare_datetime[1]) . "
"; } + print "
"; +print "

calcDaysInterval

"; $compare_dates = [ [ '2021-05-01', '2021-05-10', ], [ '2021-05-10', '2021-05-01', ], @@ -279,9 +281,21 @@ foreach ($compare_dates as $compare_date) { print "CALCDAYSINTERVAL: $compare_date[0] = $compare_date[1]: " . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1])) . "
"; print "CALCDAYSINTERVAL(named): $compare_date[0] = $compare_date[1]: " - . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], true)) . "
"; + . DgS::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], return_named:true)) . "
"; + print "CALCDAYSINTERVAL(EXCLUDE END): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], include_end_date:false)); + print "CALCDAYSINTERVAL(EXCLUDE START): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval($compare_date[0], $compare_date[1], exclude_start_date:true)); + print "CALCDAYSINTERVAL(EXCLUDE END, EXCLUDE START): $compare_date[0] = $compare_date[1]: " + . Dgs::printAr(DateTime::calcDaysInterval( + $compare_date[0], + $compare_date[1], + include_end_date:false, + exclude_start_date:true + )); } print "
"; +print "

setWeekdayNameFromIsoDow

"; // test date conversion $dow = 2; print "DOW[$dow]: " . DateTime::setWeekdayNameFromIsoDow($dow) . "
"; @@ -297,26 +311,25 @@ $date = '2022-70-242'; print "DATE-dow[$date];invalid: " . DateTime::setWeekdayNameFromDate($date) . "
"; print "DATE-dow[$date],long;invalid: " . DateTime::setWeekdayNameFromDate($date, true) . "
"; print "DOW-date[$date];invalid: " . DateTime::setWeekdayNumberFromDate($date) . "
"; -print "
"; -// check date range includes a weekend -// does not: -$start_date = '2023-07-03'; -$end_date = '2023-07-05'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-03'; -$end_date = '2023-07-10'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-03'; -$end_date = '2023-07-31'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; -$start_date = '2023-07-01'; -$end_date = '2023-07-03'; -print "Has Weekend: " . $start_date . " ~ " . $end_date . ": " - . Dgs::prBl(DateTime::dateRangeHasWeekend($start_date, $end_date)) . "
"; +print "
"; +print "

dateRangeHasWeekend

"; +// check date range includes a weekend +$has_weekend_list = [ + ['2023-07-03', '2023-07-05'], + ['2023-07-03', '2023-07-10'], + ['2023-07-03', '2023-07-31'], + ['2023-07-01', '2023-07-03'], + ['2023-07-01', '2023-07-01'], + ['2023-07-01', '2023-07-02'], + ['2023-06-30', '2023-07-01'], + ['2023-06-30', '2023-06-30'], + ['2023-07-01', '2023-06-30'], +]; +foreach ($has_weekend_list as $days) { + print "Has Weekend: " . $days[0] . " ~ " . $days[1] . ": " + . Dgs::prBl(DateTime::dateRangeHasWeekend($days[0], $days[1])) . "
"; +} print ""; @@ -460,7 +473,10 @@ function intervalStringFormatDeprecated( // print "-> V: $value | $part, $time_name | I: " . is_int($value) . " | F: " . is_float($value) // . " | " . ($value != 0 ? 'Not zero' : 'ZERO') . "
"; // var_dump($skip_last_zero); - if ($value != 0 || $skip_zero === false || $skip_last_zero === false) { + if ( + is_numeric($value) && + ($value != 0 || $skip_zero === false || $skip_last_zero === false) + ) { if ($part == 'f') { if ($truncate_nanoseconds === true) { $value = round($value, 3); diff --git a/www/admin/class_test.db.convert-placeholder.php b/www/admin/class_test.db.convert-placeholder.php index 8cca56a9..8257805c 100644 --- a/www/admin/class_test.db.convert-placeholder.php +++ b/www/admin/class_test.db.convert-placeholder.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -21,6 +21,7 @@ ob_end_flush(); use CoreLibs\Debug\Support; use CoreLibs\DB\Support\ConvertPlaceholder; +use CoreLibs\Convert\Html; $log = new CoreLibs\Logging\Logging([ 'log_folder' => BASE . LOG, @@ -28,7 +29,6 @@ $log = new CoreLibs\Logging\Logging([ 'log_per_date' => true, ]); - $PAGE_NAME = 'TEST CLASS: DB CONVERT PLACEHOLDER'; print ""; print "" . $PAGE_NAME . ""; @@ -39,10 +39,12 @@ print '

' . $PAGE_NAME . '

'; print "LOGFILE NAME: " . $log->getLogFile() . "
"; print "LOGFILE ID: " . $log->getLogFileId() . "
"; -print "Lookup Regex:
" . ConvertPlaceholder::REGEX_LOOKUP_PLACEHOLDERS . "
"; -print "Replace Named Regex:
" . ConvertPlaceholder::REGEX_REPLACE_NAMED . "
"; -print "Replace Named Regex:
" . ConvertPlaceholder::REGEX_REPLACE_QUESTION_MARK . "
"; -print "Replace Named Regex:
" . ConvertPlaceholder::REGEX_REPLACE_NUMBERED . "
"; +print "Lookup Regex:
" . Html::htmlent(ConvertPlaceholder::REGEX_LOOKUP_PLACEHOLDERS) . "
"; +print "Lookup Numbered Regex:
" . Html::htmlent(ConvertPlaceholder::REGEX_LOOKUP_NUMBERED) . "
"; +print "Replace Named Regex:
" . Html::htmlent(ConvertPlaceholder::REGEX_REPLACE_NAMED) . "
"; +print "Replace Question Mark Regex:
"
+	. Html::htmlent(ConvertPlaceholder::REGEX_REPLACE_QUESTION_MARK) . "
"; +print "Replace Numbered Regex:
" . Html::htmlent(ConvertPlaceholder::REGEX_REPLACE_NUMBERED) . "
"; $uniqid = \CoreLibs\Create\Uids::uniqIdShort(); // $binary_data = $db->dbEscapeBytea(file_get_contents('class_test.db.php') ?: ''); @@ -92,40 +94,63 @@ RETURNING some_binary SQL; -print "[ALL] Convert: " +print "[ALL] Convert: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) . "
"; echo "
"; $query = "SELECT foo FROM bar WHERE baz = :baz AND buz = :baz AND biz = :biz AND boz = :bez"; $params = [':baz' => 'SETBAZ', ':bez' => 'SETBEZ', ':biz' => 'SETBIZ']; -print "[NO PARAMS] Convert: " +print "[NO PARAMS] Convert: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) . "
"; echo "
"; $query = "SELECT foo FROM bar WHERE baz = :baz AND buz = :baz AND biz = :biz AND boz = :bez"; $params = null; -print "[NO PARAMS] Convert: " +print "[NO PARAMS] Convert: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) . "
"; echo "
"; $query = "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar <> :row_varchar"; $params = null; -print "[NO PARAMS] Convert: " +print "[NO PARAMS] Convert: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) . "
"; echo "
"; $query = "SELECT row_varchar, row_varchar_literal, row_int, row_date FROM table_with_primary_key"; $params = null; -print "[NO PARAMS] TEST: " +print "[NO PARAMS] TEST: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) . "
"; echo "
"; -print "[P-CONV]: " +$query = <<[All the same params] TEST: " + . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) + . "
"; +echo "
"; + +$query = << 1]; +print "[: param] TEST: " + . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params)) + . "
"; +echo "
"; + +print "[P-CONV]: " . Support::printAr( ConvertPlaceholder::updateParamList([ 'original' => [ @@ -187,6 +212,13 @@ SQL, 'params' => [\CoreLibs\Create\Uids::uniqIdShort(), 'string A-1', 1234], 'direction' => 'pg', ], + 'b?' => [ + 'query' => << [1234], + 'direction' => 'pg', + ], 'b:' => [ 'query' => << $data) { $query = $data['query']; $params = $data['params']; $direction = $data['direction']; - print "[$info] Convert: " + print "[$info] Convert: " . Support::printAr(ConvertPlaceholder::convertPlaceholderInQuery($query, $params, $direction)) . "
"; echo "
"; diff --git a/www/admin/class_test.db.dbReturn.php b/www/admin/class_test.db.dbReturn.php index 17166558..85d56793 100644 --- a/www/admin/class_test.db.dbReturn.php +++ b/www/admin/class_test.db.dbReturn.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.db.encryption.php b/www/admin/class_test.db.encryption.php new file mode 100644 index 00000000..6b5c6ed7 --- /dev/null +++ b/www/admin/class_test.db.encryption.php @@ -0,0 +1,166 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); +// db connection and attach logger +$db = new CoreLibs\DB\IO(DB_CONFIG, $log); +$db->log->debug('START', '=============================>'); + +$PAGE_NAME = 'TEST CLASS: DB QUERY ENCRYPTION'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print ''; +print '

' . $PAGE_NAME . '

'; + +// encryption key +$key_new = CreateKey::generateRandomKey(); +print "Secret Key NEW: " . $key_new . "
"; +// for reproducable test results +$key = 'e475c19b9a3c8363feb06b51f5b73f1dc9b6f20757d4ab89509bf5cc70ed30ec'; +print "Secret Key: " . $key . "
"; + +// test text +$text_string = "I a some deep secret"; +$text_string = "I a some deep secret ABC"; +// +$crypt = new SymmetricEncryption($key); +$encrypted = $crypt->encrypt($text_string); +$string_hashed = Hash::hashStd($text_string); +$string_hmac = Hash::hashHmac($text_string, $key); +$decrypted = $crypt->decrypt($encrypted); + +print "String: " . $text_string . "
"; +print "Encrypted: " . $encrypted . "
"; +print "Hashed: " . $string_hashed . "
"; +print "Hmac: " . $string_hmac . "
"; + +$db->dbExecParams( + <<dbGetReturningExt('cuuid'); +print "INSERTED: " . print_r($cuuid, true) . "
"; +print "LAST ERROR: " . $db->dbGetLastError(true) . "
"; + +// read back +$res = $db->dbReturnRowParams( + <<" . Support::prAr($res) . "
"; + +if ($res === false) { + echo "Failed to run query
"; +} else { + if (hash_equals($string_hashed, $res['pg_digest_text'])) { + print "libsodium and pgcrypto hash match
"; + } + if (hash_equals($string_hmac, $res['pg_hmac_text'])) { + print "libsodium and pgcrypto hash hmac match
"; + } + // do compare for PHP and pgcrypto settings + $encryptedMessage_template = <<getLiteralData()->getData(); + print "Pg decrypted PHP: " . $decrypted . "
"; + if ($decrypted == $text_string) { + print "Decryption worked
"; + } + } catch (\Exception $e) { + print "Error decrypting message: " . $e->getMessage() . "
"; + } +} + +print ""; + +// __END__ diff --git a/www/admin/class_test.db.php b/www/admin/class_test.db.php index 1326a023..4a7676e7 100644 --- a/www/admin/class_test.db.php +++ b/www/admin/class_test.db.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -76,41 +76,41 @@ $db->dbResetEncoding(); // empty calls, none of the below should fail // -$db->dbGetCursor(); +$foo = $db->dbGetCursor(); // -$db->dbGetCursorExt(); +$foo = $db->dbGetCursorExt(); // -$db->dbGetCursorPos('SELECT foo', ['bar']); +$foo = $db->dbGetCursorPos('SELECT foo', ['bar']); // -$db->dbGetCursorNumRows('SELECT foo', ['bar']); +$foo = $db->dbGetCursorNumRows('SELECT foo', ['bar']); // -$db->dbGetInsertPKName(); +$foo = $db->dbGetInsertPKName(); // -$db->dbGetInsertPK(); +$foo = $db->dbGetInsertPK(); // -$db->dbGetReturningExt(); -$db->dbGetReturningExt('foo'); -$db->dbGetReturningExt('foo', 0); -$db->dbGetReturningExt(pos:0); +$foo = $db->dbGetReturningExt(); +$foo = $db->dbGetReturningExt('foo'); +$foo = $db->dbGetReturningExt('foo', 0); +$foo = $db->dbGetReturningExt(pos:0); // -$db->dbGetReturningArray(); +$foo = $db->dbGetReturningArray(); // -$db->dbGetNumRows(); +$foo = $db->dbGetNumRows(); // -$db->dbGetNumFields(); +$foo = $db->dbGetNumFields(); // -$db->dbGetFieldNames(); +$foo = $db->dbGetFieldNames(); // -$db->dbGetFieldTypes(); +$foo = $db->dbGetFieldTypes(); // -$db->dbGetFieldNameTypes(); +$foo = $db->dbGetFieldNameTypes(); // -$db->dbGetFieldName(0); +$foo = $db->dbGetFieldName(0); // -$db->dbGetFieldType(0); -$db->dbGetFieldType('foo'); +$foo = $db->dbGetFieldType(0); +$foo = $db->dbGetFieldType('foo'); // -$db->dbGetPrepareCursorValue('foo', 'bar'); +$foo = $db->dbGetPrepareCursorValue('foo', 'bar'); // TEST CACHE READS @@ -273,8 +273,8 @@ $query_insert = <<PREPARE QUERIES
"; // READ PREPARE $q_prep = <<dbPrepare('sel_test_foo', $q_prep) === false) { // sel test with ANY () type $q_prep = "SELECT test_foo_id, test, some_bool, string_a, number_a, " - . "number_a_numeric, some_time " + . "numeric_a, some_time " . "FROM test_foo " . "WHERE test = ANY($1) " . "ORDER BY test_foo_id DESC LIMIT 5"; @@ -618,7 +618,7 @@ $test_bar = $db->dbEscapeLiteral('SOMETHING DIFFERENT'); $q = <<"; $q = <<"; print "DB RETURN PARAMS LIKE
"; $q = <<"; print "DB RETURN PARAMS ANY
"; $q = <<"; } +$stm_status = $db->dbPreparedCursorStatus(''); +print "[PGB] Empty statement name: " . $log->prAr($stm_status) . "
"; +$stm_status = $db->dbPreparedCursorStatus('pgb_sel_test_foobar'); +print "[PGB] Prepared name not match status: $stm_status
"; +$stm_status = $db->dbPreparedCursorStatus('pgb_sel_test_foo'); +print "[PGB] Prepared name match status: $stm_status
"; +$stm_status = $db->dbPreparedCursorStatus('pgb_sel_test_foo', $q_prep); +print "[PGB] prepared exists and query match status: $stm_status
"; +$stm_status = $db->dbPreparedCursorStatus('pgb_sel_test_foo', "SELECT * FROM test_foo"); +print "[PGB] prepared exists and query not match status: $stm_status
"; + $db_pgb->dbClose(); # db write class test diff --git a/www/admin/class_test.db.query-placeholder.php b/www/admin/class_test.db.query-placeholder.php index f93c39eb..0516ed05 100644 --- a/www/admin/class_test.db.query-placeholder.php +++ b/www/admin/class_test.db.query-placeholder.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -53,6 +53,9 @@ if (($dbh = $db->dbGetDbh()) instanceof \PgSql\Connection) { } else { print "NO DB HANDLER
"; } +// REGEX for placeholder count +print "Placeholder lookup regex:
" . CoreLibs\DB\Support\ConvertPlaceholder::REGEX_LOOKUP_NUMBERED . "
"; + // turn on debug replace for placeholders $db->dbSetDebugReplacePlaceholder(true); @@ -62,59 +65,136 @@ $db->dbExec("TRUNCATE test_foo"); $uniqid = \CoreLibs\Create\Uids::uniqIdShort(); $binary_data = $db->dbEscapeBytea(file_get_contents('class_test.db.php') ?: ''); $query_params = [ - $uniqid, - true, - 'STRING A', - 2, - 2.5, - 1, - date('H:m:s'), - date('Y-m-d H:i:s'), - json_encode(['a' => 'string', 'b' => 1, 'c' => 1.5, 'f' => true, 'g' => ['a', 1, 1.5]]), - null, - '{"a", "b"}', - '{1,2}', - '{"(array Text A, 5, 8.8)","(array Text B, 10, 15.2)"}', - '("Text", 4, 6.3)', - $binary_data + $uniqid, // test + true, // some_bool + 'STRING A', // string_a + 2, // number_a + 2.5, // numeric_a + 1, // smallint + date('H:m:s'), // some_internval + date('Y-m-d H:i:s'), // some_timestamp + json_encode(['a' => 'string', 'b' => 1, 'c' => 1.5, 'f' => true, 'g' => ['a', 1, 1.5]]), // json_string + null, // null_var + '{"a", "b"}', // array_char_1 + '{1,2}', // array_int_1 + '{"(array Text A, 5, 8.8)","(array Text B, 10, 15.2)"}', // array_composite + '("Text", 4, 6.3)', // composite_item + $binary_data, // some_binary + date('Y-m-d'), // some_date + date('H:i:s'), // some_time + '{"c", "d", "e"}', // array_char_2 + '{3,4,5}', // array_int_2 + 12345667778818, // bigint + 1.56, // numbrer_real + 3.75, // number_double + 124.5, // numeric_3 + \CoreLibs\Create\Uids::uuidv4() // uuid_var ]; $query_insert = <<" . print_r($db->dbGetQueryParamPlaceholders($query_insert), true) . "
";
 $status = $db->dbExecParams($query_insert, $query_params);
 echo "*
"; echo "INSERT ALL COLUMN TYPES: " . Support::printToString($query_params) . " |
" - . "QUERY: " . $db->dbGetQuery() . " |
" + . "QUERY:
" . $db->dbGetQuery() . "
|
" . "PRIMARY KEY: " . Support::printToString($db->dbGetInsertPK()) . " |
" . "RETURNING EXT:
" . print_r($db->dbGetReturningExt(), true) . "
|
" . "RETURNING RETURN:
" . print_r($db->dbGetReturningArray(), true) . "
 |
" . "ERROR: " . $db->dbGetLastError(true) . "
"; echo "
"; +print "ANY call
"; +$query = <<dbReturnParams($query, [$query_value]))) { + print "Result: " . Support::prAr($res) . "
"; +} + +echo "
"; + +echo "CASE part
"; +$query = << 1 THEN $1 + ELSE 1::INT + END)::INT +WHERE + string_a = $2 +SQL; +echo "QUERY:
" . $query . "
"; +$res = $db->dbExecParams($query, [1, 'foobar']); +print "ERROR: " . $db->dbGetLastError(true) . "
"; + +echo "
"; + // test connectors: = , <> () for query detection // convert placeholder tests @@ -131,6 +211,16 @@ SQL, 'params' => [], 'direction' => 'pg', ], + 'numbers' => [ + 'query' => << [\CoreLibs\Create\Uids::uniqIdShort(), 'string A-1', 1234], + 'direction' => 'pdo', + ], 'a?' => [ 'query' => << 'pg', ], + 'select, compare $' => [ + 'query' => <<= $1 OR number_a <= $2 OR + number_a > $3 OR number_a < $4 + OR number_a = $5 OR number_a <> $6 + SQL, + 'params' => [1, 2, 3, 4, 5, 6], + 'direction' => 'pg' + ], ]; $db->dbSetConvertPlaceholder(true); @@ -169,11 +271,12 @@ foreach ($test_queries as $info => $data) { // . "
"; if ($db->dbCheckQueryForSelect($query)) { $row = $db->dbReturnRowParams($query, $params); - print "[$info] SELECT: " . Support::prAr($row) . "
"; + print "[$info] SELECT: " . Support::prAr($row) . "
"; } else { $db->dbExecParams($query, $params); } - print "[$info] " . Support::printAr($db->dbGetPlaceholderConverted()) . "
"; + print "ERROR: " . $db->dbGetLastError(true) . "
"; + print "[$info] " . Support::printAr($db->dbGetPlaceholderConverted()) . "
"; echo "
"; } @@ -188,22 +291,29 @@ SQL, ['string A-1'] )) ) { - print "RES: " . Support::prAr($res) . "
"; + print "RES: " . Support::prAr($res) . "
"; } +print "ERROR: " . $db->dbGetLastError(true) . "
"; +echo "
"; print "CursorExt: " . Support::prAr($db->dbGetCursorExt(<<"; +// ERROR BELOW: missing params $res = $db->dbReturnRowParams(<<dbGetPlaceholderConverted()) . "
"; +print "ERROR: " . $db->dbGetLastError(true) . "
"; +echo "
"; +// ERROR BELOW: LIKE cannot have placeholder echo "dbReturn read LIKE:
"; while ( is_array($res = $db->dbReturnParams( @@ -217,6 +327,8 @@ SQL, ) { print "RES: " . Support::prAr($res) . "
"; } +print "PL: " . Support::PrAr($db->dbGetPlaceholderConverted()) . "
"; +print "ERROR: " . $db->dbGetLastError(true) . "
"; print ""; $db->log->debug('DEBUGEND', '==================================== [END]'); diff --git a/www/admin/class_test.db.single.php b/www/admin/class_test.db.single.php index 102c3ce7..e3d631c4 100644 --- a/www/admin/class_test.db.single.php +++ b/www/admin/class_test.db.single.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.db.types.php b/www/admin/class_test.db.types.php index 8f42aa66..b302c330 100644 --- a/www/admin/class_test.db.types.php +++ b/www/admin/class_test.db.types.php @@ -7,7 +7,7 @@ declare(strict_types=1); // turn on all error reporting -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -57,6 +57,43 @@ if (($dbh = $db->dbGetDbh()) instanceof \PgSql\Connection) { print "TRUNCATE test_foo
"; $db->dbExec("TRUNCATE test_foo"); +/* +BELOW IS THE FULL TABLE WITH ALL PostgreSQL Types +=> \d test_foo + Table "public.test_foo" +Column | Type | Nullable | Default +------------------+-----------------------------+----------+----------------------------------------------- +test | character varying | | +some_bool | boolean | | +string_a | character varying | | +number_a | integer | | +numeric_a | numeric | | +some_internval | interval | | +test_foo_id | integer | not null | generated always as identity +json_string | jsonb | | +some_timestamp | timestamp without time zone | | +some_binary | bytea | | +null_var | character varying | | +smallint_a | smallint | | +number_real | real | | +number_double | double precision | | +number_serial | integer | not null | nextval('test_foo_number_serial_seq'::regclass) +array_char_1 | character varying[] | | +array_char_2 | character varying[] | | +array_int_1 | integer[] | | +array_int_2 | integer[] | | +composite_item | inventory_item | | +array_composite | inventory_item[] | | +numeric_3 | numeric(3,0) | | +identity_always | bigint | not null | generated always as identity +identitiy_default | bigint | not null | generated by default as identity +uuid_var | uuid | | gen_random_uuid() +some_date | date | | +some_time | time without time zone | | +bigint_a | bigint | | +default_uuid | uuid | | gen_random_uuid() +*/ + /* $q = <<"; $query_select = << BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); +$_phpv = new CoreLibs\Check\PhpVersion(); +$phpv_class = 'CoreLibs\Check\PhpVersion'; + +$PAGE_NAME = 'TEST CLASS: PHP VERSION'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print ''; +print '

' . $PAGE_NAME . '

'; + +// fputcsv +print "

\CoreLibs\DeprecatedHelper\Deprecated84::fputcsv()

"; +$test_csv = BASE . TMP . 'DeprecatedHelper.test.csv'; +print "File: $test_csv
"; + +$fp = fopen($test_csv, "w"); +if (!is_resource($fp)) { + die("Cannot open file: $test_csv"); +} +\CoreLibs\DeprecatedHelper\Deprecated84::fputcsv($fp, ["A", "B", "C"]); +fclose($fp); + +$fp = fopen($test_csv, "r"); +if (!is_resource($fp)) { + die("Cannot open file: $test_csv"); +} +while ($entry = \CoreLibs\DeprecatedHelper\Deprecated84::fgetcsv($fp)) { + print "fgetcsv:
" . print_r($entry, true) . "
"; +} +fclose($fp); + +$out = \CoreLibs\DeprecatedHelper\Deprecated84::str_getcsv("A,B,C"); +print "str_getcsv:
" . print_r($out, true) . "
"; + +/** + * temporary different CSV function, because fgetcsv seems to be broken on some systems + * (does not read out japanese text) + * + * @param string $string full line for csv split + * @param string $encoding optional, if given, converts string to the internal encoding + * before we do anything + * @param string $delimiter sepperate character, default ',' + * @param string $enclosure string line marker, default '"' + * @param string $flag INTERN | EXTERN. if INTERN uses the PHP function, else uses explode + * @return array array with split data from input line + */ +function mtParseCSV( + string $string, + string $encoding = '', + string $delimiter = ',', + string $enclosure = '"', + string $flag = 'INTERN' +): array { + $lines = []; + if ($encoding) { + $string = \CoreLibs\Convert\Encoding::convertEncoding( + $string, + 'UTF-8', + $encoding + ); + } + if ($flag == 'INTERN') { + // split with PHP function + $lines = str_getcsv($string, $delimiter, $enclosure); + } else { + // split up with delimiter + $lines = explode(',', $string) ?: []; + } + // strip " from beginning and end of line + for ($i = 0; $i < count($lines); $i++) { + // remove line breaks + $lines[$i] = preg_replace("/\r\n?/", '', (string)$lines[$i]) ?? ''; + // lingering " at the beginning and end of the line + $lines[$i] = preg_replace("/^\"/", '', (string)$lines[$i]) ?? ''; + $lines[$i] = preg_replace("/\"$/", '', (string)$lines[$i]) ?? ''; + } + return $lines; +} + +print ""; + +// __END__ diff --git a/www/admin/class_test.email.php b/www/admin/class_test.email.php index 0e07cb7b..c23c143e 100644 --- a/www/admin/class_test.email.php +++ b/www/admin/class_test.email.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.encoding.php b/www/admin/class_test.encoding.php index bebac823..28081ccc 100644 --- a/www/admin/class_test.encoding.php +++ b/www/admin/class_test.encoding.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.encryption.php b/www/admin/class_test.encryption.php index 0846d4ea..e1823554 100644 --- a/www/admin/class_test.encryption.php +++ b/www/admin/class_test.encryption.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -18,6 +18,7 @@ require 'config.php'; $LOG_FILE_ID = 'classTest-encryption'; ob_end_flush(); +use CoreLibs\Security\AsymmetricAnonymousEncryption; use CoreLibs\Security\SymmetricEncryption; use CoreLibs\Security\CreateKey; @@ -36,6 +37,8 @@ print ""; print ''; print '

' . $PAGE_NAME . '

'; +print "

Symmetric Encryption

"; + $key = CreateKey::generateRandomKey(); print "Secret Key: " . $key . "
"; @@ -105,6 +108,49 @@ try { // $encrypted = $se->encrypt($string); // $decrypted = $se->decrypt($encrypted); +echo "
"; +print "

Asymmetric Encryption

"; + +$key_pair = CreateKey::createKeyPair(); +$public_key = CreateKey::getPublicKey($key_pair); + +$string = "I am some asymmetric secret"; +print "Message: " . $string . "
"; +$encrypted = sodium_crypto_box_seal($string, CreateKey::hex2bin($public_key)); +$message = sodium_bin2base64($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL); +print "Encrypted PL: " . $message . "
"; +$result = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL); +$decrypted = sodium_crypto_box_seal_open($result, CreateKey::hex2bin($key_pair)); +print "Decrypted PL: " . $decrypted . "
"; + +$encrypted = AsymmetricAnonymousEncryption::encryptKey($string, $public_key); +print "Encrypted ST: " . $encrypted . "
"; +$decrypted = AsymmetricAnonymousEncryption::decryptKey($encrypted, $key_pair); +print "Decrypted ST: " . $decrypted . "
"; + +$aa_crypt = new AsymmetricAnonymousEncryption($key_pair, $public_key); +$encrypted = $aa_crypt->encrypt($string); +print "Encrypted: " . $encrypted . "
"; +$decrypted = $aa_crypt->decrypt($encrypted); +print "Decrypted: " . $decrypted . "
"; + +print "Base64 encode: " . base64_encode('Some text here') . "
"; + +/// this has to fail +$crypt = new AsymmetricAnonymousEncryption(); +$crypt->setPublicKey(CreateKey::getPublicKey(CreateKey::createKeyPair())); +print "Public Key: " . $crypt->getPublicKey() . "
"; +try { + $crypt->setPublicKey(CreateKey::createKeyPair()); +} catch (RangeException $e) { + print "Invalid range:
$e
"; +} +try { + $crypt->setKeyPair(CreateKey::getPublicKey(CreateKey::createKeyPair())); +} catch (RangeException $e) { + print "Invalid range:
$e
"; +} + print ""; // __END__ diff --git a/www/admin/class_test.error_msg.php b/www/admin/class_test.error_msg.php index c83a3cc5..e174003d 100644 --- a/www/admin/class_test.error_msg.php +++ b/www/admin/class_test.error_msg.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.file.php b/www/admin/class_test.file.php index 4cdaa8a9..890adad6 100644 --- a/www/admin/class_test.file.php +++ b/www/admin/class_test.file.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.hash.php b/www/admin/class_test.hash.php index 847e2bf5..bc4d7853 100644 --- a/www/admin/class_test.hash.php +++ b/www/admin/class_test.hash.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -19,6 +19,7 @@ $LOG_FILE_ID = 'classTest-hash'; ob_end_flush(); use CoreLibs\Create\Hash; +use CoreLibs\Security\CreateKey; $log = new CoreLibs\Logging\Logging([ 'log_folder' => BASE . LOG, @@ -38,28 +39,66 @@ print '

' . $PAGE_NAME . '

'; $to_crc = 'Some text block'; // static -print "S::__CRC32B: $to_crc: " . $hash_class::__crc32b($to_crc) . "
"; -print "S::__SHA1SHORT(off): $to_crc: " . $hash_class::__sha1short($to_crc) . "
"; -print "S::__SHA1SHORT(on): $to_crc: " . $hash_class::__sha1short($to_crc, true) . "
"; -print "S::__hash(d): " . $to_crc . "/" - . Hash::STANDARD_HASH_SHORT . ": " . $hash_class::__hash($to_crc) . "
"; -foreach (['adler32', 'fnv132', 'fnv1a32', 'joaat', 'sha512'] as $__hash_c) { - print "S::__hash($__hash_c): $to_crc: " . $hash_class::__hash($to_crc, $__hash_c) . "
"; +print "S::__CRC32B: $to_crc: " . Hash::__crc32b($to_crc) . "
"; +// print "S::__SHA1SHORT(off): $to_crc: " . Hash::__sha1short($to_crc) . "
"; +print "S::hashShort(__sha1Short replace): $to_crc: " . Hash::hashShort($to_crc) . "
"; +// print "S::__SHA1SHORT(on): $to_crc: " . Hash::__sha1short($to_crc, true) . "
"; +print "S::sha1Short(__sha1Short replace): $to_crc: " . Hash::sha1Short($to_crc) . "
"; +// print "S::__hash(d): " . $to_crc . "/" +// . Hash::STANDARD_HASH_SHORT . ": " . $hash_class::__hash($to_crc) . "
"; +$to_crc_list = [ + 'Some text block', + 'Some String Text', + 'any string', +]; +foreach ($to_crc_list as $__to_crc) { + foreach (['adler32', 'fnv132', 'fnv1a32', 'joaat', 'ripemd160', 'sha256', 'sha512'] as $__hash_c) { + print "Hash::hash($__hash_c): $__to_crc: " . Hash::hash($to_crc, $__hash_c) . "
"; + } } // static use print "U-S::__CRC32B: $to_crc: " . Hash::__crc32b($to_crc) . "
"; echo "
"; $text = 'Some String Text'; +// $text = 'any string'; $type = 'crc32b'; print "Hash: " . $type . ": " . hash($type, $text) . "
"; -print "Class: " . $type . ": " . Hash::__hash($text, $type) . "
"; +// print "Class (old): " . $type . ": " . Hash::__hash($text, $type) . "
"; +print "Class (new): " . $type . ": " . Hash::hash($text, $type) . "
"; echo "
"; -print "
CURRENT STANDARD_HASH_SHORT: " . Hash::STANDARD_HASH_SHORT . "
"; -print "
CURRENT STANDARD_HASH_LONG: " . Hash::STANDARD_HASH_LONG . "
"; -print "HASH SHORT: " . $to_crc . ": " . Hash::__hash($to_crc) . "
"; -print "HASH LONG: " . $to_crc . ": " . Hash::__hashLong($to_crc) . "
"; +print "CURRENT STANDARD_HASH_SHORT: " . Hash::STANDARD_HASH_SHORT . "
"; +print "CURRENT STANDARD_HASH_LONG: " . Hash::STANDARD_HASH_LONG . "
"; +print "CURRENT STANDARD_HASH: " . Hash::STANDARD_HASH . "
"; +print "HASH SHORT: " . $to_crc . ": " . Hash::hashShort($to_crc) . "
"; +print "HASH LONG: " . $to_crc . ": " . Hash::hashLong($to_crc) . "
"; +print "HASH DEFAULT: " . $to_crc . ": " . Hash::hashStd($to_crc) . "
"; + +echo "
"; +$key = CreateKey::generateRandomKey(); +$key = "FIX KEY"; +print "Secret Key: " . $key . "
"; +print "HASHMAC DEFAULT (fix): " . $to_crc . ": " . Hash::hashHmac($to_crc, $key) . "
"; +$key = CreateKey::generateRandomKey(); +print "Secret Key: " . $key . "
"; +print "HASHMAC DEFAULT (random): " . $to_crc . ": " . Hash::hashHmac($to_crc, $key) . "
"; + +echo "
"; +$hash_types = ['crc32b', 'sha256', 'invalid']; +foreach ($hash_types as $hash_type) { + echo "Checking $hash_type:
"; + if (Hash::isValidHashType($hash_type)) { + echo "hash type: $hash_type is valid
"; + } else { + echo "hash type: $hash_type is INVALID
"; + } + if (Hash::isValidHashHmacType($hash_type)) { + echo "hash hmac type: $hash_type is valid
"; + } else { + echo "hash hmac type: $hash_type is INVALID
"; + } +} // print "UNIQU ID SHORT : " . Hash::__uniqId() . "
"; // print "UNIQU ID LONG : " . Hash::__uniqIdLong() . "
"; diff --git a/www/admin/class_test.html.php b/www/admin/class_test.html.php index b02241b2..04143e62 100644 --- a/www/admin/class_test.html.php +++ b/www/admin/class_test.html.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.html_build.block.php b/www/admin/class_test.html_build.block.php index b03ee9ee..57471b3b 100644 --- a/www/admin/class_test.html_build.block.php +++ b/www/admin/class_test.html_build.block.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.html_build.element.php b/www/admin/class_test.html_build.element.php index 6296fbd6..3d55832f 100644 --- a/www/admin/class_test.html_build.element.php +++ b/www/admin/class_test.html_build.element.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -74,8 +74,8 @@ print "EL_O:
" . print_r($el_o, true) . "
"; echo "
"; print "buildHtml():
" . htmlentities($el_o->buildHtml()) . "
"; -echo "
"; -print "phfo(\$el_o):
" . htmlentities($el_o::printHtmlFromObject($el_o, true)) . "
"; +/* echo "
"; +print "phfo(\$el_o):
" . htmlentities($el_o::printHtmlFromObject($el_o, true)) . "
"; */ echo "
"; print "phfa(\$el_list):
" . htmlentities($el_o::buildHtmlFromList($el_o_list, true)) . "
"; diff --git a/www/admin/class_test.html_build.replace.php b/www/admin/class_test.html_build.replace.php index 2e648b28..48366736 100644 --- a/www/admin/class_test.html_build.replace.php +++ b/www/admin/class_test.html_build.replace.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.image.php b/www/admin/class_test.image.php index db57964f..8e3e4d15 100644 --- a/www/admin/class_test.image.php +++ b/www/admin/class_test.image.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.json.php b/www/admin/class_test.json.php index 03244d55..6c596280 100644 --- a/www/admin/class_test.json.php +++ b/www/admin/class_test.json.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.lang.php b/www/admin/class_test.lang.php index 43a217e3..f9cf58c4 100644 --- a/www/admin/class_test.lang.php +++ b/www/admin/class_test.lang.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -34,22 +34,21 @@ use CoreLibs\Debug\Support; echo "
LIST LOCALES
"; $locale = 'en_US.UTF-8'; -$locales = L10n::listLocales($locale); +$locales = Language\L10n::listLocales($locale); print "[" . $locale . "] LOCALES: " . Support::printAr($locales) . "
"; $locale = 'en.UTF-8'; -$locales = L10n::listLocales($locale); +$locales = Language\L10n::listLocales($locale); print "[" . $locale . "] LOCALES: " . Support::printAr($locales) . "
"; echo "
PARSE LOCAL
"; $locale = 'en_US.UTF-8'; -$locale_info = L10n::parseLocale($locale); +$locale_info = Language\L10n::parseLocale($locale); print "[" . $locale . "] INFO: " . Support::printAr($locale_info) . "
"; $locale = 'en.UTF-8'; -$locale_info = L10n::parseLocale($locale); +$locale_info = Language\L10n::parseLocale($locale); print "[" . $locale . "] INFO: " . Support::printAr($locale_info) . "
"; -echo "
AUTO DETECT
"; - +/* echo "
AUTO DETECT
"; // DEPRECATED // $get_locale = Language\GetLocale::setLocale(); // print "[AUTO, DEPRECATED]: " . Support::printAr($get_locale) . "
"; @@ -103,6 +102,7 @@ $get_locale = Language\GetLocale::setLocaleFromSession( BASE . INCLUDES . LOCALE ); print "[SESSION SET INVALID]: " . Support::printAr($get_locale) . "
"; + */ // try to load non existing echo "
NEW TYPE
"; diff --git a/www/admin/class_test.logging.php b/www/admin/class_test.logging.php index 778bdd63..e1c71bfa 100644 --- a/www/admin/class_test.logging.php +++ b/www/admin/class_test.logging.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.login.php b/www/admin/class_test.login.php index 3d1327d5..cba9927c 100644 --- a/www/admin/class_test.login.php +++ b/www/admin/class_test.login.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -17,14 +17,21 @@ require 'config.php'; // define log file id $LOG_FILE_ID = 'classTest-login'; $SET_SESSION_NAME = EDIT_SESSION_NAME; + +use CoreLibs\Debug\Support; + // init login & backend class -$session = new CoreLibs\Create\Session($SET_SESSION_NAME); +$session = new CoreLibs\Create\Session($SET_SESSION_NAME, [ + 'regenerate' => 'interval', + 'regenerate_interval' => 10, // every 10 seconds +]); $log = new CoreLibs\Logging\Logging([ 'log_folder' => BASE . LOG, 'log_file_id' => $LOG_FILE_ID, 'log_per_date' => true, ]); $db = new CoreLibs\DB\IO(DB_CONFIG, $log); +$log->setLogFileId('classTest-login-override'); $login = new CoreLibs\ACL\Login( $db, $log, @@ -39,27 +46,98 @@ $login = new CoreLibs\ACL\Login( 'locale_path' => BASE . INCLUDES . LOCALE, ] ); +$log->setLogFileId($LOG_FILE_ID); ob_end_flush(); $login->loginMainCall(); $PAGE_NAME = 'TEST CLASS: LOGIN'; -print ""; -print "" . $PAGE_NAME . ""; -print ""; -print ''; -print '

' . $PAGE_NAME . '

'; +print str_replace( + '{PAGE_NAME}', + $PAGE_NAME, +<< + +{PAGE_NAME} + + + +

{PAGE_NAME}

+HTML +); + +// button logout +print << +function loginLogout() +{ + const form = document.createElement('form'); + form.method = 'post'; + const hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = 'login_logout'; + hiddenField.value = 'Logout'; + form.appendChild(hiddenField); + document.body.appendChild(form); + form.submit(); +} + +
+ +
+HTML; +// string logout +print << +
+Logout + +
+ +HTML; + +echo "SESSION ID: " . $session->getSessionIdCall() . "
"; echo "CHECK PERMISSION: " . ($login->loginCheckPermissions() ? 'OK' : 'BAD') . "
"; echo "IS ADMIN: " . ($login->loginIsAdmin() ? 'OK' : 'BAD') . "
"; echo "MIN ACCESS BASE: " . ($login->loginCheckAccessBase('admin') ? 'OK' : 'BAD') . "
"; echo "MIN ACCESS PAGE: " . ($login->loginCheckAccessPage('admin') ? 'OK' : 'BAD') . "
"; -echo "ACL: " . \CoreLibs\Debug\Support::printAr($login->loginGetAcl()) . "
"; -echo "ACL (MIN): " . \CoreLibs\Debug\Support::printAr($login->loginGetAcl()['min'] ?? []) . "
"; -echo "LOCALE: " . \CoreLibs\Debug\Support::printAr($login->loginGetLocale()) . "
"; +echo "ACL: " . Support::printAr($login->loginGetAcl()) . "
"; +echo "ACL (MIN): " . Support::printAr($login->loginGetAcl()['min'] ?? []) . "
"; +echo "LOCALE: " . Support::printAr($login->loginGetLocale()) . "
"; -echo "ECUID: " . $login->loginGetEcuid() . "
"; -echo "ECUUID: " . $login->loginGetEcuuid() . "
"; +echo "ECUID: " . $login->loginGetEuCuid() . "
"; +echo "ECUUID: " . $login->loginGetEuCuuid() . "
"; + +echo "
"; +// set + check edit access id +$edit_access_cuid = 'buRW8Gu2Lkkf'; +if (isset($login->loginGetAcl()['unit'])) { + print "EDIT ACCESS CUID: " . $edit_access_cuid . "
"; + print "ACL UNIT: " . print_r(array_keys($login->loginGetAcl()['unit']), true) . "
"; + print "ACCESS CHECK: " . Support::prBl($login->loginCheckEditAccessCuid($edit_access_cuid)) . "
"; + if ($login->loginCheckEditAccessCuid($edit_access_cuid)) { + print "Set new:" . $edit_access_cuid . "
"; + } else { + print "Load default unit id: " . $login->loginGetAcl()['unit_cuid'] . "
"; + } +} else { + print "Something went wrong with the login
"; +} + +// echo "
"; +// IP check: 'REMOTE_ADDR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP' in _SERVER +// Agent check: 'HTTP_USER_AGENT' + +print "
"; +print "PAGE lookup:
"; +$file_name = 'test_edit_base.php'; +print "Access to '$file_name': " . $log->prAr($login->loginPageAccessAllowed($file_name)) . "
"; +$file_name = 'i_do_not_exists.php'; +print "Access to '$file_name': " . $log->prAr($login->loginPageAccessAllowed($file_name)) . "
"; + +echo "
"; +print "SESSION: " . Support::printAr($_SESSION) . "
"; $login->writeLog( 'TEST LOG', @@ -70,4 +148,18 @@ $login->writeLog( write_type:'JSON' ); +echo "
"; +print "

Legacy Lookups

"; + +$edit_access_id = 1; +$edit_access_cuid = $login->loginGetEditAccessCuidFromId($edit_access_id); +$edit_access_id_rev = null; +if (is_string($edit_access_cuid)) { + $edit_access_id_rev = $login->loginGetEditAccessIdFromCuid($edit_access_cuid); +} +print "EA ID: " . $edit_access_id . "
"; +print "EA CUID: " . $log->prAr($edit_access_cuid) . "
"; +print "REV EA CUID: " . $log->prAr($edit_access_id_rev) . "
"; +$log->info('This is a test'); + print ""; diff --git a/www/admin/class_test.math.php b/www/admin/class_test.math.php index d3af1f39..dfa40f3b 100644 --- a/www/admin/class_test.math.php +++ b/www/admin/class_test.math.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.memoryusage.php b/www/admin/class_test.memoryusage.php index 7e955a85..b105af9e 100644 --- a/www/admin/class_test.memoryusage.php +++ b/www/admin/class_test.memoryusage.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.mime.php b/www/admin/class_test.mime.php index f1bdab61..c2d1b233 100644 --- a/www/admin/class_test.mime.php +++ b/www/admin/class_test.mime.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.output.form.php b/www/admin/class_test.output.form.php index 1a1d4066..fec7be5e 100644 --- a/www/admin/class_test.output.form.php +++ b/www/admin/class_test.output.form.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -29,15 +29,17 @@ $table_arrays = []; $table_arrays[\CoreLibs\Get\System::getPageName(1)] = [ // form fields mtaching up with db fields 'table_array' => [ + 'foo', + 'bar' ], // laod query - 'load_query' => '', + 'load_query' => 'SELECT uuid_nr, foo, bar FROM test', // database table to load from - 'table_name' => '', + 'table_name' => 'test', // for load dro pdown, format output 'show_fields' => [ [ - 'name' => 'name' + 'name' => 'foo' ], [ 'name' => 'enabled', diff --git a/www/admin/class_test.password.php b/www/admin/class_test.password.php index 0d1c390a..7176fc80 100644 --- a/www/admin/class_test.password.php +++ b/www/admin/class_test.password.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -37,6 +37,8 @@ print ""; print ''; print '

' . $PAGE_NAME . '

'; +print "PHP Version: " . PHP_VERSION . "
"; + $password = 'something1234'; $enc_password = $_password->passwordSet($password); print "PASSWORD: $password: " . $enc_password . "
"; @@ -51,6 +53,20 @@ print "PASSWORD REHASH: " . (string)$password_class::passwordRehashCheck($enc_pa // direct static print "S::PASSWORD VERFIY: " . (string)PwdChk::passwordVerify($password, $enc_password) . "
"; +if (PHP_VERSION_ID < 80400) { + $rehash_test = '$2y$10$EgWJ2WE73DWi.hIyFRCdpejLXTvHbmTK3LEOclO1tAvXAXUNuUS4W'; + $rehash_test_throw = '$2y$12$EgWJ2WE73DWi.hIyFRCdpejLXTvHbmTK3LEOclO1tAvXAXUNuUS4W'; +} else { + $rehash_test = '$2y$12$EgWJ2WE73DWi.hIyFRCdpejLXTvHbmTK3LEOclO1tAvXAXUNuUS4W'; + $rehash_test_throw = '$2y$10$EgWJ2WE73DWi.hIyFRCdpejLXTvHbmTK3LEOclO1tAvXAXUNuUS4W'; +} +if (PwdChk::passwordRehashCheck($rehash_test)) { + print "Bad password [BAD]
"; +} +if (PwdChk::passwordRehashCheck($rehash_test_throw)) { + print "Bad password [OK]
"; +} + print ""; // __END__ diff --git a/www/admin/class_test.php b/www/admin/class_test.php index d511bcf8..0e2d6ad4 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -62,19 +62,40 @@ $backend = new CoreLibs\Admin\Backend( $backend->db->dbInfo(true); ob_end_flush(); -print ""; -print "TEST CLASS"; -print ""; +print << + +TEST CLASS + + + +
+ +
+HTML; // key: file name, value; name $test_files = [ 'class_test.db.php' => 'Class Test: DB', 'class_test.db.types.php' => 'Class Test: DB column type convert', - 'class_test.db.query-placeholder.php' => 'Class Test: DB query placeholder convert', + 'class_test.db.query-placeholder.php' => 'Class Test: DB placeholder queries', 'class_test.db.dbReturn.php' => 'Class Test: DB dbReturn', 'class_test.db.single.php' => 'Class Test: DB single query tests', 'class_test.db.convert-placeholder.php' => 'Class Test: DB convert placeholder', - 'class_test.db.sqlite.php' => 'Class Test: DB: SqLite', + 'class_test.db.encryption.php' => 'Class Test: DB pgcrypto', 'class_test.convert.colors.php' => 'Class Test: CONVERT COLORS', 'class_test.check.colors.php' => 'Class Test: CHECK COLORS', 'class_test.mime.php' => 'Class Test: MIME', @@ -121,6 +142,7 @@ $test_files = [ 'class_test.error_msg.php' => 'Class Test: ERROR MSG', 'class_test.url-requests.curl.php' => 'Class Test: URL REQUESTS: CURL', 'subfolder/class_test.config.direct.php' => 'Class Test: CONFIG DIRECT SUB', + 'class_test.deprecated.helper.php' => 'Class Test: DEPRECATED HELPERS', ]; asort($test_files); @@ -129,33 +151,20 @@ foreach ($test_files as $file => $name) { print ''; } + +print "
"; +print "ECUID: " . $session->get('LOGIN_EUCUID') . "
"; +print "ECUUID: " . $session->get('LOGIN_EUCUUID') . "
"; + print "
"; -print "L: " . Support::dumpVar($locale) . "
"; +print "LOCALE: " . Support::dumpVar($locale) . "
"; // print all _ENV vars set print "
READ _ENV ARRAY:
"; print Support::dumpVar(array_map('htmlentities', $_ENV)); -// set + check edit access id -$edit_access_id = 3; -if (isset($login->loginGetAcl()['unit'])) { - print "ACL UNIT: " . print_r(array_keys($login->loginGetAcl()['unit']), true) . "
"; - print "ACCESS CHECK: " . (string)$login->loginCheckEditAccess($edit_access_id) . "
"; - if ($login->loginCheckEditAccess($edit_access_id)) { - $backend->edit_access_id = $edit_access_id; - } else { - $backend->edit_access_id = $login->loginGetAcl()['unit_id']; - } -} else { - print "Something went wrong with the login
"; -} // $backend->log->debug('SESSION', \CoreLibs\Debug\Support::dumpVar($_SESSION)); -print '
'; -print 'Logout'; -print ''; -print '
'; - +print "
"; print "Log Level: " . $backend->log->getLoggingLevel()->getName() . "
"; print "Log ID: " . $backend->log->getLogFileId() . "
"; print "Log Date: " . $backend->log->getLogDate() . "
"; @@ -177,26 +186,7 @@ foreach ( $log->debug('SOME MARK', 'Some error output'); -// INTERNAL SET -print "EDIT ACCESS ID: " . $backend->edit_access_id . "
"; -// print "ACL:
".$backend->print_ar($login->loginGetAcl())."
"; -// $log->debug('ACL', "ACL: " . \CoreLibs\Debug\Support::dumpVar($login->loginGetAcl())); -// print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; -// print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; -// $result = array_flip( -// array_filter( -// array_flip($login->default_acl_list), -// function ($key) { -// if (is_numeric($key)) { -// return $key; -// } -// } -// ) -// ); -// print "DEFAULT ACL:
".$backend->print_ar($result)."
"; -// DEPRICATED CALL -// $backend->adbSetACL($login->loginGetAcl()); - +print "
"; print "THIS HOST: " . HOST_NAME . ", with PROTOCOL: " . HOST_PROTOCOL . " is running SSL: " . HOST_SSL . "
"; print "DIR: " . DIR . "
"; print "BASE: " . BASE . "
"; @@ -206,8 +196,8 @@ print "HOST: " . HOST_NAME . " => DB HOST: " . DB_CONFIG_NAME . " => " . Support print "DS is: " . DIRECTORY_SEPARATOR . "
"; print "SERVER HOST: " . $_SERVER['HTTP_HOST'] . "
"; -print "ECUID: " . $session->get('ECUID') . "
"; -print "ECUUID: " . $session->get('ECUUID') . "
"; +print "
READ _SERVER ARRAY:
"; +print Support::dumpVar(array_map('htmlentities', $_SERVER)); print ""; diff --git a/www/admin/class_test.phpv.php b/www/admin/class_test.phpv.php index 63ac0bf5..648dca85 100644 --- a/www/admin/class_test.phpv.php +++ b/www/admin/class_test.phpv.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -28,8 +28,6 @@ $log = new CoreLibs\Logging\Logging([ $_phpv = new CoreLibs\Check\PhpVersion(); $phpv_class = 'CoreLibs\Check\PhpVersion'; -// define a list of from to color sets for conversion test - $PAGE_NAME = 'TEST CLASS: PHP VERSION'; print ""; print "" . $PAGE_NAME . ""; diff --git a/www/admin/class_test.randomkey.php b/www/admin/class_test.randomkey.php index d49d8e0c..4416b63d 100644 --- a/www/admin/class_test.randomkey.php +++ b/www/admin/class_test.randomkey.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.readenvfile.php b/www/admin/class_test.readenvfile.php index 9449bee8..adb046d6 100644 --- a/www/admin/class_test.readenvfile.php +++ b/www/admin/class_test.readenvfile.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -34,10 +34,12 @@ print '

' . $PAGE_NAME . '

'; print "ALREADY from config.php: " . \CoreLibs\Debug\Support::printAr($_ENV) . "
"; +// This is now in \gullevek\dotenv\DotEnv::readEnvFile(...) + // test .env in local -$status = \CoreLibs\Get\DotEnv::readEnvFile('.', 'test.env'); +/* $status = \CoreLibs\Get\DotEnv::readEnvFile('.', 'test.env'); print "test.env: STATUS: " . $status . "
"; -print "AFTER reading test.env file: " . \CoreLibs\Debug\Support::printAr($_ENV) . "
"; +print "AFTER reading test.env file: " . \CoreLibs\Debug\Support::printAr($_ENV) . "
"; */ print ""; // ;; diff --git a/www/admin/class_test.runningtime.php b/www/admin/class_test.runningtime.php index 27acde98..8e33dd8c 100644 --- a/www/admin/class_test.runningtime.php +++ b/www/admin/class_test.runningtime.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.session.php b/www/admin/class_test.session.php index ed9439ef..139e6a80 100644 --- a/www/admin/class_test.session.php +++ b/www/admin/class_test.session.php @@ -2,7 +2,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); /** * Undocumented function @@ -86,8 +86,10 @@ if (!isset($_SESSION['counter'])) { $_SESSION['counter']++; print "[READ] A " . $var . ": " . ($_SESSION[$var] ?? '{UNSET}') . "
"; $_SESSION[$var] = $value; +/** @phpstan-ignore-next-line nullCoalesce.offset */ print "[READ] B " . $var . ": " . ($_SESSION[$var] ?? '{UNSET}') . "
"; print "[READ] Confirm " . $var . " is " . $value . ": " +/** @phpstan-ignore-next-line equal.alwaysTrue, nullCoalesce.offset */ . (($_SESSION[$var] ?? '') == $value ? 'Matching' : 'Not matching') . "
"; // test set wrappers methods @@ -146,7 +148,7 @@ $_SESSION['this_will_be_written'] = 'not empty'; // open again with same name $session_name = 'class-test-session'; try { - $session_alt = new Session($session_name, auto_write_close:true); + $session_alt = new Session($session_name, ['auto_write_close' => true]); print "[4 SET] Current session id: " . $session_alt->getSessionId() . "
"; print "[4 SET] Current session auto write close: " . ($session_alt->checkAutoWriteClose() ? 'Yes' : 'No') . "
"; print "[START AGAIN] Current session id: " . $session_alt->getSessionId() . "
"; diff --git a/www/admin/class_test.session.read.php b/www/admin/class_test.session.read.php index b2e6e8e3..5750e430 100644 --- a/www/admin/class_test.session.read.php +++ b/www/admin/class_test.session.read.php @@ -2,7 +2,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); /** * Undocumented function diff --git a/www/admin/class_test.smarty.php b/www/admin/class_test.smarty.php index df44f34f..49e95ae9 100644 --- a/www/admin/class_test.smarty.php +++ b/www/admin/class_test.smarty.php @@ -4,9 +4,11 @@ * @phan-file-suppress PhanTypeSuspiciousStringExpression */ +// FIXME: Smarty Class must be updated for PHP 8.4 + declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); @@ -33,6 +35,7 @@ $l10n = new \CoreLibs\Language\L10n( ); $smarty = new CoreLibs\Template\SmartyExtend( $l10n, + $log, CACHE_ID, COMPILE_ID, ); @@ -45,6 +48,7 @@ $adm = new CoreLibs\Admin\Backend( ); $adm->DATA['adm_set'] = 'SET from admin class'; + $PAGE_NAME = 'TEST CLASS: SMARTY'; print ""; print "" . $PAGE_NAME . ""; diff --git a/www/admin/class_test.strings.php b/www/admin/class_test.strings.php index 93c5bf0a..f1ffb689 100644 --- a/www/admin/class_test.strings.php +++ b/www/admin/class_test.strings.php @@ -2,7 +2,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.system.php b/www/admin/class_test.system.php index f59a68a1..f84bf19f 100644 --- a/www/admin/class_test.system.php +++ b/www/admin/class_test.system.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.token.php b/www/admin/class_test.token.php index f46d5166..be098831 100644 --- a/www/admin/class_test.token.php +++ b/www/admin/class_test.token.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.uids.php b/www/admin/class_test.uids.php index 3302fe8b..d01598d5 100644 --- a/www/admin/class_test.uids.php +++ b/www/admin/class_test.uids.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index c43ce781..b3773c23 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/class_test.varistype.php b/www/admin/class_test.varistype.php index 57f7487a..fc757965 100644 --- a/www/admin/class_test.varistype.php +++ b/www/admin/class_test.varistype.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/layout/javascript/edit.jq.js b/www/admin/layout/javascript/edit.jq.js index 52da0f1c..dba5f852 100644 --- a/www/admin/layout/javascript/edit.jq.js +++ b/www/admin/layout/javascript/edit.jq.js @@ -1,7 +1,9 @@ -/* general edit javascript */ -/* jquery version */ +/* +general edit javascript +jquery version +*/ -/* jshint esversion: 11 */ +/** @deprecated Do not use this anymore, use utils.js or utils.min.js */ /* global i18n */ @@ -18,11 +20,28 @@ if (!DEBUG) { var GL_OB_S = 100; var GL_OB_BASE = 100; +/** + * Gets html element or throws an error + * @param {string} el_id Element ID to get + * @returns {HTMLElement} + * @throws Error + * @deprecated use utils.js + */ +function loadEl(el_id) +{ + let el = document.getElementById(el_id); + if (el === null) { + throw new Error('Cannot find: ' + el_id); + } + return el; +} + /** * opens a popup window with winName and given features (string) * @param {String} theURL the url * @param {String} winName window name * @param {Object} features popup features + * @deprecated use utils.js */ function pop(theURL, winName, features) // eslint-disable-line no-unused-vars { @@ -33,6 +52,7 @@ function pop(theURL, winName, features) // eslint-disable-line no-unused-vars /** * automatically resize a text area based on the amount of lines in it * @param {string} ta_id element id + * @deprecated use utils.js */ function expandTA(ta_id) // eslint-disable-line no-unused-vars { @@ -58,6 +78,7 @@ function expandTA(ta_id) // eslint-disable-line no-unused-vars /** * wrapper to get the real window size for the current browser window * @return {Object} object with width/height + * @deprecated use utils.js */ function getWindowSize() { @@ -73,6 +94,7 @@ function getWindowSize() /** * wrapper to get the correct scroll offset * @return {Object} object with x/y px + * @deprecated use utils.js */ function getScrollOffset() { @@ -88,6 +110,7 @@ function getScrollOffset() /** * wrapper to get the correct scroll offset for opener page (from popup) * @return {Object} object with x/y px + * @deprecated use utils.js */ function getScrollOffsetOpener() // eslint-disable-line no-unused-vars { @@ -105,6 +128,7 @@ function getScrollOffsetOpener() // eslint-disable-line no-unused-vars * @param {String} id element to center * @param {Boolean} left if true centers to the middle from the left * @param {Boolean} top if true centers to the middle from the top + * @deprecated use utils.js */ function setCenter(id, left, top) { @@ -142,6 +166,7 @@ function setCenter(id, left, top) * @param {Number} [offset=0] offset from top, default is 0 (px) * @param {Number} [duration=500] animation time, default 500ms * @param {String} [base='body,html'] base element for offset scroll + * @deprecated use utils.js */ function goToPos(element, offset = 0, duration = 500, base = 'body,html') // eslint-disable-line no-unused-vars { @@ -156,11 +181,25 @@ function goToPos(element, offset = 0, duration = 500, base = 'body,html') // esl } } +/** + * go to element, scroll + * non jquery + * @param {string} target + * @deprecated use utils.js +*/ +function goTo(target) // eslint-disable-line no-unused-vars +{ + loadEl(target).scrollIntoView({ + behavior: 'smooth' + }); +} + /** * uses the i18n object created in the translation template * that is filled from gettext in PHP * @param {String} string text to translate * @return {String} translated text (based on PHP selected language) + * @deprecated use utils.js */ function __(string) { @@ -177,37 +216,70 @@ function __(string) * First, checks if it isn't implemented yet. * @param {String} String.prototype.format string with elements to be replaced * @return {String} Formated string + * @deprecated use utils.js */ if (!String.prototype.format) { String.prototype.format = function() { - var args = arguments; - return this.replace(/{(\d+)}/g, function(match, number) - { - return typeof args[number] != 'undefined' ? - args[number] : - match - ; - }); + console.error('[DEPRECATED] use formatString'); + return formatString(this, arguments); + }; +} + +/** + * simple sprintf formater for replace + * usage: "{0} is cool, {1} is not".format("Alpha", "Beta"); + * First, checks if it isn't implemented yet. + * @param {String} string String with {..} entries + * @param {...any} args List of replacement + * @returns {String} Escaped string + * @deprecated use utils.js + */ +function formatString(string, ...args) +{ + return string.replace(/{(\d+)}/g, function(match, number) + { + return typeof args[number] != 'undefined' ? + args[number] : + match + ; + }); +} + +/** + * round to digits (float) + * @param {Number} Number.prototype.round Float type number to round + * @param {Number} prec Precision to round to + * @return {Float} Rounded number + * @deprecated use utils.js + */ +if (Number.prototype.round) { + Number.prototype.round = function (prec) { + console.error('[DEPRECATED] use roundPrecision'); + return roundPrecision(this, prec); }; } /** * round to digits (float) - * @param {Float} Number.prototype.round Float type number to round - * @param {Number} prec Precision to round to - * @return {Float} Rounded number + * @param {Number} number Float type number to round + * @param {Number} precision Precision to round to + * @return {Number} Rounded number + * @deprecated use utils.js */ -if (Number.prototype.round) { - Number.prototype.round = function (prec) { - return Math.round(this * Math.pow(10, prec)) / Math.pow(10, prec); - }; +function roundPrecision(number, precision) +{ + if (!isNaN(number) || !isNaN(precision)) { + return number; + } + return Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision); } /** * formats flat number 123456 to 123,456 * @param {Number} x number to be formated * @return {String} formatted with , in thousands + * @deprecated use utils.js */ function numberWithCommas(x) // eslint-disable-line no-unused-vars { @@ -220,6 +292,7 @@ function numberWithCommas(x) // eslint-disable-line no-unused-vars * converts line breaks to br * @param {String} string any string * @return {String} string with
+ * @deprecated use utils.js */ function convertLBtoBR(string) // eslint-disable-line no-unused-vars { @@ -228,51 +301,78 @@ function convertLBtoBR(string) // eslint-disable-line no-unused-vars /** * escape HTML string - * @param {String} !String.prototype.escapeHTML HTML data string to be escaped - * @return {String} escaped string + * @param {String} String.prototype.escapeHTML HTML data string to be escaped + * @return {String} escaped string + * @deprecated use utils.js */ if (!String.prototype.escapeHTML) { String.prototype.escapeHTML = function() { - return this.replace(/[&<>"'/]/g, function (s) { - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - }; - - return entityMap[s]; - }); + console.error('[DEPRECATED] use escapeHtml'); + return escapeHtml(this); }; } /** * unescape a HTML encoded string - * @param {String} !String.prototype.unescapeHTML data with escaped entries - * @return {String} HTML formated string + * @param {String} String.prototype.unescapeHTML data with escaped entries + * @return {String} HTML formated string + * @deprecated use utils.js */ if (!String.prototype.unescapeHTML) { String.prototype.unescapeHTML = function() { - return this.replace(/&[#\w]+;/g, function (s) { - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': '\'', - '/': '/' - }; - - return entityMap[s]; - }); + console.error('[DEPRECATED] use unescapeHtml'); + return unescapeHtml(this); }; } +/** + * Escapes HTML in string + * @param {String} string Text to escape HTML in + * @returns {String} + * @deprecated use utils.js + */ +function escapeHtml(string) +{ + return string.replace(/[&<>"'/]/g, function (s) { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + return entityMap[s]; + }); +} + +/** + * Unescape a HTML encoded string + * @param {String} string Text to unescape HTML in + * @returns {String} + * @deprecated use utils.js + */ +function unescapeHtml(string) +{ + return string.replace(/&[#\w]+;/g, function (s) { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', + '/': '/' + }; + + return entityMap[s]; + }); +} + /** * returns current timestamp (unix timestamp) * @return {Number} timestamp (in milliseconds) + * @deprecated use utils.js */ function getTimestamp() // eslint-disable-line no-unused-vars { @@ -285,6 +385,7 @@ function getTimestamp() // eslint-disable-line no-unused-vars * i.e. 0-255 -> '00'-'ff' * @param {Number} dec decimal string * @return {String} hex encdoded number + * @deprecated use utils.js */ function dec2hex(dec) { @@ -296,6 +397,7 @@ function dec2hex(dec) * only works on mondern browsers * @param {Number} len length of unique id string * @return {String} random string in length of len + * @deprecated use utils.js */ function generateId(len) // eslint-disable-line no-unused-vars { @@ -309,6 +411,7 @@ function generateId(len) // eslint-disable-line no-unused-vars * works on all browsers * after many runs it will create duplicates * @return {String} not true random string + * @deprecated use utils.js */ function randomIdF() // eslint-disable-line no-unused-vars { @@ -322,6 +425,7 @@ function randomIdF() // eslint-disable-line no-unused-vars * @param {Number} min minimum int number inclusive * @param {Number} max maximumg int number inclusive * @return {Number} Random number + * @deprecated use utils.js */ function getRandomIntInclusive(min, max) // eslint-disable-line no-unused-vars { @@ -335,6 +439,7 @@ function getRandomIntInclusive(min, max) // eslint-disable-line no-unused-vars * check if name is a function * @param {string} name Name of function to check if exists * @return {Boolean} true/false + * @deprecated use utils.js */ function isFunction(name) // eslint-disable-line no-unused-vars { @@ -354,6 +459,7 @@ function isFunction(name) // eslint-disable-line no-unused-vars * @param {mixed} context context (window or first namespace) * hidden next are all the arguments * @return {mixed} Return values from functon + * @deprecated use utils.js */ function executeFunctionByName(functionName, context /*, args */) // eslint-disable-line no-unused-vars { @@ -370,6 +476,7 @@ function executeFunctionByName(functionName, context /*, args */) // eslint-disa * checks if a variable is an object * @param {Mixed} val possible object * @return {Boolean} true/false if it is an object or not + * @deprecated use utils.js */ function isObject(val) { @@ -383,6 +490,7 @@ function isObject(val) * get the length of an object (entries) * @param {Object} object object to check * @return {Number} number of entry + * @deprecated use utils.js */ function getObjectCount(object) { @@ -394,6 +502,7 @@ function getObjectCount(object) * @param {String} key key name * @param {Object} object object to search key in * @return {Boolean} true/false if key exists in object + * @deprecated use utils.js */ function keyInObject(key, object) { @@ -402,9 +511,10 @@ function keyInObject(key, object) /** * returns matching key of value - * @param {Object} obj object to search value in - * @param {Mixed} value any value (String, Number, etc) - * @return {String} the key found for the first matching value + * @param {Object} object object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {String} the key found for the first matching value + * @deprecated use utils.js */ function getKeyByValue(object, value) // eslint-disable-line no-unused-vars { @@ -416,9 +526,10 @@ function getKeyByValue(object, value) // eslint-disable-line no-unused-vars /** * returns true if value is found in object with a key - * @param {Object} obj object to search value in - * @param {Mixed} value any value (String, Number, etc) - * @return {Boolean} true on value found, false on not found + * @param {Object} object object to search value in + * @param {Mixed} value any value (String, Number, etc) + * @return {Boolean} true on value found, false on not found + * @deprecated use utils.js */ function valueInObject(object, value) // eslint-disable-line no-unused-vars { @@ -434,6 +545,7 @@ function valueInObject(object, value) // eslint-disable-line no-unused-vars * or if JSON.parse(JSON.stringify(obj)) is failing * @param {Object} inObject Object to copy * @return {Object} Copied Object + * @deprecated use utils.js */ function deepCopyFunction(inObject) { @@ -457,6 +569,7 @@ function deepCopyFunction(inObject) * checks if a DOM element actually exists * @param {String} id Element id to check for * @return {Boolean} true if element exists, false on failure + * @deprecated use utils.js */ function exists(id) { @@ -468,6 +581,7 @@ function exists(id) * currently precision is fixed, if dynamic needs check for max/min precision * @param {Number} bytes bytes in int * @return {String} string in GB/MB/KB + * @deprecated use utils.js */ function formatBytes(bytes) // eslint-disable-line no-unused-vars { @@ -484,6 +598,7 @@ function formatBytes(bytes) // eslint-disable-line no-unused-vars * like formatBytes, but returns bytes for <1KB and not 0.n KB * @param {Number} bytes bytes in int * @return {String} string in GB/MB/KB + * @deprecated use utils.js */ function formatBytesLong(bytes) // eslint-disable-line no-unused-vars { @@ -496,6 +611,7 @@ function formatBytesLong(bytes) // eslint-disable-line no-unused-vars * Convert a string with B/K/M/etc into a byte number * @param {String|Number} bytes Any string with B/K/M/etc * @return {String|Number} A byte number, or original string as is + * @deprecated use utils.js */ function stringByteFormat(bytes) // eslint-disable-line no-unused-vars { @@ -526,6 +642,7 @@ function stringByteFormat(bytes) // eslint-disable-line no-unused-vars /** * prints out error messages based on data available from the browser * @param {Object} err error from try/catch block + * @deprecated use utils.js */ function errorCatch(err) { @@ -533,22 +650,20 @@ function errorCatch(err) if (err.stack) { // only FF if (err.lineNumber) { - console.log('ERROR[%s:%s] %s', err.name, err.lineNumber, err.message); + console.error('ERROR[%s:%s] ', err.name, err.lineNumber, err); } else if (err.line) { // only Safari - console.log('ERROR[%s:%s] %s', err.name, err.line, err.message); + console.error('ERROR[%s:%s] ', err.name, err.line, err); } else { - console.log('ERROR[%s] %s', err.name, err.message); + console.error('ERROR[%s] ', err.name, err); } - // stack trace - console.log('ERROR[stack] %s', err.stack); } else if (err.number) { // IE - console.log('ERROR[%s:%s] %s', err.name, err.number, err.message); - console.log('ERROR[description] %s', err.description); + console.error('ERROR[%s:%s] %s', err.name, err.number, err.message); + console.error('ERROR[description] %s', err.description); } else { // the rest - console.log('ERROR[%s] %s', err.name, err.message); + console.error('ERROR[%s] %s', err.name, err.message); } } @@ -571,6 +686,7 @@ function errorCatch(err) * @param {String} loc location name for action indicator * default empty. for console.log * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + * @deprecated use utils.js */ function actionIndicator(loc, overlay = true) // eslint-disable-line no-unused-vars { @@ -587,6 +703,7 @@ function actionIndicator(loc, overlay = true) // eslint-disable-line no-unused-v * @param {String} loc location name for action indicator * default empty. for console.log * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + * @deprecated use utils.js */ function actionIndicatorShow(loc, overlay = true) { @@ -609,6 +726,7 @@ function actionIndicatorShow(loc, overlay = true) * @param {String} loc location name for action indicator * default empty. for console.log * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block + * @deprecated use utils.js */ function actionIndicatorHide(loc, overlay = true) { @@ -621,6 +739,7 @@ function actionIndicatorHide(loc, overlay = true) /** * shows the overlay box or if already visible, bumps the zIndex to 100 + * @deprecated use utils.js */ function overlayBoxShow() { @@ -635,6 +754,7 @@ function overlayBoxShow() /** * hides the overlay box or if zIndex is 100 bumps it down to previous level + * @deprecated use utils.js */ function overlayBoxHide() { @@ -648,6 +768,7 @@ function overlayBoxHide() /** * position the overlay block box and shows it + * @deprecated use utils.js */ function setOverlayBox() // eslint-disable-line no-unused-vars { @@ -658,6 +779,7 @@ function setOverlayBox() // eslint-disable-line no-unused-vars /** * opposite of set, always hides overlay box + * @deprecated use utils.js */ function hideOverlayBox() // eslint-disable-line no-unused-vars { @@ -668,6 +790,7 @@ function hideOverlayBox() // eslint-disable-line no-unused-vars /** * the abort call, clears the action box and hides it and the overlay box + * @deprecated use utils.js */ function ClearCall() // eslint-disable-line no-unused-vars { @@ -689,6 +812,7 @@ function ClearCall() // eslint-disable-line no-unused-vars * zIndex of 1000 * - indicator is page centered * @param {String} loc ID string, only used for console log + * @deprecated use utils.js */ function showActionIndicator(loc) // eslint-disable-line no-unused-vars { @@ -727,6 +851,7 @@ function showActionIndicator(loc) // eslint-disable-line no-unused-vars * the overlayBox is not hidden but the zIndex * is set to this value * @param {String} loc ID string, only used for console log + * @deprecated use utils.js */ function hideActionIndicator(loc) // eslint-disable-line no-unused-vars { @@ -750,6 +875,7 @@ function hideActionIndicator(loc) // eslint-disable-line no-unused-vars /** * checks if overlayBox exists, if not it is * added as hidden item at the body end + * @deprecated use utils.js */ function checkOverlayExists() { @@ -767,6 +893,7 @@ function checkOverlayExists() * if not visible show and set zIndex to 10 (GL_OB_BASE) * if visible, add +1 to the GL_OB_S variable and * up zIndex by this value + * @deprecated use utils.js */ function showOverlayBoxLayers(el_id) // eslint-disable-line no-unused-vars { @@ -799,8 +926,9 @@ function showOverlayBoxLayers(el_id) // eslint-disable-line no-unused-vars * and set zIndex and GL_OB_S to 0 * else just set zIndex to the new GL_OB_S value * @param {String} el_id Target to hide layer + * @deprecated use utils.js */ -function hideOverlayBoxLayers(el_id) +function hideOverlayBoxLayers(el_id='') { // console.log('HIDE overlaybox: %s', GL_OB_S); // remove on layer @@ -824,6 +952,7 @@ function hideOverlayBoxLayers(el_id) /** * only for single action box + * @deprecated use utils.js */ function clearCallActionBox() // eslint-disable-line no-unused-vars { @@ -841,6 +970,7 @@ function clearCallActionBox() // eslint-disable-line no-unused-vars * @param {Array} [css=[]] array for css tags * @param {Object} [options={}] anything else (value, placeholder, OnClick, style) * @return {Object} created element as an object + * @deprecated use utils.js */ function cel(tag, id = '', content = '', css = [], options = {}) { @@ -861,6 +991,7 @@ function cel(tag, id = '', content = '', css = [], options = {}) * @param {Object} attach the object to be attached * @param {String} [id=''] optional id, if given search in base for this id and attach there * @return {Object} "none", technically there is no return needed as it is global attach + * @deprecated use utils.js */ function ael(base, attach, id = '') { @@ -891,6 +1022,7 @@ function ael(base, attach, id = '') * @param {Object} base object to where we attach the elements * @param {...Object} attach attach 1..n: attach directly to the base element those attachments * @return {Object} "none", technically there is no return needed, global attach + * @deprecated use utils.js */ function aelx(base, ...attach) { @@ -907,6 +1039,7 @@ function aelx(base, ...attach) * @param {Object} base object to where we attach the elements * @param {Array} attach array of objects to attach * @return {Object} "none", technically there is no return needed, global attach + * @deprecated use utils.js */ function aelxar(base, attach) // eslint-disable-line no-unused-vars { @@ -921,6 +1054,7 @@ function aelxar(base, attach) // eslint-disable-line no-unused-vars * resets the sub elements of the base element given * @param {Object} base cel created element * @return {Object} returns reset base element + * @deprecated use utils.js */ function rel(base) // eslint-disable-line no-unused-vars { @@ -933,6 +1067,7 @@ function rel(base) // eslint-disable-line no-unused-vars * @param {Object} _element element to work one * @param {String} css style sheet to remove (name) * @return {Object} returns full element + * @deprecated use utils.js */ function rcssel(_element, css) { @@ -948,6 +1083,7 @@ function rcssel(_element, css) * @param {Object} _element element to work on * @param {String} css style sheet to add (name) * @return {Object} returns full element + * @deprecated use utils.js */ function acssel(_element, css) { @@ -965,6 +1101,7 @@ function acssel(_element, css) * @param {String} rcss style to remove (name) * @param {String} acss style to add (name) * @return {Object} returns full element + * @deprecated use utils.js */ function scssel(_element, rcss, acss) // eslint-disable-line no-unused-vars { @@ -977,6 +1114,7 @@ function scssel(_element, rcss, acss) // eslint-disable-line no-unused-vars * that can be inserted into the page * @param {Object} tree object tree with dom element declarations * @return {String} HTML string that can be used as innerHTML + * @deprecated use utils.js */ function phfo(tree) { @@ -1083,6 +1221,7 @@ function phfo(tree) * Is like tree.sub call * @param {Array} list Array of cel created objects * @return {String} HTML String + * @deprecated use utils.js */ function phfa(list) // eslint-disable-line no-unused-vars { @@ -1109,11 +1248,14 @@ function phfa(list) // eslint-disable-line no-unused-vars * @param {String} [sort=''] if empty as is, else allowed 'keys', * 'values' all others are ignored * @return {String} html with build options block + * @deprecated use utils.js */ function html_options(name, data, selected = '', options_only = false, return_string = false, sort = '') // eslint-disable-line no-unused-vars { // wrapper to new call - return html_options_block(name, data, selected, false, options_only, return_string, sort); + return html_options_block( + name, data, selected, 0, options_only, return_string, sort + ); } /** @@ -1134,9 +1276,11 @@ function html_options(name, data, selected = '', options_only = false, return_st * 'values' all others are ignored * @param {String} [onchange=''] onchange trigger call, default unset * @return {String} html with build options block + * @deprecated use utils.js */ -function html_options_block(name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = '') -{ +function html_options_block( + name, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = '' +) { var content = []; var element_select; var select_options = {}; @@ -1173,7 +1317,8 @@ function html_options_block(name, data, selected = '', multiple = 0, options_onl // basic options init options = { 'label': value, - 'value': key + 'value': key, + 'selected': '' }; // add selected if matching if (multiple == 0 && !Array.isArray(selected) && selected == key) { @@ -1184,7 +1329,7 @@ function html_options_block(name, data, selected = '', multiple = 0, options_onl options.selected = ''; } // create the element option - element_option = cel('option', '', value, '', options); + element_option = cel('option', '', value, [], options); // attach it to the select element ael(element_select, element_option); } @@ -1214,6 +1359,7 @@ function html_options_block(name, data, selected = '', multiple = 0, options_onl * @param {String} name name/id * @param {Object} data array of options * @param {String} [sort=''] if empty as is, else allowed 'keys', 'values' + * @deprecated use utils.js * all others are ignored */ function html_options_refill(name, data, sort = '') // eslint-disable-line no-unused-vars @@ -1236,7 +1382,7 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un [].forEach.call(document.querySelectorAll('#' + name + ' :checked'), function(elm) { option_selected = elm.value; }); - document.getElementById(name).innerHTML = ''; + loadEl(name).innerHTML = ''; for (const key of data_list) { value = data[key]; // console.log('add [%s] options: key: %s, value: %s', name, key, value); @@ -1247,7 +1393,7 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un if (key == option_selected) { element_option.selected = true; } - document.getElementById(name).appendChild(element_option); + loadEl(name).appendChild(element_option); } } } @@ -1262,6 +1408,7 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un * @param {String} [return_key=''] if set only returns this key entry * or empty for none * @return {Object|String} parameter entry list + * @deprecated use utils.js */ function parseQueryString(query = '', return_key = '') // eslint-disable-line no-unused-vars { @@ -1311,11 +1458,12 @@ function parseQueryString(query = '', return_key = '') // eslint-disable-line no * all parameters are returned * @param {String} [query=''] different query string to parse, if not * set (default) the current window href is used - * @param {Bool} [single=false] if set to true then only the first found + * @param {Boolean} [single=false] if set to true then only the first found * will be returned * @return {Object|Array|String} if search is empty, object, if search is set * and only one entry, then string, else array * unless single is true + * @deprecated use utils.js */ function getQueryStringParam(search = '', query = '', single = false) // eslint-disable-line no-unused-vars { @@ -1323,7 +1471,7 @@ function getQueryStringParam(search = '', query = '', single = false) // eslint- query = window.location.href; } const url = new URL(query); - let param = ''; + let param = null; if (search) { let _params = url.searchParams.getAll(search); if (_params.length == 1 || single === true) { @@ -1353,6 +1501,7 @@ function getQueryStringParam(search = '', query = '', single = false) // eslint- // *** MASTER logout call /** * submits basic data for form logout + * @deprecated use utils.js */ function loginLogout() // eslint-disable-line no-unused-vars { @@ -1373,6 +1522,7 @@ function loginLogout() // eslint-disable-line no-unused-vars * @param {String} [header_id='mainHeader'] the target for the main element block * if not set mainHeader is assumed * this is the target div for the "loginRow" + * @deprecated use utils.js */ function createLoginRow(login_string, header_id = 'mainHeader') // eslint-disable-line no-unused-vars { @@ -1408,6 +1558,7 @@ function createLoginRow(login_string, header_id = 'mainHeader') // eslint-disabl * @param {String} [header_id='mainHeader'] the target for the main element block * if not set mainHeader is assumed * this is the target div for the "menuRow" + * @deprecated use utils.js */ function createNavMenu(nav_menu, header_id = 'mainHeader') // eslint-disable-line no-unused-vars { diff --git a/www/admin/layout/javascript/edit.pt.js b/www/admin/layout/javascript/edit.pt.js index c338a1d8..35dfed5c 100644 --- a/www/admin/layout/javascript/edit.pt.js +++ b/www/admin/layout/javascript/edit.pt.js @@ -1,5 +1,11 @@ -/* general edit javascript */ -/* prototype version */ +/* +general edit javascript +prototype version +*/ + +/** @deprecated Do not use this anymore, use utils.js */ + +throw new Error("Prototype Support is deprected, please switch to jquery and utils.js/utils.min.js"); /* jshint esversion: 6 */ @@ -25,7 +31,7 @@ function pop(theURL, winName, features) { /** * automatically resize a text area based on the amount of lines in it - * @param {[string} ta_id element id + * @param {string} ta_id element id */ function expandTA(ta_id) { var ta; diff --git a/www/admin/layout/javascript/translateTest-ja_JP.UTF-8.js b/www/admin/layout/javascript/translateTest-ja_JP.UTF-8.js new file mode 100644 index 00000000..6e221691 --- /dev/null +++ b/www/admin/layout/javascript/translateTest-ja_JP.UTF-8.js @@ -0,0 +1,5 @@ +var i18n = { + "Original": "Translated" +}; + +// __END__ diff --git a/www/admin/layout/javascript/utils.js b/www/admin/layout/javascript/utils.js new file mode 100644 index 00000000..52124baa --- /dev/null +++ b/www/admin/layout/javascript/utils.js @@ -0,0 +1,1567 @@ +// src/utils/JavaScriptHelpers.mjs +function errorCatch(err) { + if (err.stack) { + if (err.lineNumber) { + console.error("ERROR[%s:%s] ", err.name, err.lineNumber, err); + } else if (err.line) { + console.error("ERROR[%s:%s] ", err.name, err.line, err); + } else { + console.error("ERROR[%s] ", err.name, err); + } + } else if (err.number) { + console.error("ERROR[%s:%s] %s", err.name, err.number, err.message); + console.error("ERROR[description] %s", err.description); + } else { + console.error("ERROR[%s] %s", err.name, err.message); + } +} +function isFunction(name) { + if (typeof window[name] !== "undefined" && typeof window[name] === "function") { + return true; + } else { + return false; + } +} +function executeFunctionByName(functionName, context) { + var args = Array.prototype.slice.call(arguments, 2); + var namespaces = functionName.split("."); + var func = namespaces.pop(); + if (func == void 0) { + throw new Error("Cannot get function from namespaces: " + functionName); + } + for (var i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; + } + return context[func].apply(context, args); +} +function isObject(val) { + if (val === null) { + return false; + } + return typeof val === "function" || typeof val === "object"; +} +function getObjectCount(object) { + if (!isObject(object)) { + return -1; + } + return Object.keys(object).length; +} +function keyInObject(key, object) { + return objectKeyExists(object, key); +} +function objectKeyExists(object, key) { + return Object.prototype.hasOwnProperty.call(object, key) ? true : false; +} +function getKeyByValue(object, value) { + return Object.keys(object).find((key) => object[key] === value) ?? ""; +} +function valueInObject(object, value) { + return objectValueExists(object, value); +} +function objectValueExists(object, value) { + return Object.keys(object).find((key) => object[key] === value) ? true : false; +} +function deepCopyFunction(inObject) { + var outObject, value, key; + if (typeof inObject !== "object" || inObject === null) { + return inObject; + } + outObject = Array.isArray(inObject) ? [] : {}; + for (key in inObject) { + value = inObject[key]; + outObject[key] = deepCopyFunction(value); + } + return outObject; +} + +// src/utils/DomHelpers.mjs +function loadEl(el_id) { + let el = document.getElementById(el_id); + if (el === null) { + throw new Error("Cannot find: " + el_id); + } + return el; +} +function pop(theURL, winName, features) { + let __winName = window.open(theURL, winName, features); + if (__winName == null) { + return; + } + __winName.focus(); +} +function expandTA(ta_id) { + let ta = this.loadEl(ta_id); + if (ta instanceof HTMLElement && ta.getAttribute("type") !== "textarea") { + throw new Error("Element is not a textarea: " + ta_id); + } + let maxChars = parseInt(ta.getAttribute("cols") ?? "0"); + let ta_value = ta.getAttribute("value"); + let theRows = []; + if (ta_value != null) { + theRows = ta_value.split("\n"); + } + var numNewRows = 0; + for (var i = 0; i < theRows.length; i++) { + if (theRows[i].length + 2 > maxChars) { + numNewRows += Math.ceil((theRows[i].length + 2) / maxChars); + } + } + ta.setAttribute("row", (numNewRows + theRows.length).toString()); +} +function exists(id) { + return $("#" + id).length > 0 ? true : false; +} + +// src/utils/HtmlElementCreator.mjs +var HtmlElementCreator = class { + /** + * reates object for DOM element creation flow + * @param {String} tag must set tag (div, span, etc) + * @param {String} [id=''] optional set for id, if input, select will be used for name + * @param {String} [content=''] text content inside, is skipped if sub elements exist + * @param {Array} [css=[]] array for css tags + * @param {Object} [options={}] anything else (value, placeholder, OnClick, style) + * @return {Object} created element as an object + */ + cel(tag, id = "", content = "", css = [], options = {}) { + return { + tag, + id, + // override name if set, else id is used. Only for input/button + name: options.name, + content, + css, + options, + sub: [] + }; + } + /** + * attach a cel created object to another to create a basic DOM tree + * @param {Object} base object where to attach/search + * @param {Object} attach the object to be attached + * @param {String} [id=''] optional id, if given search in base for this id and attach there + * @return {Object} "none", technically there is no return needed as it is global attach + */ + ael(base, attach, id = "") { + if (id) { + if (base.id == id) { + base.sub.push(deepCopyFunction(attach)); + } else { + if (isObject(base.sub) && base.sub.length > 0) { + for (var i = 0; i < base.sub.length; i++) { + this.ael(base.sub[i], attach, id); + } + } + } + } else { + base.sub.push(deepCopyFunction(attach)); + } + return base; + } + /** + * directly attach n elements to one master base element + * this type does not support attach with optional id + * @param {Object} base object to where we attach the elements + * @param {...Object} attach attach 1..n: attach directly to the base element those attachments + * @return {Object} "none", technically there is no return needed, global attach + */ + aelx(base, ...attach) { + for (var i = 0; i < attach.length; i++) { + base.sub.push(deepCopyFunction(attach[i])); + } + return base; + } + /** + * same as aelx, but instead of using objects as parameters + * get an array of objects to attach + * @param {Object} base object to where we attach the elements + * @param {Array} attach array of objects to attach + * @return {Object} "none", technically there is no return needed, global attach + */ + aelxar(base, attach) { + for (var i = 0; i < attach.length; i++) { + base.sub.push(deepCopyFunction(attach[i])); + } + return base; + } + /** + * resets the sub elements of the base element given + * @param {Object} base cel created element + * @return {Object} returns reset base element + */ + rel(base) { + base.sub = []; + return base; + } + /** + * searches and removes style from css array + * @param {Object} _element element to work one + * @param {String} css style sheet to remove (name) + * @return {Object} returns full element + */ + rcssel(_element, css) { + var css_index = _element.css.indexOf(css); + if (css_index > -1) { + _element.css.splice(css_index, 1); + } + return _element; + } + /** + * adds a new style sheet to the element given + * @param {Object} _element element to work on + * @param {String} css style sheet to add (name) + * @return {Object} returns full element + */ + acssel(_element, css) { + var css_index = _element.css.indexOf(css); + if (css_index == -1) { + _element.css.push(css); + } + return _element; + } + /** + * removes one css and adds another + * is a wrapper around rcssel/acssel + * @param {Object} _element element to work on + * @param {String} rcss style to remove (name) + * @param {String} acss style to add (name) + * @return {Object} returns full element + */ + scssel(_element, rcss, acss) { + this.rcssel(_element, rcss); + this.acssel(_element, acss); + return _element; + } + /** + * parses the object tree created with cel/ael and converts it into an HTML string + * that can be inserted into the page + * @param {Object} tree object tree with dom element declarations + * @return {String} HTML string that can be used as innerHTML + */ + phfo(tree) { + let name_elements = [ + "button", + "fieldset", + "form", + "iframe", + "input", + "map", + "meta", + "object", + "output", + "param", + "select", + "textarea" + ]; + let skip_options = [ + "id", + "name", + "class" + ]; + let no_close = [ + "input", + "br", + "img", + "hr", + "area", + "col", + "keygen", + "wbr", + "track", + "source", + "param", + "command", + // only in header + "base", + "meta", + "link", + "embed" + ]; + var content = []; + var line = "<" + tree.tag; + var i; + if (tree.id) { + line += ' id="' + tree.id + '"'; + if (name_elements.includes(tree.tag)) { + line += ' name="' + (tree.name ? tree.name : tree.id) + '"'; + } + } + if (isObject(tree.css) && tree.css.length > 0) { + line += ' class="'; + for (i = 0; i < tree.css.length; i++) { + line += tree.css[i] + " "; + } + line = line.slice(0, -1); + line += '"'; + } + if (isObject(tree.options)) { + for (const [key, item] of Object.entries(tree.options)) { + if (!skip_options.includes(key)) { + line += " " + key + '="' + item + '"'; + } + } + } + line += ">"; + content.push(line); + if (isObject(tree.sub) && tree.sub.length > 0) { + if (tree.content) { + content.push(tree.content); + } + for (i = 0; i < tree.sub.length; i++) { + content.push(this.phfo(tree.sub[i])); + } + } else if (tree.content) { + content.push(tree.content); + } + if (!no_close.includes(tree.tag)) { + content.push(""); + } + return content.join(""); + } + /** + * Create HTML elements from array list + * as a flat element without master object file + * Is like tree.sub call + * @param {Array} list Array of cel created objects + * @return {String} HTML String + */ + phfa(list) { + var content = []; + for (var i = 0; i < list.length; i++) { + content.push(this.phfo(list[i])); + } + return content.join(""); + } +}; + +// src/utils/HtmlHelpers.mjs +var dom = new HtmlElementCreator(); +function escapeHtml(string) { + return string.replace(/[&<>"'/]/g, function(s) { + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/" + }; + return entityMap[s]; + }); +} +function unescapeHtml(string) { + return string.replace(/&[#\w]+;/g, function(s) { + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + "/": "/" + }; + return entityMap[s]; + }); +} +function html_options(name, data, selected = "", options_only = false, return_string = false, sort = "") { + return this.html_options_block( + name, + data, + selected, + 0, + options_only, + return_string, + sort + ); +} +function html_options_block(name, data, selected = "", multiple = 0, options_only = false, return_string = false, sort = "", onchange = "") { + var content = []; + var element_select; + var select_options = {}; + var element_option; + var data_list = []; + var value; + var options = {}; + if (multiple > 0) { + select_options.multiple = ""; + if (multiple > 1) { + select_options.size = multiple; + } + } + if (onchange) { + select_options.OnChange = onchange; + } + element_select = dom.cel("select", name, "", [], select_options); + if (sort == "keys") { + data_list = Object.keys(data).sort(); + } else if (sort == "values") { + data_list = Object.keys(data).sort((a, b) => ("" + data[a]).localeCompare(data[b])); + } else { + data_list = Object.keys(data); + } + for (const key of data_list) { + value = data[key]; + options = { + "label": value, + "value": key, + "selected": "" + }; + if (multiple == 0 && !Array.isArray(selected) && selected == key) { + options.selected = ""; + } + if (multiple == 1 && Array.isArray(selected) && selected.indexOf(key) != -1) { + options.selected = ""; + } + element_option = dom.cel("option", "", value, [], options); + dom.ael(element_select, element_option); + } + if (!options_only) { + if (return_string) { + content.push(dom.phfo(element_select)); + return content.join(""); + } else { + return element_select; + } + } else { + if (return_string) { + for (var i = 0; i < element_select.sub.length; i++) { + content.push(dom.phfo(element_select.sub[i])); + } + return content.join(""); + } else { + return element_select.sub; + } + } +} +function html_options_refill(name, data, sort = "") { + var element_option; + var option_selected; + var data_list = []; + var value; + if (document.getElementById(name)) { + if (sort == "keys") { + data_list = Object.keys(data).sort(); + } else if (sort == "values") { + data_list = Object.keys(data).sort((a, b) => ("" + data[a]).localeCompare(data[b])); + } else { + data_list = Object.keys(data); + } + [].forEach.call(document.querySelectorAll("#" + name + " :checked"), function(elm) { + option_selected = elm.value; + }); + loadEl(name).innerHTML = ""; + for (const key of data_list) { + value = data[key]; + element_option = document.createElement("option"); + element_option.label = value; + element_option.value = key; + element_option.innerHTML = value; + if (key == option_selected) { + element_option.selected = true; + } + loadEl(name).appendChild(element_option); + } + } +} + +// src/utils/MathHelpers.mjs +function dec2hex(dec) { + return ("0x" + dec.toString(16)).substring(-2); +} +function getRandomIntInclusive(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1) + min); +} +function roundPrecision(number, precision) { + if (isNaN(number) || isNaN(precision)) { + return number; + } + return Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision); +} + +// src/utils/StringHelpers.mjs +function formatString(string, ...args) { + return string.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != "undefined" ? args[number] : match; + }); +} +function numberWithCommas(number) { + var parts = number.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); +} +function convertLBtoBR(string) { + return string.replace(/(?:\r\n|\r|\n)/g, "
"); +} + +// src/utils/DateTimeHelpers.mjs +function getTimestamp() { + var date = /* @__PURE__ */ new Date(); + return date.getTime(); +} + +// src/utils/UniqIdGenerators.mjs +function generateId(len) { + var arr = new Uint8Array((len || 40) / 2); + (window.crypto || // @ts-ignore + window.msCrypto).getRandomValues(arr); + return Array.from(arr, self.dec2hex).join(""); +} +function randomIdF() { + return Math.random().toString(36).substring(2); +} + +// src/utils/ResizingAndMove.mjs +function getWindowSize() { + var width, height; + width = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth); + height = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight); + return { + width, + height + }; +} +function getScrollOffset() { + var left, top; + left = window.pageXOffset || (window.document.documentElement.scrollLeft || window.document.body.scrollLeft); + top = window.pageYOffset || (window.document.documentElement.scrollTop || window.document.body.scrollTop); + return { + left, + top + }; +} +function getScrollOffsetOpener() { + var left, top; + left = opener.window.pageXOffset || (opener.document.documentElement.scrollLeft || opener.document.body.scrollLeft); + top = opener.window.pageYOffset || (opener.document.documentElement.scrollTop || opener.document.body.scrollTop); + return { + left, + top + }; +} +function setCenter(id, left, top) { + var dimensions = { + height: $("#" + id).height() ?? 0, + width: $("#" + id).width() ?? 0 + }; + var type = $("#" + id).css("position"); + var viewport = this.getWindowSize(); + var offset = this.getScrollOffset(); + if (left) { + $("#" + id).css({ + left: viewport.width / 2 - dimensions.width / 2 + offset.left + "px" + }); + } + if (top) { + var top_pos = type == "fixed" ? viewport.height / 2 - dimensions.height / 2 : viewport.height / 2 - dimensions.height / 2 + offset.top; + $("#" + id).css({ + top: top_pos + "px" + }); + } +} +function goToPos(element, offset = 0, duration = 500, base = "body,html") { + try { + let element_offset = $("#" + element).offset(); + if (element_offset == void 0) { + return; + } + if ($("#" + element).length) { + $(base).animate({ + scrollTop: element_offset.top - offset + }, duration); + } + } catch (err) { + errorCatch(err); + } +} +function goTo(target) { + loadEl(target).scrollIntoView({ + behavior: "smooth" + }); +} + +// src/utils/FormatBytes.mjs +function formatBytes(bytes) { + var i = -1; + if (typeof bytes === "bigint") { + bytes = Number(bytes); + } + if (isNaN(bytes)) { + return bytes.toString(); + } + do { + bytes = bytes / 1024; + i++; + } while (bytes > 99); + return Math.round(bytes * Math.pow(10, 2)) / Math.pow(10, 2) + ["kB", "MB", "GB", "TB", "PB", "EB"][i]; +} +function formatBytesLong(bytes) { + if (typeof bytes === "bigint") { + bytes = Number(bytes); + } + if (isNaN(bytes)) { + return bytes.toString(); + } + let negative = false; + if (bytes < 0) { + negative = true; + bytes *= -1; + } + var i = Math.floor(Math.log(bytes) / Math.log(1024)); + var sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + return (negative ? "-" : "") + ((bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i]).toString(); +} +function stringByteFormat(bytes, raw = false) { + if (!(typeof bytes === "string" || bytes instanceof String)) { + return bytes.toString(); + } + let valid_units = "bkmgtpezy"; + let regex = /([\d.,]*)\s?(eb|pb|tb|gb|mb|kb|e|p|t|g|m|k|b)$/i; + let matches = bytes.match(regex); + if (matches !== null) { + let m1 = parseFloat(matches[1].replace(/[^0-9.]/, "")); + let m2 = matches[2].replace(/[^bkmgtpezy]/i, "").charAt(0).toLowerCase(); + if (m2) { + bytes = m1 * Math.pow(1024, valid_units.indexOf(m2)); + } + } + if (raw) { + return bytes; + } + return Math.round(bytes); +} + +// src/utils/UrlParser.mjs +function parseQueryString(query = "", return_key = "", single = false) { + return getQueryStringParam(return_key, query, single); +} +function getQueryStringParam(search = "", query = "", single = false) { + if (!query) { + query = window.location.href; + } + const url = new URL(query); + let param = null; + if (search) { + let _params = url.searchParams.getAll(search); + if (_params.length == 1 || single === true) { + param = _params[0]; + } else if (_params.length > 1) { + param = _params; + } + } else { + param = {}; + for (const [key] of url.searchParams.entries()) { + if (typeof param[key] === "undefined") { + let _params = url.searchParams.getAll(key); + param[key] = _params.length < 2 || single === true ? _params[0] : _params; + } + } + } + return param; +} + +// src/utils/LoginLogout.mjs +function loginLogout() { + const form = document.createElement("form"); + form.method = "post"; + const hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "login_logout"; + hiddenField.value = "Logout"; + form.appendChild(hiddenField); + document.body.appendChild(form); + form.submit(); +} + +// src/utils/ActionIndicatorOverlayBox.mjs +function actionIndicator(loc, overlay = true) { + if ($("#indicator").is(":visible")) { + this.actionIndicatorHide(loc, overlay); + } else { + this.actionIndicatorShow(loc, overlay); + } +} +function actionIndicatorShow(loc, overlay = true) { + if (!$("#indicator").is(":visible")) { + if (!$("#indicator").hasClass("progress")) { + $("#indicator").addClass("progress"); + } + setCenter("indicator", true, true); + $("#indicator").show(); + } + if (overlay === true) { + this.overlayBoxShow(); + } +} +function actionIndicatorHide(loc, overlay = true) { + $("#indicator").hide(); + if (overlay === true) { + overlayBoxHide(); + } +} +function overlayBoxShow() { + if ($("#overlayBox").is(":visible")) { + $("#overlayBox").css("zIndex", "100"); + } else { + $("#overlayBox").show(); + $("#overlayBox").css("zIndex", "98"); + } +} +function overlayBoxHide() { + if (parseInt($("#overlayBox").css("zIndex")) >= 100) { + $("#overlayBox").css("zIndex", "98"); + } else { + $("#overlayBox").hide(); + } +} +function setOverlayBox() { + if (!$("#overlayBox").is(":visible")) { + $("#overlayBox").show(); + } +} +function hideOverlayBox() { + if ($("#overlayBox").is(":visible")) { + $("#overlayBox").hide(); + } +} +function ClearCall() { + $("#actionBox").html(""); + $("#actionBox").hide(); + $("#overlayBox").hide(); +} +var ActionIndicatorOverlayBox = class { + // open overlay boxes counter for z-index + #GL_OB_S = 100; + #GL_OB_BASE = 100; + /** + * show action indicator + * - checks if not existing and add + * - only shows if not visible (else ignore) + * - overlaybox check is called and shown on a fixzed + * zIndex of 1000 + * - indicator is page centered + * @param {String} loc ID string, only used for console log + */ + showActionIndicator(loc) { + if ($("#indicator").length == 0) { + var el = document.createElement("div"); + el.className = "progress hide"; + el.id = "indicator"; + $("body").append(el); + } else if (!$("#indicator").hasClass("progress")) { + $("#indicator").addClass("progress").hide(); + } + if (!$("#indicator").is(":visible")) { + this.checkOverlayExists(); + if (!$("#overlayBox").is(":visible")) { + $("#overlayBox").show(); + } + $("#overlayBox").css("zIndex", 1e3); + $("#indicator").show(); + setCenter("indicator", true, true); + } + } + /** + * hide action indicator, if it is visiable + * If the global variable GL_OB_S is > GL_OB_BASE then + * the overlayBox is not hidden but the zIndex + * is set to this value + * @param {String} loc ID string, only used for console log + */ + hideActionIndicator(loc) { + if ($("#indicator").is(":visible")) { + $("#indicator").hide(); + if (this.#GL_OB_S > this.#GL_OB_BASE) { + $("#overlayBox").css("zIndex", this.#GL_OB_S); + } else { + $("#overlayBox").hide(); + $("#overlayBox").css("zIndex", this.#GL_OB_BASE); + } + } + } + /** + * checks if overlayBox exists, if not it is + * added as hidden item at the body end + */ + checkOverlayExists() { + if ($("#overlayBox").length == 0) { + var el = document.createElement("div"); + el.className = "overlayBoxElement hide"; + el.id = "overlayBox"; + $("body").append(el); + } + } + /** + * show overlay box + * if not visible show and set zIndex to 10 (GL_OB_BASE) + * if visible, add +1 to the GL_OB_S variable and + * up zIndex by this value + */ + showOverlayBoxLayers(el_id) { + if (!$("#overlayBox").is(":visible")) { + $("#overlayBox").show(); + $("#overlayBox").css("zIndex", this.#GL_OB_BASE); + this.#GL_OB_S = this.#GL_OB_BASE; + } + this.#GL_OB_S++; + $("#overlayBox").css("zIndex", this.#GL_OB_S); + if (el_id) { + if ($("#" + el_id).length > 0) { + $("#" + el_id).css("zIndex", this.#GL_OB_S + 1); + $("#" + el_id).show(); + } + } + } + /** + * hide overlay box + * lower GL_OB_S value by -1 + * if we are 10 (GL_OB_BASE) or below hide the overlayIndex + * and set zIndex and GL_OB_S to 0 + * else just set zIndex to the new GL_OB_S value + * @param {String} el_id Target to hide layer + */ + hideOverlayBoxLayers(el_id = "") { + this.#GL_OB_S--; + if (this.#GL_OB_S <= this.#GL_OB_BASE) { + this.#GL_OB_S = this.#GL_OB_BASE; + $("#overlayBox").hide(); + $("#overlayBox").css("zIndex", this.#GL_OB_BASE); + } else { + $("#overlayBox").css("zIndex", this.#GL_OB_S); + } + if (el_id) { + $("#" + el_id).hide(); + $("#" + el_id).css("zIndex", 0); + } + } + /** + * only for single action box + */ + clearCallActionBox() { + $("#actionBox").html(""); + $("#actionBox").hide(); + this.hideOverlayBoxLayers(); + } +}; + +// src/utils/l10nTranslation.mjs +var l10nTranslation = class { + #i18n = {}; + constructor(i18n2) { + this.#i18n = i18n2; + } + /** + * uses the i18n object created in the translation template + * that is filled from gettext in PHP + * @param {String} string text to translate + * @return {String} translated text (based on PHP selected language) + */ + __(string) { + if (typeof this.#i18n !== "undefined" && isObject(this.#i18n) && this.#i18n[string]) { + return this.#i18n[string]; + } else { + return string; + } + } +}; + +// src/utils/ActionBox.mjs +var ActionBox = class { + // open overlay boxes counter for z-index + zIndex = { + base: 100, + max: 110, + indicator: 0, + boxes: {}, + active: [], + top: "" + }; + // general action box storage + action_box_storage = {}; + // set to 10 min (*60 for seconds, *1000 for microseconds) + action_box_cache_timeout = 10 * 60 * 1e3; + hec; + l10n; + /** + * action box creator + * @param {Object} hec HtmlElementCreator + * @param {Object} l10n l10nTranslation + */ + constructor(hec2, l10n2) { + this.hec = hec2; + this.l10n = l10n2; + } + /** + * Show an action box + * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new + * @param {string} [content=''] content to add to the box + * @param {array} [action_box_css=[]] additional css elements for the action box + * @param {number} [override=0] override size adjust + * @param {number} [content_override=0] override content size adjust + */ + showFillActionBox(target_id = "actionBox", content = "", action_box_css = [], override = 0, content_override = 0) { + this.fillActionBox(target_id, content, action_box_css); + this.showActionBox(target_id, override, content_override); + } + /** + * Fill action box with content, create it if it does not existgs + * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new + * @param {string} [content=''] content to add to the box + * @param {array} [action_box_css=[]] additional css elements for the action box + */ + fillActionBox(target_id = "actionBox", content = "", action_box_css = []) { + if (!exists(target_id)) { + $("#mainContainer").after( + this.hec.phfo(this.hec.cel("div", target_id, "", ["actionBoxElement", "hide"].concat(action_box_css))) + ); + } + $("#" + target_id).html(content); + } + /** + * Adjust the size of the action box + * @param {string} [target_id='actionBox'] which actionBox to work on + * @param {number} [override=0] override size adjust + * @param {number} [content_override=0] override content size adjust + */ + adjustActionBox(target_id = "actionBox", override = 0, content_override = 0) { + this.adjustActionBoxHeight(target_id, override, content_override); + setCenter(target_id, true, true); + } + /** + * hide any open action boxes and hide overlay + */ + hideAllActionBoxes() { + $('#actionBox, div[id^="actionBox-"].actionBoxElement').hide(); + $("#overlayBox").hide(); + } + /** + * hide action box, but do not clear content + * DEPRECATED + * @param {string} [target_id='actionBox'] + */ + hideActionBox(target_id = "actionBox") { + this.closeActionBoxFloat(target_id, false); + } + /** + * Just show and adjust the box + * DEPRECAED + * @param {string} [target_id='actionBox'] which actionBox to work on + * @param {number} [override=0] override size adjust + * @param {number} [content_override=0] override content size adjust + * @param {Boolean} [hide_all=false] if set to true, hide all other action boxes + */ + showActionBox(target_id = "actionBox", override = 0, content_override = 0, hide_all = true) { + this.showActionBoxFloat(target_id, override, content_override, hide_all); + } + /** + * close an action box with default clear content + * for just hide use hideActionBox + * DEPRECATED + * @param {String} [target_id='actionBox'] which action box to close, default is set + * @param {Boolean} [clean=true] if set to false will not remove html content, just hide + */ + closeActionBox(target_id = "actionBox", clean = true) { + this.closeActionBoxFloat(target_id, clean); + } + /** + * TODO: better stacked action box: OPEN + * @param {string} [target_id='actionBox'] which actionBox to work on + * @param {number} [override=0] override size adjust + * @param {number} [content_override=0] override content size adjust + * @param {boolean} [hide_all=false] if set to true, hide all other action boxes + */ + showActionBoxFloat(target_id = "actionBox", override = 0, content_override = 0, hide_all = false) { + if (hide_all === true) { + this.hideAllActionBoxes(); + } + if (!exists("overlayBox")) { + $("body").prepend(this.hec.phfo(this.hec.cel("div", "overlayBox", "", ["overlayBoxElement"]))); + $("#overlayBox").css("zIndex", this.zIndex.base); + } + $("#overlayBox").show(); + if (!keyInObject(target_id, this.zIndex.boxes)) { + this.zIndex.boxes[target_id] = this.zIndex.max; + this.zIndex.max += 10; + } else if (this.zIndex.boxes[target_id] + 10 < this.zIndex.max) { + this.zIndex.boxes[target_id] = this.zIndex.max; + this.zIndex.max += 10; + } + if (!this.zIndex.indicator) { + $("#overlayBox").css("zIndex", this.zIndex.boxes[target_id] - 1); + } + $("#" + target_id).css("zIndex", this.zIndex.boxes[target_id]).show(); + if (this.zIndex.active.indexOf(target_id) == -1) { + this.zIndex.active.push(target_id); + } + this.zIndex.top = target_id; + this.adjustActionBox(target_id, override, content_override); + } + /** + * TODO: better stacked action box: CLOSE + * @param {String} [target_id='actionBox'] which action box to close, default is set + * @param {Boolean} [clean=true] if set to false will not remove html content, just hide + */ + closeActionBoxFloat(target_id = "actionBox", clean = true) { + if (!exists(target_id)) { + return; + } + if (keyInObject(target_id, this.action_box_storage) && clean === true) { + this.action_box_storage[target_id] = {}; + } + if (clean === true) { + $("#" + target_id).html(""); + } + $("#" + target_id).hide(); + let idx = this.zIndex.active.indexOf(target_id); + this.zIndex.active.splice(idx, 1); + let visible_zIndexes = $('#actionBox:visible, div[id^="actionBox-"].actionBoxElement:visible').map((i, el) => ({ + id: el.id, + zIndex: $("#" + el.id).css("zIndex") + })).get(); + if (visible_zIndexes.length > 0) { + let max_zIndex = 0; + let max_el_id = ""; + for (let zIndex_el of visible_zIndexes) { + if (parseInt(zIndex_el.zIndex) > max_zIndex) { + max_zIndex = parseInt(zIndex_el.zIndex); + max_el_id = zIndex_el.id; + } + } + $("#overlayBox").css("zIndex", max_zIndex - 1); + this.zIndex.top = max_el_id; + } else { + $("#overlayBox").hide(); + } + } + /** + * create a new action box and fill it with basic elements + * @param {String} [target_id='actionBox'] + * @param {String} [title=''] + * @param {Object} [contents={}] + * @param {Object} [headers={}] + * @param {Boolean} [show_close=true] + * @param {Object} [settings={}] Optional settings, eg style sheets + */ + createActionBox(target_id = "actionBox", title = "", contents = {}, headers = {}, settings = {}, show_close = true) { + if (!keyInObject(target_id, this.action_box_storage)) { + this.action_box_storage[target_id] = {}; + } + let header_css = []; + if (keyInObject("header_css", settings)) { + header_css = settings.header_css; + } + let action_box_css = []; + if (keyInObject("action_box_css", settings)) { + action_box_css = settings.action_box_css; + } + let elements = []; + elements.push(this.hec.phfo( + this.hec.aelx( + this.hec.cel("div", target_id + "_title", "", ["actionBoxTitle", "flx-spbt"].concat(header_css)), + ...show_close === true ? [ + // title + this.hec.cel("div", "", title, ["fs-b", "w-80"]), + // close button + this.hec.aelx( + this.hec.cel("div", target_id + "_title_close_button", "", ["w-20", "tar"]), + this.hec.cel( + "input", + target_id + "_title_close", + "", + ["button-close", "fs-s"], + { + type: "button", + value: this.l10n.__("Close"), + OnClick: "closeActionBox('" + target_id + "', false);" + } + ) + ) + ] : [ + this.hec.cel("div", "", title, ["fs-b", "w-100"]) + ] + ) + )); + if (getObjectCount(headers) > 0) { + if (keyInObject("raw_string", headers)) { + elements.push(headers.raw_string); + } else { + elements.push(this.hec.phfo(headers)); + } + } + if (getObjectCount(contents) > 0) { + if (keyInObject("raw_string", contents)) { + elements.push(contents.raw_string); + } else { + elements.push(this.hec.phfo(contents)); + } + } else { + elements.push(this.hec.phfo(this.hec.cel("div", target_id + "_content", "", []))); + } + elements.push(this.hec.phfo( + this.hec.aelx( + this.hec.cel("div", target_id + "_footer", "", ["pd-5", "flx-spbt"]), + ...show_close === true ? [ + // dummy spacer + this.hec.cel("div", "", "", ["fs-b", "w-80"]), + // close button + this.hec.aelx( + this.hec.cel("div", target_id + "_footer_close_button", "", ["tar", "w-20"]), + this.hec.cel( + "input", + target_id + "_footer_close", + "", + ["button-close", "fs-s"], + { + type: "button", + value: this.l10n.__("Close"), + OnClick: "closeActionBox('" + target_id + "', false);" + } + ) + ) + ] : [ + this.hec.cel("div", "", "", ["fs-b", "w-100"]) + ] + ) + )); + elements.push(this.hec.phfo(this.hec.cel("input", target_id + "-cache_time", "", [], { + type: "hidden", + value: Date.now() + }))); + this.fillActionBox(target_id, elements.join(""), action_box_css); + } + /** + * adjusts the action box height based on content and window height of browser + * TODO: border on outside/and other margin things need to be added in overall adjustment + * @param {String} [target_id='actionBox'] target id, if not set, fall back to default + * @param {Number} [override=0] override value to add to the actionBox height + * @param {Number} [content_override=0] override the value from _content block if it exists + */ + adjustActionBoxHeight(target_id = "actionBox", override = 0, content_override = 0) { + var new_height = 0; + var dim = {}; + var abc_dim = {}; + var content_id = ""; + if (isNaN(override)) { + override = 0; + } + if (isNaN(content_override)) { + content_override = 0; + } + switch (target_id) { + case "actionBox": + content_id = "action_box"; + break; + case "actionBoxSub": + content_id = "action_box_sub"; + break; + default: + content_id = target_id; + break; + } + $.each([target_id, content_id + "_content"], function(i, v) { + $("#" + v).css({ + "height": "", + "width": "" + }); + }); + if (exists(content_id + "_title")) { + dim.height = $("#" + content_id + "_title").outerHeight(); + console.log("Target: %s, Action box Title: %s", target_id, dim.height); + new_height += dim.height ?? 0; + } + if (exists(content_id + "_header")) { + dim.height = $("#" + content_id + "_header").outerHeight(); + console.log("Target: %s, Action box Header: %s", target_id, dim.height); + new_height += dim.height ?? 0; + } + if (exists(content_id + "_content")) { + if (content_override > 0) { + console.log("Target: %s, Action box Content Override: %s", target_id, content_override); + new_height += content_override; + } else { + abc_dim.height = $("#" + content_id + "_content").outerHeight(); + console.log("Target: %s, Action box Content: %s", target_id, abc_dim.height); + new_height += abc_dim.height ?? 0; + } + } + if (exists(content_id + "_footer")) { + dim.height = $("#" + content_id + "_footer").outerHeight(); + console.log("Target: %s, Action box Footer: %s", target_id, dim.height); + new_height += dim.height ?? 0; + } + new_height += override; + var viewport = getWindowSize(); + if (new_height >= viewport.height) { + if (exists(content_id + "_content")) { + if (!$("#" + content_id + "_content").hasClass("of-s-y")) { + $("#" + content_id + "_content").addClass("of-s-y"); + } + } + console.log("Target: %s, Viewport: %s, ActionBox (NH): %s, ABcontent: %s, ABouter: %s", target_id, viewport.height, new_height, abc_dim.height, $("#" + target_id).outerHeight()); + var m_height = viewport.height - (new_height - (abc_dim.height ?? 0)); + console.log("Target: %s, New ABcontent: %s", target_id, m_height); + $("#" + content_id + "_content").css("height", m_height + "px"); + new_height = new_height - (abc_dim.height ?? 0) + m_height; + console.log("Target: %s, New Hight: %s", target_id, new_height); + } else { + if (exists(content_id + "_content")) { + if ($("#" + content_id + "_content").hasClass("of-s-y")) { + $("#" + content_id + "_content").removeClass("of-s-y"); + } + } + } + console.log("Target: %s, Action Box new height: %s px (override %s px, content override %s px), window height: %s px, Visible Height: %s px", target_id, new_height, override, content_override, viewport.height, $("#" + content_id).outerHeight()); + $("#" + target_id).css("height", new_height + "px"); + } +}; + +// src/utils/LoginNavMenu.mjs +var LoginNavMenu = class { + hec; + l10n; + /** + * action box creator + * @param {Object} hec HtmlElementCreator + * @param {Object} l10n l10nTranslation + */ + constructor(hec2, l10n2) { + this.hec = hec2; + this.l10n = l10n2; + } + /** + * create login string and logout button elements + * @param {String} login_string the login string to show on the left + * @param {String} [header_id='mainHeader'] the target for the main element block + * if not set mainHeader is assumed + * this is the target div for the "loginRow" + */ + createLoginRow(login_string, header_id = "mainHeader") { + if (exists(header_id)) { + if (!exists("loginRow")) { + $("#" + header_id).html(this.hec.phfo(this.hec.cel("div", "loginRow", "", ["loginRow", "flx-spbt"]))); + } + $("#loginRow").html(this.hec.phfo(this.hec.cel("div", "loginRow-name", login_string))); + $("#loginRow").append(this.hec.phfo(this.hec.cel("div", "loginRow-info", ""))); + $("#loginRow").append(this.hec.phfo( + this.hec.aelx( + // outer div + this.hec.cel("div", "loginRow-logout"), + // inner element + this.hec.cel("input", "logout", "", [], { + value: this.l10n.__("Logout"), + type: "button", + onClick: "loginLogout()" + }) + ) + )); + } + } + /** + * create the top nav menu that switches physical between pages + * (edit access data based) + * @param {Object} nav_menu the built nav menu with highlight info + * @param {String} [header_id='mainHeader'] the target for the main element block + * if not set mainHeader is assumed + * this is the target div for the "menuRow" + */ + createNavMenu(nav_menu, header_id = "mainHeader") { + if (isObject(nav_menu) && getObjectCount(nav_menu) > 1) { + if (!exists("menuRow")) { + $("#" + header_id).html(this.hec.phfo(this.hec.cel("div", "menuRow", "", ["menuRow", "flx-s"]))); + } + var content = []; + $.each(nav_menu, function(key, item) { + if (key != 0) { + content.push(this.hec.phfo(this.hec.cel("div", "", "·", ["pd-2"]))); + } + if (item.enabled) { + if (window.location.href.indexOf(item.url) != -1) { + item.selected = 1; + } + content.push(this.hec.phfo( + this.hec.aelx( + this.hec.cel("div"), + this.hec.cel("a", "", item.name, ["pd-2"].concat(item.selected ? "highlight" : ""), { + href: item.url + }) + ) + )); + } + }); + $("#menuRow").html(content.join("")); + } else { + $("#menuRow").hide(); + } + } +}; + +// src/utils.mjs +var aiob = new ActionIndicatorOverlayBox(); +var hec = new HtmlElementCreator(); +var l10n = new l10nTranslation(typeof i18n === "undefined" ? {} : i18n); +var ab = new ActionBox(hec, l10n); +var lnm = new LoginNavMenu(hec, l10n); +if (!String.prototype.format) { + String.prototype.format = function() { + console.error("[DEPRECATED] use StringHelpers.formatString"); + return formatString(this, arguments); + }; +} +if (Number.prototype.round) { + Number.prototype.round = function(prec) { + console.error("[DEPRECATED] use MathHelpers.roundPrecision"); + return roundPrecision(this, prec); + }; +} +if (!String.prototype.escapeHTML) { + String.prototype.escapeHTML = function() { + console.error("[DEPRECATED] use HtmlHelpers.escapeHtml"); + return escapeHtml(this); + }; +} +if (!String.prototype.unescapeHTML) { + String.prototype.unescapeHTML = function() { + console.error("[DEPRECATED] use HtmlHelpers.unescapeHtml"); + return unescapeHtml(this); + }; +} +function escapeHtml2(string) { + return escapeHtml(string); +} +function roundPrecision2(number, prec) { + return roundPrecision(number, prec); +} +function formatString2(string, ...args) { + return formatString(string, ...args); +} +function unescapeHtml2(string) { + return unescapeHtml(string); +} +function loadEl2(el_id) { + return loadEl(el_id); +} +function pop2(theURL, winName, features) { + pop(theURL, winName, features); +} +function expandTA2(ta_id) { + expandTA(ta_id); +} +function getWindowSize2() { + return getWindowSize(); +} +function getScrollOffset2() { + return getScrollOffset(); +} +function getScrollOffsetOpener2() { + return getScrollOffsetOpener(); +} +function setCenter2(id, left, top) { + setCenter(id, left, top); +} +function goToPos2(element, offset = 0, duration = 500, base = "body,html") { + goToPos(element, offset, duration, base); +} +function goTo2(target) { + goTo(target); +} +function __(string) { + return l10n.__(string); +} +function numberWithCommas2(x) { + return numberWithCommas(x); +} +function convertLBtoBR2(string) { + return convertLBtoBR(string); +} +function getTimestamp2() { + return getTimestamp(); +} +function dec2hex2(dec) { + return dec2hex(dec); +} +function generateId2(len) { + return generateId(len); +} +function randomIdF2() { + return randomIdF(); +} +function getRandomIntInclusive2(min, max) { + return getRandomIntInclusive(min, max); +} +function isFunction2(name) { + return isFunction(name); +} +function executeFunctionByName2(functionName, context) { + return executeFunctionByName(functionName, context); +} +function isObject2(val) { + return isObject(val); +} +function getObjectCount2(object) { + return getObjectCount(object); +} +function keyInObject2(key, object) { + return keyInObject(key, object); +} +function getKeyByValue2(object, value) { + return getKeyByValue(object, value); +} +function valueInObject2(object, value) { + return valueInObject(object, value); +} +function deepCopyFunction2(inObject) { + return deepCopyFunction(inObject); +} +function exists2(id) { + return exists(id); +} +function formatBytes2(bytes) { + return formatBytes(bytes); +} +function formatBytesLong2(bytes) { + return formatBytesLong(bytes); +} +function stringByteFormat2(bytes) { + return stringByteFormat(bytes); +} +function errorCatch2(err) { + errorCatch(err); +} +function actionIndicator2(loc, overlay = true) { + actionIndicator(loc, overlay); +} +function actionIndicatorShow2(loc, overlay = true) { + actionIndicatorShow(loc, overlay); +} +function actionIndicatorHide2(loc, overlay = true) { + actionIndicatorHide(loc, overlay); +} +function overlayBoxShow2() { + overlayBoxShow(); +} +function overlayBoxHide2() { + overlayBoxHide(); +} +function setOverlayBox2() { + setOverlayBox(); +} +function hideOverlayBox2() { + hideOverlayBox(); +} +function ClearCall2() { + ClearCall(); +} +function showActionIndicator(loc) { + aiob.showActionIndicator(loc); +} +function hideActionIndicator(loc) { + aiob.hideActionIndicator(loc); +} +function checkOverlayExists() { + aiob.checkOverlayExists(); +} +function showOverlayBoxLayers(el_id) { + aiob.showOverlayBoxLayers(el_id); +} +function hideOverlayBoxLayers(el_id = "") { + aiob.hideOverlayBoxLayers(el_id); +} +function clearCallActionBox() { + aiob.clearCallActionBox(); +} +function cel(tag, id = "", content = "", css = [], options = {}) { + return hec.cel(tag, id, content, css, options); +} +function ael(base, attach, id = "") { + return hec.ael(base, attach, id); +} +function aelx(base, ...attach) { + return hec.aelx(base, ...attach); +} +function aelxar(base, attach) { + return hec.aelxar(base, attach); +} +function rel(base) { + return hec.rel(base); +} +function rcssel(_element, css) { + return hec.rcssel(_element, css); +} +function acssel(_element, css) { + return hec.acssel(_element, css); +} +function scssel(_element, rcss, acss) { + hec.scssel(_element, rcss, acss); +} +function phfo(tree) { + return hec.phfo(tree); +} +function phfa(list) { + return hec.phfa(list); +} +function html_options2(name, data, selected = "", options_only = false, return_string = false, sort = "") { + return html_options(name, data, selected, options_only, return_string, sort); +} +function html_options_block2(name, data, selected = "", multiple = 0, options_only = false, return_string = false, sort = "", onchange = "") { + return html_options_block( + name, + data, + selected, + multiple, + options_only, + return_string, + sort, + onchange + ); +} +function html_options_refill2(name, data, sort = "") { + html_options_refill(name, data, sort); +} +function parseQueryString2(query = "", return_key = "") { + return parseQueryString(query, return_key); +} +function getQueryStringParam2(search = "", query = "", single = false) { + return getQueryStringParam(search, query, single); +} +function loginLogout2() { + loginLogout(); +} +function createLoginRow(login_string, header_id = "mainHeader") { + lnm.createLoginRow(login_string, header_id); +} +function createNavMenu(nav_menu, header_id = "mainHeader") { + lnm.createNavMenu(nav_menu, header_id); +} +function showFillActionBox(target_id = "actionBox", content = "", action_box_css = [], override = 0, content_override = 0) { + ab.showFillActionBox(target_id, content, action_box_css, override, content_override); +} +function fillActionBox(target_id = "actionBox", content = "", action_box_css = []) { + ab.fillActionBox(target_id, content, action_box_css); +} +function adjustActionBox(target_id = "actionBox", override = 0, content_override = 0) { + ab.adjustActionBox(target_id, override, content_override); +} +function hideAllActionBoxes() { + ab.hideAllActionBoxes(); +} +function hideActionBox(target_id = "actionBox") { + ab.hideActionBox(target_id); +} +function showActionBox(target_id = "actionBox", override = 0, content_override = 0, hide_all = true) { + ab.showActionBox(target_id, override, content_override, hide_all); +} +function closeActionBox(target_id = "actionBox", clean = true) { + ab.closeActionBox(target_id, clean); +} +function showActionBoxFloat(target_id = "actionBox", override = 0, content_override = 0, hide_all = false) { + ab.showActionBoxFloat(target_id, override, content_override, hide_all); +} +function closeActionBoxFloat(target_id = "actionBox", clean = true) { + ab.closeActionBoxFloat(target_id, clean); +} +function createActionBox(target_id = "actionBox", title = "", contents = {}, headers = {}, settings = {}, show_close = true) { + ab.createActionBox(target_id, title, contents, headers, settings, show_close); +} +function adjustActionBoxHeight(target_id = "actionBox", override = 0, content_override = 0) { + ab.adjustActionBoxHeight(target_id, override, content_override); +} diff --git a/www/admin/layout/javascript/utils.min.js b/www/admin/layout/javascript/utils.min.js new file mode 100644 index 00000000..782dc1ce --- /dev/null +++ b/www/admin/layout/javascript/utils.min.js @@ -0,0 +1,3 @@ +function errorCatch(err){err.stack?err.lineNumber?console.error("ERROR[%s:%s] ",err.name,err.lineNumber,err):err.line?console.error("ERROR[%s:%s] ",err.name,err.line,err):console.error("ERROR[%s] ",err.name,err):err.number?(console.error("ERROR[%s:%s] %s",err.name,err.number,err.message),console.error("ERROR[description] %s",err.description)):console.error("ERROR[%s] %s",err.name,err.message)}function isFunction(name){return typeof window[name]<"u"&&typeof window[name]=="function"}function executeFunctionByName(functionName,context){var args=Array.prototype.slice.call(arguments,2),namespaces=functionName.split("."),func=namespaces.pop();if(func==null)throw new Error("Cannot get function from namespaces: "+functionName);for(var i=0;iobject[key]===value)??""}function valueInObject(object,value){return objectValueExists(object,value)}function objectValueExists(object,value){return!!Object.keys(object).find(key=>object[key]===value)}function deepCopyFunction(inObject){var outObject,value,key;if(typeof inObject!="object"||inObject===null)return inObject;outObject=Array.isArray(inObject)?[]:{};for(key in inObject)value=inObject[key],outObject[key]=deepCopyFunction(value);return outObject}function loadEl(el_id){let el=document.getElementById(el_id);if(el===null)throw new Error("Cannot find: "+el_id);return el}function pop(theURL,winName,features){let __winName=window.open(theURL,winName,features);__winName?.focus()}function expandTA(ta_id){let ta=this.loadEl(ta_id);if(ta instanceof HTMLElement&&ta.getAttribute("type")!=="textarea")throw new Error("Element is not a textarea: "+ta_id);let maxChars=parseInt(ta.getAttribute("cols")??"0"),ta_value=ta.getAttribute("value"),theRows=[];ta_value!=null&&(theRows=ta_value.split(` +`));for(var numNewRows=0,i=0;imaxChars&&(numNewRows+=Math.ceil((theRows[i].length+2)/maxChars));ta.setAttribute("row",(numNewRows+theRows.length).toString())}function exists(id){return $("#"+id).length>0}var HtmlElementCreator=class{cel(tag,id="",content="",css=[],options={}){return{tag,id,name:options.name,content,css,options,sub:[]}}ael(base,attach,id=""){if(id){if(base.id==id)base.sub.push(deepCopyFunction(attach));else if(isObject(base.sub)&&base.sub.length>0)for(var i=0;i-1&&_element.css.splice(css_index,1),_element}acssel(_element,css){var css_index=_element.css.indexOf(css);return css_index==-1&&_element.css.push(css),_element}scssel(_element,rcss,acss){return this.rcssel(_element,rcss),this.acssel(_element,acss),_element}phfo(tree){let name_elements=["button","fieldset","form","iframe","input","map","meta","object","output","param","select","textarea"],skip_options=["id","name","class"],no_close=["input","br","img","hr","area","col","keygen","wbr","track","source","param","command","base","meta","link","embed"];var content=[],line="<"+tree.tag,i;if(tree.id&&(line+=' id="'+tree.id+'"',name_elements.includes(tree.tag)&&(line+=' name="'+(tree.name?tree.name:tree.id)+'"')),isObject(tree.css)&&tree.css.length>0){for(line+=' class="',i=0;i0)for(tree.content&&content.push(tree.content),i=0;i"),content.join("")}phfa(list){for(var content=[],i=0;i"'/]/g,function(s){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return entityMap[s]})}function unescapeHtml(string){return string.replace(/&[#\w]+;/g,function(s){var entityMap={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/"};return entityMap[s]})}function html_options(name,data,selected="",options_only=!1,return_string=!1,sort=""){return this.html_options_block(name,data,selected,0,options_only,return_string,sort)}function html_options_block(name,data,selected="",multiple=0,options_only=!1,return_string=!1,sort="",onchange=""){var content=[],element_select,select_options={},element_option,data_list=[],value,options={};multiple>0&&(select_options.multiple="",multiple>1&&(select_options.size=multiple)),onchange&&(select_options.OnChange=onchange),element_select=dom.cel("select",name,"",[],select_options),sort=="keys"?data_list=Object.keys(data).sort():sort=="values"?data_list=Object.keys(data).sort((a,b)=>(""+data[a]).localeCompare(data[b])):data_list=Object.keys(data);for(let key of data_list)value=data[key],options={label:value,value:key,selected:""},multiple==0&&!Array.isArray(selected)&&selected==key&&(options.selected=""),multiple==1&&Array.isArray(selected)&&selected.indexOf(key)!=-1&&(options.selected=""),element_option=dom.cel("option","",value,[],options),dom.ael(element_select,element_option);if(options_only)if(return_string){for(var i=0;i(""+data[a]).localeCompare(data[b])):data_list=Object.keys(data),[].forEach.call(document.querySelectorAll("#"+name+" :checked"),function(elm){option_selected=elm.value}),loadEl(name).innerHTML="";for(let key of data_list)value=data[key],element_option=document.createElement("option"),element_option.label=value,element_option.value=key,element_option.innerHTML=value,key==option_selected&&(element_option.selected=!0),loadEl(name).appendChild(element_option)}}function dec2hex(dec){return("0x"+dec.toString(16)).substring(-2)}function getRandomIntInclusive(min,max){return min=Math.ceil(min),max=Math.floor(max),Math.floor(Math.random()*(max-min+1)+min)}function roundPrecision(number,precision){return isNaN(number)||isNaN(precision)?number:Math.round(number*Math.pow(10,precision))/Math.pow(10,precision)}function formatString(string,...args){return string.replace(/{(\d+)}/g,function(match,number){return typeof args[number]<"u"?args[number]:match})}function numberWithCommas(number){var parts=number.toString().split(".");return parts[0]=parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),parts.join(".")}function convertLBtoBR(string){return string.replace(/(?:\r\n|\r|\n)/g,"
")}function getTimestamp(){var date=new Date;return date.getTime()}function generateId(len){var arr=new Uint8Array((len||40)/2);return(window.crypto||window.msCrypto).getRandomValues(arr),Array.from(arr,self.dec2hex).join("")}function randomIdF(){return Math.random().toString(36).substring(2)}function getWindowSize(){var width,height;return width=window.innerWidth||window.document.documentElement.clientWidth||window.document.body.clientWidth,height=window.innerHeight||window.document.documentElement.clientHeight||window.document.body.clientHeight,{width,height}}function getScrollOffset(){var left,top;return left=window.pageXOffset||window.document.documentElement.scrollLeft||window.document.body.scrollLeft,top=window.pageYOffset||window.document.documentElement.scrollTop||window.document.body.scrollTop,{left,top}}function getScrollOffsetOpener(){var left,top;return left=opener.window.pageXOffset||opener.document.documentElement.scrollLeft||opener.document.body.scrollLeft,top=opener.window.pageYOffset||opener.document.documentElement.scrollTop||opener.document.body.scrollTop,{left,top}}function setCenter(id,left,top){var dimensions={height:$("#"+id).height()??0,width:$("#"+id).width()??0},type=$("#"+id).css("position"),viewport=this.getWindowSize(),offset=this.getScrollOffset();if(left&&$("#"+id).css({left:viewport.width/2-dimensions.width/2+offset.left+"px"}),top){var top_pos=type=="fixed"?viewport.height/2-dimensions.height/2:viewport.height/2-dimensions.height/2+offset.top;$("#"+id).css({top:top_pos+"px"})}}function goToPos(element,offset=0,duration=500,base="body,html"){try{let element_offset=$("#"+element).offset();if(element_offset==null)return;$("#"+element).length&&$(base).animate({scrollTop:element_offset.top-offset},duration)}catch(err){errorCatch(err)}}function goTo(target){loadEl(target).scrollIntoView({behavior:"smooth"})}function formatBytes(bytes){var i=-1;if(typeof bytes=="bigint"&&(bytes=Number(bytes)),isNaN(bytes))return bytes.toString();do bytes=bytes/1024,i++;while(bytes>99);return Math.round(bytes*Math.pow(10,2))/Math.pow(10,2)+["kB","MB","GB","TB","PB","EB"][i]}function formatBytesLong(bytes){if(typeof bytes=="bigint"&&(bytes=Number(bytes)),isNaN(bytes))return bytes.toString();let negative=!1;bytes<0&&(negative=!0,bytes*=-1);var i=Math.floor(Math.log(bytes)/Math.log(1024)),sizes=["B","KB","MB","GB","TB","PB","EB","ZB","YB"];return(negative?"-":"")+((bytes/Math.pow(1024,i)).toFixed(2)+" "+sizes[i]).toString()}function stringByteFormat(bytes,raw=!1){if(!(typeof bytes=="string"||bytes instanceof String))return bytes.toString();let valid_units="bkmgtpezy",regex=/([\d.,]*)\s?(eb|pb|tb|gb|mb|kb|e|p|t|g|m|k|b)$/i,matches=bytes.match(regex);if(matches!==null){let m1=parseFloat(matches[1].replace(/[^0-9.]/,"")),m2=matches[2].replace(/[^bkmgtpezy]/i,"").charAt(0).toLowerCase();m2&&(bytes=m1*Math.pow(1024,valid_units.indexOf(m2)))}return raw?bytes:Math.round(bytes)}function parseQueryString(query="",return_key="",single=!1){return getQueryStringParam(return_key,query,single)}function getQueryStringParam(search="",query="",single=!1){query||(query=window.location.href);let url=new URL(query),param=null;if(search){let _params=url.searchParams.getAll(search);_params.length==1||single===!0?param=_params[0]:_params.length>1&&(param=_params)}else{param={};for(let[key]of url.searchParams.entries())if(typeof param[key]>"u"){let _params=url.searchParams.getAll(key);param[key]=_params.length<2||single===!0?_params[0]:_params}}return param}function loginLogout(){let form=document.createElement("form");form.method="post";let hiddenField=document.createElement("input");hiddenField.type="hidden",hiddenField.name="login_logout",hiddenField.value="Logout",form.appendChild(hiddenField),document.body.appendChild(form),form.submit()}function actionIndicator(loc,overlay=!0){$("#indicator").is(":visible")?this.actionIndicatorHide(loc,overlay):this.actionIndicatorShow(loc,overlay)}function actionIndicatorShow(loc,overlay=!0){$("#indicator").is(":visible")||($("#indicator").hasClass("progress")||$("#indicator").addClass("progress"),setCenter("indicator",!0,!0),$("#indicator").show()),overlay===!0&&this.overlayBoxShow()}function actionIndicatorHide(loc,overlay=!0){$("#indicator").hide(),overlay===!0&&overlayBoxHide()}function overlayBoxShow(){$("#overlayBox").is(":visible")?$("#overlayBox").css("zIndex","100"):($("#overlayBox").show(),$("#overlayBox").css("zIndex","98"))}function overlayBoxHide(){parseInt($("#overlayBox").css("zIndex"))>=100?$("#overlayBox").css("zIndex","98"):$("#overlayBox").hide()}function setOverlayBox(){$("#overlayBox").is(":visible")||$("#overlayBox").show()}function hideOverlayBox(){$("#overlayBox").is(":visible")&&$("#overlayBox").hide()}function ClearCall(){$("#actionBox").html(""),$("#actionBox").hide(),$("#overlayBox").hide()}var ActionIndicatorOverlayBox=class{#GL_OB_S=100;#GL_OB_BASE=100;showActionIndicator(loc){if($("#indicator").length==0){var el=document.createElement("div");el.className="progress hide",el.id="indicator",$("body").append(el)}else $("#indicator").hasClass("progress")||$("#indicator").addClass("progress").hide();$("#indicator").is(":visible")||(this.checkOverlayExists(),$("#overlayBox").is(":visible")||$("#overlayBox").show(),$("#overlayBox").css("zIndex",1e3),$("#indicator").show(),setCenter("indicator",!0,!0))}hideActionIndicator(loc){$("#indicator").is(":visible")&&($("#indicator").hide(),this.#GL_OB_S>this.#GL_OB_BASE?$("#overlayBox").css("zIndex",this.#GL_OB_S):($("#overlayBox").hide(),$("#overlayBox").css("zIndex",this.#GL_OB_BASE)))}checkOverlayExists(){if($("#overlayBox").length==0){var el=document.createElement("div");el.className="overlayBoxElement hide",el.id="overlayBox",$("body").append(el)}}showOverlayBoxLayers(el_id){$("#overlayBox").is(":visible")||($("#overlayBox").show(),$("#overlayBox").css("zIndex",this.#GL_OB_BASE),this.#GL_OB_S=this.#GL_OB_BASE),this.#GL_OB_S++,$("#overlayBox").css("zIndex",this.#GL_OB_S),el_id&&$("#"+el_id).length>0&&($("#"+el_id).css("zIndex",this.#GL_OB_S+1),$("#"+el_id).show())}hideOverlayBoxLayers(el_id=""){this.#GL_OB_S--,this.#GL_OB_S<=this.#GL_OB_BASE?(this.#GL_OB_S=this.#GL_OB_BASE,$("#overlayBox").hide(),$("#overlayBox").css("zIndex",this.#GL_OB_BASE)):$("#overlayBox").css("zIndex",this.#GL_OB_S),el_id&&($("#"+el_id).hide(),$("#"+el_id).css("zIndex",0))}clearCallActionBox(){$("#actionBox").html(""),$("#actionBox").hide(),this.hideOverlayBoxLayers()}};var l10nTranslation=class{#i18n={};constructor(i18n2){this.#i18n=i18n2}__(string){return typeof this.#i18n<"u"&&isObject(this.#i18n)&&this.#i18n[string]?this.#i18n[string]:string}};var ActionBox=class{zIndex={base:100,max:110,indicator:0,boxes:{},active:[],top:""};action_box_storage={};action_box_cache_timeout=10*60*1e3;hec;l10n;constructor(hec2,l10n2){this.hec=hec2,this.l10n=l10n2}showFillActionBox(target_id="actionBox",content="",action_box_css=[],override=0,content_override=0){this.fillActionBox(target_id,content,action_box_css),this.showActionBox(target_id,override,content_override)}fillActionBox(target_id="actionBox",content="",action_box_css=[]){exists(target_id)||$("#mainContainer").after(this.hec.phfo(this.hec.cel("div",target_id,"",["actionBoxElement","hide"].concat(action_box_css)))),$("#"+target_id).html(content)}adjustActionBox(target_id="actionBox",override=0,content_override=0){this.adjustActionBoxHeight(target_id,override,content_override),setCenter(target_id,!0,!0)}hideAllActionBoxes(){$('#actionBox, div[id^="actionBox-"].actionBoxElement').hide(),$("#overlayBox").hide()}hideActionBox(target_id="actionBox"){this.closeActionBoxFloat(target_id,!1)}showActionBox(target_id="actionBox",override=0,content_override=0,hide_all=!0){this.showActionBoxFloat(target_id,override,content_override,hide_all)}closeActionBox(target_id="actionBox",clean=!0){this.closeActionBoxFloat(target_id,clean)}showActionBoxFloat(target_id="actionBox",override=0,content_override=0,hide_all=!1){hide_all===!0&&this.hideAllActionBoxes(),exists("overlayBox")||($("body").prepend(this.hec.phfo(this.hec.cel("div","overlayBox","",["overlayBoxElement"]))),$("#overlayBox").css("zIndex",this.zIndex.base)),$("#overlayBox").show(),keyInObject(target_id,this.zIndex.boxes)?this.zIndex.boxes[target_id]+10({id:el.id,zIndex:$("#"+el.id).css("zIndex")})).get();if(visible_zIndexes.length>0){let max_zIndex=0,max_el_id="";for(let zIndex_el of visible_zIndexes)parseInt(zIndex_el.zIndex)>max_zIndex&&(max_zIndex=parseInt(zIndex_el.zIndex),max_el_id=zIndex_el.id);$("#overlayBox").css("zIndex",max_zIndex-1),this.zIndex.top=max_el_id}else $("#overlayBox").hide()}createActionBox(target_id="actionBox",title="",contents={},headers={},settings={},show_close=!0){keyInObject(target_id,this.action_box_storage)||(this.action_box_storage[target_id]={});let header_css=[];keyInObject("header_css",settings)&&(header_css=settings.header_css);let action_box_css=[];keyInObject("action_box_css",settings)&&(action_box_css=settings.action_box_css);let elements=[];elements.push(this.hec.phfo(this.hec.aelx(this.hec.cel("div",target_id+"_title","",["actionBoxTitle","flx-spbt"].concat(header_css)),...show_close===!0?[this.hec.cel("div","",title,["fs-b","w-80"]),this.hec.aelx(this.hec.cel("div",target_id+"_title_close_button","",["w-20","tar"]),this.hec.cel("input",target_id+"_title_close","",["button-close","fs-s"],{type:"button",value:this.l10n.__("Close"),OnClick:"closeActionBox('"+target_id+"', false);"}))]:[this.hec.cel("div","",title,["fs-b","w-100"])]))),getObjectCount(headers)>0&&(keyInObject("raw_string",headers)?elements.push(headers.raw_string):elements.push(this.hec.phfo(headers))),getObjectCount(contents)>0?keyInObject("raw_string",contents)?elements.push(contents.raw_string):elements.push(this.hec.phfo(contents)):elements.push(this.hec.phfo(this.hec.cel("div",target_id+"_content","",[]))),elements.push(this.hec.phfo(this.hec.aelx(this.hec.cel("div",target_id+"_footer","",["pd-5","flx-spbt"]),...show_close===!0?[this.hec.cel("div","","",["fs-b","w-80"]),this.hec.aelx(this.hec.cel("div",target_id+"_footer_close_button","",["tar","w-20"]),this.hec.cel("input",target_id+"_footer_close","",["button-close","fs-s"],{type:"button",value:this.l10n.__("Close"),OnClick:"closeActionBox('"+target_id+"', false);"}))]:[this.hec.cel("div","","",["fs-b","w-100"])]))),elements.push(this.hec.phfo(this.hec.cel("input",target_id+"-cache_time","",[],{type:"hidden",value:Date.now()}))),this.fillActionBox(target_id,elements.join(""),action_box_css)}adjustActionBoxHeight(target_id="actionBox",override=0,content_override=0){var new_height=0,dim={},abc_dim={},content_id="";switch(isNaN(override)&&(override=0),isNaN(content_override)&&(content_override=0),target_id){case"actionBox":content_id="action_box";break;case"actionBoxSub":content_id="action_box_sub";break;default:content_id=target_id;break}$.each([target_id,content_id+"_content"],function(i,v){$("#"+v).css({height:"",width:""})}),exists(content_id+"_title")&&(dim.height=$("#"+content_id+"_title").outerHeight(),console.log("Target: %s, Action box Title: %s",target_id,dim.height),new_height+=dim.height??0),exists(content_id+"_header")&&(dim.height=$("#"+content_id+"_header").outerHeight(),console.log("Target: %s, Action box Header: %s",target_id,dim.height),new_height+=dim.height??0),exists(content_id+"_content")&&(content_override>0?(console.log("Target: %s, Action box Content Override: %s",target_id,content_override),new_height+=content_override):(abc_dim.height=$("#"+content_id+"_content").outerHeight(),console.log("Target: %s, Action box Content: %s",target_id,abc_dim.height),new_height+=abc_dim.height??0)),exists(content_id+"_footer")&&(dim.height=$("#"+content_id+"_footer").outerHeight(),console.log("Target: %s, Action box Footer: %s",target_id,dim.height),new_height+=dim.height??0),new_height+=override;var viewport=getWindowSize();if(new_height>=viewport.height){exists(content_id+"_content")&&($("#"+content_id+"_content").hasClass("of-s-y")||$("#"+content_id+"_content").addClass("of-s-y")),console.log("Target: %s, Viewport: %s, ActionBox (NH): %s, ABcontent: %s, ABouter: %s",target_id,viewport.height,new_height,abc_dim.height,$("#"+target_id).outerHeight());var m_height=viewport.height-(new_height-(abc_dim.height??0));console.log("Target: %s, New ABcontent: %s",target_id,m_height),$("#"+content_id+"_content").css("height",m_height+"px"),new_height=new_height-(abc_dim.height??0)+m_height,console.log("Target: %s, New Hight: %s",target_id,new_height)}else exists(content_id+"_content")&&$("#"+content_id+"_content").hasClass("of-s-y")&&$("#"+content_id+"_content").removeClass("of-s-y");console.log("Target: %s, Action Box new height: %s px (override %s px, content override %s px), window height: %s px, Visible Height: %s px",target_id,new_height,override,content_override,viewport.height,$("#"+content_id).outerHeight()),$("#"+target_id).css("height",new_height+"px")}};var LoginNavMenu=class{hec;l10n;constructor(hec2,l10n2){this.hec=hec2,this.l10n=l10n2}createLoginRow(login_string,header_id="mainHeader"){exists(header_id)&&(exists("loginRow")||$("#"+header_id).html(this.hec.phfo(this.hec.cel("div","loginRow","",["loginRow","flx-spbt"]))),$("#loginRow").html(this.hec.phfo(this.hec.cel("div","loginRow-name",login_string))),$("#loginRow").append(this.hec.phfo(this.hec.cel("div","loginRow-info",""))),$("#loginRow").append(this.hec.phfo(this.hec.aelx(this.hec.cel("div","loginRow-logout"),this.hec.cel("input","logout","",[],{value:this.l10n.__("Logout"),type:"button",onClick:"loginLogout()"})))))}createNavMenu(nav_menu,header_id="mainHeader"){if(isObject(nav_menu)&&getObjectCount(nav_menu)>1){exists("menuRow")||$("#"+header_id).html(this.hec.phfo(this.hec.cel("div","menuRow","",["menuRow","flx-s"])));var content=[];$.each(nav_menu,function(key,item){key!=0&&content.push(this.hec.phfo(this.hec.cel("div","","·",["pd-2"]))),item.enabled&&(window.location.href.indexOf(item.url)!=-1&&(item.selected=1),content.push(this.hec.phfo(this.hec.aelx(this.hec.cel("div"),this.hec.cel("a","",item.name,["pd-2"].concat(item.selected?"highlight":""),{href:item.url})))))}),$("#menuRow").html(content.join(""))}else $("#menuRow").hide()}};var aiob=new ActionIndicatorOverlayBox,hec=new HtmlElementCreator,l10n=new l10nTranslation(typeof i18n>"u"?{}:i18n),ab=new ActionBox(hec,l10n),lnm=new LoginNavMenu(hec,l10n);String.prototype.format||(String.prototype.format=function(){return console.error("[DEPRECATED] use StringHelpers.formatString"),formatString(this,arguments)}),Number.prototype.round&&(Number.prototype.round=function(prec){return console.error("[DEPRECATED] use MathHelpers.roundPrecision"),roundPrecision(this,prec)}),String.prototype.escapeHTML||(String.prototype.escapeHTML=function(){return console.error("[DEPRECATED] use HtmlHelpers.escapeHtml"),escapeHtml(this)}),String.prototype.unescapeHTML||(String.prototype.unescapeHTML=function(){return console.error("[DEPRECATED] use HtmlHelpers.unescapeHtml"),unescapeHtml(this)});function escapeHtml2(string){return escapeHtml(string)}function roundPrecision2(number,prec){return roundPrecision(number,prec)}function formatString2(string,...args){return formatString(string,...args)}function unescapeHtml2(string){return unescapeHtml(string)}function loadEl2(el_id){return loadEl(el_id)}function pop2(theURL,winName,features){pop(theURL,winName,features)}function expandTA2(ta_id){expandTA(ta_id)}function getWindowSize2(){return getWindowSize()}function getScrollOffset2(){return getScrollOffset()}function getScrollOffsetOpener2(){return getScrollOffsetOpener()}function setCenter2(id,left,top){setCenter(id,left,top)}function goToPos2(element,offset=0,duration=500,base="body,html"){goToPos(element,offset,duration,base)}function goTo2(target){goTo(target)}function __(string){return l10n.__(string)}function numberWithCommas2(x){return numberWithCommas(x)}function convertLBtoBR2(string){return convertLBtoBR(string)}function getTimestamp2(){return getTimestamp()}function dec2hex2(dec){return dec2hex(dec)}function generateId2(len){return generateId(len)}function randomIdF2(){return randomIdF()}function getRandomIntInclusive2(min,max){return getRandomIntInclusive(min,max)}function isFunction2(name){return isFunction(name)}function executeFunctionByName2(functionName,context){return executeFunctionByName(functionName,context)}function isObject2(val){return isObject(val)}function getObjectCount2(object){return getObjectCount(object)}function keyInObject2(key,object){return keyInObject(key,object)}function getKeyByValue2(object,value){return getKeyByValue(object,value)}function valueInObject2(object,value){return valueInObject(object,value)}function deepCopyFunction2(inObject){return deepCopyFunction(inObject)}function exists2(id){return exists(id)}function formatBytes2(bytes){return formatBytes(bytes)}function formatBytesLong2(bytes){return formatBytesLong(bytes)}function stringByteFormat2(bytes){return stringByteFormat(bytes)}function errorCatch2(err){errorCatch(err)}function actionIndicator2(loc,overlay=!0){actionIndicator(loc,overlay)}function actionIndicatorShow2(loc,overlay=!0){actionIndicatorShow(loc,overlay)}function actionIndicatorHide2(loc,overlay=!0){actionIndicatorHide(loc,overlay)}function overlayBoxShow2(){overlayBoxShow()}function overlayBoxHide2(){overlayBoxHide()}function setOverlayBox2(){setOverlayBox()}function hideOverlayBox2(){hideOverlayBox()}function ClearCall2(){ClearCall()}function showActionIndicator(loc){aiob.showActionIndicator(loc)}function hideActionIndicator(loc){aiob.hideActionIndicator(loc)}function checkOverlayExists(){aiob.checkOverlayExists()}function showOverlayBoxLayers(el_id){aiob.showOverlayBoxLayers(el_id)}function hideOverlayBoxLayers(el_id=""){aiob.hideOverlayBoxLayers(el_id)}function clearCallActionBox(){aiob.clearCallActionBox()}function cel(tag,id="",content="",css=[],options={}){return hec.cel(tag,id,content,css,options)}function ael(base,attach,id=""){return hec.ael(base,attach,id)}function aelx(base,...attach){return hec.aelx(base,...attach)}function aelxar(base,attach){return hec.aelxar(base,attach)}function rel(base){return hec.rel(base)}function rcssel(_element,css){return hec.rcssel(_element,css)}function acssel(_element,css){return hec.acssel(_element,css)}function scssel(_element,rcss,acss){hec.scssel(_element,rcss,acss)}function phfo(tree){return hec.phfo(tree)}function phfa(list){return hec.phfa(list)}function html_options2(name,data,selected="",options_only=!1,return_string=!1,sort=""){return html_options(name,data,selected,options_only,return_string,sort)}function html_options_block2(name,data,selected="",multiple=0,options_only=!1,return_string=!1,sort="",onchange=""){return html_options_block(name,data,selected,multiple,options_only,return_string,sort,onchange)}function html_options_refill2(name,data,sort=""){html_options_refill(name,data,sort)}function parseQueryString2(query="",return_key=""){return parseQueryString(query,return_key)}function getQueryStringParam2(search="",query="",single=!1){return getQueryStringParam(search,query,single)}function loginLogout2(){loginLogout()}function createLoginRow(login_string,header_id="mainHeader"){lnm.createLoginRow(login_string,header_id)}function createNavMenu(nav_menu,header_id="mainHeader"){lnm.createNavMenu(nav_menu,header_id)}function showFillActionBox(target_id="actionBox",content="",action_box_css=[],override=0,content_override=0){ab.showFillActionBox(target_id,content,action_box_css,override,content_override)}function fillActionBox(target_id="actionBox",content="",action_box_css=[]){ab.fillActionBox(target_id,content,action_box_css)}function adjustActionBox(target_id="actionBox",override=0,content_override=0){ab.adjustActionBox(target_id,override,content_override)}function hideAllActionBoxes(){ab.hideAllActionBoxes()}function hideActionBox(target_id="actionBox"){ab.hideActionBox(target_id)}function showActionBox(target_id="actionBox",override=0,content_override=0,hide_all=!0){ab.showActionBox(target_id,override,content_override,hide_all)}function closeActionBox(target_id="actionBox",clean=!0){ab.closeActionBox(target_id,clean)}function showActionBoxFloat(target_id="actionBox",override=0,content_override=0,hide_all=!1){ab.showActionBoxFloat(target_id,override,content_override,hide_all)}function closeActionBoxFloat(target_id="actionBox",clean=!0){ab.closeActionBoxFloat(target_id,clean)}function createActionBox(target_id="actionBox",title="",contents={},headers={},settings={},show_close=!0){ab.createActionBox(target_id,title,contents,headers,settings,show_close)}function adjustActionBoxHeight(target_id="actionBox",override=0,content_override=0){ab.adjustActionBoxHeight(target_id,override,content_override)} +//# sourceMappingURL=utils.min.js.map diff --git a/www/admin/layout/javascript/utils.min.js.map b/www/admin/layout/javascript/utils.min.js.map new file mode 100644 index 00000000..86cf0870 --- /dev/null +++ b/www/admin/layout/javascript/utils.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../src/utils/JavaScriptHelpers.mjs", "../../../src/utils/DomHelpers.mjs", "../../../src/utils/HtmlElementCreator.mjs", "../../../src/utils/HtmlHelpers.mjs", "../../../src/utils/MathHelpers.mjs", "../../../src/utils/StringHelpers.mjs", "../../../src/utils/DateTimeHelpers.mjs", "../../../src/utils/UniqIdGenerators.mjs", "../../../src/utils/ResizingAndMove.mjs", "../../../src/utils/FormatBytes.mjs", "../../../src/utils/UrlParser.mjs", "../../../src/utils/LoginLogout.mjs", "../../../src/utils/ActionIndicatorOverlayBox.mjs", "../../../src/utils/l10nTranslation.mjs", "../../../src/utils/ActionBox.mjs", "../../../src/utils/LoginNavMenu.mjs", "../../../src/utils.mjs"], + "sourcesContent": ["/*\nDescription: JavaScript Helpers\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport {\n\terrorCatch, isFunction, executeFunctionByName,\n\tisObject, getObjectCount,\n\tkeyInObject, objectKeyExists,\n\tgetKeyByValue, valueInObject, objectValueExists,\n\tdeepCopyFunction\n};\n\n/**\n * prints out error messages based on data available from the browser\n * @param {Object} err error from try/catch block\n */\nfunction errorCatch(err)\n{\n\t// for FF & Chrome\n\tif (err.stack) {\n\t\t// only FF\n\t\tif (err.lineNumber) {\n\t\t\tconsole.error('ERROR[%s:%s] ', err.name, err.lineNumber, err);\n\t\t} else if (err.line) {\n\t\t\t// only Safari\n\t\t\tconsole.error('ERROR[%s:%s] ', err.name, err.line, err);\n\t\t} else {\n\t\t\tconsole.error('ERROR[%s] ', err.name, err);\n\t\t}\n\t} else if (err.number) {\n\t\t// IE\n\t\tconsole.error('ERROR[%s:%s] %s', err.name, err.number, err.message);\n\t\tconsole.error('ERROR[description] %s', err.description);\n\t} else {\n\t\t// the rest\n\t\tconsole.error('ERROR[%s] %s', err.name, err.message);\n\t}\n}\n\n/**\n * check if name is a function\n * @param {string} name Name of function to check if exists\n * @return {Boolean} true/false\n */\nfunction isFunction(name)\n{\n\tif (typeof window[name] !== 'undefined' &&\n\t\ttypeof window[name] === 'function') {\n\t\treturn true;\n\t} else {\n\t\treturn false;\n\t}\n}\n\n/**\n * call a function by its string name\n * https://stackoverflow.com/a/359910\n * example: executeFunctionByName(\"My.Namespace.functionName\", window, arguments);\n * @param {string} functionName The function name or namespace + function\n * @param {any} context context (window or first namespace)\n * hidden next are all the arguments\n * @return {any} Return values from functon\n */\nfunction executeFunctionByName(functionName, context /*, args */)\n{\n\tvar args = Array.prototype.slice.call(arguments, 2);\n\tvar namespaces = functionName.split('.');\n\tvar func = namespaces.pop();\n\tif (func == undefined) {\n\t\tthrow new Error(\"Cannot get function from namespaces: \" + functionName);\n\t}\n\tfor (var i = 0; i < namespaces.length; i++) {\n\t\tcontext = context[namespaces[i]];\n\t}\n\treturn context[func].apply(context, args);\n}\n\n/**\n * checks if a variable is an object\n * @param {any} val possible object\n * @return {Boolean} true/false if it is an object or not\n */\nfunction isObject(val)\n{\n\tif (val === null) {\n\t\treturn false;\n\t}\n\treturn ((typeof val === 'function') || (typeof val === 'object'));\n}\n\n/**\n * get the length of an object (entries)\n * @param {Object} object object to check\n * @return {Number} number of entry, or -1 if not object\n */\nfunction getObjectCount(object)\n{\n\tif (!isObject(object)) {\n\t\treturn -1;\n\t}\n\treturn Object.keys(object).length;\n}\n\n/**\n * checks if a key exists in a given object\n * @param {String} key key name\n * @param {Object} object object to search key in\n * @return {Boolean} true/false if key exists in object\n * @deprecated Use objectKeyExists\n */\nfunction keyInObject(key, object)\n{\n\treturn objectKeyExists(object, key);\n}\n\n/**\n * This is the correct order and will superseed keyInObject\n * @param {Object} object object to search key in\n * @param {String} key key name\n * @returns {Boolean} true/false if key exists in object\n */\nfunction objectKeyExists(object, key)\n{\n\treturn Object.prototype.hasOwnProperty.call(object, key) ? true : false;\n}\n\n/**\n * returns matching key of value\n * @param {Object} object object to search value in\n * @param {any} value any value (String, Number, etc)\n * @return {String} the key found for the first matching value\n */\nfunction getKeyByValue(object, value)\n{\n\treturn Object.keys(object).find(key => object[key] === value) ?? '';\n}\n\n/**\n * returns true if value is found in object with a key\n * @param {Object} object object to search value in\n * @param {any} value any value (String, Number, etc)\n * @return {Boolean} true on value found, false on not found\n * @deprecated use objectValueExists\n */\nfunction valueInObject(object, value)\n{\n\treturn objectValueExists(object, value);\n}\n\n/**\n * returns true if value is found in object with a key\n * @param {Object} object object to search value in\n * @param {any} value any value (String, Number, etc)\n * @return {Boolean} true on value found, false on not found\n */\nfunction objectValueExists(object, value)\n{\n\treturn Object.keys(object).find(key => object[key] === value) ? true : false;\n}\n\n/**\n * true deep copy for Javascript objects\n * if Object.assign({}, obj) is not working (shallow)\n * or if JSON.parse(JSON.stringify(obj)) is failing\n * @param {Object} inObject Object to copy\n * @return {Object} Copied Object\n */\nfunction deepCopyFunction(inObject)\n{\n\tvar outObject, value, key;\n\tif (typeof inObject !== 'object' || inObject === null) {\n\t\t// Return the value if inObject is not an object\n\t\treturn inObject;\n\t}\n\t// Create an array or object to hold the values\n\toutObject = Array.isArray(inObject) ? [] : {};\n\t// loop over ech entry in object\n\tfor (key in inObject) {\n\t\tvalue = inObject[key];\n\t\t// Recursively (deep) copy for nested objects, including arrays\n\t\toutObject[key] = deepCopyFunction(value);\n\t}\n\n\treturn outObject;\n}\n\n// __END__\n", "/*\nDescription: DOM Helpers\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { loadEl, pop, expandTA, exists };\n\n/**\n * Gets html element or throws an error\n * @param {string} el_id Element ID to get\n * @returns {HTMLElement}\n * @throws Error\n */\nfunction loadEl(el_id)\n{\n\tlet el = document.getElementById(el_id);\n\tif (el === null) {\n\t\tthrow new Error('Cannot find: ' + el_id);\n\t}\n\treturn el;\n}\n\n/**\n * opens a popup window with winName and given features (string)\n * @param {String} theURL the url\n * @param {String} winName window name\n * @param {Object} features popup features\n */\nfunction pop(theURL, winName, features)\n{\n\tlet __winName = window.open(theURL, winName, features);\n\tif (__winName == null) {\n\t\treturn;\n\t}\n\t__winName.focus();\n}\n\n/**\n * automatically resize a text area based on the amount of lines in it\n * @param {string} ta_id element id\n */\nfunction expandTA(ta_id)\n{\n\tlet ta = this.loadEl(ta_id);\n\tif (ta instanceof HTMLElement && ta.getAttribute('type') !== \"textarea\") {\n\t\tthrow new Error(\"Element is not a textarea: \" + ta_id);\n\t}\n\tlet maxChars = parseInt(ta.getAttribute('cols') ?? \"0\");\n\tlet ta_value = ta.getAttribute('value');\n\tlet theRows = [];\n\tif (ta_value != null) {\n\t\ttheRows = ta_value.split('\\n');\n\t}\n\tvar numNewRows = 0;\n\n\tfor ( var i = 0; i < theRows.length; i++ ) {\n\t\tif ((theRows[i].length+2) > maxChars) {\n\t\t\tnumNewRows += Math.ceil( (theRows[i].length+2) / maxChars ) ;\n\t\t}\n\t}\n\tta.setAttribute('row', (numNewRows + theRows.length).toString());\n}\n\n/**\n * checks if a DOM element actually exists\n * @param {String} id Element id to check for\n * @return {Boolean} true if element exists, false on failure\n */\nfunction exists(id)\n{\n\treturn $('#' + id).length > 0 ? true : false;\n}\n\n// __END__\n", "/*\nDescription: DOM Management and HTML builder\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport {\n\tHtmlElementCreator,\n\t// deprecated name\n\tHtmlElementCreator as DomManagement\n};\nimport { deepCopyFunction, isObject } from './JavaScriptHelpers.mjs';\n\nclass HtmlElementCreator {\n\t/**\n\t * reates object for DOM element creation flow\n\t * @param {String} tag must set tag (div, span, etc)\n\t * @param {String} [id=''] optional set for id, if input, select will be used for name\n\t * @param {String} [content=''] text content inside, is skipped if sub elements exist\n\t * @param {Array} [css=[]] array for css tags\n\t * @param {Object} [options={}] anything else (value, placeholder, OnClick, style)\n\t * @return {Object} created element as an object\n\t */\n\tcel(tag, id = '', content = '', css = [], options = {})\n\t{\n\t\treturn {\n\t\t\ttag: tag,\n\t\t\tid: id,\n\t\t\t// override name if set, else id is used. Only for input/button\n\t\t\tname: options.name,\n\t\t\tcontent: content,\n\t\t\tcss: css,\n\t\t\toptions: options,\n\t\t\tsub: []\n\t\t};\n\t}\n\n\t/**\n\t * attach a cel created object to another to create a basic DOM tree\n\t * @param {Object} base object where to attach/search\n\t * @param {Object} attach the object to be attached\n\t * @param {String} [id=''] optional id, if given search in base for this id and attach there\n\t * @return {Object} \"none\", technically there is no return needed as it is global attach\n\t */\n\tael(base, attach, id = '')\n\t{\n\t\tif (id) {\n\t\t\t// base id match already\n\t\t\tif (base.id == id) {\n\t\t\t\tbase.sub.push(deepCopyFunction(attach));\n\t\t\t} else {\n\t\t\t\t// sub check\n\t\t\t\tif (isObject(base.sub) && base.sub.length > 0) {\n\t\t\t\t\tfor (var i = 0; i < base.sub.length; i ++) {\n\t\t\t\t\t\t// recursive call to sub element\n\t\t\t\t\t\tthis.ael(base.sub[i], attach, id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tbase.sub.push(deepCopyFunction(attach));\n\t\t}\n\t\treturn base;\n\t}\n\n\t/**\n\t * directly attach n elements to one master base element\n\t * this type does not support attach with optional id\n\t * @param {Object} base object to where we attach the elements\n\t * @param {...Object} attach attach 1..n: attach directly to the base element those attachments\n\t * @return {Object} \"none\", technically there is no return needed, global attach\n\t */\n\taelx(base, ...attach)\n\t{\n\t\tfor (var i = 0; i < attach.length; i ++) {\n\t\t\tbase.sub.push(deepCopyFunction(attach[i]));\n\t\t}\n\t\treturn base;\n\t}\n\n\t/**\n\t * same as aelx, but instead of using objects as parameters\n\t * get an array of objects to attach\n\t * @param {Object} base object to where we attach the elements\n\t * @param {Array} attach array of objects to attach\n\t * @return {Object} \"none\", technically there is no return needed, global attach\n\t */\n\taelxar(base, attach)\n\t{\n\t\tfor (var i = 0; i < attach.length; i ++) {\n\t\t\tbase.sub.push(deepCopyFunction(attach[i]));\n\t\t}\n\t\treturn base;\n\t}\n\n\t/**\n\t * resets the sub elements of the base element given\n\t * @param {Object} base cel created element\n\t * @return {Object} returns reset base element\n\t */\n\trel(base)\n\t{\n\t\tbase.sub = [];\n\t\treturn base;\n\t}\n\n\t/**\n\t * searches and removes style from css array\n\t * @param {Object} _element element to work one\n\t * @param {String} css style sheet to remove (name)\n\t * @return {Object} returns full element\n\t */\n\trcssel(_element, css)\n\t{\n\t\tvar css_index = _element.css.indexOf(css);\n\t\tif (css_index > -1) {\n\t\t\t_element.css.splice(css_index, 1);\n\t\t}\n\t\treturn _element;\n\t}\n\n\t/**\n\t * adds a new style sheet to the element given\n\t * @param {Object} _element element to work on\n\t * @param {String} css style sheet to add (name)\n\t * @return {Object} returns full element\n\t */\n\tacssel(_element, css)\n\t{\n\t\tvar css_index = _element.css.indexOf(css);\n\t\tif (css_index == -1) {\n\t\t\t_element.css.push(css);\n\t\t}\n\t\treturn _element;\n\t}\n\n\t/**\n\t * removes one css and adds another\n\t * is a wrapper around rcssel/acssel\n\t * @param {Object} _element element to work on\n\t * @param {String} rcss style to remove (name)\n\t * @param {String} acss style to add (name)\n\t * @return {Object} returns full element\n\t */\n\tscssel(_element, rcss, acss)\n\t{\n\t\tthis.rcssel(_element, rcss);\n\t\tthis.acssel(_element, acss);\n\t\treturn _element;\n\t}\n\n\t/**\n\t * parses the object tree created with cel/ael and converts it into an HTML string\n\t * that can be inserted into the page\n\t * @param {Object} tree object tree with dom element declarations\n\t * @return {String} HTML string that can be used as innerHTML\n\t */\n\tphfo(tree)\n\t{\n\t\tlet name_elements = [\n\t\t\t'button',\n\t\t\t'fieldset',\n\t\t\t'form',\n\t\t\t'iframe',\n\t\t\t'input',\n\t\t\t'map',\n\t\t\t'meta',\n\t\t\t'object',\n\t\t\t'output',\n\t\t\t'param',\n\t\t\t'select',\n\t\t\t'textarea',\n\t\t];\n\t\tlet skip_options = [\n\t\t\t'id',\n\t\t\t'name',\n\t\t\t'class',\n\t\t];\n\t\tlet no_close = [\n\t\t\t'input',\n\t\t\t'br',\n\t\t\t'img',\n\t\t\t'hr',\n\t\t\t'area',\n\t\t\t'col',\n\t\t\t'keygen',\n\t\t\t'wbr',\n\t\t\t'track',\n\t\t\t'source',\n\t\t\t'param',\n\t\t\t'command',\n\t\t\t// only in header\n\t\t\t'base',\n\t\t\t'meta',\n\t\t\t'link',\n\t\t\t'embed',\n\t\t];\n\t\t// holds the elements\n\t\tvar content = [];\n\t\t// main part line\n\t\tvar line = '<' + tree.tag;\n\t\tvar i;\n\t\t// first id, if set\n\t\tif (tree.id) {\n\t\t\tline += ' id=\"' + tree.id + '\"';\n\t\t\t// if anything input (input, textarea, select then add name too)\n\t\t\tif (name_elements.includes(tree.tag)) {\n\t\t\t\tline += ' name=\"' + (tree.name ? tree.name : tree.id) + '\"';\n\t\t\t}\n\t\t}\n\t\t// second CSS\n\t\tif (isObject(tree.css) && tree.css.length > 0) {\n\t\t\tline += ' class=\"';\n\t\t\tfor (i = 0; i < tree.css.length; i ++) {\n\t\t\t\tline += tree.css[i] + ' ';\n\t\t\t}\n\t\t\t// strip last space\n\t\t\tline = line.slice(0, -1);\n\t\t\tline += '\"';\n\t\t}\n\t\t// options is anything key = \"data\"\n\t\tif (isObject(tree.options)) {\n\t\t\t// ignores id, name, class as key\n\t\t\tfor (const [key, item] of Object.entries(tree.options)) {\n\t\t\t\tif (!skip_options.includes(key)) {\n\t\t\t\t\tline += ' ' + key + '=\"' + item + '\"';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// finish open tag\n\t\tline += '>';\n\t\t// push finished line\n\t\tcontent.push(line);\n\t\t// dive into sub tree to attach sub nodes\n\t\t// NOTES: we can have content (text) AND sub nodes at the same level\n\t\t// CONTENT (TEXT) takes preference over SUB NODE in order\n\t\tif (isObject(tree.sub) && tree.sub.length > 0) {\n\t\t\tif (tree.content) {\n\t\t\t\tcontent.push(tree.content);\n\t\t\t}\n\t\t\tfor (i = 0; i < tree.sub.length; i ++) {\n\t\t\t\tcontent.push(this.phfo(tree.sub[i]));\n\t\t\t}\n\t\t} else if (tree.content) {\n\t\t\tcontent.push(tree.content);\n\t\t}\n\t\t// if not input, image or br, then close\n\t\tif (\n\t\t\t!no_close.includes(tree.tag)\n\t\t) {\n\t\t\tcontent.push('');\n\t\t}\n\t\t// combine to string\n\t\treturn content.join('');\n\t}\n\n\t/**\n\t * Create HTML elements from array list\n\t * as a flat element without master object file\n\t * Is like tree.sub call\n\t * @param {Array} list Array of cel created objects\n\t * @return {String} HTML String\n\t */\n\tphfa(list)\n\t{\n\t\tvar content = [];\n\t\tfor (var i = 0; i < list.length; i ++) {\n\t\t\tcontent.push(this.phfo(list[i]));\n\t\t}\n\t\treturn content.join('');\n\t}\n}\n\n// __EMD__\n", "/*\nDescription: HTML Helpers\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { escapeHtml, unescapeHtml, html_options, html_options_block, html_options_refill };\nimport { loadEl} from './DomHelpers.mjs';\nimport { DomManagement } from './HtmlElementCreator.mjs';\nlet dom = new DomManagement();\n\n/**\n * Escapes HTML in string\n * @param {String} string Text to escape HTML in\n * @returns {String}\n */\nfunction escapeHtml(string)\n{\n\treturn string.replace(/[&<>\"'/]/g, function (s) {\n\t\tvar entityMap = {\n\t\t\t'&': '&',\n\t\t\t'<': '<',\n\t\t\t'>': '>',\n\t\t\t'\"': '"',\n\t\t\t'\\'': ''',\n\t\t\t'/': '/'\n\t\t};\n\n\t\treturn entityMap[s];\n\t});\n}\n\n/**\n * Unescape a HTML encoded string\n * @param {String} string Text to unescape HTML in\n * @returns {String}\n */\nfunction unescapeHtml(string)\n{\n\treturn string.replace(/&[#\\w]+;/g, function (s) {\n\t\tvar entityMap = {\n\t\t\t'&': '&',\n\t\t\t'<': '<',\n\t\t\t'>': '>',\n\t\t\t'"': '\"',\n\t\t\t''': '\\'',\n\t\t\t'/': '/'\n\t\t};\n\n\t\treturn entityMap[s];\n\t});\n}\n\n// BLOCK: html wrappers for quickly creating html data blocks\n\n/**\n * NOTE: OLD FORMAT which misses multiple block set\n * creates an select/options drop down block.\n * the array needs to be key -> value format.\n * key is for the option id and value is for the data output\n * @param {String} name name/id\n * @param {Object} data array for the options\n * @param {String} [selected=''] selected item uid\n * @param {Boolean} [options_only=false] if this is true, it will not print the select part\n * @param {Boolean} [return_string=false] return as string and not as element\n * @param {String} [sort=''] if empty as is, else allowed 'keys',\n * 'values' all others are ignored\n * @return {String} html with build options block\n * @deprecated html_options_block\n */\nfunction html_options(name, data, selected = '', options_only = false, return_string = false, sort = '')\n{\n\t// wrapper to new call\n\treturn this.html_options_block(\n\t\tname, data, selected, 0, options_only, return_string, sort\n\t);\n}\n\n/**\n * NOTE: USE THIS CALL, the above one is deprecated\n * creates an select/options drop down block.\n * the array needs to be key -> value format.\n * key is for the option id and value is for the data output\n * @param {String} name name/id\n * @param {Object} data array for the options\n * @param {String} [selected=''] selected item uid\n * @param {Number} [multiple=0] if this is 1 or larger, the drop down\n * will be turned into multiple select\n * the number sets the size value unless it is 1,\n * then it is default\n * @param {Boolean} [options_only=false] if this is true, it will not print the select part\n * @param {Boolean} [return_string=false] return as string and not as element\n * @param {String} [sort=''] if empty as is, else allowed 'keys',\n * 'values' all others are ignored\n * @param {String} [onchange=''] onchange trigger call, default unset\n * @return {String} html with build options block\n */\nfunction html_options_block(\n\tname, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = ''\n) {\n\tvar content = [];\n\tvar element_select;\n\tvar select_options = {};\n\tvar element_option;\n\tvar data_list = []; // for sorted output\n\tvar value;\n\tvar options = {};\n\t// var option;\n\tif (multiple > 0) {\n\t\tselect_options.multiple = '';\n\t\tif (multiple > 1) {\n\t\t\tselect_options.size = multiple;\n\t\t}\n\t}\n\tif (onchange) {\n\t\tselect_options.OnChange = onchange;\n\t}\n\t// set outside select, gets stripped on return if options only is true\n\telement_select = dom.cel('select', name, '', [], select_options);\n\t// console.log('Call for %s, options: %s', name, options_only);\n\tif (sort == 'keys') {\n\t\tdata_list = Object.keys(data).sort();\n\t} else if (sort == 'values') {\n\t\tdata_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b]));\n\t} else {\n\t\tdata_list = Object.keys(data);\n\t}\n\t// console.log('ORDER: %s', data_list);\n\t// use the previously sorted list\n\t// for (const [key, value] of Object.entries(data)) {\n\tfor (const key of data_list) {\n\t\tvalue = data[key];\n\t\t// console.log('create [%s] options: key: %s, value: %s', name, key, value);\n\t\t// basic options init\n\t\toptions = {\n\t\t\t'label': value,\n\t\t\t'value': key,\n\t\t\t'selected': ''\n\t\t};\n\t\t// add selected if matching\n\t\tif (multiple == 0 && !Array.isArray(selected) && selected == key) {\n\t\t\toptions.selected = '';\n\t\t}\n\t\t// for multiple, we match selected as array\n\t\tif (multiple == 1 && Array.isArray(selected) && selected.indexOf(key) != -1) {\n\t\t\toptions.selected = '';\n\t\t}\n\t\t// create the element option\n\t\telement_option = dom.cel('option', '', value, [], options);\n\t\t// attach it to the select element\n\t\tdom.ael(element_select, element_option);\n\t}\n\t// if with select part, convert to text\n\tif (!options_only) {\n\t\tif (return_string) {\n\t\t\tcontent.push(dom.phfo(element_select));\n\t\t\treturn content.join('');\n\t\t} else {\n\t\t\treturn element_select;\n\t\t}\n\t} else {\n\t\t// strip select part\n\t\tif (return_string) {\n\t\t\tfor (var i = 0; i < element_select.sub.length; i ++) {\n\t\t\t\tcontent.push(dom.phfo(element_select.sub[i]));\n\t\t\t}\n\t\t\treturn content.join('');\n\t\t} else {\n\t\t\treturn element_select.sub;\n\t\t}\n\t}\n}\n\n/**\n * refills a select box with options and keeps the selected\n * @param {String} name name/id\n * @param {Object} data array of options\n * @param {String} [sort=''] if empty as is, else allowed 'keys', 'values'\n * all others are ignored\n */\nfunction html_options_refill(name, data, sort = '')\n{\n\tvar element_option;\n\tvar option_selected;\n\tvar data_list = []; // for sorted output\n\tvar value;\n\t// skip if not exists\n\tif (document.getElementById(name)) {\n\t\t// console.log('Call for %s, options: %s', name, options_only);\n\t\tif (sort == 'keys') {\n\t\t\tdata_list = Object.keys(data).sort();\n\t\t} else if (sort == 'values') {\n\t\t\tdata_list = Object.keys(data).sort((a, b) => ('' + data[a]).localeCompare(data[b]));\n\t\t} else {\n\t\t\tdata_list = Object.keys(data);\n\t\t}\n\t\t// first read in existing ones from the options and get the selected one\n\t\t[].forEach.call(document.querySelectorAll('#' + name + ' :checked'), function(elm) {\n\t\t\toption_selected = elm.value;\n\t\t});\n\t\tloadEl(name).innerHTML = '';\n\t\tfor (const key of data_list) {\n\t\t\tvalue = data[key];\n\t\t\t// console.log('add [%s] options: key: %s, value: %s', name, key, value);\n\t\t\telement_option = document.createElement('option');\n\t\t\telement_option.label = value;\n\t\t\telement_option.value = key;\n\t\t\telement_option.innerHTML = value;\n\t\t\tif (key == option_selected) {\n\t\t\t\telement_option.selected = true;\n\t\t\t}\n\t\t\tloadEl(name).appendChild(element_option);\n\t\t}\n\t}\n}\n\n// __EMD__\n", "/*\nDescription: Math Helpers\nDate: 2025/3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { dec2hex, getRandomIntInclusive, roundPrecision };\n\n/**\n * dec2hex :: Integer -> String\n * i.e. 0-255 -> '00'-'ff'\n * @param {Number} dec decimal string\n * @return {String} hex encdoded number, prefix with 0x\n */\nfunction dec2hex(dec)\n{\n\treturn ('0x' + dec.toString(16)).substring(-2);\n}\n\n/**\n * generate a number between min/max\n * with min/max inclusive.\n * eg: 1,5 will create a number ranging from 1 o 5\n * @param {Number} min minimum int number inclusive\n * @param {Number} max maximum int number inclusive\n * @return {Number} Random number\n */\nfunction getRandomIntInclusive(min, max)\n{\n\tmin = Math.ceil(min);\n\tmax = Math.floor(max);\n\t// The maximum is inclusive and the minimum is inclusive\n\treturn Math.floor(Math.random() * (max - min + 1) + min);\n}\n\n/**\n * Round a number to precision\n * @param {Number} number Number to round\n * @param {Number} precision Precision value\n * @returns {Number}\n */\nfunction roundPrecision(number, precision)\n{\n\tif (isNaN(number) || isNaN(precision)) {\n\t\treturn number;\n\t}\n\treturn Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision);\n}\n\n// __END__\n", "/*\nDescription: String Helpers\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { formatString, numberWithCommas, convertLBtoBR };\n\n/**\n * simple sprintf formater for replace\n * usage: formatString(\"{0} is cool, {1} is not\", \"Alpha\", \"Beta\");\n * First, checks if it isn't implemented yet.\n * @param {String} string String with {..} entries\n * @param {...any} args List of replacement\n * @returns {String} Escaped string\n */\nfunction formatString(string, ...args)\n{\n\treturn string.replace(/{(\\d+)}/g, function(match, number)\n\t{\n\t\treturn typeof args[number] != 'undefined' ?\n\t\t\targs[number] :\n\t\t\tmatch\n\t\t;\n\t});\n}\n/**\n * formats flat number 123456 to 123,456\n * @param {Number} number number to be formated\n * @return {String} formatted with , in thousands\n */\nfunction numberWithCommas(number)\n{\n\tvar parts = number.toString().split('.');\n\tparts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n\treturn parts.join('.');\n}\n\n/**\n * converts line breaks to br\n * @param {String} string any string\n * @return {String} string with
\n */\nfunction convertLBtoBR(string)\n{\n\treturn string.replace(/(?:\\r\\n|\\r|\\n)/g, '
');\n}\n\n// __END__\n", "/*\nDescription: Date Time functions\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { getTimestamp };\n\n/**\n * returns current timestamp (unix timestamp)\n * @return {Number} timestamp (in milliseconds)\n */\nfunction getTimestamp()\n{\n\tvar date = new Date();\n\treturn date.getTime();\n}\n\n// __END__\n", "/*\nDescription: Generate unique ids\nDate: 2025/3/7\nCreator: Clemens Schwaighofer\n*/\n\nexport { generateId, randomIdF };\n\n\n/**\n * generateId :: Integer -> String\n * only works on mondern browsers\n * @param {Number} len length of unique id string\n * @return {String} random string in length of len\n */\nfunction generateId(len)\n{\n\tvar arr = new Uint8Array((len || 40) / 2);\n\t(\n\t\twindow.crypto ||\n\t\t// @ts-ignore\n\t\twindow.msCrypto\n\t).getRandomValues(arr);\n\treturn Array.from(arr, self.dec2hex).join('');\n}\n\n/**\n * creates a pseudo random string of 10 or 11 characters\n * works on all browsers\n * after many runs it will create duplicates\n * NOTE: no idea why this sometimes returns 10 or 11\n * @return {String} not true random string\n */\nfunction randomIdF()\n{\n\treturn Math.random().toString(36).substring(2);\n}\n", "/*\nDescription: Resize and Move Javascript\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nimport { errorCatch} from './JavaScriptHelpers.mjs';\nimport { loadEl } from './DomHelpers.mjs';\nexport { getWindowSize, getScrollOffset, getScrollOffsetOpener, setCenter, goToPos, goTo };\n\n/**\n * wrapper to get the real window size for the current browser window\n * @return {Object} object with width/height\n */\nfunction getWindowSize()\n{\n\tvar width, height;\n\twidth = window.innerWidth || (window.document.documentElement.clientWidth || window.document.body.clientWidth);\n\theight = window.innerHeight || (window.document.documentElement.clientHeight || window.document.body.clientHeight);\n\treturn {\n\t\twidth: width,\n\t\theight: height\n\t};\n}\n\n/**\n * wrapper to get the correct scroll offset\n * @return {Object} object with x/y px\n */\nfunction getScrollOffset()\n{\n\tvar left, top;\n\tleft = window.pageXOffset || (window.document.documentElement.scrollLeft || window.document.body.scrollLeft);\n\ttop = window.pageYOffset || (window.document.documentElement.scrollTop || window.document.body.scrollTop);\n\treturn {\n\t\tleft: left,\n\t\ttop: top\n\t};\n}\n\n/**\n * wrapper to get the correct scroll offset for opener page (from popup)\n * @return {Object} object with x/y px\n */\nfunction getScrollOffsetOpener()\n{\n\tvar left, top;\n\tleft = opener.window.pageXOffset || (opener.document.documentElement.scrollLeft || opener.document.body.scrollLeft);\n\ttop = opener.window.pageYOffset || (opener.document.documentElement.scrollTop || opener.document.body.scrollTop);\n\treturn {\n\t\tleft: left,\n\t\ttop: top\n\t};\n}\n\n/**\n * centers div to current window size middle\n * @param {String} id element to center\n * @param {Boolean} left if true centers to the middle from the left\n * @param {Boolean} top if true centers to the middle from the top\n */\nfunction setCenter(id, left, top)\n{\n\t// get size of id\n\tvar dimensions = {\n\t\theight: $('#' + id).height() ?? 0,\n\t\twidth: $('#' + id).width() ?? 0\n\t};\n\tvar type = $('#' + id).css('position');\n\tvar viewport = this.getWindowSize();\n\tvar offset = this.getScrollOffset();\n\n\t// console.log('Id %s, type: %s, dimensions %s x %s, viewport %s x %s', id, type, dimensions.width, dimensions.height, viewport.width, viewport.height);\n\t// console.log('Scrolloffset left: %s, top: %s', offset.left, offset.top);\n\t// console.log('Left: %s, Top: %s (%s)', parseInt((viewport.width / 2) - (dimensions.width / 2) + offset.left), parseInt((viewport.height / 2) - (dimensions.height / 2) + offset.top), parseInt((viewport.height / 2) - (dimensions.height / 2)));\n\tif (left) {\n\t\t$('#' + id).css({\n\t\t\tleft: (viewport.width / 2) - (dimensions.width / 2) + offset.left + 'px'\n\t\t});\n\t}\n\tif (top) {\n\t\t// if we have fixed, we do not add the offset, else it moves out of the screen\n\t\tvar top_pos = type == 'fixed' ?\n\t\t\t(viewport.height / 2) - (dimensions.height / 2) :\n\t\t\t(viewport.height / 2) - (dimensions.height / 2) + offset.top;\n\t\t$('#' + id).css({\n\t\t\ttop: top_pos + 'px'\n\t\t});\n\t}\n}\n\n/**\n * goes to an element id position\n * @param {String} element element id to move to\n * @param {Number} [offset=0] offset from top, default is 0 (px)\n * @param {Number} [duration=500] animation time, default 500ms\n * @param {String} [base='body,html'] base element for offset scroll\n */\nfunction goToPos(element, offset = 0, duration = 500, base = 'body,html')\n{\n\ttry {\n\t\tlet element_offset = $('#' + element).offset();\n\t\tif (element_offset == undefined) {\n\t\t\treturn;\n\t\t}\n\t\tif ($('#' + element).length) {\n\t\t\t$(base).animate({\n\t\t\t\tscrollTop: element_offset.top - offset\n\t\t\t}, duration);\n\t\t}\n\t} catch (err) {\n\t\terrorCatch(err);\n\t}\n}\n\n/**\n * go to element, scroll\n * non jquery\n * @param {string} target\n*/\nfunction goTo(target)\n{\n\tloadEl(target).scrollIntoView({\n\t\tbehavior: 'smooth'\n\t});\n}\n\n// __END__\n", "/*\nDescription: Byte string formatting\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { formatBytes, formatBytesLong, stringByteFormat };\n\n/**\n * converts a int number into bytes with prefix in two decimals precision\n * currently precision is fixed, if dynamic needs check for max/min precision\n * @param {Number|BigInt} bytes bytes in int\n * @return {String} string in GB/MB/KB\n */\nfunction formatBytes(bytes)\n{\n\tvar i = -1;\n\t// If this ia bigint -> convert to number, we need the decimals\n\tif (typeof bytes === \"bigint\") {\n\t\tbytes = Number(bytes);\n\t}\n\tif (isNaN(bytes)) {\n\t\treturn bytes.toString();\n\t}\n\tdo {\n\t\tbytes = bytes / 1024;\n\t\ti++;\n\t} while (bytes > 99);\n\treturn (\n\t\tMath.round(bytes * Math.pow(10, 2)) / Math.pow(10, 2)\n\t) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];\n}\n\n/**\n * like formatBytes, but returns bytes for <1KB and not 0.n KB\n * @param {Number|BigInt} bytes bytes in int\n * @return {String} string in GB/MB/KB\n */\nfunction formatBytesLong(bytes)\n{\n\t// If this ia bigint -> convert to number, we need the decimals\n\tif (typeof bytes === \"bigint\") {\n\t\tbytes = Number(bytes);\n\t}\n\tif (isNaN(bytes)) {\n\t\treturn bytes.toString();\n\t}\n\tlet negative = false;\n\tif (bytes < 0) {\n\t\tnegative = true;\n\t\tbytes *= -1;\n\t}\n\tvar i = Math.floor(Math.log(bytes) / Math.log(1024));\n\tvar sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n\treturn (negative ? '-' : '') + (\n\t\t(\n\t\t\tbytes /\n\t\t\tMath.pow(1024, i)\n\t\t).toFixed(2)\n\t\t// * 1 + ' ' + sizes[i]\n\t\t+ ' ' + sizes[i]\n\t).toString();\n}\n\n/**\n * Convert a string with B/K/M/etc into a byte number\n * @param {String|Number|Object} bytes Any string with B/K/M/etc\n * @param {Boolean} raw [default=false] Return not rounded values\n * @return {String|Number} A byte number, or original string as is\n */\nfunction stringByteFormat(bytes, raw=false)\n{\n\t// if anything not string return\n\tif (!(typeof bytes === 'string' || bytes instanceof String)) {\n\t\treturn bytes.toString();\n\t}\n\t// for pow exponent list\n\tlet valid_units = 'bkmgtpezy';\n\t// valid string that can be converted\n\tlet regex = /([\\d.,]*)\\s?(eb|pb|tb|gb|mb|kb|e|p|t|g|m|k|b)$/i;\n\tlet matches = bytes.match(regex);\n\t// if nothing found, return original input\n\tif (matches !== null) {\n\t\t// remove all non valid entries outside numbers and .\n\t\t// convert to float number\n\t\tlet m1 = parseFloat(matches[1].replace(/[^0-9.]/,''));\n\t\t// only get the FIRST letter from the size, convert it to lower case\n\t\tlet m2 = matches[2].replace(/[^bkmgtpezy]/i, '').charAt(0).toLowerCase();\n\t\tif (m2) {\n\t\t\t// use the position in the valid unit list to do the math conversion\n\t\t\tbytes = m1 * Math.pow(1024, valid_units.indexOf(m2));\n\t\t}\n\t}\n\t// if we want to have the raw data returned\n\tif (raw) {\n\t\treturn bytes;\n\t}\n\treturn Math.round(bytes);\n}\n\n// __END__\n", "/*\nDescription: HTML Helpers\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { parseQueryString, getQueryStringParam };\n\n/**\n * NOTE: this original code was wrong, now using URL and parsing through\n * getQueryStringParam\n * parses a query string from window.location.search.substring(1)\n * ALTERNATIVE CODE\n * var url = new URL(window.location.href);\n * param_uid = url.searchParams.get('uid');\n * @param {String} [query=''] the query string to parse\n * if not set will auto fill\n * @param {String} [return_key=''] if set only returns this key entry\n * or empty for none\n * @param {Boolean} [single=false] if set to true then only the first found\n * will be returned\n * @return {Object|String} parameter entry list\n */\nfunction parseQueryString(query = '', return_key = '', single = false)\n{\n\treturn getQueryStringParam(return_key, query, single);\n}\n\n/**\n * searches query parameters for entry and returns data either as string or array\n * if no search is given the whole parameters are returned as an object\n * if a parameter is set several times it will be returned as an array\n * if search parameter set and nothing found and empty string is returned\n * if no parametes exist and no serach is set and empty object is returned\n * @param {String} [search=''] if set searches for this entry, if empty\n * all parameters are returned\n * @param {String} [query=''] different query string to parse, if not\n * set (default) the current window href is used\n * @param {Boolean} [single=false] if set to true then only the first found\n * will be returned\n * @return {Object|Array|String} if search is empty, object, if search is set\n * and only one entry, then string, else array\n * unless single is true\n */\nfunction getQueryStringParam(search = '', query = '', single = false)\n{\n\tif (!query) {\n\t\tquery = window.location.href;\n\t}\n\tconst url = new URL(query);\n\tlet param = null;\n\tif (search) {\n\t\tlet _params = url.searchParams.getAll(search);\n\t\tif (_params.length == 1 || single === true) {\n\t\t\tparam = _params[0];\n\t\t} else if (_params.length > 1) {\n\t\t\tparam = _params;\n\t\t}\n\t} else {\n\t\t// will be object, so declare it one\n\t\tparam = {};\n\t\t// loop over paramenters\n\t\tfor (const [key] of url.searchParams.entries()) {\n\t\t\t// check if not yet set\n\t\t\tif (typeof param[key] === 'undefined') {\n\t\t\t\t// get the parameters multiple\n\t\t\t\tlet _params = url.searchParams.getAll(key);\n\t\t\t\t// if 1 set as string, else attach array as is\n\t\t\t\tparam[key] = _params.length < 2 || single === true ?\n\t\t\t\t\t_params[0] :\n\t\t\t\t\t_params;\n\t\t\t}\n\t\t}\n\t}\n\treturn param;\n}\n\n// __EMD__\n", "/*\nDescription: Login access and menu\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { loginLogout };\n\n/**\n * submits basic data for form logout\n */\nfunction loginLogout()\n{\n\tconst form = document.createElement('form');\n\tform.method = 'post';\n\tconst hiddenField = document.createElement('input');\n\thiddenField.type = 'hidden';\n\thiddenField.name = 'login_logout';\n\thiddenField.value = 'Logout';\n\tform.appendChild(hiddenField);\n\tdocument.body.appendChild(form);\n\tform.submit();\n}\n\n// __END__\n", "/*\nDescription: Action Indicator and Overlay\nDate: 2025/2/7\nCreator: Clemens Schwaighofer\n*/\n\nimport { setCenter } from './ResizingAndMove.mjs';\nexport {\n\tActionIndicatorOverlayBox,\n\tactionIndicator, actionIndicatorShow, actionIndicatorHide, overlayBoxShow,\n\toverlayBoxHide, setOverlayBox, hideOverlayBox, ClearCall\n};\n\n/*************************************************************\n * OLD action indicator and overlay boxes calls\n * DO NOT USE\n * actionIndicator -> showActionIndicator\n * actionIndicator -> hideActionIndicator\n * actionIndicatorShow -> showActionIndicator\n * actionIndicatorHide -> hideActionIndicator\n * overlayBoxShow -> showOverlayBoxLayers\n * overlayBoxHide -> hideOverlayBoxLayers\n * setOverlayBox -> showOverlayBoxLayers\n * hideOverlayBox -> hideOverlayBoxLayers\n * ClearCall -> clearCallActionBox\n * ***********************************************************/\n\n/**\n * show or hide the \"do\" overlay\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n * @deprecated showActionIndicator, hideActionIndicator\n */\nfunction actionIndicator(loc, overlay = true)\n{\n\tif ($('#indicator').is(':visible')) {\n\t\tthis.actionIndicatorHide(loc, overlay);\n\t} else {\n\t\tthis.actionIndicatorShow(loc, overlay);\n\t}\n}\n\n/**\n * explicit show for action Indicator\n * instead of automatically show or hide, do on command show\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n * @deprecated showActionIndicator, hideActionIndicator\n */\nfunction actionIndicatorShow(loc, overlay = true)\n{\n\t// console.log('{Indicator}: SHOW [%s]', loc);\n\tif (!$('#indicator').is(':visible')) {\n\t\tif (!$('#indicator').hasClass('progress')) {\n\t\t\t$('#indicator').addClass('progress');\n\t\t}\n\t\tsetCenter('indicator', true, true);\n\t\t$('#indicator').show();\n\t}\n\tif (overlay === true) {\n\t\tthis.overlayBoxShow();\n\t}\n}\n\n/**\n * explicit hide for action Indicator\n * instead of automatically show or hide, do on command hide\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n * @deprecated hideActionIndicator\n */\nfunction actionIndicatorHide(loc, overlay = true)\n{\n\t// console.log('{Indicator}: HIDE [%s]', loc);\n\t$('#indicator').hide();\n\tif (overlay === true) {\n\t\toverlayBoxHide();\n\t}\n}\n\n/**\n * shows the overlay box or if already visible, bumps the zIndex to 100\n * @deprecated showOverlayBoxLayers\n */\nfunction overlayBoxShow()\n{\n\t// check if overlay box exists and if yes set the z-index to 100\n\tif ($('#overlayBox').is(':visible')) {\n\t\t$('#overlayBox').css('zIndex', '100');\n\t} else {\n\t\t$('#overlayBox').show();\n\t\t$('#overlayBox').css('zIndex', '98');\n\t}\n}\n\n/**\n * hides the overlay box or if zIndex is 100 bumps it down to previous level\n * @deprecated hideOverlayBoxLayers\n */\nfunction overlayBoxHide()\n{\n\t// if the overlay box z-index is 100, do no hide, but set to 98\n\tif (parseInt($('#overlayBox').css('zIndex')) >= 100) {\n\t\t$('#overlayBox').css('zIndex', '98');\n\t} else {\n\t\t$('#overlayBox').hide();\n\t}\n}\n\n/**\n * position the overlay block box and shows it\n * @deprecated showOverlayBoxLayers\n */\nfunction setOverlayBox()\n{\n\tif (!$('#overlayBox').is(':visible')) {\n\t\t$('#overlayBox').show();\n\t}\n}\n\n/**\n * opposite of set, always hides overlay box\n * @deprecated hideOverlayBoxLayers\n */\nfunction hideOverlayBox()\n{\n\tif ($('#overlayBox').is(':visible')) {\n\t\t$('#overlayBox').hide();\n\t}\n}\n\n/**\n * the abort call, clears the action box and hides it and the overlay box\n * @deprecated clearCallActionBox\n */\nfunction ClearCall()\n{\n\t$('#actionBox').html('');\n\t$('#actionBox').hide();\n\t$('#overlayBox').hide();\n}\n\n/*\nThe below class will need the following CSS set\n\nProgress indicator (#indicator):\n.progress {\n\twidth: 100px;\n\theight: 100px;\n\tbackground: rgba(255, 255, 255, 0.6);\n\tborder: 20px solid rgba(255, 255, 255 ,0.25);\n\tborder-left-color: rgba(3, 155, 229 ,1);\n\tborder-top-color: rgba(3, 155, 229 ,1);\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\tanimation: progress-move 600ms infinite linear;\n\tleft: 0;\n\ttop: 0;\n\tposition: absolute;\n\tz-index: 1000;\n}\n@keyframes progress-move {\n\tto {\n\t\ttransform: rotate(1turn)\n\t}\n}\n\nOverlay box darken background (#overlayBox):\n.overlayBoxElement {\n\tbackground-color: rgba(0, 0, 0, 0.3);\n\theight: 100%;\n\tleft: 0;\n\tposition: fixed;\n\ttop: 0;\n\twidth: 100%;\n\tz-index: 98;\n}\n*/\n\nclass ActionIndicatorOverlayBox {\n\n\t// open overlay boxes counter for z-index\n\t#GL_OB_S = 100;\n\t#GL_OB_BASE = 100;\n\n\t/**\n\t * show action indicator\n\t * - checks if not existing and add\n\t * - only shows if not visible (else ignore)\n\t * - overlaybox check is called and shown on a fixzed\n\t * zIndex of 1000\n\t * - indicator is page centered\n\t * @param {String} loc ID string, only used for console log\n\t */\n\tshowActionIndicator(loc) // eslint-disable-line no-unused-vars\n\t{\n\t\t// console.log('{Indicator}: SHOW [%s]', loc);\n\t\t// check if indicator element exists\n\t\tif ($('#indicator').length == 0) {\n\t\t\tvar el = document.createElement('div');\n\t\t\tel.className = 'progress hide';\n\t\t\tel.id = 'indicator';\n\t\t\t$('body').append(el);\n\t\t} else if (!$('#indicator').hasClass('progress')) {\n\t\t\t// if I add a class it will not be hidden anymore\n\t\t\t// hide it\n\t\t\t$('#indicator').addClass('progress').hide();\n\t\t}\n\t\t// indicator not visible\n\t\tif (!$('#indicator').is(':visible')) {\n\t\t\t// check if overlay box element exits\n\t\t\tthis.checkOverlayExists();\n\t\t\t// if not visible show\n\t\t\tif (!$('#overlayBox').is(':visible')) {\n\t\t\t\t$('#overlayBox').show();\n\t\t\t}\n\t\t\t// always set to 1000 zIndex to be top\n\t\t\t$('#overlayBox').css('zIndex', 1000);\n\t\t\t// show indicator\n\t\t\t$('#indicator').show();\n\t\t\t// center it\n\t\t\tsetCenter('indicator', true, true);\n\t\t}\n\t}\n\n\t/**\n\t * hide action indicator, if it is visiable\n\t * If the global variable GL_OB_S is > GL_OB_BASE then\n\t * the overlayBox is not hidden but the zIndex\n\t * is set to this value\n\t * @param {String} loc ID string, only used for console log\n\t */\n\thideActionIndicator(loc) // eslint-disable-line no-unused-vars\n\t{\n\t\t// console.log('{Indicator}: HIDE [%s]', loc);\n\t\t// check if indicator is visible\n\t\tif ($('#indicator').is(':visible')) {\n\t\t\t// hide indicator\n\t\t\t$('#indicator').hide();\n\t\t\t// if global overlay box count is > 0\n\t\t\t// then set it to this level and keep\n\t\t\tif (this.#GL_OB_S > this.#GL_OB_BASE) {\n\t\t\t\t$('#overlayBox').css('zIndex', this.#GL_OB_S);\n\t\t\t} else {\n\t\t\t\t// else hide overlay box and set zIndex to 0\n\t\t\t\t$('#overlayBox').hide();\n\t\t\t\t$('#overlayBox').css('zIndex', this.#GL_OB_BASE);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * checks if overlayBox exists, if not it is\n\t * added as hidden item at the body end\n\t */\n\tcheckOverlayExists()\n\t{\n\t\t// check if overlay box exists, if not create it\n\t\tif ($('#overlayBox').length == 0) {\n\t\t\tvar el = document.createElement('div');\n\t\t\tel.className = 'overlayBoxElement hide';\n\t\t\tel.id = 'overlayBox';\n\t\t\t$('body').append(el);\n\t\t}\n\t}\n\n\t/**\n\t * show overlay box\n\t * if not visible show and set zIndex to 10 (GL_OB_BASE)\n\t * if visible, add +1 to the GL_OB_S variable and\n\t * up zIndex by this value\n\t */\n\tshowOverlayBoxLayers(el_id)\n\t{\n\t\t// console.log('SHOW overlaybox: %s', GL_OB_S);\n\t\t// if overlay box is not visible show and set zIndex to 0\n\t\tif (!$('#overlayBox').is(':visible')) {\n\t\t\t$('#overlayBox').show();\n\t\t\t$('#overlayBox').css('zIndex', this.#GL_OB_BASE);\n\t\t\t// also set start variable to 0\n\t\t\tthis.#GL_OB_S = this.#GL_OB_BASE;\n\t\t}\n\t\t// up the overlay box counter by 1\n\t\tthis.#GL_OB_S ++;\n\t\t// set zIndex\n\t\t$('#overlayBox').css('zIndex', this.#GL_OB_S);\n\t\t// if element given raise zIndex and show\n\t\tif (el_id) {\n\t\t\tif ($('#' + el_id).length > 0) {\n\t\t\t\t$('#' + el_id).css('zIndex', this.#GL_OB_S + 1);\n\t\t\t\t$('#' + el_id).show();\n\t\t\t}\n\t\t}\n\t\t// console.log('SHOW overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex'));\n\t}\n\n\t/**\n\t * hide overlay box\n\t * lower GL_OB_S value by -1\n\t * if we are 10 (GL_OB_BASE) or below hide the overlayIndex\n\t * and set zIndex and GL_OB_S to 0\n\t * else just set zIndex to the new GL_OB_S value\n\t * @param {String} el_id Target to hide layer\n\t */\n\thideOverlayBoxLayers(el_id='')\n\t{\n\t\t// console.log('HIDE overlaybox: %s', GL_OB_S);\n\t\t// remove on layer\n\t\tthis.#GL_OB_S --;\n\t\t// if 0 or lower (overflow) hide it and\n\t\t// set zIndex to 0\n\t\tif (this.#GL_OB_S <= this.#GL_OB_BASE) {\n\t\t\tthis.#GL_OB_S = this.#GL_OB_BASE;\n\t\t\t$('#overlayBox').hide();\n\t\t\t$('#overlayBox').css('zIndex', this.#GL_OB_BASE);\n\t\t} else {\n\t\t\t// if OB_S > 0 then set new zIndex\n\t\t\t$('#overlayBox').css('zIndex', this.#GL_OB_S);\n\t\t}\n\t\tif (el_id) {\n\t\t\t$('#' + el_id).hide();\n\t\t\t$('#' + el_id).css('zIndex', 0);\n\t\t}\n\t\t// console.log('HIDE overlaybox NEW zIndex: %s', $('#overlayBox').css('zIndex'));\n\t}\n\n\t/**\n\t * only for single action box\n\t */\n\tclearCallActionBox()\n\t{\n\t\t$('#actionBox').html('');\n\t\t$('#actionBox').hide();\n\t\tthis.hideOverlayBoxLayers();\n\t}\n}\n\n\n// __END__\n", "/*\nDescription: Translation call\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { l10nTranslation };\nimport { isObject } from './JavaScriptHelpers.mjs';\n\nclass l10nTranslation {\n\n\t#i18n = {};\n\n\tconstructor(i18n) {\n\t\tthis.#i18n = i18n;\n\n\t}\n\t/**\n\t * uses the i18n object created in the translation template\n\t * that is filled from gettext in PHP\n\t * @param {String} string text to translate\n\t * @return {String} translated text (based on PHP selected language)\n\t */\n\t__(string)\n\t{\n\t\tif (typeof this.#i18n !== 'undefined' && isObject(this.#i18n) && this.#i18n[string]) {\n\t\t\treturn this.#i18n[string];\n\t\t} else {\n\t\t\treturn string;\n\t\t}\n\t}\n}\n\n// __END__\n", "/*\nDescription: Action Box handling\nDate: 2025/3/7\nCreator: Clemens Schwaighofer\n*/\n\nexport { ActionBox };\nimport { keyInObject, getObjectCount } from './JavaScriptHelpers.mjs';\nimport { exists } from './DomHelpers.mjs';\nimport { setCenter, getWindowSize } from './ResizingAndMove.mjs';\n\nclass ActionBox {\n\n\t// open overlay boxes counter for z-index\n\tzIndex = {\n\t\tbase: 100,\n\t\tmax: 110,\n\t\tindicator: 0,\n\t\tboxes: {},\n\t\tactive: [],\n\t\ttop: ''\n\t};\n\t// general action box storage\n\taction_box_storage = {};\n\t// set to 10 min (*60 for seconds, *1000 for microseconds)\n\taction_box_cache_timeout = 10 * 60 * 1000;\n\n\thec;\n\tl10n;\n\n\t/**\n\t * action box creator\n\t * @param {Object} hec HtmlElementCreator\n\t * @param {Object} l10n l10nTranslation\n\t */\n\tconstructor(hec, l10n)\n\t{\n\t\tthis.hec = hec;\n\t\tthis.l10n = l10n;\n\t}\n\n\t/**\n\t * Show an action box\n\t * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new\n\t * @param {string} [content=''] content to add to the box\n\t * @param {array} [action_box_css=[]] additional css elements for the action box\n\t * @param {number} [override=0] override size adjust\n\t * @param {number} [content_override=0] override content size adjust\n\t */\n\tshowFillActionBox(target_id = 'actionBox', content = '', action_box_css = [], override = 0, content_override = 0)\n\t{\n\t\t// fill content\n\t\tthis.fillActionBox(target_id, content, action_box_css);\n\t\t// show the box\n\t\tthis.showActionBox(target_id, override, content_override);\n\t}\n\n\t/**\n\t * Fill action box with content, create it if it does not existgs\n\t * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new\n\t * @param {string} [content=''] content to add to the box\n\t * @param {array} [action_box_css=[]] additional css elements for the action box\n\t */\n\tfillActionBox(target_id = 'actionBox', content = '', action_box_css = [])\n\t{\n\t\t// show action box, calc height + center\n\t\tif (!exists(target_id)) {\n\t\t\t// add at the bottom\n\t\t\t$('#mainContainer').after(\n\t\t\t\tthis.hec.phfo(this.hec.cel('div', target_id, '', ['actionBoxElement', 'hide'].concat(action_box_css)))\n\t\t\t);\n\t\t}\n\t\t// add the info box\n\t\t$('#' + target_id).html(content);\n\t}\n\n\t/**\n\t * Adjust the size of the action box\n\t * @param {string} [target_id='actionBox'] which actionBox to work on\n\t * @param {number} [override=0] override size adjust\n\t * @param {number} [content_override=0] override content size adjust\n\t */\n\tadjustActionBox(target_id = 'actionBox', override = 0, content_override = 0)\n\t{\n\t\t// adjust box size\n\t\tthis.adjustActionBoxHeight(target_id, override, content_override);\n\t\t// center the alert box\n\t\tsetCenter(target_id, true, true);\n\t}\n\n\t/**\n\t * hide any open action boxes and hide overlay\n\t */\n\thideAllActionBoxes()\n\t{\n\t\t// hide all action boxes that might exist\n\t\t$('#actionBox, div[id^=\"actionBox-\"].actionBoxElement').hide();\n\t\t// hideOverlayBoxLayers();\n\t\t$('#overlayBox').hide();\n\t}\n\n\t/**\n\t * hide action box, but do not clear content\n\t * DEPRECATED\n\t * @param {string} [target_id='actionBox']\n\t */\n\thideActionBox(target_id = 'actionBox')\n\t{\n\t\tthis.closeActionBoxFloat(target_id, false);\n\t}\n\n\t/**\n\t * Just show and adjust the box\n\t * DEPRECAED\n\t * @param {string} [target_id='actionBox'] which actionBox to work on\n\t * @param {number} [override=0] override size adjust\n\t * @param {number} [content_override=0] override content size adjust\n\t * @param {Boolean} [hide_all=false] if set to true, hide all other action boxes\n\t */\n\tshowActionBox(target_id = 'actionBox', override = 0, content_override = 0, hide_all = true)\n\t{\n\t\tthis.showActionBoxFloat(target_id, override, content_override, hide_all);\n\t}\n\n\t/**\n\t * close an action box with default clear content\n\t * for just hide use hideActionBox\n\t * DEPRECATED\n\t * @param {String} [target_id='actionBox'] which action box to close, default is set\n\t * @param {Boolean} [clean=true] if set to false will not remove html content, just hide\n\t */\n\tcloseActionBox(target_id = 'actionBox', clean = true)\n\t{\n\t\t// set the target/content ids\n\t\tthis.closeActionBoxFloat(target_id, clean);\n\t}\n\n\t/**\n\t * TODO: better stacked action box: OPEN\n\t * @param {string} [target_id='actionBox'] which actionBox to work on\n\t * @param {number} [override=0] override size adjust\n\t * @param {number} [content_override=0] override content size adjust\n\t * @param {boolean} [hide_all=false] if set to true, hide all other action boxes\n\t */\n\tshowActionBoxFloat(target_id = 'actionBox', override = 0, content_override = 0, hide_all = false)\n\t{\n\t\tif (hide_all === true) {\n\t\t\t// hide all action boxes if they are currently open\n\t\t\tthis.hideAllActionBoxes();\n\t\t}\n\t\t// if no box, created if\n\t\tif (!exists('overlayBox')) {\n\t\t\t$('body').prepend(this.hec.phfo(this.hec.cel('div', 'overlayBox', '', ['overlayBoxElement'])));\n\t\t\t$('#overlayBox').css('zIndex', this.zIndex.base);\n\t\t}\n\t\t// adjust zIndex so its above all other and set action box zindex +1\n\t\t$('#overlayBox').show();\n\t\tif (!keyInObject(target_id, this.zIndex.boxes)) {\n\t\t\tthis.zIndex.boxes[target_id] = this.zIndex.max;\n\t\t\t// increase by ten\n\t\t\tthis.zIndex.max += 10;\n\t\t} else if (this.zIndex.boxes[target_id] + 10 < this.zIndex.max) {\n\t\t\t// see if this is the highest level, if not move up and write no max zIndex\n\t\t\t// move it up to be the new top and move the others down\n\t\t\t// [loop, order by value]\n\t\t\t// current hack, increase max\n\t\t\tthis.zIndex.boxes[target_id] = this.zIndex.max;\n\t\t\tthis.zIndex.max += 10;\n\t\t}\n\t\t// make sure the overlayBox is one level below this\n\t\t// unless there is an active \"indicator\" index\n\t\tif (!this.zIndex.indicator) {\n\t\t\t$('#overlayBox').css('zIndex', this.zIndex.boxes[target_id] - 1);\n\t\t}\n\t\t$('#' + target_id).css('zIndex', this.zIndex.boxes[target_id]).show();\n\t\t// set target to this new level\n\t\t// @ts-ignore\n\t\tif (this.zIndex.active.indexOf(target_id) == -1) {\n\t\t\t// @ts-ignore\n\t\t\tthis.zIndex.active.push(target_id);\n\t\t}\n\t\tthis.zIndex.top = target_id;\n\t\t// adjust size\n\t\tthis.adjustActionBox(target_id, override, content_override);\n\t}\n\n\t/**\n\t * TODO: better stacked action box: CLOSE\n\t * @param {String} [target_id='actionBox'] which action box to close, default is set\n\t * @param {Boolean} [clean=true] if set to false will not remove html content, just hide\n\t */\n\tcloseActionBoxFloat(target_id = 'actionBox', clean = true)\n\t{\n\t\t// do nothing if this does not exist\n\t\tif (!exists(target_id)) {\n\t\t\treturn;\n\t\t}\n\t\t// clear storage object\n\t\tif (\n\t\t\tkeyInObject(target_id, this.action_box_storage) && clean === true\n\t\t) {\n\t\t\tthis.action_box_storage[target_id] = {};\n\t\t}\n\t\tif (clean === true) {\n\t\t\t$('#' + target_id).html('');\n\t\t}\n\t\t$('#' + target_id).hide();\n\t\t// remove from active list\n\t\t// @ts-ignore\n\t\tlet idx = this.zIndex.active.indexOf(target_id);\n\t\tthis.zIndex.active.splice(idx, 1);\n\t\t// do we have any visible action boxes.\n\t\t// find the highest zIndex and set overlayBox to this -1\n\t\t// @ts-ignore\n\t\tlet visible_zIndexes = $('#actionBox:visible, div[id^=\"actionBox-\"].actionBoxElement:visible').map((i, el) => ({\n\t\t\tid: el.id,\n\t\t\tzIndex: $('#' + el.id).css('zIndex')\n\t\t})).get();\n\t\tif (visible_zIndexes.length > 0) {\n\t\t\tlet max_zIndex = 0;\n\t\t\tlet max_el_id = '';\n\t\t\tfor (let zIndex_el of visible_zIndexes) {\n\t\t\t\tif (parseInt(zIndex_el.zIndex) > max_zIndex) {\n\t\t\t\t\tmax_zIndex = parseInt(zIndex_el.zIndex);\n\t\t\t\t\tmax_el_id = zIndex_el.id;\n\t\t\t\t}\n\t\t\t}\n\t\t\t$('#overlayBox').css('zIndex', max_zIndex - 1);\n\t\t\tthis.zIndex.top = max_el_id;\n\t\t} else {\n\t\t\t$('#overlayBox').hide();\n\t\t}\n\t}\n\n\t/**\n\t * create a new action box and fill it with basic elements\n\t * @param {String} [target_id='actionBox']\n\t * @param {String} [title='']\n\t * @param {Object} [contents={}]\n\t * @param {Object} [headers={}]\n\t * @param {Boolean} [show_close=true]\n\t * @param {Object} [settings={}] Optional settings, eg style sheets\n\t */\n\tcreateActionBox(\n\t\ttarget_id = 'actionBox',\n\t\ttitle = '',\n\t\tcontents = {},\n\t\theaders = {},\n\t\tsettings = {},\n\t\tshow_close = true\n\t) {\n\t\tif (!keyInObject(target_id, this.action_box_storage)) {\n\t\t\tthis.action_box_storage[target_id] = {};\n\t\t}\n\t\t// settings can have the following\n\t\t// : header_css:[]\n\t\t// : action_box_css:[]\n\t\tlet header_css = [];\n\t\tif (keyInObject('header_css', settings)) {\n\t\t\theader_css = settings.header_css;\n\t\t}\n\t\tlet action_box_css = [];\n\t\tif (keyInObject('action_box_css', settings)) {\n\t\t\taction_box_css = settings.action_box_css;\n\t\t}\n\t\tlet elements = [];\n\t\t// add title + close button to actionBox\n\t\telements.push(this.hec.phfo(\n\t\t\tthis.hec.aelx(this.hec.cel('div', target_id + '_title', '', ['actionBoxTitle', 'flx-spbt'].concat(header_css)),\n\t\t\t\t...show_close === true ? [\n\t\t\t\t\t// title\n\t\t\t\t\tthis.hec.cel('div', '', title, ['fs-b', 'w-80']),\n\t\t\t\t\t// close button\n\t\t\t\t\tthis.hec.aelx(this.hec.cel('div', target_id + '_title_close_button', '', ['w-20', 'tar']),\n\t\t\t\t\t\tthis.hec.cel('input', target_id + '_title_close', '', ['button-close', 'fs-s'],\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t\tvalue: this.l10n.__('Close'),\n\t\t\t\t\t\t\t\tOnClick: 'closeActionBox(\\'' + target_id + '\\', false);'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t] : [\n\t\t\t\t\tthis.hec.cel('div', '', title, ['fs-b', 'w-100'])\n\t\t\t\t]\n\t\t\t)\n\t\t));\n\t\t// if we have header content, add that here\n\t\tif (getObjectCount(headers) > 0) {\n\t\t\t// if the element has an entry called \"raw_string\" then this does not need to be converted\n\t\t\tif (keyInObject('raw_string', headers)) {\n\t\t\t\telements.push(headers.raw_string);\n\t\t\t} else {\n\t\t\t\telements.push(this.hec.phfo(headers));\n\t\t\t}\n\t\t}\n\t\t// main content part (this should NOT be empty), if empty, add empty _content block\n\t\tif (getObjectCount(contents) > 0) {\n\t\t\t// if the element has an entry called \"raw_string\" then this does not need to be converted\n\t\t\tif (keyInObject('raw_string', contents)) {\n\t\t\t\telements.push(contents.raw_string);\n\t\t\t} else {\n\t\t\t\telements.push(this.hec.phfo(contents));\n\t\t\t}\n\t\t} else {\n\t\t\telements.push(this.hec.phfo(this.hec.cel('div', target_id + '_content', '', [])));\n\t\t}\n\t\t// footer clear call\n\t\telements.push(this.hec.phfo(\n\t\t\tthis.hec.aelx(this.hec.cel('div', target_id + '_footer', '', ['pd-5', 'flx-spbt']),\n\t\t\t\t...show_close === true ? [\n\t\t\t\t\t// dummy spacer\n\t\t\t\t\tthis.hec.cel('div', '', '', ['fs-b', 'w-80']),\n\t\t\t\t\t// close button\n\t\t\t\t\tthis.hec.aelx(this.hec.cel('div', target_id + '_footer_close_button', '', ['tar', 'w-20']),\n\t\t\t\t\t\tthis.hec.cel('input', target_id + '_footer_close', '', ['button-close', 'fs-s'],\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\t\t\tvalue: this.l10n.__('Close'),\n\t\t\t\t\t\t\t\tOnClick: 'closeActionBox(\\'' + target_id + '\\', false);'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t] : [\n\t\t\t\t\tthis.hec.cel('div', '', '', ['fs-b', 'w-100'])\n\t\t\t\t]\n\t\t\t)\n\t\t));\n\t\telements.push(this.hec.phfo(this.hec.cel('input', target_id + '-cache_time', '', [], {\n\t\t\ttype: 'hidden',\n\t\t\tvalue: Date.now()\n\t\t})));\n\t\tthis.fillActionBox(target_id, elements.join(''), action_box_css);\n\t}\n\n\t/**\n\t * adjusts the action box height based on content and window height of browser\n\t * TODO: border on outside/and other margin things need to be added in overall adjustment\n\t * @param {String} [target_id='actionBox'] target id, if not set, fall back to default\n\t * @param {Number} [override=0] override value to add to the actionBox height\n\t * @param {Number} [content_override=0] override the value from _content block if it exists\n\t */\n\tadjustActionBoxHeight(target_id = 'actionBox', override = 0, content_override = 0)\n\t{\n\t\tvar new_height = 0;\n\t\tvar dim = {};\n\t\tvar abc_dim = {};\n\t\tvar content_id = '';\n\t\t// make sure it is a number\n\t\tif (isNaN(override)) {\n\t\t\toverride = 0;\n\t\t}\n\t\tif (isNaN(content_override)) {\n\t\t\tcontent_override = 0;\n\t\t}\n\t\t// set the target/content ids\n\t\tswitch (target_id) {\n\t\t\tcase 'actionBox':\n\t\t\t\tcontent_id = 'action_box';\n\t\t\t\tbreak;\n\t\t\tcase 'actionBoxSub':\n\t\t\t\tcontent_id ='action_box_sub';\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tcontent_id = target_id;\n\t\t\t\tbreak;\n\t\t}\n\t\t// first remove any height, left, top style entris from target and content blocks\n\t\t// @ts-ignore\n\t\t$.each([target_id, content_id + '_content'], function(i, v) {\n\t\t\t$('#' + v).css({\n\t\t\t\t'height': '',\n\t\t\t\t'width': ''\n\t\t\t});\n\t\t});\n\t\tif (exists(content_id + '_title')) {\n\t\t\tdim.height = $('#' + content_id + '_title').outerHeight();\n\t\t\tconsole.log('Target: %s, Action box Title: %s', target_id, dim.height);\n\t\t\tnew_height += dim.height ?? 0;\n\t\t}\n\t\tif (exists(content_id + '_header')) {\n\t\t\tdim.height = $('#' + content_id + '_header').outerHeight();\n\t\t\tconsole.log('Target: %s, Action box Header: %s', target_id, dim.height);\n\t\t\tnew_height += dim.height ?? 0;\n\t\t}\n\t\tif (exists(content_id + '_content')) {\n\t\t\tif (content_override > 0) {\n\t\t\t\tconsole.log('Target: %s, Action box Content Override: %s', target_id, content_override);\n\t\t\t\tnew_height += content_override;\n\t\t\t} else {\n\t\t\t\tabc_dim.height = $('#' + content_id + '_content').outerHeight();\n\t\t\t\tconsole.log('Target: %s, Action box Content: %s', target_id, abc_dim.height);\n\t\t\t\tnew_height += abc_dim.height ?? 0;\n\t\t\t}\n\t\t}\n\t\t// always there sets\n\t\tif (exists(content_id + '_footer')) {\n\t\t\tdim.height = $('#' + content_id + '_footer').outerHeight();\n\t\t\tconsole.log('Target: %s, Action box Footer: %s', target_id, dim.height);\n\t\t\tnew_height += dim.height ?? 0;\n\t\t}\n\t\t// get difference for the rest from outer box\n\t\t// console.log('Target: %s, Action box outer: %s, Content: %s, New: %s', target_id, $('#' + target_id).outerHeight(), $('#' + content_id + '_content').outerHeight(), new_height);\n\t\t// new_height += ($('#' + target_id).outerHeight() - new_height) + override;\n\t\tnew_height += override;\n\t\t// get border width top-bottom from action Box, we need to remove this from the final height\n\t\t// console.log('Target: %s, Border top: %s', target_id, $('#' + target_id).css('border-top-width'));\n\t\t// get window size and check if content is bigger\n\t\tvar viewport = getWindowSize();\n\t\tif (new_height >= viewport.height) {\n\t\t\t// resize the action box content and set overflow [of-s-y]\n\t\t\tif (exists(content_id + '_content')) {\n\t\t\t\tif (!$('#' + content_id + '_content').hasClass('of-s-y')) {\n\t\t\t\t\t$('#' + content_id + '_content').addClass('of-s-y');\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsole.log('Target: %s, Viewport: %s, ActionBox (NH): %s, ABcontent: %s, ABouter: %s', target_id, viewport.height, new_height, abc_dim.height, $('#' + target_id).outerHeight());\n\t\t\t// the height off window - all - action box gives new action box height\n\t\t\tvar m_height = viewport.height - (new_height - (abc_dim.height ?? 0));\n\t\t\tconsole.log('Target: %s, New ABcontent: %s', target_id, m_height);\n\t\t\t$('#' + content_id + '_content').css('height', m_height + 'px');\n\t\t\tnew_height = new_height - (abc_dim.height ?? 0) + m_height;\n\t\t\tconsole.log('Target: %s, New Hight: %s', target_id, new_height);\n\t\t} else {\n\t\t\t// if size ok, check if overflow scoll is set, remove it\n\t\t\tif (exists(content_id + '_content')) {\n\t\t\t\tif ($('#' + content_id + '_content').hasClass('of-s-y')) {\n\t\t\t\t\t$('#' + content_id + '_content').removeClass('of-s-y');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconsole.log('Target: %s, Action Box new height: %s px (override %s px, content override %s px), window height: %s px, Visible Height: %s px', target_id, new_height, override, content_override, viewport.height, $('#' + content_id).outerHeight());\n\t\t// adjust height\n\t\t$('#' + target_id).css('height', new_height + 'px');\n\t}\n}\n\n// __EMD__\n", "/*\nDescription: Login access and menu\nDate: 2025//3/6\nCreator: Clemens Schwaighofer\n*/\n\nexport { LoginNavMenu };\nimport { isObject, getObjectCount } from './JavaScriptHelpers.mjs';\nimport { exists } from './DomHelpers.mjs';\n\nclass LoginNavMenu {\n\n\thec;\n\tl10n;\n\n\t/**\n\t * action box creator\n\t * @param {Object} hec HtmlElementCreator\n\t * @param {Object} l10n l10nTranslation\n\t */\n\tconstructor(hec, l10n)\n\t{\n\t\tthis.hec = hec;\n\t\tthis.l10n = l10n;\n\t}\n\n\t/**\n\t * create login string and logout button elements\n\t * @param {String} login_string the login string to show on the left\n\t * @param {String} [header_id='mainHeader'] the target for the main element block\n\t * if not set mainHeader is assumed\n\t * this is the target div for the \"loginRow\"\n\t */\n\tcreateLoginRow(login_string, header_id = 'mainHeader')\n\t{\n\t\t// if header does not exist, we do nothing\n\t\tif (exists(header_id)) {\n\t\t\t// that row must exist already, if not it must be the first in the \"mainHeader\"\n\t\t\tif (!exists('loginRow')) {\n\t\t\t\t$('#' + header_id).html(this.hec.phfo(this.hec.cel('div', 'loginRow', '', ['loginRow', 'flx-spbt'])));\n\t\t\t}\n\t\t\t// clear out just in case for first entry\n\t\t\t// fill with div name & login/logout button\n\t\t\t$('#loginRow').html(this.hec.phfo(this.hec.cel('div', 'loginRow-name', login_string)));\n\t\t\t$('#loginRow').append(this.hec.phfo(this.hec.cel('div', 'loginRow-info', '')));\n\t\t\t$('#loginRow').append(this.hec.phfo(\n\t\t\t\tthis.hec.aelx(\n\t\t\t\t\t// outer div\n\t\t\t\t\tthis.hec.cel('div', 'loginRow-logout'),\n\t\t\t\t\t// inner element\n\t\t\t\t\tthis.hec.cel('input', 'logout', '', [], {\n\t\t\t\t\t\tvalue: this.l10n.__('Logout'),\n\t\t\t\t\t\ttype: 'button',\n\t\t\t\t\t\tonClick: 'loginLogout()'\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t));\n\t\t}\n\t}\n\n\t/**\n\t * create the top nav menu that switches physical between pages\n\t * (edit access data based)\n\t * @param {Object} nav_menu the built nav menu with highlight info\n\t * @param {String} [header_id='mainHeader'] the target for the main element block\n\t * if not set mainHeader is assumed\n\t * this is the target div for the \"menuRow\"\n\t */\n\tcreateNavMenu(nav_menu, header_id = 'mainHeader')\n\t{\n\t\t// must be an object\n\t\tif (isObject(nav_menu) && getObjectCount(nav_menu) > 1) {\n\t\t\t// do we have more than one entry, if not, do not show (single page)\n\t\t\tif (!exists('menuRow')) {\n\t\t\t\t$('#' + header_id).html(this.hec.phfo(this.hec.cel('div', 'menuRow', '', ['menuRow', 'flx-s'])));\n\t\t\t}\n\t\t\tvar content = [];\n\t\t\t$.each(nav_menu, function(key, item) {\n\t\t\t\t// key is number\n\t\t\t\t// item is object with entries\n\t\t\t\tif (key != 0) {\n\t\t\t\t\tcontent.push(this.hec.phfo(this.hec.cel('div', '', '·', ['pd-2'])));\n\t\t\t\t}\n\t\t\t\t// ignore item.popup for now\n\t\t\t\tif (item.enabled) {\n\t\t\t\t\t// set selected based on window.location.href as the php set will not work\n\t\t\t\t\tif (window.location.href.indexOf(item.url) != -1) {\n\t\t\t\t\t\titem.selected = 1;\n\t\t\t\t\t}\n\t\t\t\t\t// create the entry\n\t\t\t\t\tcontent.push(this.hec.phfo(\n\t\t\t\t\t\tthis.hec.aelx(\n\t\t\t\t\t\t\tthis.hec.cel('div'),\n\t\t\t\t\t\t\tthis.hec.cel('a', '', item.name, ['pd-2'].concat(item.selected ? 'highlight': ''), {\n\t\t\t\t\t\t\t\thref: item.url\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t)\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t});\n\t\t\t$('#menuRow').html(content.join(''));\n\t\t} else {\n\t\t\t$('#menuRow').hide();\n\t\t}\n\t}\n\n}\n\n// __END__\n", "/*\n * general edit javascript\n * former name: edit.jq.js\n * This is the jquery version\n * NOTE: jquey parts will be deprecated\n*/\n\nimport {\n\terrorCatch as _errorCatch,\n\tisFunction as _isFunction,\n\texecuteFunctionByName as _executeFunctionByName,\n\tisObject as _isObject,\n\tgetObjectCount as _getObjectCount,\n\tkeyInObject as _keyInObject,\n\tgetKeyByValue as _getKeyByValue,\n\tvalueInObject as _valueInObject,\n\tdeepCopyFunction as _deepCopyFunction\n} from './utils/JavaScriptHelpers.mjs';\nimport {\n\tescapeHtml as _escapeHtml,\n\tunescapeHtml as _unescapeHtml,\n\thtml_options as _html_options,\n\thtml_options_block as _html_options_block,\n\thtml_options_refill as _html_options_refill\n} from './utils/HtmlHelpers.mjs';\nimport {\n\tloadEl as _loadEl,\n\tpop as _pop,\n\texpandTA as _expandTA,\n\texists as _exists\n} from './utils/DomHelpers.mjs';\nimport {\n\tdec2hex as _dec2hex,\n\tgetRandomIntInclusive as _getRandomIntInclusive,\n\troundPrecision as _roundPrecision\n} from './utils/MathHelpers.mjs';\nimport {\n\tformatString as _formatString,\n\tnumberWithCommas as _numberWithCommas,\n\tconvertLBtoBR as _convertLBtoBR\n} from './utils/StringHelpers.mjs';\nimport {\n\tgetTimestamp as _getTimestamp\n} from './utils/DateTimeHelpers.mjs';\nimport {\n\tgenerateId as _generateId,\n\trandomIdF as _randomIdF,\n} from './utils/UniqIdGenerators.mjs';\nimport {\n\tgetWindowSize as _getWindowSize,\n\tgetScrollOffset as _getScrollOffset,\n\tgetScrollOffsetOpener as _getScrollOffsetOpener,\n\tsetCenter as _setCenter,\n\tgoToPos as _goToPos,\n\tgoTo as _goTo\n} from './utils/ResizingAndMove.mjs';\nimport {\n\tformatBytes as _formatBytes,\n\tformatBytesLong as _formatBytesLong,\n\tstringByteFormat as _stringByteFormat\n} from './utils/FormatBytes.mjs';\nimport {\n\tparseQueryString as _parseQueryString,\n\tgetQueryStringParam as _getQueryStringParam\n} from './utils/UrlParser.mjs';\nimport {\n\tloginLogout as _loginLogout,\n} from './utils/LoginLogout.mjs';\nimport {\n\tActionIndicatorOverlayBox,\n\tactionIndicator as _actionIndicator,\n\tactionIndicatorShow as _actionIndicatorShow,\n\tactionIndicatorHide as _actionIndicatorHide,\n\toverlayBoxShow as _overlayBoxShow,\n\toverlayBoxHide as _overlayBoxHide,\n\tsetOverlayBox as _setOverlayBox,\n\thideOverlayBox as _hideOverlayBox,\n\tClearCall as _ClearCall\n} from './utils/ActionIndicatorOverlayBox.mjs';\nimport { l10nTranslation } from './utils/l10nTranslation.mjs';\nimport { HtmlElementCreator } from './utils/HtmlElementCreator.mjs';\nimport { ActionBox } from './utils/ActionBox.mjs';\nimport { LoginNavMenu } from './utils/LoginNavMenu.mjs';\n\nlet aiob = new ActionIndicatorOverlayBox();\nlet hec = new HtmlElementCreator();\n// if ( undef === \"undefined\") {\n// @ts-ignore\n// eslint-disable-next-line no-undef\nlet l10n = new l10nTranslation(typeof i18n === \"undefined\" ? {} : i18n);\nlet ab = new ActionBox(hec, l10n);\nlet lnm = new LoginNavMenu(hec, l10n);\n\n// MARK: deprecated String/Number override\n\n/**\n * simple sprintf formater for replace\n * usage: \"{0} is cool, {1} is not\".format(\"Alpha\", \"Beta\");\n * First, checks if it isn't implemented yet.\n * @param {String} String.prototype.format string with elements to be replaced\n * @return {String} Formated string\n * @deprecated StringHelpers.formatString\n */\n// @ts-ignore\nif (!String.prototype.format) {\n\t// @ts-ignore\n\tString.prototype.format = function()\n\t{\n\t\tconsole.error('[DEPRECATED] use StringHelpers.formatString');\n\t\t// @ts-ignore\n\t\treturn _formatString(this, arguments);\n\t};\n}\n\n/**\n * round to digits (float)\n * @param {Number} Number.prototype.round Float type number to round\n * @param {Number} prec Precision to round to\n * @return {Float} Rounded number\n * @deprecated use MathHelpers.roundPrecision\n */\n// @ts-ignore\nif (Number.prototype.round) {\n\t// @ts-ignore\n\tNumber.prototype.round = function (prec) {\n\t\tconsole.error('[DEPRECATED] use MathHelpers.roundPrecision');\n\t\t// @ts-ignore\n\t\treturn _roundPrecision(this, prec);\n\t};\n}\n\n/**\n * escape HTML string\n * @param {String} String.prototype.escapeHTML HTML data string to be escaped\n * @return {String} escaped string\n * @deprecated use HtmlHelpers.escapeHtml\n */\n// @ts-ignore\nif (!String.prototype.escapeHTML) {\n\t// @ts-ignore\n\tString.prototype.escapeHTML = function() {\n\t\tconsole.error('[DEPRECATED] use HtmlHelpers.escapeHtml');\n\t\t// @ts-ignore\n\t\treturn _escapeHtml(this);\n\t};\n}\n\n/**\n * unescape a HTML encoded string\n * @param {String} String.prototype.unescapeHTML data with escaped entries\n * @return {String} HTML formated string\n * @deprecated use HtmlHelpers.unescapeHtml\n */\n// @ts-ignore\nif (!String.prototype.unescapeHTML) {\n\t// @ts-ignore\n\tString.prototype.unescapeHTML = function() {\n\t\tconsole.error('[DEPRECATED] use HtmlHelpers.unescapeHtml');\n\t\t// @ts-ignore\n\t\treturn _unescapeHtml(this);\n\t};\n}\n\n// MARK: general collection\n\n/**\n *\n * @param {String} string\n * @returns {String}\n */\n// @ts-ignore\nfunction escapeHtml(string) // eslint-disable-line no-unused-vars\n{\n\treturn _escapeHtml(string);\n}\n\n/**\n * round to digits (float)\n * @param {Number} number Float type number to round\n * @param {Number} prec Precision to round to\n * @return {Number} Rounded number\n */\n// @ts-ignore\nfunction roundPrecision(number, prec) // eslint-disable-line no-unused-vars\n{\n\treturn _roundPrecision(number, prec);\n}\n\n/**\n * simple sprintf formater for replace\n * usage: \"{0} is cool, {1} is not\".format(\"Alpha\", \"Beta\");\n * First, checks if it isn't implemented yet.\n * @param {String} string String with elements to be replaced\n * @return {String} Formated string\n * @deprecated StringHelpe\n */\n// @ts-ignore\nfunction formatString(string, ...args) // eslint-disable-line no-unused-vars\n{\n\treturn _formatString(string, ...args);\n}\n\n/**\n *\n * @param {String} string\n * @returns {String}\n */\n// @ts-ignore\nfunction unescapeHtml(string) // eslint-disable-line no-unused-vars\n{\n\treturn _unescapeHtml(string);\n}\n\n/**\n * Gets html element or throws an error\n * @param {string} el_id Element ID to get\n * @returns {HTMLElement}\n * @throws Error\n */\n// @ts-ignore\nfunction loadEl(el_id) // eslint-disable-line no-unused-vars\n{\n\treturn _loadEl(el_id);\n}\n\n/**\n * opens a pop_ window with winName and given features (string)\n * @param {String} theURL the url\n * @param {String} winName window name\n * @param {Object} features pop_ features\n */\n// @ts-ignore\nfunction pop(theURL, winName, features) // eslint-disable-line no-unused-vars\n{\n\t_pop(theURL, winName, features);\n}\n\n/**\n * automatically resize a text area based on the amount of lines in it\n * @param {string} ta_id element id\n */\n// @ts-ignore\nfunction expandTA(ta_id) // eslint-disable-line no-unused-vars\n{\n\t_expandTA(ta_id);\n}\n\n/**\n * wrapper to get the real window size for the current browser window\n * @return {Object} object with width/height\n */\n// @ts-ignore\nfunction getWindowSize() // eslint-disable-line no-unused-vars\n{\n\treturn _getWindowSize();\n}\n\n/**\n * wrapper to get the correct scroll offset\n * @return {Object} object with x/y px\n */\n// @ts-ignore\nfunction getScrollOffset() // eslint-disable-line no-unused-vars\n{\n\treturn _getScrollOffset();\n}\n\n/**\n * wrapper to get the correct scroll offset for opener page (from pop_)\n * @return {Object} object with x/y px\n */\n// @ts-ignore\nfunction getScrollOffsetOpener() // eslint-disable-line no-unused-vars\n{\n\treturn _getScrollOffsetOpener();\n}\n\n/**\n * centers div to current window size middle\n * @param {String} id element to center\n * @param {Boolean} left if true centers to the middle from the left\n * @param {Boolean} top if true centers to the middle from the top\n */\n// @ts-ignore\nfunction setCenter(id, left, top) // eslint-disable-line no-unused-vars\n{\n\t_setCenter(id, left, top);\n}\n\n/**\n * goes to an element id position\n * @param {String} element element id to move to\n * @param {Number} [offset=0] offset from top, default is 0 (px)\n * @param {Number} [duration=500] animation time, default 500ms\n * @param {String} [base='body,html'] base element for offset scroll\n */\n// @ts-ignore\nfunction goToPos(element, offset = 0, duration = 500, base = 'body,html') // eslint-disable-line no-unused-vars\n{\n\t_goToPos(element, offset, duration, base);\n}\n\n/**\n * go to element, scroll\n * non jquery\n * @param {string} target\n*/\n// @ts-ignore\nfunction goTo(target) // eslint-disable-line no-unused-vars\n{\n\t_goTo(target);\n}\n\n/**\n * uses the i18n object created in the translation template\n * that is filled from gettext in PHP\n * @param {String} string text to translate\n * @return {String} translated text (based on PHP selected language)\n */\n// @ts-ignore\nfunction __(string) // eslint-disable-line no-unused-vars\n{\n\treturn l10n.__(string);\n}\n\n/**\n * formats flat number 123456 to 123,456\n * @param {Number} x number to be formated\n * @return {String} formatted with , in thousands\n */\n// @ts-ignore\nfunction numberWithCommas(x) // eslint-disable-line no-unused-vars\n{\n\treturn _numberWithCommas(x);\n}\n\n/**\n * converts line breaks to br\n * @param {String} string any string\n * @return {String} string with
\n */\n// @ts-ignore\nfunction convertLBtoBR(string) // eslint-disable-line no-unused-vars\n{\n\treturn _convertLBtoBR(string);\n}\n\n/**\n * returns current timestamp (unix timestamp)\n * @return {Number} timestamp (in milliseconds)\n */\n// @ts-ignore\nfunction getTimestamp() // eslint-disable-line no-unused-vars\n{\n\treturn _getTimestamp();\n}\n\n/**\n * dec2hex :: Integer -> String\n * i.e. 0-255 -> '00'-'ff'\n * @param {Number} dec decimal string\n * @return {String} hex encdoded number\n */\n// @ts-ignore\nfunction dec2hex(dec) // eslint-disable-line no-unused-vars\n{\n\treturn _dec2hex(dec);\n}\n\n/**\n * generateId :: Integer -> String\n * only works on mondern browsers\n * @param {Number} len length of unique id string\n * @return {String} random string in length of len\n */\n// @ts-ignore\nfunction generateId(len) // eslint-disable-line no-unused-vars\n{\n\treturn _generateId(len);\n}\n\n/**\n * creates a pseudo random string of 10 characters\n * works on all browsers\n * after many runs it will create d_licates\n * @return {String} not true random string\n */\n// @ts-ignore\nfunction randomIdF() // eslint-disable-line no-unused-vars\n{\n\treturn _randomIdF();\n}\n\n/**\n * generate a number between min/max\n * with min/max inclusive.\n * eg: 1,5 will create a number ranging from 1 o 5\n * @param {Number} min minimum int number inclusive\n * @param {Number} max maximumg int number inclusive\n * @return {Number} Random number\n */\n// @ts-ignore\nfunction getRandomIntInclusive(min, max) // eslint-disable-line no-unused-vars\n{\n\treturn _getRandomIntInclusive(min, max);\n}\n\n/**\n * check if name is a function\n * @param {string} name Name of function to check if exists\n * @return {Boolean} true/false\n */\n// @ts-ignore\nfunction isFunction(name) // eslint-disable-line no-unused-vars\n{\n\treturn _isFunction(name);\n}\n\n/**\n * call a function by its string name\n * https://stackoverflow.com/a/359910\n * example: executeFunctionByName(\"My.Namespace.functionName\", window, arguments);\n * @param {string} functionName The function name or namespace + function\n * @param {any} context context (window or first namespace)\n * hidden next are all the arguments\n * @return {any} Return values from functon\n */\n// @ts-ignore\nfunction executeFunctionByName(functionName, context) // eslint-disable-line no-unused-vars\n{\n\treturn _executeFunctionByName(functionName, context);\n}\n\n/**\n * checks if a variable is an object\n * @param {any} val possible object\n * @return {Boolean} true/false if it is an object or not\n */\n// @ts-ignore\nfunction isObject(val) // eslint-disable-line no-unused-vars\n{\n\treturn _isObject(val);\n}\n\n/**\n * get the length of an object (entries)\n * @param {Object} object object to check\n * @return {Number} number of entry\n */\n// @ts-ignore\nfunction getObjectCount(object) // eslint-disable-line no-unused-vars\n{\n\treturn _getObjectCount(object);\n}\n\n/**\n * checks if a key exists in a given object\n * @param {String} key key name\n * @param {Object} object object to search key in\n * @return {Boolean} true/false if key exists in object\n */\n// @ts-ignore\nfunction keyInObject(key, object) // eslint-disable-line no-unused-vars\n{\n\treturn _keyInObject(key, object);\n}\n\n/**\n * returns matching key of value\n * @param {Object} object object to search value in\n * @param {any} value any value (String, Number, etc)\n * @return {String} the key found for the first matching value\n */\n// @ts-ignore\nfunction getKeyByValue(object, value) // eslint-disable-line no-unused-vars\n{\n\treturn _getKeyByValue(object, value);\n}\n\n/**\n * returns true if value is found in object with a key\n * @param {Object} object object to search value in\n * @param {any} value any value (String, Number, etc)\n * @return {Boolean} true on value found, false on not found\n */\n// @ts-ignore\nfunction valueInObject(object, value) // eslint-disable-line no-unused-vars\n{\n\treturn _valueInObject(object, value);\n}\n\n/**\n * true deep copy for Javascript objects\n * if Object.assign({}, obj) is not working (shallow)\n * or if JSON.parse(JSON.stringify(obj)) is failing\n * @param {Object} inObject Object to copy\n * @return {Object} Copied Object\n */\n// @ts-ignore\nfunction deepCopyFunction(inObject) // eslint-disable-line no-unused-vars\n{\n\treturn _deepCopyFunction(inObject);\n}\n\n/**\n * checks if a DOM element actually exists\n * @param {String} id Element id to check for\n * @return {Boolean} true if element exists, false on failure\n */\n// @ts-ignore\nfunction exists(id) // eslint-disable-line no-unused-vars\n{\n\treturn _exists(id);\n}\n\n/**\n * converts a int number into bytes with prefix in two decimals precision\n * currently precision is fixed, if dynamic needs check for max/min precision\n * @param {Number} bytes bytes in int\n * @return {String} string in GB/MB/KB\n */\n// @ts-ignore\nfunction formatBytes(bytes) // eslint-disable-line no-unused-vars\n{\n\treturn _formatBytes(bytes);\n}\n\n/**\n * like formatBytes, but returns bytes for <1KB and not 0.n KB\n * @param {Number} bytes bytes in int\n * @return {String} string in GB/MB/KB\n */\n// @ts-ignore\nfunction formatBytesLong(bytes) // eslint-disable-line no-unused-vars\n{\n\treturn _formatBytesLong(bytes);\n}\n\n/**\n * Convert a string with B/K/M/etc into a byte number\n * @param {String|Number} bytes Any string with B/K/M/etc\n * @return {String|Number} A byte number, or original string as is\n */\n// @ts-ignore\nfunction stringByteFormat(bytes) // eslint-disable-line no-unused-vars\n{\n\treturn _stringByteFormat(bytes);\n}\n\n/**\n * prints out error messages based on data available from the browser\n * @param {Object} err error from try/catch block\n */\n// @ts-ignore\nfunction errorCatch(err) // eslint-disable-line no-unused-vars\n{\n\t_errorCatch(err);\n}\n\n// MARK: ActionIndicatorOverlayBoxLegacy\n\n/*************************************************************\n * OLD action indicator and overlay boxes calls\n * DO NOT USE\n * actionIndicator -> showActionIndicator\n * actionIndicator -> hideActionIndicator\n * actionIndicatorShow -> showActionIndicator\n * actionIndicatorHide -> hideActionIndicator\n * overlayBoxShow -> showOverlayBoxLayers\n * overlayBoxHide -> hideOverlayBoxLayers\n * setOverlayBox -> showOverlayBoxLayers\n * hideOverlayBox -> hideOverlayBoxLayers\n * ClearCall -> ClearCallActionBox\n * ***********************************************************/\n\n/**\n * show or hide the \"do\" overlay\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n */\n// @ts-ignore\nfunction actionIndicator(loc, overlay = true) // eslint-disable-line no-unused-vars\n{\n\t_actionIndicator(loc, overlay);\n}\n\n/**\n * explicit show for action Indicator\n * instead of automatically show or hide, do on command show\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n */\n// @ts-ignore\nfunction actionIndicatorShow(loc, overlay = true) // eslint-disable-line no-unused-vars\n{\n\t_actionIndicatorShow(loc, overlay);\n}\n\n/**\n * explicit hide for action Indicator\n * instead of automatically show or hide, do on command hide\n * @param {String} loc location name for action indicator\n * default empty. for console.log\n * @param {Boolean} [overlay=true] override the auto hide/show over the overlay div block\n */\n// @ts-ignore\nfunction actionIndicatorHide(loc, overlay = true) // eslint-disable-line no-unused-vars\n{\n\t_actionIndicatorHide(loc, overlay);\n}\n\n/**\n * shows the overlay box or if already visible, bumps the zIndex to 100\n */\n// @ts-ignore\nfunction overlayBoxShow() // eslint-disable-line no-unused-vars\n{\n\t_overlayBoxShow();\n}\n\n/**\n * hides the overlay box or if zIndex is 100 bumps it down to previous level\n */\n// @ts-ignore\nfunction overlayBoxHide() // eslint-disable-line no-unused-vars\n{\n\t_overlayBoxHide();\n}\n\n/**\n * position the overlay block box and shows it\n */\n// @ts-ignore\nfunction setOverlayBox() // eslint-disable-line no-unused-vars\n{\n\t_setOverlayBox();\n}\n\n/**\n * opposite of set, always hides overlay box\n */\n// @ts-ignore\nfunction hideOverlayBox() // eslint-disable-line no-unused-vars\n{\n\t_hideOverlayBox();\n}\n\n/**\n * the abort call, clears the action box and hides it and the overlay box\n */\n// @ts-ignore\nfunction ClearCall() // eslint-disable-line no-unused-vars\n{\n\t_ClearCall();\n}\n\n// MARK: ActionIndicatorOverlayBox\n\n/*************************************************************\n * NEW action indicator and overlay box calls\n * USE THIS\n * ***********************************************************/\n\n/**\n * show action indicator\n * - checks if not existing and add\n * - only shows if not visible (else ignore)\n * - overlaybox check is called and shown on a fixzed\n * zIndex of 1000\n * - indicator is page centered\n * @param {String} loc ID string, only used for console log\n */\n// @ts-ignore\nfunction showActionIndicator(loc) // eslint-disable-line no-unused-vars\n{\n\taiob.showActionIndicator(loc);\n}\n\n/**\n * hide action indicator, if it is visiable\n * If the global variable GL_OB_S is > GL_OB_BASE then\n * the overlayBox is not hidden but the zIndex\n * is set to this value\n * @param {String} loc ID string, only used for console log\n */\n// @ts-ignore\nfunction hideActionIndicator(loc) // eslint-disable-line no-unused-vars\n{\n\taiob.hideActionIndicator(loc);\n}\n\n/**\n * checks if overlayBox exists, if not it is\n * added as hidden item at the body end\n */\n// @ts-ignore\nfunction checkOverlayExists() // eslint-disable-line no-unused-vars\n{\n\taiob.checkOverlayExists();\n}\n\n/**\n * show overlay box\n * if not visible show and set zIndex to 10 (GL_OB_BASE)\n * if visible, add +1 to the GL_OB_S variable and\n * _ zIndex by this value\n */\n// @ts-ignore\nfunction showOverlayBoxLayers(el_id) // eslint-disable-line no-unused-vars\n{\n\taiob.showOverlayBoxLayers(el_id);\n}\n\n/**\n * hide overlay box\n * lower GL_OB_S value by -1\n * if we are 10 (GL_OB_BASE) or below hide the overlayIndex\n * and set zIndex and GL_OB_S to 0\n * else just set zIndex to the new GL_OB_S value\n * @param {String} el_id Target to hide layer\n */\n// @ts-ignore\nfunction hideOverlayBoxLayers(el_id='') // eslint-disable-line no-unused-vars\n{\n\taiob.hideOverlayBoxLayers(el_id);\n}\n\n/**\n * only for single action box\n */\n// @ts-ignore\nfunction clearCallActionBox() // eslint-disable-line no-unused-vars\n{\n\taiob.clearCallActionBox();\n}\n\n// MARK: DOM MANAGEMENT FUNCTIONS\n/**\n * reates object for DOM element creation flow\n * @param {String} tag must set tag (div, span, etc)\n * @param {String} [id=''] optional set for id, if input, select will be used for name\n * @param {String} [content=''] text content inside, is skipped if sub elements exist\n * @param {Array} [css=[]] array for css tags\n * @param {Object} [options={}] anything else (value, placeholder, OnClick, style)\n * @return {Object} created element as an object\n */\n// @ts-ignore\nfunction cel(tag, id = '', content = '', css = [], options = {}) // eslint-disable-line no-unused-vars\n{\n\treturn hec.cel(tag, id, content, css, options);\n}\n\n/**\n * attach a cel created object to another to create a basic DOM tree\n * @param {Object} base object where to attach/search\n * @param {Object} attach the object to be attached\n * @param {String} [id=''] optional id, if given search in base for this id and attach there\n * @return {Object} \"none\", technically there is no return needed as it is global attach\n */\n// @ts-ignore\nfunction ael(base, attach, id = '') // eslint-disable-line no-unused-vars\n{\n\treturn hec.ael(base, attach, id);\n}\n\n/**\n * directly attach n elements to one master base element\n * this type does not s_port attach with optional id\n * @param {Object} base object to where we attach the elements\n * @param {...Object} attach attach 1..n: attach directly to the base element those attachments\n * @return {Object} \"none\", technically there is no return needed, global attach\n */\n// @ts-ignore\nfunction aelx(base, ...attach) // eslint-disable-line no-unused-vars\n{\n\treturn hec.aelx(base, ...attach);\n}\n\n/**\n * same as aelx, but instead of using objects as parameters\n * get an array of objects to attach\n * @param {Object} base object to where we attach the elements\n * @param {Array} attach array of objects to attach\n * @return {Object} \"none\", technically there is no return needed, global attach\n */\n// @ts-ignore\nfunction aelxar(base, attach) // eslint-disable-line no-unused-vars\n{\n\treturn hec.aelxar(base, attach);\n}\n\n/**\n * resets the sub elements of the base element given\n * @param {Object} base cel created element\n * @return {Object} returns reset base element\n */\n// @ts-ignore\nfunction rel(base) // eslint-disable-line no-unused-vars\n{\n\treturn hec.rel(base);\n}\n\n/**\n * searches and removes style from css array\n * @param {Object} _element element to work one\n * @param {String} css style sheet to remove (name)\n * @return {Object} returns full element\n */\n// @ts-ignore\nfunction rcssel(_element, css) // eslint-disable-line no-unused-vars\n{\n\treturn hec.rcssel(_element, css);\n}\n\n/**\n * adds a new style sheet to the element given\n * @param {Object} _element element to work on\n * @param {String} css style sheet to add (name)\n * @return {Object} returns full element\n */\n// @ts-ignore\nfunction acssel(_element, css) // eslint-disable-line no-unused-vars\n{\n\treturn hec.acssel(_element, css);\n}\n\n/**\n * removes one css and adds another\n * is a wrapper around rcssel/acssel\n * @param {Object} _element element to work on\n * @param {String} rcss style to remove (name)\n * @param {String} acss style to add (name)\n * @return {Object} returns full element\n */\n// @ts-ignore\nfunction scssel(_element, rcss, acss) // eslint-disable-line no-unused-vars\n{\n\thec.scssel(_element, rcss, acss);\n}\n\n/**\n * parses the object tree created with cel/ael and converts it into an HTML string\n * that can be inserted into the page\n * @param {Object} tree object tree with dom element declarations\n * @return {String} HTML string that can be used as innerHTML\n */\n// @ts-ignore\nfunction phfo(tree) // eslint-disable-line no-unused-vars\n{\n\treturn hec.phfo(tree);\n}\n\n/**\n * Create HTML elements from array list\n * as a flat element without master object file\n * Is like tree.sub call\n * @param {Array} list Array of cel created objects\n * @return {String} HTML String\n */\n// @ts-ignore\nfunction phfa(list) // eslint-disable-line no-unused-vars\n{\n\treturn hec.phfa(list);\n}\n// *** DOM MANAGEMENT FUNCTIONS\n\n// MARK: HTML Helpers\n// BLOCK: html wrappers for quickly creating html data blocks\n\n/**\n * NOTE: OLD FORMAT which misses multiple block set\n * creates an select/options drop down block.\n * the array needs to be key -> value format.\n * key is for the option id and value is for the data output\n * @param {String} name name/id\n * @param {Object} data array for the options\n * @param {String} [selected=''] selected item uid\n * @param {Boolean} [options_only=false] if this is true, it will not print the select part\n * @param {Boolean} [return_string=false] return as string and not as element\n * @param {String} [sort=''] if empty as is, else allowed 'keys',\n * 'values' all others are ignored\n * @return {String} html with build options block\n */\n// @ts-ignore\nfunction html_options(name, data, selected = '', options_only = false, return_string = false, sort = '') // eslint-disable-line no-unused-vars\n{\n\treturn _html_options(name, data, selected, options_only, return_string, sort);\n}\n\n/**\n * NOTE: USE THIS CALL, the above one is deprecated\n * creates an select/options drop down block.\n * the array needs to be key -> value format.\n * key is for the option id and value is for the data output\n * @param {String} name name/id\n * @param {Object} data array for the options\n * @param {String} [selected=''] selected item uid\n * @param {Number} [multiple=0] if this is 1 or larger, the drop down\n * will be turned into multiple select\n * the number sets the size value unless it is 1,\n * then it is default\n * @param {Boolean} [options_only=false] if this is true, it will not print the select part\n * @param {Boolean} [return_string=false] return as string and not as element\n * @param {String} [sort=''] if empty as is, else allowed 'keys',\n * 'values' all others are ignored\n * @param {String} [onchange=''] onchange trigger call, default unset\n * @return {String} html with build options block\n */\n// @ts-ignore\nfunction html_options_block( // eslint-disable-line no-unused-vars\n\tname, data, selected = '', multiple = 0, options_only = false, return_string = false, sort = '', onchange = ''\n) {\n\treturn _html_options_block(\n\t\tname, data, selected, multiple, options_only, return_string, sort, onchange\n\t);\n}\n\n/**\n * refills a select box with options and keeps the selected\n * @param {String} name name/id\n * @param {Object} data array of options\n * @param {String} [sort=''] if empty as is, else allowed 'keys', 'values'\n * all others are ignored\n */\n// @ts-ignore\nfunction html_options_refill(name, data, sort = '') // eslint-disable-line no-unused-vars\n{\n\t_html_options_refill(name, data, sort);\n}\n\n// MARK: URL\n\n/**\n * parses a query string from window.location.search.substring(1)\n * ALTERNATIVE CODE\n * var url = new URL(window.location.href);\n * param_uid = url.searchParams.get('uid');\n * @param {String} [query=''] the query string to parse\n * if not set will auto fill\n * @param {String} [return_key=''] if set only returns this key entry\n * or empty for none\n * @return {Object|String} parameter entry list\n */\n// @ts-ignore\nfunction parseQueryString(query = '', return_key = '') // eslint-disable-line no-unused-vars\n{\n\treturn _parseQueryString(query, return_key);\n}\n\n/**\n * searches query parameters for entry and returns data either as string or array\n * if no search is given the whole parameters are returned as an object\n * if a parameter is set several times it will be returned as an array\n * if search parameter set and nothing found and empty string is returned\n * if no parametes exist and no serach is set and empty object is returned\n * @param {String} [search=''] if set searches for this entry, if empty\n * all parameters are returned\n * @param {String} [query=''] different query string to parse, if not\n * set (default) the current window href is used\n * @param {Boolean} [single=false] if set to true then only the first found\n * will be returned\n * @return {Object|Array|String} if search is empty, object, if search is set\n * and only one entry, then string, else array\n * unless single is true\n */\n// @ts-ignore\nfunction getQueryStringParam(search = '', query = '', single = false) // eslint-disable-line no-unused-vars\n{\n\treturn _getQueryStringParam(search, query, single);\n}\n\n// MARK: ACL LOGIN\n// *** MASTER logout call\n/**\n * submits basic data for form logout\n */\n// @ts-ignore\nfunction loginLogout() // eslint-disable-line no-unused-vars\n{\n\t_loginLogout();\n}\n\n/**\n * create login string and logout button elements\n * @param {String} login_string the login string to show on the left\n * @param {String} [header_id='mainHeader'] the target for the main element block\n * if not set mainHeader is assumed\n * this is the target div for the \"loginRow\"\n */\n// @ts-ignore\nfunction createLoginRow(login_string, header_id = 'mainHeader') // eslint-disable-line no-unused-vars\n{\n\tlnm.createLoginRow(login_string, header_id);\n}\n\n/**\n * create the top nav menu that switches physical between pages\n * (edit access data based)\n * @param {Object} nav_menu the built nav menu with highlight info\n * @param {String} [header_id='mainHeader'] the target for the main element block\n * if not set mainHeader is assumed\n * this is the target div for the \"menuRow\"\n */\n// @ts-ignore\nfunction createNavMenu(nav_menu, header_id = 'mainHeader') // eslint-disable-line no-unused-vars\n{\n\tlnm.createNavMenu(nav_menu, header_id);\n}\n\n// MARK: ACTION BOX\n\n/**\n * Show an action box\n * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new\n * @param {string} [content=''] content to add to the box\n * @param {array} [action_box_css=[]] additional css elements for the action box\n * @param {number} [override=0] override size adjust\n * @param {number} [content_override=0] override content size adjust\n */\n// @ts-ignore\nfunction showFillActionBox(target_id = 'actionBox', content = '', action_box_css = [], override = 0, content_override = 0) // eslint-disable-line no-unused-vars\n{\n\tab.showFillActionBox(target_id, content, action_box_css, override, content_override);\n}\n\n/**\n * Fill action box with content, create it if it does not existgs\n * @param {string} [target_id='actionBox'] where to attach content to, if not exists, create new\n * @param {string} [content=''] content to add to the box\n * @param {array} [action_box_css=[]] additional css elements for the action box\n */\n// @ts-ignore\nfunction fillActionBox(target_id = 'actionBox', content = '', action_box_css = []) // eslint-disable-line no-unused-vars\n{\n\t// show action box, calc height + center\n\tab.fillActionBox(target_id, content, action_box_css);\n}\n\n/**\n * Adjust the size of the action box\n * @param {string} [target_id='actionBox'] which actionBox to work on\n * @param {number} [override=0] override size adjust\n * @param {number} [content_override=0] override content size adjust\n */\n// @ts-ignore\nfunction adjustActionBox(target_id = 'actionBox', override = 0, content_override = 0) // eslint-disable-line no-unused-vars\n{\n\tab.adjustActionBox(target_id, override, content_override);\n}\n\n/**\n * hide any open action boxes and hide overlay\n */\n// @ts-ignore\nfunction hideAllActionBoxes() // eslint-disable-line no-unused-vars\n{\n\tab.hideAllActionBoxes();\n}\n\n/**\n * hide action box, but do not clear content\n * DEPRECATED\n * @param {string} [target_id='actionBox']\n */\n// @ts-ignore\nfunction hideActionBox(target_id = 'actionBox') // eslint-disable-line no-unused-vars\n{\n\tab.hideActionBox(target_id);\n}\n\n/**\n * Just show and adjust the box\n * DEPRECAED\n * @param {string} [target_id='actionBox'] which actionBox to work on\n * @param {number} [override=0] override size adjust\n * @param {number} [content_override=0] override content size adjust\n * @param {Boolean} [hide_all=false] if set to true, hide all other action boxes\n */\n// @ts-ignore\nfunction showActionBox(target_id = 'actionBox', override = 0, content_override = 0, hide_all = true) // eslint-disable-line no-unused-vars\n{\n\tab.showActionBox(target_id, override, content_override, hide_all);\n}\n\n/**\n * close an action box with default clear content\n * for just hide use hideActionBox\n * DEPRECATED\n * @param {String} [target_id='actionBox'] which action box to close, default is set\n * @param {Boolean} [clean=true] if set to false will not remove html content, just hide\n */\n// @ts-ignore\nfunction closeActionBox(target_id = 'actionBox', clean = true) // eslint-disable-line no-unused-vars\n{\n\t// set the target/content ids\n\tab.closeActionBox(target_id, clean);\n}\n\n/**\n * TODO: better stacked action box: OPEN\n * @param {string} [target_id='actionBox'] which actionBox to work on\n * @param {number} [override=0] override size adjust\n * @param {number} [content_override=0] override content size adjust\n * @param {Boolean} [hide_all=false] if set to true, hide all other action boxes\n */\n// @ts-ignore\nfunction showActionBoxFloat(target_id = 'actionBox', override = 0, content_override = 0, hide_all = false) // eslint-disable-line no-unused-vars\n{\n\tab.showActionBoxFloat(target_id, override, content_override, hide_all);\n}\n\n/**\n * TODO: better stacked action box: CLOSE\n * @param {String} [target_id='actionBox'] which action box to close, default is set\n * @param {Boolean} [clean=true] if set to false will not remove html content, just hide\n */\n// @ts-ignore\nfunction closeActionBoxFloat(target_id = 'actionBox', clean = true) // eslint-disable-line no-unused-vars\n{\n\tab.closeActionBoxFloat(target_id, clean);\n}\n\n/**\n * create a new action box and fill it with basic elements\n * @param {String} [target_id='actionBox']\n * @param {String} [title='']\n * @param {Object} [contents={}]\n * @param {Object} [headers={}]\n * @param {Boolean} [show_close=true]\n * @param {Object} [settings={}] Optional settings, eg style sheets\n */\n// @ts-ignore\nfunction createActionBox( // eslint-disable-line no-unused-vars\n\ttarget_id = 'actionBox',\n\ttitle = '',\n\tcontents = {},\n\theaders = {},\n\tsettings = {},\n\tshow_close = true\n) {\n\tab.createActionBox(target_id, title, contents, headers, settings, show_close);\n}\n\n/**\n * adjusts the action box height based on content and window height of browser\n * TODO: border on outside/and other margin things need to be added in overall adjustment\n * @param {String} [target_id='actionBox'] target id, if not set, fall back to default\n * @param {Number} [override=0] override value to add to the actionBox height\n * @param {Number} [content_override=0] override the value from _content block if it exists\n */\n// @ts-ignore\nfunction adjustActionBoxHeight(target_id = 'actionBox', override = 0, content_override = 0) // eslint-disable-line no-unused-vars\n{\n\tab.adjustActionBoxHeight(target_id, override, content_override);\n}\n\n/* END */\n"], + "mappings": "AAkBA,SAAS,WAAW,IACpB,CAEK,IAAI,MAEH,IAAI,WACP,QAAQ,MAAM,gBAAiB,IAAI,KAAM,IAAI,WAAY,GAAG,EAClD,IAAI,KAEd,QAAQ,MAAM,gBAAiB,IAAI,KAAM,IAAI,KAAM,GAAG,EAEtD,QAAQ,MAAM,aAAc,IAAI,KAAM,GAAG,EAEhC,IAAI,QAEd,QAAQ,MAAM,kBAAmB,IAAI,KAAM,IAAI,OAAQ,IAAI,OAAO,EAClE,QAAQ,MAAM,wBAAyB,IAAI,WAAW,GAGtD,QAAQ,MAAM,eAAgB,IAAI,KAAM,IAAI,OAAO,CAErD,CAOA,SAAS,WAAW,KACpB,CACC,OAAI,OAAO,OAAO,IAAI,EAAM,KAC3B,OAAO,OAAO,IAAI,GAAM,UAK1B,CAWA,SAAS,sBAAsB,aAAc,QAC7C,CACC,IAAI,KAAO,MAAM,UAAU,MAAM,KAAK,UAAW,CAAC,EAC9C,WAAa,aAAa,MAAM,GAAG,EACnC,KAAO,WAAW,IAAI,EAC1B,GAAI,MAAQ,KACX,MAAM,IAAI,MAAM,wCAA0C,YAAY,EAEvE,QAAS,EAAI,EAAG,EAAI,WAAW,OAAQ,IACtC,QAAU,QAAQ,WAAW,CAAC,CAAC,EAEhC,OAAO,QAAQ,IAAI,EAAE,MAAM,QAAS,IAAI,CACzC,CAOA,SAAS,SAAS,IAClB,CACC,OAAI,MAAQ,KACJ,GAEC,OAAO,KAAQ,YAAgB,OAAO,KAAQ,QACxD,CAOA,SAAS,eAAe,OACxB,CACC,OAAK,SAAS,MAAM,EAGb,OAAO,KAAK,MAAM,EAAE,OAFnB,EAGT,CASA,SAAS,YAAY,IAAK,OAC1B,CACC,OAAO,gBAAgB,OAAQ,GAAG,CACnC,CAQA,SAAS,gBAAgB,OAAQ,IACjC,CACC,MAAO,SAAO,UAAU,eAAe,KAAK,OAAQ,GAAG,CACxD,CAQA,SAAS,cAAc,OAAQ,MAC/B,CACC,OAAO,OAAO,KAAK,MAAM,EAAE,KAAK,KAAO,OAAO,GAAG,IAAM,KAAK,GAAK,EAClE,CASA,SAAS,cAAc,OAAQ,MAC/B,CACC,OAAO,kBAAkB,OAAQ,KAAK,CACvC,CAQA,SAAS,kBAAkB,OAAQ,MACnC,CACC,MAAO,SAAO,KAAK,MAAM,EAAE,KAAK,KAAO,OAAO,GAAG,IAAM,KAAK,CAC7D,CASA,SAAS,iBAAiB,SAC1B,CACC,IAAI,UAAW,MAAO,IACtB,GAAI,OAAO,UAAa,UAAY,WAAa,KAEhD,OAAO,SAGR,UAAY,MAAM,QAAQ,QAAQ,EAAI,CAAC,EAAI,CAAC,EAE5C,IAAK,OAAO,SACX,MAAQ,SAAS,GAAG,EAEpB,UAAU,GAAG,EAAI,iBAAiB,KAAK,EAGxC,OAAO,SACR,CC5KA,SAAS,OAAO,MAChB,CACC,IAAI,GAAK,SAAS,eAAe,KAAK,EACtC,GAAI,KAAO,KACV,MAAM,IAAI,MAAM,gBAAkB,KAAK,EAExC,OAAO,EACR,CAQA,SAAS,IAAI,OAAQ,QAAS,SAC9B,CACC,IAAI,UAAY,OAAO,KAAK,OAAQ,QAAS,QAAQ,EAIrD,WAAU,MAAM,CACjB,CAMA,SAAS,SAAS,MAClB,CACC,IAAI,GAAK,KAAK,OAAO,KAAK,EAC1B,GAAI,cAAc,aAAe,GAAG,aAAa,MAAM,IAAM,WAC5D,MAAM,IAAI,MAAM,8BAAgC,KAAK,EAEtD,IAAI,SAAW,SAAS,GAAG,aAAa,MAAM,GAAK,GAAG,EAClD,SAAW,GAAG,aAAa,OAAO,EAClC,QAAU,CAAC,EACX,UAAY,OACf,QAAU,SAAS,MAAM;AAAA,CAAI,GAI9B,QAFI,WAAa,EAEP,EAAI,EAAG,EAAI,QAAQ,OAAQ,IAC/B,QAAQ,CAAC,EAAE,OAAO,EAAK,WAC3B,YAAc,KAAK,MAAO,QAAQ,CAAC,EAAE,OAAO,GAAK,QAAS,GAG5D,GAAG,aAAa,OAAQ,WAAa,QAAQ,QAAQ,SAAS,CAAC,CAChE,CAOA,SAAS,OAAO,GAChB,CACC,OAAO,EAAE,IAAM,EAAE,EAAE,OAAS,CAC7B,CC3DA,IAAM,mBAAN,KAAyB,CAUxB,IAAI,IAAK,GAAK,GAAI,QAAU,GAAI,IAAM,CAAC,EAAG,QAAU,CAAC,EACrD,CACC,MAAO,CACN,IACA,GAEA,KAAM,QAAQ,KACd,QACA,IACA,QACA,IAAK,CAAC,CACP,CACD,CASA,IAAI,KAAM,OAAQ,GAAK,GACvB,CACC,GAAI,IAEH,GAAI,KAAK,IAAM,GACd,KAAK,IAAI,KAAK,iBAAiB,MAAM,CAAC,UAGlC,SAAS,KAAK,GAAG,GAAK,KAAK,IAAI,OAAS,EAC3C,QAAS,EAAI,EAAG,EAAI,KAAK,IAAI,OAAQ,IAEpC,KAAK,IAAI,KAAK,IAAI,CAAC,EAAG,OAAQ,EAAE,OAKnC,KAAK,IAAI,KAAK,iBAAiB,MAAM,CAAC,EAEvC,OAAO,IACR,CASA,KAAK,QAAS,OACd,CACC,QAAS,EAAI,EAAG,EAAI,OAAO,OAAQ,IAClC,KAAK,IAAI,KAAK,iBAAiB,OAAO,CAAC,CAAC,CAAC,EAE1C,OAAO,IACR,CASA,OAAO,KAAM,OACb,CACC,QAAS,EAAI,EAAG,EAAI,OAAO,OAAQ,IAClC,KAAK,IAAI,KAAK,iBAAiB,OAAO,CAAC,CAAC,CAAC,EAE1C,OAAO,IACR,CAOA,IAAI,KACJ,CACC,YAAK,IAAM,CAAC,EACL,IACR,CAQA,OAAO,SAAU,IACjB,CACC,IAAI,UAAY,SAAS,IAAI,QAAQ,GAAG,EACxC,OAAI,UAAY,IACf,SAAS,IAAI,OAAO,UAAW,CAAC,EAE1B,QACR,CAQA,OAAO,SAAU,IACjB,CACC,IAAI,UAAY,SAAS,IAAI,QAAQ,GAAG,EACxC,OAAI,WAAa,IAChB,SAAS,IAAI,KAAK,GAAG,EAEf,QACR,CAUA,OAAO,SAAU,KAAM,KACvB,CACC,YAAK,OAAO,SAAU,IAAI,EAC1B,KAAK,OAAO,SAAU,IAAI,EACnB,QACR,CAQA,KAAK,KACL,CACC,IAAI,cAAgB,CACnB,SACA,WACA,OACA,SACA,QACA,MACA,OACA,SACA,SACA,QACA,SACA,UACD,EACI,aAAe,CAClB,KACA,OACA,OACD,EACI,SAAW,CACd,QACA,KACA,MACA,KACA,OACA,MACA,SACA,MACA,QACA,SACA,QACA,UAEA,OACA,OACA,OACA,OACD,EAEA,IAAI,QAAU,CAAC,EAEX,KAAO,IAAM,KAAK,IAClB,EAUJ,GARI,KAAK,KACR,MAAQ,QAAU,KAAK,GAAK,IAExB,cAAc,SAAS,KAAK,GAAG,IAClC,MAAQ,WAAa,KAAK,KAAO,KAAK,KAAO,KAAK,IAAM,MAItD,SAAS,KAAK,GAAG,GAAK,KAAK,IAAI,OAAS,EAAG,CAE9C,IADA,MAAQ,WACH,EAAI,EAAG,EAAI,KAAK,IAAI,OAAQ,IAChC,MAAQ,KAAK,IAAI,CAAC,EAAI,IAGvB,KAAO,KAAK,MAAM,EAAG,EAAE,EACvB,MAAQ,GACT,CAEA,GAAI,SAAS,KAAK,OAAO,EAExB,OAAW,CAAC,IAAK,IAAI,IAAK,OAAO,QAAQ,KAAK,OAAO,EAC/C,aAAa,SAAS,GAAG,IAC7B,MAAQ,IAAM,IAAM,KAAO,KAAO,KAWrC,GANA,MAAQ,IAER,QAAQ,KAAK,IAAI,EAIb,SAAS,KAAK,GAAG,GAAK,KAAK,IAAI,OAAS,EAI3C,IAHI,KAAK,SACR,QAAQ,KAAK,KAAK,OAAO,EAErB,EAAI,EAAG,EAAI,KAAK,IAAI,OAAQ,IAChC,QAAQ,KAAK,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,OAE1B,KAAK,SACf,QAAQ,KAAK,KAAK,OAAO,EAG1B,OACE,SAAS,SAAS,KAAK,GAAG,GAE3B,QAAQ,KAAK,KAAO,KAAK,IAAM,GAAG,EAG5B,QAAQ,KAAK,EAAE,CACvB,CASA,KAAK,KACL,CAEC,QADI,QAAU,CAAC,EACN,EAAI,EAAG,EAAI,KAAK,OAAQ,IAChC,QAAQ,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,EAEhC,OAAO,QAAQ,KAAK,EAAE,CACvB,CACD,ECtQA,IAAI,IAAM,IAAI,mBAOd,SAAS,WAAW,OACpB,CACC,OAAO,OAAO,QAAQ,YAAa,SAAU,EAAG,CAC/C,IAAI,UAAY,CACf,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAM,QACN,IAAK,QACN,EAEA,OAAO,UAAU,CAAC,CACnB,CAAC,CACF,CAOA,SAAS,aAAa,OACtB,CACC,OAAO,OAAO,QAAQ,YAAa,SAAU,EAAG,CAC/C,IAAI,UAAY,CACf,QAAS,IACT,OAAQ,IACR,OAAQ,IACR,SAAU,IACV,QAAS,IACT,SAAU,GACX,EAEA,OAAO,UAAU,CAAC,CACnB,CAAC,CACF,CAmBA,SAAS,aAAa,KAAM,KAAM,SAAW,GAAI,aAAe,GAAO,cAAgB,GAAO,KAAO,GACrG,CAEC,OAAO,KAAK,mBACX,KAAM,KAAM,SAAU,EAAG,aAAc,cAAe,IACvD,CACD,CAqBA,SAAS,mBACR,KAAM,KAAM,SAAW,GAAI,SAAW,EAAG,aAAe,GAAO,cAAgB,GAAO,KAAO,GAAI,SAAW,GAC3G,CACD,IAAI,QAAU,CAAC,EACX,eACA,eAAiB,CAAC,EAClB,eACA,UAAY,CAAC,EACb,MACA,QAAU,CAAC,EAEX,SAAW,IACd,eAAe,SAAW,GACtB,SAAW,IACd,eAAe,KAAO,WAGpB,WACH,eAAe,SAAW,UAG3B,eAAiB,IAAI,IAAI,SAAU,KAAM,GAAI,CAAC,EAAG,cAAc,EAE3D,MAAQ,OACX,UAAY,OAAO,KAAK,IAAI,EAAE,KAAK,EACzB,MAAQ,SAClB,UAAY,OAAO,KAAK,IAAI,EAAE,KAAK,CAAC,EAAG,KAAO,GAAK,KAAK,CAAC,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,EAElF,UAAY,OAAO,KAAK,IAAI,EAK7B,QAAW,OAAO,UACjB,MAAQ,KAAK,GAAG,EAGhB,QAAU,CACT,MAAS,MACT,MAAS,IACT,SAAY,EACb,EAEI,UAAY,GAAK,CAAC,MAAM,QAAQ,QAAQ,GAAK,UAAY,MAC5D,QAAQ,SAAW,IAGhB,UAAY,GAAK,MAAM,QAAQ,QAAQ,GAAK,SAAS,QAAQ,GAAG,GAAK,KACxE,QAAQ,SAAW,IAGpB,eAAiB,IAAI,IAAI,SAAU,GAAI,MAAO,CAAC,EAAG,OAAO,EAEzD,IAAI,IAAI,eAAgB,cAAc,EAGvC,GAAK,aASJ,GAAI,cAAe,CAClB,QAAS,EAAI,EAAG,EAAI,eAAe,IAAI,OAAQ,IAC9C,QAAQ,KAAK,IAAI,KAAK,eAAe,IAAI,CAAC,CAAC,CAAC,EAE7C,OAAO,QAAQ,KAAK,EAAE,CACvB,KACC,QAAO,eAAe,QAdvB,QAAI,eACH,QAAQ,KAAK,IAAI,KAAK,cAAc,CAAC,EAC9B,QAAQ,KAAK,EAAE,GAEf,cAaV,CASA,SAAS,oBAAoB,KAAM,KAAM,KAAO,GAChD,CACC,IAAI,eACA,gBACA,UAAY,CAAC,EACb,MAEJ,GAAI,SAAS,eAAe,IAAI,EAAG,CAE9B,MAAQ,OACX,UAAY,OAAO,KAAK,IAAI,EAAE,KAAK,EACzB,MAAQ,SAClB,UAAY,OAAO,KAAK,IAAI,EAAE,KAAK,CAAC,EAAG,KAAO,GAAK,KAAK,CAAC,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,EAElF,UAAY,OAAO,KAAK,IAAI,EAG7B,CAAC,EAAE,QAAQ,KAAK,SAAS,iBAAiB,IAAM,KAAO,WAAW,EAAG,SAAS,IAAK,CAClF,gBAAkB,IAAI,KACvB,CAAC,EACD,OAAO,IAAI,EAAE,UAAY,GACzB,QAAW,OAAO,UACjB,MAAQ,KAAK,GAAG,EAEhB,eAAiB,SAAS,cAAc,QAAQ,EAChD,eAAe,MAAQ,MACvB,eAAe,MAAQ,IACvB,eAAe,UAAY,MACvB,KAAO,kBACV,eAAe,SAAW,IAE3B,OAAO,IAAI,EAAE,YAAY,cAAc,CAEzC,CACD,CCxMA,SAAS,QAAQ,IACjB,CACC,OAAQ,KAAO,IAAI,SAAS,EAAE,GAAG,UAAU,EAAE,CAC9C,CAUA,SAAS,sBAAsB,IAAK,IACpC,CACC,WAAM,KAAK,KAAK,GAAG,EACnB,IAAM,KAAK,MAAM,GAAG,EAEb,KAAK,MAAM,KAAK,OAAO,GAAK,IAAM,IAAM,GAAK,GAAG,CACxD,CAQA,SAAS,eAAe,OAAQ,UAChC,CACC,OAAI,MAAM,MAAM,GAAK,MAAM,SAAS,EAC5B,OAED,KAAK,MAAM,OAAS,KAAK,IAAI,GAAI,SAAS,CAAC,EAAI,KAAK,IAAI,GAAI,SAAS,CAC7E,CC/BA,SAAS,aAAa,UAAW,KACjC,CACC,OAAO,OAAO,QAAQ,WAAY,SAAS,MAAO,OAClD,CACC,OAAO,OAAO,KAAK,MAAM,EAAK,IAC7B,KAAK,MAAM,EACX,KAEF,CAAC,CACF,CAMA,SAAS,iBAAiB,OAC1B,CACC,IAAI,MAAQ,OAAO,SAAS,EAAE,MAAM,GAAG,EACvC,aAAM,CAAC,EAAI,MAAM,CAAC,EAAE,QAAQ,wBAAyB,GAAG,EACjD,MAAM,KAAK,GAAG,CACtB,CAOA,SAAS,cAAc,OACvB,CACC,OAAO,OAAO,QAAQ,kBAAmB,MAAM,CAChD,CClCA,SAAS,cACT,CACC,IAAI,KAAO,IAAI,KACf,OAAO,KAAK,QAAQ,CACrB,CCDA,SAAS,WAAW,IACpB,CACC,IAAI,IAAM,IAAI,YAAY,KAAO,IAAM,CAAC,EACxC,OACC,OAAO,QAEP,OAAO,UACN,gBAAgB,GAAG,EACd,MAAM,KAAK,IAAK,KAAK,OAAO,EAAE,KAAK,EAAE,CAC7C,CASA,SAAS,WACT,CACC,OAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,CAAC,CAC9C,CCtBA,SAAS,eACT,CACC,IAAI,MAAO,OACX,aAAQ,OAAO,YAAe,OAAO,SAAS,gBAAgB,aAAe,OAAO,SAAS,KAAK,YAClG,OAAS,OAAO,aAAgB,OAAO,SAAS,gBAAgB,cAAgB,OAAO,SAAS,KAAK,aAC9F,CACN,MACA,MACD,CACD,CAMA,SAAS,iBACT,CACC,IAAI,KAAM,IACV,YAAO,OAAO,aAAgB,OAAO,SAAS,gBAAgB,YAAc,OAAO,SAAS,KAAK,WACjG,IAAM,OAAO,aAAgB,OAAO,SAAS,gBAAgB,WAAa,OAAO,SAAS,KAAK,UACxF,CACN,KACA,GACD,CACD,CAMA,SAAS,uBACT,CACC,IAAI,KAAM,IACV,YAAO,OAAO,OAAO,aAAgB,OAAO,SAAS,gBAAgB,YAAc,OAAO,SAAS,KAAK,WACxG,IAAM,OAAO,OAAO,aAAgB,OAAO,SAAS,gBAAgB,WAAa,OAAO,SAAS,KAAK,UAC/F,CACN,KACA,GACD,CACD,CAQA,SAAS,UAAU,GAAI,KAAM,IAC7B,CAEC,IAAI,WAAa,CAChB,OAAQ,EAAE,IAAM,EAAE,EAAE,OAAO,GAAK,EAChC,MAAO,EAAE,IAAM,EAAE,EAAE,MAAM,GAAK,CAC/B,EACI,KAAO,EAAE,IAAM,EAAE,EAAE,IAAI,UAAU,EACjC,SAAW,KAAK,cAAc,EAC9B,OAAS,KAAK,gBAAgB,EAUlC,GALI,MACH,EAAE,IAAM,EAAE,EAAE,IAAI,CACf,KAAO,SAAS,MAAQ,EAAM,WAAW,MAAQ,EAAK,OAAO,KAAO,IACrE,CAAC,EAEE,IAAK,CAER,IAAI,QAAU,MAAQ,QACpB,SAAS,OAAS,EAAM,WAAW,OAAS,EAC5C,SAAS,OAAS,EAAM,WAAW,OAAS,EAAK,OAAO,IAC1D,EAAE,IAAM,EAAE,EAAE,IAAI,CACf,IAAK,QAAU,IAChB,CAAC,CACF,CACD,CASA,SAAS,QAAQ,QAAS,OAAS,EAAG,SAAW,IAAK,KAAO,YAC7D,CACC,GAAI,CACH,IAAI,eAAiB,EAAE,IAAM,OAAO,EAAE,OAAO,EAC7C,GAAI,gBAAkB,KACrB,OAEG,EAAE,IAAM,OAAO,EAAE,QACpB,EAAE,IAAI,EAAE,QAAQ,CACf,UAAW,eAAe,IAAM,MACjC,EAAG,QAAQ,CAEb,OAAS,IAAK,CACb,WAAW,GAAG,CACf,CACD,CAOA,SAAS,KAAK,OACd,CACC,OAAO,MAAM,EAAE,eAAe,CAC7B,SAAU,QACX,CAAC,CACF,CC/GA,SAAS,YAAY,MACrB,CACC,IAAI,EAAI,GAKR,GAHI,OAAO,OAAU,WACpB,MAAQ,OAAO,KAAK,GAEjB,MAAM,KAAK,EACd,OAAO,MAAM,SAAS,EAEvB,GACC,MAAQ,MAAQ,KAChB,UACQ,MAAQ,IACjB,OACC,KAAK,MAAM,MAAQ,KAAK,IAAI,GAAI,CAAC,CAAC,EAAI,KAAK,IAAI,GAAI,CAAC,EACjD,CAAC,KAAM,KAAM,KAAM,KAAM,KAAM,IAAI,EAAE,CAAC,CAC3C,CAOA,SAAS,gBAAgB,MACzB,CAKC,GAHI,OAAO,OAAU,WACpB,MAAQ,OAAO,KAAK,GAEjB,MAAM,KAAK,EACd,OAAO,MAAM,SAAS,EAEvB,IAAI,SAAW,GACX,MAAQ,IACX,SAAW,GACX,OAAS,IAEV,IAAI,EAAI,KAAK,MAAM,KAAK,IAAI,KAAK,EAAI,KAAK,IAAI,IAAI,CAAC,EAC/C,MAAQ,CAAC,IAAK,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,IAAI,EAChE,OAAQ,SAAW,IAAM,MAEvB,MACA,KAAK,IAAI,KAAM,CAAC,GACf,QAAQ,CAAC,EAET,IAAM,MAAM,CAAC,GACd,SAAS,CACZ,CAQA,SAAS,iBAAiB,MAAO,IAAI,GACrC,CAEC,GAAI,EAAE,OAAO,OAAU,UAAY,iBAAiB,QACnD,OAAO,MAAM,SAAS,EAGvB,IAAI,YAAc,YAEd,MAAQ,kDACR,QAAU,MAAM,MAAM,KAAK,EAE/B,GAAI,UAAY,KAAM,CAGrB,IAAI,GAAK,WAAW,QAAQ,CAAC,EAAE,QAAQ,UAAU,EAAE,CAAC,EAEhD,GAAK,QAAQ,CAAC,EAAE,QAAQ,gBAAiB,EAAE,EAAE,OAAO,CAAC,EAAE,YAAY,EACnE,KAEH,MAAQ,GAAK,KAAK,IAAI,KAAM,YAAY,QAAQ,EAAE,CAAC,EAErD,CAEA,OAAI,IACI,MAED,KAAK,MAAM,KAAK,CACxB,CC3EA,SAAS,iBAAiB,MAAQ,GAAI,WAAa,GAAI,OAAS,GAChE,CACC,OAAO,oBAAoB,WAAY,MAAO,MAAM,CACrD,CAkBA,SAAS,oBAAoB,OAAS,GAAI,MAAQ,GAAI,OAAS,GAC/D,CACM,QACJ,MAAQ,OAAO,SAAS,MAEzB,IAAM,IAAM,IAAI,IAAI,KAAK,EACrB,MAAQ,KACZ,GAAI,OAAQ,CACX,IAAI,QAAU,IAAI,aAAa,OAAO,MAAM,EACxC,QAAQ,QAAU,GAAK,SAAW,GACrC,MAAQ,QAAQ,CAAC,EACP,QAAQ,OAAS,IAC3B,MAAQ,QAEV,KAAO,CAEN,MAAQ,CAAC,EAET,OAAW,CAAC,GAAG,IAAK,IAAI,aAAa,QAAQ,EAE5C,GAAI,OAAO,MAAM,GAAG,EAAM,IAAa,CAEtC,IAAI,QAAU,IAAI,aAAa,OAAO,GAAG,EAEzC,MAAM,GAAG,EAAI,QAAQ,OAAS,GAAK,SAAW,GAC7C,QAAQ,CAAC,EACT,OACF,CAEF,CACA,OAAO,KACR,CChEA,SAAS,aACT,CACC,IAAM,KAAO,SAAS,cAAc,MAAM,EAC1C,KAAK,OAAS,OACd,IAAM,YAAc,SAAS,cAAc,OAAO,EAClD,YAAY,KAAO,SACnB,YAAY,KAAO,eACnB,YAAY,MAAQ,SACpB,KAAK,YAAY,WAAW,EAC5B,SAAS,KAAK,YAAY,IAAI,EAC9B,KAAK,OAAO,CACb,CCYA,SAAS,gBAAgB,IAAK,QAAU,GACxC,CACK,EAAE,YAAY,EAAE,GAAG,UAAU,EAChC,KAAK,oBAAoB,IAAK,OAAO,EAErC,KAAK,oBAAoB,IAAK,OAAO,CAEvC,CAUA,SAAS,oBAAoB,IAAK,QAAU,GAC5C,CAEM,EAAE,YAAY,EAAE,GAAG,UAAU,IAC5B,EAAE,YAAY,EAAE,SAAS,UAAU,GACvC,EAAE,YAAY,EAAE,SAAS,UAAU,EAEpC,UAAU,YAAa,GAAM,EAAI,EACjC,EAAE,YAAY,EAAE,KAAK,GAElB,UAAY,IACf,KAAK,eAAe,CAEtB,CAUA,SAAS,oBAAoB,IAAK,QAAU,GAC5C,CAEC,EAAE,YAAY,EAAE,KAAK,EACjB,UAAY,IACf,eAAe,CAEjB,CAMA,SAAS,gBACT,CAEK,EAAE,aAAa,EAAE,GAAG,UAAU,EACjC,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,GAEpC,EAAE,aAAa,EAAE,KAAK,EACtB,EAAE,aAAa,EAAE,IAAI,SAAU,IAAI,EAErC,CAMA,SAAS,gBACT,CAEK,SAAS,EAAE,aAAa,EAAE,IAAI,QAAQ,CAAC,GAAK,IAC/C,EAAE,aAAa,EAAE,IAAI,SAAU,IAAI,EAEnC,EAAE,aAAa,EAAE,KAAK,CAExB,CAMA,SAAS,eACT,CACM,EAAE,aAAa,EAAE,GAAG,UAAU,GAClC,EAAE,aAAa,EAAE,KAAK,CAExB,CAMA,SAAS,gBACT,CACK,EAAE,aAAa,EAAE,GAAG,UAAU,GACjC,EAAE,aAAa,EAAE,KAAK,CAExB,CAMA,SAAS,WACT,CACC,EAAE,YAAY,EAAE,KAAK,EAAE,EACvB,EAAE,YAAY,EAAE,KAAK,EACrB,EAAE,aAAa,EAAE,KAAK,CACvB,CAuCA,IAAM,0BAAN,KAAgC,CAG/B,SAAW,IACX,YAAc,IAWd,oBAAoB,IACpB,CAGC,GAAI,EAAE,YAAY,EAAE,QAAU,EAAG,CAChC,IAAI,GAAK,SAAS,cAAc,KAAK,EACrC,GAAG,UAAY,gBACf,GAAG,GAAK,YACR,EAAE,MAAM,EAAE,OAAO,EAAE,CACpB,MAAY,EAAE,YAAY,EAAE,SAAS,UAAU,GAG9C,EAAE,YAAY,EAAE,SAAS,UAAU,EAAE,KAAK,EAGtC,EAAE,YAAY,EAAE,GAAG,UAAU,IAEjC,KAAK,mBAAmB,EAEnB,EAAE,aAAa,EAAE,GAAG,UAAU,GAClC,EAAE,aAAa,EAAE,KAAK,EAGvB,EAAE,aAAa,EAAE,IAAI,SAAU,GAAI,EAEnC,EAAE,YAAY,EAAE,KAAK,EAErB,UAAU,YAAa,GAAM,EAAI,EAEnC,CASA,oBAAoB,IACpB,CAGK,EAAE,YAAY,EAAE,GAAG,UAAU,IAEhC,EAAE,YAAY,EAAE,KAAK,EAGjB,KAAK,SAAW,KAAK,YACxB,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,QAAQ,GAG5C,EAAE,aAAa,EAAE,KAAK,EACtB,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,WAAW,GAGlD,CAMA,oBACA,CAEC,GAAI,EAAE,aAAa,EAAE,QAAU,EAAG,CACjC,IAAI,GAAK,SAAS,cAAc,KAAK,EACrC,GAAG,UAAY,yBACf,GAAG,GAAK,aACR,EAAE,MAAM,EAAE,OAAO,EAAE,CACpB,CACD,CAQA,qBAAqB,MACrB,CAGM,EAAE,aAAa,EAAE,GAAG,UAAU,IAClC,EAAE,aAAa,EAAE,KAAK,EACtB,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,WAAW,EAE/C,KAAK,SAAW,KAAK,aAGtB,KAAK,WAEL,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,QAAQ,EAExC,OACC,EAAE,IAAM,KAAK,EAAE,OAAS,IAC3B,EAAE,IAAM,KAAK,EAAE,IAAI,SAAU,KAAK,SAAW,CAAC,EAC9C,EAAE,IAAM,KAAK,EAAE,KAAK,EAIvB,CAUA,qBAAqB,MAAM,GAC3B,CAGC,KAAK,WAGD,KAAK,UAAY,KAAK,aACzB,KAAK,SAAW,KAAK,YACrB,EAAE,aAAa,EAAE,KAAK,EACtB,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,WAAW,GAG/C,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,QAAQ,EAEzC,QACH,EAAE,IAAM,KAAK,EAAE,KAAK,EACpB,EAAE,IAAM,KAAK,EAAE,IAAI,SAAU,CAAC,EAGhC,CAKA,oBACA,CACC,EAAE,YAAY,EAAE,KAAK,EAAE,EACvB,EAAE,YAAY,EAAE,KAAK,EACrB,KAAK,qBAAqB,CAC3B,CACD,ECzUA,IAAM,gBAAN,KAAsB,CAErB,MAAQ,CAAC,EAET,YAAYA,MAAM,CACjB,KAAK,MAAQA,KAEd,CAOA,GAAG,OACH,CACC,OAAI,OAAO,KAAK,MAAU,KAAe,SAAS,KAAK,KAAK,GAAK,KAAK,MAAM,MAAM,EAC1E,KAAK,MAAM,MAAM,EAEjB,MAET,CACD,ECpBA,IAAM,UAAN,KAAgB,CAGf,OAAS,CACR,KAAM,IACN,IAAK,IACL,UAAW,EACX,MAAO,CAAC,EACR,OAAQ,CAAC,EACT,IAAK,EACN,EAEA,mBAAqB,CAAC,EAEtB,yBAA2B,GAAK,GAAK,IAErC,IACA,KAOA,YAAYC,KAAKC,MACjB,CACC,KAAK,IAAMD,KACX,KAAK,KAAOC,KACb,CAUA,kBAAkB,UAAY,YAAa,QAAU,GAAI,eAAiB,CAAC,EAAG,SAAW,EAAG,iBAAmB,EAC/G,CAEC,KAAK,cAAc,UAAW,QAAS,cAAc,EAErD,KAAK,cAAc,UAAW,SAAU,gBAAgB,CACzD,CAQA,cAAc,UAAY,YAAa,QAAU,GAAI,eAAiB,CAAC,EACvE,CAEM,OAAO,SAAS,GAEpB,EAAE,gBAAgB,EAAE,MACnB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAW,GAAI,CAAC,mBAAoB,MAAM,EAAE,OAAO,cAAc,CAAC,CAAC,CACtG,EAGD,EAAE,IAAM,SAAS,EAAE,KAAK,OAAO,CAChC,CAQA,gBAAgB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAC1E,CAEC,KAAK,sBAAsB,UAAW,SAAU,gBAAgB,EAEhE,UAAU,UAAW,GAAM,EAAI,CAChC,CAKA,oBACA,CAEC,EAAE,oDAAoD,EAAE,KAAK,EAE7D,EAAE,aAAa,EAAE,KAAK,CACvB,CAOA,cAAc,UAAY,YAC1B,CACC,KAAK,oBAAoB,UAAW,EAAK,CAC1C,CAUA,cAAc,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAAG,SAAW,GACtF,CACC,KAAK,mBAAmB,UAAW,SAAU,iBAAkB,QAAQ,CACxE,CASA,eAAe,UAAY,YAAa,MAAQ,GAChD,CAEC,KAAK,oBAAoB,UAAW,KAAK,CAC1C,CASA,mBAAmB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAAG,SAAW,GAC3F,CACK,WAAa,IAEhB,KAAK,mBAAmB,EAGpB,OAAO,YAAY,IACvB,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,aAAc,GAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAC7F,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,OAAO,IAAI,GAGhD,EAAE,aAAa,EAAE,KAAK,EACjB,YAAY,UAAW,KAAK,OAAO,KAAK,EAIlC,KAAK,OAAO,MAAM,SAAS,EAAI,GAAK,KAAK,OAAO,MAK1D,KAAK,OAAO,MAAM,SAAS,EAAI,KAAK,OAAO,IAC3C,KAAK,OAAO,KAAO,KATnB,KAAK,OAAO,MAAM,SAAS,EAAI,KAAK,OAAO,IAE3C,KAAK,OAAO,KAAO,IAWf,KAAK,OAAO,WAChB,EAAE,aAAa,EAAE,IAAI,SAAU,KAAK,OAAO,MAAM,SAAS,EAAI,CAAC,EAEhE,EAAE,IAAM,SAAS,EAAE,IAAI,SAAU,KAAK,OAAO,MAAM,SAAS,CAAC,EAAE,KAAK,EAGhE,KAAK,OAAO,OAAO,QAAQ,SAAS,GAAK,IAE5C,KAAK,OAAO,OAAO,KAAK,SAAS,EAElC,KAAK,OAAO,IAAM,UAElB,KAAK,gBAAgB,UAAW,SAAU,gBAAgB,CAC3D,CAOA,oBAAoB,UAAY,YAAa,MAAQ,GACrD,CAEC,GAAI,CAAC,OAAO,SAAS,EACpB,OAIA,YAAY,UAAW,KAAK,kBAAkB,GAAK,QAAU,KAE7D,KAAK,mBAAmB,SAAS,EAAI,CAAC,GAEnC,QAAU,IACb,EAAE,IAAM,SAAS,EAAE,KAAK,EAAE,EAE3B,EAAE,IAAM,SAAS,EAAE,KAAK,EAGxB,IAAI,IAAM,KAAK,OAAO,OAAO,QAAQ,SAAS,EAC9C,KAAK,OAAO,OAAO,OAAO,IAAK,CAAC,EAIhC,IAAI,iBAAmB,EAAE,oEAAoE,EAAE,IAAI,CAAC,EAAG,MAAQ,CAC9G,GAAI,GAAG,GACP,OAAQ,EAAE,IAAM,GAAG,EAAE,EAAE,IAAI,QAAQ,CACpC,EAAE,EAAE,IAAI,EACR,GAAI,iBAAiB,OAAS,EAAG,CAChC,IAAI,WAAa,EACb,UAAY,GAChB,QAAS,aAAa,iBACjB,SAAS,UAAU,MAAM,EAAI,aAChC,WAAa,SAAS,UAAU,MAAM,EACtC,UAAY,UAAU,IAGxB,EAAE,aAAa,EAAE,IAAI,SAAU,WAAa,CAAC,EAC7C,KAAK,OAAO,IAAM,SACnB,MACC,EAAE,aAAa,EAAE,KAAK,CAExB,CAWA,gBACC,UAAY,YACZ,MAAQ,GACR,SAAW,CAAC,EACZ,QAAU,CAAC,EACX,SAAW,CAAC,EACZ,WAAa,GACZ,CACI,YAAY,UAAW,KAAK,kBAAkB,IAClD,KAAK,mBAAmB,SAAS,EAAI,CAAC,GAKvC,IAAI,WAAa,CAAC,EACd,YAAY,aAAc,QAAQ,IACrC,WAAa,SAAS,YAEvB,IAAI,eAAiB,CAAC,EAClB,YAAY,iBAAkB,QAAQ,IACzC,eAAiB,SAAS,gBAE3B,IAAI,SAAW,CAAC,EAEhB,SAAS,KAAK,KAAK,IAAI,KACtB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAY,SAAU,GAAI,CAAC,iBAAkB,UAAU,EAAE,OAAO,UAAU,CAAC,EAC5G,GAAG,aAAe,GAAO,CAExB,KAAK,IAAI,IAAI,MAAO,GAAI,MAAO,CAAC,OAAQ,MAAM,CAAC,EAE/C,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAY,sBAAuB,GAAI,CAAC,OAAQ,KAAK,CAAC,EACvF,KAAK,IAAI,IAAI,QAAS,UAAY,eAAgB,GAAI,CAAC,eAAgB,MAAM,EAC5E,CACC,KAAM,SACN,MAAO,KAAK,KAAK,GAAG,OAAO,EAC3B,QAAS,mBAAsB,UAAY,YAC5C,CACD,CACD,CACD,EAAI,CACH,KAAK,IAAI,IAAI,MAAO,GAAI,MAAO,CAAC,OAAQ,OAAO,CAAC,CACjD,CACD,CACD,CAAC,EAEG,eAAe,OAAO,EAAI,IAEzB,YAAY,aAAc,OAAO,EACpC,SAAS,KAAK,QAAQ,UAAU,EAEhC,SAAS,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC,GAIlC,eAAe,QAAQ,EAAI,EAE1B,YAAY,aAAc,QAAQ,EACrC,SAAS,KAAK,SAAS,UAAU,EAEjC,SAAS,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC,EAGtC,SAAS,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAY,WAAY,GAAI,CAAC,CAAC,CAAC,CAAC,EAGjF,SAAS,KAAK,KAAK,IAAI,KACtB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAY,UAAW,GAAI,CAAC,OAAQ,UAAU,CAAC,EAChF,GAAG,aAAe,GAAO,CAExB,KAAK,IAAI,IAAI,MAAO,GAAI,GAAI,CAAC,OAAQ,MAAM,CAAC,EAE5C,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAY,uBAAwB,GAAI,CAAC,MAAO,MAAM,CAAC,EACxF,KAAK,IAAI,IAAI,QAAS,UAAY,gBAAiB,GAAI,CAAC,eAAgB,MAAM,EAC7E,CACC,KAAM,SACN,MAAO,KAAK,KAAK,GAAG,OAAO,EAC3B,QAAS,mBAAsB,UAAY,YAC5C,CACD,CACD,CACD,EAAI,CACH,KAAK,IAAI,IAAI,MAAO,GAAI,GAAI,CAAC,OAAQ,OAAO,CAAC,CAC9C,CACD,CACD,CAAC,EACD,SAAS,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,QAAS,UAAY,cAAe,GAAI,CAAC,EAAG,CACpF,KAAM,SACN,MAAO,KAAK,IAAI,CACjB,CAAC,CAAC,CAAC,EACH,KAAK,cAAc,UAAW,SAAS,KAAK,EAAE,EAAG,cAAc,CAChE,CASA,sBAAsB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAChF,CACC,IAAI,WAAa,EACb,IAAM,CAAC,EACP,QAAU,CAAC,EACX,WAAa,GASjB,OAPI,MAAM,QAAQ,IACjB,SAAW,GAER,MAAM,gBAAgB,IACzB,iBAAmB,GAGZ,UAAW,CAClB,IAAK,YACJ,WAAa,aACb,MACD,IAAK,eACJ,WAAY,iBACZ,MACD,QACC,WAAa,UACb,KACF,CAGA,EAAE,KAAK,CAAC,UAAW,WAAa,UAAU,EAAG,SAAS,EAAG,EAAG,CAC3D,EAAE,IAAM,CAAC,EAAE,IAAI,CACd,OAAU,GACV,MAAS,EACV,CAAC,CACF,CAAC,EACG,OAAO,WAAa,QAAQ,IAC/B,IAAI,OAAS,EAAE,IAAM,WAAa,QAAQ,EAAE,YAAY,EACxD,QAAQ,IAAI,mCAAoC,UAAW,IAAI,MAAM,EACrE,YAAc,IAAI,QAAU,GAEzB,OAAO,WAAa,SAAS,IAChC,IAAI,OAAS,EAAE,IAAM,WAAa,SAAS,EAAE,YAAY,EACzD,QAAQ,IAAI,oCAAqC,UAAW,IAAI,MAAM,EACtE,YAAc,IAAI,QAAU,GAEzB,OAAO,WAAa,UAAU,IAC7B,iBAAmB,GACtB,QAAQ,IAAI,8CAA+C,UAAW,gBAAgB,EACtF,YAAc,mBAEd,QAAQ,OAAS,EAAE,IAAM,WAAa,UAAU,EAAE,YAAY,EAC9D,QAAQ,IAAI,qCAAsC,UAAW,QAAQ,MAAM,EAC3E,YAAc,QAAQ,QAAU,IAI9B,OAAO,WAAa,SAAS,IAChC,IAAI,OAAS,EAAE,IAAM,WAAa,SAAS,EAAE,YAAY,EACzD,QAAQ,IAAI,oCAAqC,UAAW,IAAI,MAAM,EACtE,YAAc,IAAI,QAAU,GAK7B,YAAc,SAId,IAAI,SAAW,cAAc,EAC7B,GAAI,YAAc,SAAS,OAAQ,CAE9B,OAAO,WAAa,UAAU,IAC5B,EAAE,IAAM,WAAa,UAAU,EAAE,SAAS,QAAQ,GACtD,EAAE,IAAM,WAAa,UAAU,EAAE,SAAS,QAAQ,GAGpD,QAAQ,IAAI,2EAA4E,UAAW,SAAS,OAAQ,WAAY,QAAQ,OAAQ,EAAE,IAAM,SAAS,EAAE,YAAY,CAAC,EAEhL,IAAI,SAAW,SAAS,QAAU,YAAc,QAAQ,QAAU,IAClE,QAAQ,IAAI,gCAAiC,UAAW,QAAQ,EAChE,EAAE,IAAM,WAAa,UAAU,EAAE,IAAI,SAAU,SAAW,IAAI,EAC9D,WAAa,YAAc,QAAQ,QAAU,GAAK,SAClD,QAAQ,IAAI,4BAA6B,UAAW,UAAU,CAC/D,MAEK,OAAO,WAAa,UAAU,GAC7B,EAAE,IAAM,WAAa,UAAU,EAAE,SAAS,QAAQ,GACrD,EAAE,IAAM,WAAa,UAAU,EAAE,YAAY,QAAQ,EAIxD,QAAQ,IAAI,iIAAkI,UAAW,WAAY,SAAU,iBAAkB,SAAS,OAAQ,EAAE,IAAM,UAAU,EAAE,YAAY,CAAC,EAEnP,EAAE,IAAM,SAAS,EAAE,IAAI,SAAU,WAAa,IAAI,CACnD,CACD,ECzaA,IAAM,aAAN,KAAmB,CAElB,IACA,KAOA,YAAYC,KAAKC,MACjB,CACC,KAAK,IAAMD,KACX,KAAK,KAAOC,KACb,CASA,eAAe,aAAc,UAAY,aACzC,CAEK,OAAO,SAAS,IAEd,OAAO,UAAU,GACrB,EAAE,IAAM,SAAS,EAAE,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,WAAY,GAAI,CAAC,WAAY,UAAU,CAAC,CAAC,CAAC,EAIrG,EAAE,WAAW,EAAE,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,gBAAiB,YAAY,CAAC,CAAC,EACrF,EAAE,WAAW,EAAE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,gBAAiB,EAAE,CAAC,CAAC,EAC7E,EAAE,WAAW,EAAE,OAAO,KAAK,IAAI,KAC9B,KAAK,IAAI,KAER,KAAK,IAAI,IAAI,MAAO,iBAAiB,EAErC,KAAK,IAAI,IAAI,QAAS,SAAU,GAAI,CAAC,EAAG,CACvC,MAAO,KAAK,KAAK,GAAG,QAAQ,EAC5B,KAAM,SACN,QAAS,eACV,CAAC,CACF,CACD,CAAC,EAEH,CAUA,cAAc,SAAU,UAAY,aACpC,CAEC,GAAI,SAAS,QAAQ,GAAK,eAAe,QAAQ,EAAI,EAAG,CAElD,OAAO,SAAS,GACpB,EAAE,IAAM,SAAS,EAAE,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,UAAW,GAAI,CAAC,UAAW,OAAO,CAAC,CAAC,CAAC,EAEhG,IAAI,QAAU,CAAC,EACf,EAAE,KAAK,SAAU,SAAS,IAAK,KAAM,CAGhC,KAAO,GACV,QAAQ,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,MAAO,GAAI,WAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAGtE,KAAK,UAEJ,OAAO,SAAS,KAAK,QAAQ,KAAK,GAAG,GAAK,KAC7C,KAAK,SAAW,GAGjB,QAAQ,KAAK,KAAK,IAAI,KACrB,KAAK,IAAI,KACR,KAAK,IAAI,IAAI,KAAK,EAClB,KAAK,IAAI,IAAI,IAAK,GAAI,KAAK,KAAM,CAAC,MAAM,EAAE,OAAO,KAAK,SAAW,YAAa,EAAE,EAAG,CAClF,KAAM,KAAK,GACZ,CAAC,CACF,CACD,CAAC,EAEH,CAAC,EACD,EAAE,UAAU,EAAE,KAAK,QAAQ,KAAK,EAAE,CAAC,CACpC,MACC,EAAE,UAAU,EAAE,KAAK,CAErB,CAED,ECtBA,IAAI,KAAO,IAAI,0BACX,IAAM,IAAI,mBAIV,KAAO,IAAI,gBAAgB,OAAO,KAAS,IAAc,CAAC,EAAI,IAAI,EAClE,GAAK,IAAI,UAAU,IAAK,IAAI,EAC5B,IAAM,IAAI,aAAa,IAAK,IAAI,EAa/B,OAAO,UAAU,SAErB,OAAO,UAAU,OAAS,UAC1B,CACC,eAAQ,MAAM,6CAA6C,EAEpD,aAAc,KAAM,SAAS,CACrC,GAWG,OAAO,UAAU,QAEpB,OAAO,UAAU,MAAQ,SAAU,KAAM,CACxC,eAAQ,MAAM,6CAA6C,EAEpD,eAAgB,KAAM,IAAI,CAClC,GAUI,OAAO,UAAU,aAErB,OAAO,UAAU,WAAa,UAAW,CACxC,eAAQ,MAAM,yCAAyC,EAEhD,WAAY,IAAI,CACxB,GAUI,OAAO,UAAU,eAErB,OAAO,UAAU,aAAe,UAAW,CAC1C,eAAQ,MAAM,2CAA2C,EAElD,aAAc,IAAI,CAC1B,GAWD,SAASC,YAAW,OACpB,CACC,OAAO,WAAY,MAAM,CAC1B,CASA,SAASC,gBAAe,OAAQ,KAChC,CACC,OAAO,eAAgB,OAAQ,IAAI,CACpC,CAWA,SAASC,cAAa,UAAW,KACjC,CACC,OAAO,aAAc,OAAQ,GAAG,IAAI,CACrC,CAQA,SAASC,cAAa,OACtB,CACC,OAAO,aAAc,MAAM,CAC5B,CASA,SAASC,QAAO,MAChB,CACC,OAAO,OAAQ,KAAK,CACrB,CASA,SAASC,KAAI,OAAQ,QAAS,SAC9B,CACC,IAAK,OAAQ,QAAS,QAAQ,CAC/B,CAOA,SAASC,UAAS,MAClB,CACC,SAAU,KAAK,CAChB,CAOA,SAASC,gBACT,CACC,OAAO,cAAe,CACvB,CAOA,SAASC,kBACT,CACC,OAAO,gBAAiB,CACzB,CAOA,SAASC,wBACT,CACC,OAAO,sBAAuB,CAC/B,CASA,SAASC,WAAU,GAAI,KAAM,IAC7B,CACC,UAAW,GAAI,KAAM,GAAG,CACzB,CAUA,SAASC,SAAQ,QAAS,OAAS,EAAG,SAAW,IAAK,KAAO,YAC7D,CACC,QAAS,QAAS,OAAQ,SAAU,IAAI,CACzC,CAQA,SAASC,MAAK,OACd,CACC,KAAM,MAAM,CACb,CASA,SAAS,GAAG,OACZ,CACC,OAAO,KAAK,GAAG,MAAM,CACtB,CAQA,SAASC,kBAAiB,EAC1B,CACC,OAAO,iBAAkB,CAAC,CAC3B,CAQA,SAASC,eAAc,OACvB,CACC,OAAO,cAAe,MAAM,CAC7B,CAOA,SAASC,eACT,CACC,OAAO,aAAc,CACtB,CASA,SAASC,SAAQ,IACjB,CACC,OAAO,QAAS,GAAG,CACpB,CASA,SAASC,YAAW,IACpB,CACC,OAAO,WAAY,GAAG,CACvB,CASA,SAASC,YACT,CACC,OAAO,UAAW,CACnB,CAWA,SAASC,uBAAsB,IAAK,IACpC,CACC,OAAO,sBAAuB,IAAK,GAAG,CACvC,CAQA,SAASC,YAAW,KACpB,CACC,OAAO,WAAY,IAAI,CACxB,CAYA,SAASC,uBAAsB,aAAc,QAC7C,CACC,OAAO,sBAAuB,aAAc,OAAO,CACpD,CAQA,SAASC,UAAS,IAClB,CACC,OAAO,SAAU,GAAG,CACrB,CAQA,SAASC,gBAAe,OACxB,CACC,OAAO,eAAgB,MAAM,CAC9B,CASA,SAASC,aAAY,IAAK,OAC1B,CACC,OAAO,YAAa,IAAK,MAAM,CAChC,CASA,SAASC,eAAc,OAAQ,MAC/B,CACC,OAAO,cAAe,OAAQ,KAAK,CACpC,CASA,SAASC,eAAc,OAAQ,MAC/B,CACC,OAAO,cAAe,OAAQ,KAAK,CACpC,CAUA,SAASC,kBAAiB,SAC1B,CACC,OAAO,iBAAkB,QAAQ,CAClC,CAQA,SAASC,QAAO,GAChB,CACC,OAAO,OAAQ,EAAE,CAClB,CASA,SAASC,aAAY,MACrB,CACC,OAAO,YAAa,KAAK,CAC1B,CAQA,SAASC,iBAAgB,MACzB,CACC,OAAO,gBAAiB,KAAK,CAC9B,CAQA,SAASC,kBAAiB,MAC1B,CACC,OAAO,iBAAkB,KAAK,CAC/B,CAOA,SAASC,YAAW,IACpB,CACC,WAAY,GAAG,CAChB,CAyBA,SAASC,iBAAgB,IAAK,QAAU,GACxC,CACC,gBAAiB,IAAK,OAAO,CAC9B,CAUA,SAASC,qBAAoB,IAAK,QAAU,GAC5C,CACC,oBAAqB,IAAK,OAAO,CAClC,CAUA,SAASC,qBAAoB,IAAK,QAAU,GAC5C,CACC,oBAAqB,IAAK,OAAO,CAClC,CAMA,SAASC,iBACT,CACC,eAAgB,CACjB,CAMA,SAASC,iBACT,CACC,eAAgB,CACjB,CAMA,SAASC,gBACT,CACC,cAAe,CAChB,CAMA,SAASC,iBACT,CACC,eAAgB,CACjB,CAMA,SAASC,YACT,CACC,UAAW,CACZ,CAmBA,SAAS,oBAAoB,IAC7B,CACC,KAAK,oBAAoB,GAAG,CAC7B,CAUA,SAAS,oBAAoB,IAC7B,CACC,KAAK,oBAAoB,GAAG,CAC7B,CAOA,SAAS,oBACT,CACC,KAAK,mBAAmB,CACzB,CASA,SAAS,qBAAqB,MAC9B,CACC,KAAK,qBAAqB,KAAK,CAChC,CAWA,SAAS,qBAAqB,MAAM,GACpC,CACC,KAAK,qBAAqB,KAAK,CAChC,CAMA,SAAS,oBACT,CACC,KAAK,mBAAmB,CACzB,CAaA,SAAS,IAAI,IAAK,GAAK,GAAI,QAAU,GAAI,IAAM,CAAC,EAAG,QAAU,CAAC,EAC9D,CACC,OAAO,IAAI,IAAI,IAAK,GAAI,QAAS,IAAK,OAAO,CAC9C,CAUA,SAAS,IAAI,KAAM,OAAQ,GAAK,GAChC,CACC,OAAO,IAAI,IAAI,KAAM,OAAQ,EAAE,CAChC,CAUA,SAAS,KAAK,QAAS,OACvB,CACC,OAAO,IAAI,KAAK,KAAM,GAAG,MAAM,CAChC,CAUA,SAAS,OAAO,KAAM,OACtB,CACC,OAAO,IAAI,OAAO,KAAM,MAAM,CAC/B,CAQA,SAAS,IAAI,KACb,CACC,OAAO,IAAI,IAAI,IAAI,CACpB,CASA,SAAS,OAAO,SAAU,IAC1B,CACC,OAAO,IAAI,OAAO,SAAU,GAAG,CAChC,CASA,SAAS,OAAO,SAAU,IAC1B,CACC,OAAO,IAAI,OAAO,SAAU,GAAG,CAChC,CAWA,SAAS,OAAO,SAAU,KAAM,KAChC,CACC,IAAI,OAAO,SAAU,KAAM,IAAI,CAChC,CASA,SAAS,KAAK,KACd,CACC,OAAO,IAAI,KAAK,IAAI,CACrB,CAUA,SAAS,KAAK,KACd,CACC,OAAO,IAAI,KAAK,IAAI,CACrB,CAqBA,SAASC,cAAa,KAAM,KAAM,SAAW,GAAI,aAAe,GAAO,cAAgB,GAAO,KAAO,GACrG,CACC,OAAO,aAAc,KAAM,KAAM,SAAU,aAAc,cAAe,IAAI,CAC7E,CAsBA,SAASC,oBACR,KAAM,KAAM,SAAW,GAAI,SAAW,EAAG,aAAe,GAAO,cAAgB,GAAO,KAAO,GAAI,SAAW,GAC3G,CACD,OAAO,mBACN,KAAM,KAAM,SAAU,SAAU,aAAc,cAAe,KAAM,QACpE,CACD,CAUA,SAASC,qBAAoB,KAAM,KAAM,KAAO,GAChD,CACC,oBAAqB,KAAM,KAAM,IAAI,CACtC,CAgBA,SAASC,kBAAiB,MAAQ,GAAI,WAAa,GACnD,CACC,OAAO,iBAAkB,MAAO,UAAU,CAC3C,CAmBA,SAASC,qBAAoB,OAAS,GAAI,MAAQ,GAAI,OAAS,GAC/D,CACC,OAAO,oBAAqB,OAAQ,MAAO,MAAM,CAClD,CAQA,SAASC,cACT,CACC,YAAa,CACd,CAUA,SAAS,eAAe,aAAc,UAAY,aAClD,CACC,IAAI,eAAe,aAAc,SAAS,CAC3C,CAWA,SAAS,cAAc,SAAU,UAAY,aAC7C,CACC,IAAI,cAAc,SAAU,SAAS,CACtC,CAaA,SAAS,kBAAkB,UAAY,YAAa,QAAU,GAAI,eAAiB,CAAC,EAAG,SAAW,EAAG,iBAAmB,EACxH,CACC,GAAG,kBAAkB,UAAW,QAAS,eAAgB,SAAU,gBAAgB,CACpF,CASA,SAAS,cAAc,UAAY,YAAa,QAAU,GAAI,eAAiB,CAAC,EAChF,CAEC,GAAG,cAAc,UAAW,QAAS,cAAc,CACpD,CASA,SAAS,gBAAgB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EACnF,CACC,GAAG,gBAAgB,UAAW,SAAU,gBAAgB,CACzD,CAMA,SAAS,oBACT,CACC,GAAG,mBAAmB,CACvB,CAQA,SAAS,cAAc,UAAY,YACnC,CACC,GAAG,cAAc,SAAS,CAC3B,CAWA,SAAS,cAAc,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAAG,SAAW,GAC/F,CACC,GAAG,cAAc,UAAW,SAAU,iBAAkB,QAAQ,CACjE,CAUA,SAAS,eAAe,UAAY,YAAa,MAAQ,GACzD,CAEC,GAAG,eAAe,UAAW,KAAK,CACnC,CAUA,SAAS,mBAAmB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EAAG,SAAW,GACpG,CACC,GAAG,mBAAmB,UAAW,SAAU,iBAAkB,QAAQ,CACtE,CAQA,SAAS,oBAAoB,UAAY,YAAa,MAAQ,GAC9D,CACC,GAAG,oBAAoB,UAAW,KAAK,CACxC,CAYA,SAAS,gBACR,UAAY,YACZ,MAAQ,GACR,SAAW,CAAC,EACZ,QAAU,CAAC,EACX,SAAW,CAAC,EACZ,WAAa,GACZ,CACD,GAAG,gBAAgB,UAAW,MAAO,SAAU,QAAS,SAAU,UAAU,CAC7E,CAUA,SAAS,sBAAsB,UAAY,YAAa,SAAW,EAAG,iBAAmB,EACzF,CACC,GAAG,sBAAsB,UAAW,SAAU,gBAAgB,CAC/D", + "names": ["i18n", "hec", "l10n", "hec", "l10n", "escapeHtml", "roundPrecision", "formatString", "unescapeHtml", "loadEl", "pop", "expandTA", "getWindowSize", "getScrollOffset", "getScrollOffsetOpener", "setCenter", "goToPos", "goTo", "numberWithCommas", "convertLBtoBR", "getTimestamp", "dec2hex", "generateId", "randomIdF", "getRandomIntInclusive", "isFunction", "executeFunctionByName", "isObject", "getObjectCount", "keyInObject", "getKeyByValue", "valueInObject", "deepCopyFunction", "exists", "formatBytes", "formatBytesLong", "stringByteFormat", "errorCatch", "actionIndicator", "actionIndicatorShow", "actionIndicatorHide", "overlayBoxShow", "overlayBoxHide", "setOverlayBox", "hideOverlayBox", "ClearCall", "html_options", "html_options_block", "html_options_refill", "parseQueryString", "getQueryStringParam", "loginLogout"] +} diff --git a/www/admin/subfolder/class_test.config.direct.php b/www/admin/subfolder/class_test.config.direct.php index de6d32de..43d27810 100644 --- a/www/admin/subfolder/class_test.config.direct.php +++ b/www/admin/subfolder/class_test.config.direct.php @@ -6,7 +6,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/admin/test.javascript.html b/www/admin/test.javascript.html new file mode 100644 index 00000000..e76020f7 --- /dev/null +++ b/www/admin/test.javascript.html @@ -0,0 +1,37 @@ + + + JavaScript Test + + + + + +
+

JavaScript tests

+
+
+
+ + diff --git a/www/admin/test_edit_base.php b/www/admin/test_edit_base.php index 81558f72..ebda77ee 100644 --- a/www/admin/test_edit_base.php +++ b/www/admin/test_edit_base.php @@ -4,7 +4,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); +error_reporting(E_ALL | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR); ob_start(); diff --git a/www/composer.json b/www/composer.json index 74c42f31..d723baf3 100644 --- a/www/composer.json +++ b/www/composer.json @@ -21,9 +21,10 @@ } }, "require": { - "egrajp/smarty-extended": "^4.3", + "egrajp/smarty-extended": "^5.4", "php": ">=8.1", "gullevek/dotenv": "^2.0", - "psr/log": "^2.0 || ^3.0" + "psr/log": "^2.0 || ^3.0", + "php-privacy/openpgp": "^2.1" } } diff --git a/www/configs/.target.example b/www/configs/.target.example new file mode 100644 index 00000000..62f4277b --- /dev/null +++ b/www/configs/.target.example @@ -0,0 +1,3 @@ +# target can be live, stage, test, dev +# this overrides the SITE set "location" entry +TARGET= diff --git a/www/configs/config.master.php b/www/configs/config.master.php index 9d5b9985..70c52463 100644 --- a/www/configs/config.master.php +++ b/www/configs/config.master.php @@ -78,42 +78,11 @@ define('TEMPLATES_C', 'templates_c' . DIRECTORY_SEPARATOR); // template base define('TEMPLATES', 'templates' . DIRECTORY_SEPARATOR); -/************* HASH / ACL DEFAULT / ERROR SETTINGS / SMARTY *************/ +/************* HASH / ACL DEFAULT *************/ // default hash type define('DEFAULT_HASH', 'sha256'); // default acl level -define('DEFAULT_ACL_LEVEL', 80); -// SSL host name -// define('SSL_HOST', $_ENV['SSL_HOST'] ?? ''); -// error page strictness, Default is 3 -// 1: only show error page as the last mesure if really no mid & aid can be loaded and found at all -// 2: if template not found, do not search, show error template -// 3: if default template is not found, show error template, do not fall back to default tree -// 4: very strict, even on normal fixable errors through error -// define('ERROR_STRICT', 3); -// allow page caching in general, set to 'false' if you do debugging or development! -// define('ALLOW_SMARTY_CACHE', false); -// cache life time, in second', default here is 2 days (172800s) -// -1 is never expire cache -// define('SMARTY_CACHE_LIFETIME', -1); - -/************* LOGOUT ********************/ -// logout target -define('LOGOUT_TARGET', ''); - -/************* AJAX / ACCESS *************/ -// ajax request type -define('AJAX_REQUEST_TYPE', 'POST'); -// what AJAX type to use -define('USE_PROTOTYPE', false); -define('USE_SCRIPTACULOUS', false); -define('USE_JQUERY', true); - -/************* LAYOUT WIDTHS *************/ -define('PAGE_WIDTH', '100%'); -define('CONTENT_WIDTH', '100%'); -// the default template name -define('MASTER_TEMPLATE_NAME', 'main_body.tpl'); +define('DEFAULT_ACL_LEVEL', $ENV['DEFAULT_ACL_LEVEL'] ?? 80); /************* OVERALL CONTROL NAMES *************/ // BELOW has HAS to be changed @@ -136,24 +105,15 @@ define('COMPILE_ID', 'COMPILE_' . BASE_NAME . '_' . SERVER_NAME_HASH); /************* LANGUAGE / ENCODING *******/ // default lang + encoding -define('DEFAULT_LOCALE', 'en_US.UTF-8'); +define('DEFAULT_LOCALE', $_ENV['LOCALE'] ?? 'en_US.UTF-8'); // default web page encoding setting -define('DEFAULT_ENCODING', 'UTF-8'); +define('DEFAULT_ENCODING', (string)array_pad(explode('.', DEFAULT_LOCALE, 2), 2, 'UTF-8')[1]); -/************* QUEUE TABLE *************/ -// if we have a dev/live system -// set_live is a per page/per item -// live_queue is a global queue system -// define('QUEUE', 'live_queue'); - -/************* DB PATHS (PostgreSQL) *****************/ -// schema names, can also be defined per -define('PUBLIC_SCHEMA', 'public'); -define('DEV_SCHEMA', 'public'); -define('TEST_SCHEMA', 'public'); -define('LIVE_SCHEMA', 'public'); -define('GLOBAL_DB_SCHEMA', ''); -define('LOGIN_DB_SCHEMA', ''); +/************* HOST NAME *****************/ +// get the name without the port +list($HOST_NAME) = array_pad(explode(':', $_SERVER['HTTP_HOST'], 2), 2, null); +// set HOST name +define('HOST_NAME', $HOST_NAME); /************* CORE HOST SETTINGS *****************/ if (file_exists(BASE . CONFIGS . 'config.host.php')) { @@ -162,6 +122,14 @@ if (file_exists(BASE . CONFIGS . 'config.host.php')) { if (!isset($SITE_CONFIG)) { $SITE_CONFIG = []; } +// BAIL ON MISSING MASTER SITE CONFIG +if (!isset($SITE_CONFIG[HOST_NAME]['location'])) { + throw new \InvalidArgumentException( + 'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator' + ); +} +// set target first +define('TARGET', $_ENV['TARGET'] ?? $SITE_CONFIG[HOST_NAME]['location'] ?? 'test'); /************* DB ACCESS *****************/ if (file_exists(BASE . CONFIGS . 'config.db.php')) { require BASE . CONFIGS . 'config.db.php'; @@ -175,17 +143,6 @@ if (file_exists(BASE . CONFIGS . 'config.path.php')) { } /************* MASTER INIT *****************/ -// live frontend pages -// ** missing live domains ** -// get the name without the port -[$HOST_NAME] = array_pad(explode(':', $_SERVER['HTTP_HOST'], 2), 2, null); -// set HOST name -define('HOST_NAME', $HOST_NAME); -// BAIL ON MISSING MASTER SITE CONFIG -if (!isset($SITE_CONFIG[HOST_NAME]['location'])) { - echo 'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator'; - exit; -} // BAIL ON MISSING DB CONFIG: // we have either no db selction for this host but have db config entries // or we have a db selection but no db config as array or empty @@ -200,8 +157,9 @@ if ( empty($DB_CONFIG[$SITE_CONFIG[HOST_NAME]['db_host']])) ) ) { - echo 'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator'; - exit; + throw new \InvalidArgumentException( + 'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator' + ); } // set SSL on $is_secure = false; @@ -235,35 +193,31 @@ define('DB_CONFIG', $DB_CONFIG[DB_CONFIG_NAME] ?? [ ]); // because we can't change constant, but we want to for db debug flag $GLOBALS['DB_CONFIG_SET'] = DB_CONFIG; -// define('DB_CONFIG_TARGET', SITE_CONFIG[$HOST_NAME]['db_host_target']); -// define('DB_CONFIG_OTHER', SITE_CONFIG[$HOST_NAME]['db_host_other']); -// override for login and global schemas -// where the edit* tables are -// define('LOGIN_DB_SCHEMA', PUBLIC_SCHEMA); -// where global tables are that are used by all schemas (eg queue tables for online, etc) -// define('GLOBAL_DB_SCHEMA', PUBLIC_SCHEMA); // debug settings, site lang, etc -define('TARGET', $SITE_CONFIG[HOST_NAME]['location'] ?? 'test'); define('DEBUG_LEVEL', $SITE_CONFIG[HOST_NAME]['debug_level'] ?? 'debug'); define('SITE_LOCALE', $SITE_CONFIG[HOST_NAME]['site_locale'] ?? DEFAULT_LOCALE); define('SITE_DOMAIN', str_replace(DIRECTORY_SEPARATOR, '', CONTENT_PATH)); define('SITE_ENCODING', $SITE_CONFIG[HOST_NAME]['site_encoding'] ?? DEFAULT_ENCODING); define('LOGIN_ENABLED', $SITE_CONFIG[HOST_NAME]['login_enabled'] ?? false); define('AUTH', $SITE_CONFIG[HOST_NAME]['auth'] ?? false); -// paths -// define('CSV_PATH', $PATHS[TARGET]['csv_path'] ?? ''); -// define('EXPORT_SCRIPT', $PATHS[TARGET]['perl_bin'] ?? ''); -// define('REDIRECT_URL', $PATHS[TARGET]['redirect_url'] ?? ''); +// NOTE: everything below is smarty related and should be removed from here /************* GENERAL PAGE TITLE ********/ define('G_TITLE', $_ENV['G_TITLE'] ?? ''); - +/************* LAYOUT WIDTHS *************/ +define('PAGE_WIDTH', $_ENV['SMARTY.PAGE_WIDTH'] ?? '100%'); +define('CONTENT_WIDTH', $_ENV['SMARTY.CONTENT_WIDTH'] ?? '100%'); +// the default template name +define('MASTER_TEMPLATE_NAME', $_ENV['MASTER_TEMPLATE_NAME'] ?? 'main_body.tpl'); +/************* JS LIBRARIES *************/ +define('USE_PROTOTYPE', false); +define('USE_SCRIPTACULOUS', false); +define('USE_JQUERY', true); /************ STYLE SHEETS / JS **********/ -define('ADMIN_STYLESHEET', 'edit.css'); -define('ADMIN_JAVASCRIPT', 'edit.js'); +define('ADMIN_STYLESHEET', $_ENV['ADMIN.STYLESHEET'] ?? 'edit.css'); +define('ADMIN_JAVASCRIPT', $_ENV['ADMIN.JAVASCRIPT'] ?? 'edit.js'); define('STYLESHEET', $_ENV['STYLESHEET'] ?? 'frontend.css'); define('JAVASCRIPT', $_ENV['JAVASCRIPT'] ?? 'frontend.js'); - // anything optional /************* INTERNAL ******************/ // any other global definitons in the config.other.php diff --git a/www/configs/config.other.php b/www/configs/config.other.php index 3a02357d..f9f0320f 100644 --- a/www/configs/config.other.php +++ b/www/configs/config.other.php @@ -15,6 +15,12 @@ define('EDIT_BASE_STYLESHEET', 'edit.css'); // define('SOME_ID', ); +/************* QUEUE TABLE *************/ +// if we have a dev/live system +// set_live is a per page/per item +// live_queue is a global queue system +// define('QUEUE', 'live_queue'); + /************* CONVERT *******************/ // this only needed if the external thumbnail create is used $paths = [ diff --git a/www/configs/config.path.php b/www/configs/config.path.php index 69a78d00..c3f6cb84 100644 --- a/www/configs/config.path.php +++ b/www/configs/config.path.php @@ -35,4 +35,9 @@ define('CONTENT_PATH', $folder . DIRECTORY_SEPARATOR); ], ];*/ +// paths +// define('CSV_PATH', $PATHS[TARGET]['csv_path'] ?? ''); +// define('EXPORT_SCRIPT', $PATHS[TARGET]['perl_bin'] ?? ''); +// define('REDIRECT_URL', $PATHS[TARGET]['redirect_url'] ?? ''); + // __END__ diff --git a/www/configs/config.php b/www/configs/config.php index 9f884b73..9358099f 100644 --- a/www/configs/config.php +++ b/www/configs/config.php @@ -53,6 +53,11 @@ for ( \gullevek\dotEnv\DotEnv::readEnvFile( $__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH ); + // load target file if it exists + \gullevek\dotEnv\DotEnv::readEnvFile( + $__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH, + '.target' + ); // load master config file that loads all other config files require $__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH . 'config.master.php'; break; diff --git a/www/includes/admin_header.php b/www/includes/admin_header.php index d6176102..b83f36d5 100644 --- a/www/includes/admin_header.php +++ b/www/includes/admin_header.php @@ -91,7 +91,7 @@ $l10n = new \CoreLibs\Language\L10n( ); // create smarty object -$smarty = new \CoreLibs\Template\SmartyExtend($l10n, CACHE_ID, COMPILE_ID); +$smarty = new \CoreLibs\Template\SmartyExtend($l10n, $log, CACHE_ID, COMPILE_ID); // create new Backend class with db and loger attached $cms = new \CoreLibs\Admin\Backend($db, $log, $session, $l10n, DEFAULT_ACL_LEVEL); // the menu show flag (what menu to show) @@ -116,7 +116,7 @@ $data = [ // log action // no log if login if (!$login->loginActionRun()) { - $login->writeLog('Submit', $data, $cms->adbGetActionSet(), 'BINARY'); + $login->writeLog('Submit', $data, action_set:$cms->adbGetActionSet(), write_type:'BINARY'); } //------------------------------ logging end diff --git a/www/lib/CoreLibs/ACL/Login.php b/www/lib/CoreLibs/ACL/Login.php index 691b03d6..e9d3dc97 100644 --- a/www/lib/CoreLibs/ACL/Login.php +++ b/www/lib/CoreLibs/ACL/Login.php @@ -14,13 +14,14 @@ * will be a class one day * * descrption of session_vars -* DEBUG_ALL - set to one, prints out error_msg var at end of php execution -* DB_DEBUG - prints out database debugs (query, etc) -* GROUP_LEVEL - the level he can access (numeric) -* USER_NAME - login name from user -* LANG - lang to show edit interface (not yet used) +* TODO: Update session var info +* [DEPRECATED] DEBUG_ALL - set to one, prints out error_msg var at end of php execution +* [DEPRECATED] DB_DEBUG - prints out database debugs (query, etc) +* [REMOVED] LOGIN_GROUP_LEVEL - the level he can access (numeric) +* LOGIN_USER_NAME - login name from user +* [DEPRECATED] LANG - lang to show edit interface (not yet used) * DEFAULT_CHARSET - in connection with LANG (not yet used) -* PAGES - array of hashes +* LOGIN_PAGES - array of hashes * edit_page_id - ID from the edit_pages table * filename - name of the file * page_name - name in menu @@ -75,18 +76,18 @@ use CoreLibs\Convert\Json; class Login { /** @var ?int the user id var*/ - private ?int $euid; + private ?int $edit_user_id; /** @var ?string the user cuid (note will be super seeded with uuid v4 later) */ - private ?string $ecuid; - /** @var ?string UUIDv4, will superseed the ecuid and replace euid as login id */ - private ?string $ecuuid; + private ?string $edit_user_cuid; + /** @var ?string UUIDv4, will superseed the eucuid and replace euid as login id */ + private ?string $edit_user_cuuid; /** @var string _GET/_POST loginUserId parameter for non password login */ private string $login_user_id = ''; /** @var string source, either _GET or _POST or empty */ private string $login_user_id_source = ''; /** @var bool set to true if illegal characters where found in the login user id string */ private bool $login_user_id_unclear = false; - // is set to one if login okay, or EUID is set and user is okay to access this page + // is set to one if login okay, or EUCUUID is set and user is okay to access this page /** @var bool */ private bool $permission_okay = false; /** @var string pressed login */ @@ -180,6 +181,7 @@ class Login private array $login_template = [ 'strings' => [], 'password_change' => '', + 'password_forgot' => '', 'template' => '' ]; @@ -215,6 +217,16 @@ class Login 'path' => '', ]; + /** @var int resync interval time in minutes */ + private const DEFAULT_AUTH_RESYNC_INTERVAL = 5 * 60; + /** @var int the session max garbage collection life time */ + // private const DEFAULT_SESSION_GC_MAXLIFETIME = ; + private int $default_session_gc_maxlifetime; + /** @var int in how many minutes an auth resync is done */ + private int $auth_resync_interval; + /** @var bool set the enhanced header security */ + private bool $header_enhance_security = false; + /** @var \CoreLibs\Logging\Logging logger */ public \CoreLibs\Logging\Logging $log; /** @var \CoreLibs\DB\IO database */ @@ -228,17 +240,16 @@ class Login * constructor, does ALL, opens db, works through connection checks, * finishes itself * - * @param \CoreLibs\DB\IO $db Database connection class - * @param \CoreLibs\Logging\Logging $log Logging class - * @param \CoreLibs\Create\Session $session Session interface class - * @param array $options Login ACL settings - * $auto_login [default true] DEPRECATED, moved into options + * @param \CoreLibs\DB\IO $db Database connection class + * @param \CoreLibs\Logging\Logging $log Logging class + * @param \CoreLibs\Create\Session $session Session interface class + * @param array $options Login ACL settings */ public function __construct( \CoreLibs\DB\IO $db, \CoreLibs\Logging\Logging $log, \CoreLibs\Create\Session $session, - array $options = [] + array $options = [], ) { // attach db class $this->db = $db; @@ -247,148 +258,19 @@ class Login // attach session class $this->session = $session; + $this->default_session_gc_maxlifetime = (int)ini_get("session.gc_maxlifetime"); + // set and check options if (false === $this->loginSetOptions($options)) { // on failure, exit echo "Could not set options"; $this->loginTerminate('Could not set options', 3000); } - - // string key, msg: string, flag: e (error), o (ok) - $this->login_error_msg = [ - '0' => [ - 'msg' => 'No error', - 'flag' => 'o' - ], - // actually obsolete - '100' => [ - 'msg' => '[EUID] came in as GET/POST!', - 'flag' => 'e', - ], - // query errors - '1009' => [ - 'msg' => 'Login query reading failed', - 'flag' => 'e', - ], - // user not found - '1010' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // blowfish password wrong - '1011' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // fallback md5 password wrong - '1012' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // new password_hash wrong - '1013' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - '1101' => [ - 'msg' => 'Login Failed - Login User ID must be validated', - 'flag' => 'e' - ], - '1102' => [ - 'msg' => 'Login Failed - Login User ID is outside valid date range', - 'flag' => 'e' - ], - '102' => [ - 'msg' => 'Login Failed - Please enter username and password', - 'flag' => 'e' - ], - '103' => [ - 'msg' => 'You do not have the rights to access this Page', - 'flag' => 'e' - ], - '104' => [ - 'msg' => 'Login Failed - User not enabled', - 'flag' => 'e' - ], - '105' => [ - 'msg' => 'Login Failed - User is locked', - 'flag' => 'e' - ], - '106' => [ - 'msg' => 'Login Failed - User is deleted', - 'flag' => 'e' - ], - '107' => [ - 'msg' => 'Login Failed - User in locked via date period', - 'flag' => 'e' - ], - '108' => [ - 'msg' => 'Login Failed - User is locked via Login User ID', - 'flag' => 'e' - ], - '109' => [ - 'msg' => 'Check permission query reading failed', - 'flag' => 'e' - ], - // actually this is an illegal user, but I mask it - '220' => [ - 'msg' => 'Password change - The user could not be found', - 'flag' => 'e' - ], - '200' => [ - 'msg' => 'Password change - Please enter username and old password', - 'flag' => 'e' - ], - '201' => [ - 'msg' => 'Password change - The user could not be found', - 'flag' => 'e' - ], - '202' => [ - 'msg' => 'Password change - The old password is not correct', - 'flag' => 'e' - ], - '203' => [ - 'msg' => 'Password change - Please fill out both new password fields', - 'flag' => 'e' - ], - '204' => [ - 'msg' => 'Password change - The new passwords do not match', - 'flag' => 'e' - ], - // we should also not here WHAT is valid - '205' => [ - 'msg' => 'Password change - The new password is not in a valid format', - 'flag' => 'e' - ], - // for OK password change - '300' => [ - 'msg' => 'Password change successful', - 'flag' => 'o' - ], - // this is bad bad error - '9999' => [ - 'msg' => 'Necessary crypt engine could not be found. Login is impossible', - 'flag' => 'e' - ], - ]; - - // read the current edit_access_right list into an array - $q = "SELECT level, type, name FROM edit_access_right " - . "WHERE level >= 0 ORDER BY level"; - while (is_array($res = $this->db->dbReturn($q))) { - // level to description format (numeric) - $this->default_acl_list[$res['level']] = [ - 'type' => $res['type'], - 'name' => $res['name'] - ]; - $this->default_acl_list_type[(string)$res['type']] = (int)$res['level']; - } - // write that into the session - $this->session->setMany([ - 'DEFAULT_ACL_LIST' => $this->default_acl_list, - 'DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type, - ]); - + // init error array + $this->loginInitErrorMessages(); + // acess right list + $this->loginLoadAccessRightList(); + // log allowed write flags $this->loginSetEditLogWriteTypeAvailable(); // this will be deprecated @@ -398,7 +280,7 @@ class Login } // ************************************************************************* - // **** PROTECTED INTERNAL + // **** MARK: PROTECTED INTERNAL // ************************************************************************* /** @@ -416,6 +298,7 @@ class Login } else { $this->log->critical($message, ['code' => $code]); } + // TODO throw error and not exit exit($code); } @@ -441,7 +324,7 @@ class Login } // ************************************************************************* - // **** PRIVATE INTERNAL + // **** MARK: PRIVATE INTERNAL // ************************************************************************* /** @@ -540,14 +423,9 @@ class Login // LOGOUT TARGET if (!isset($options['logout_target'])) { - if (defined('LOGOUT_TARGET')) { - trigger_error( - 'loginMainCall: LOGOUT_TARGET should not be used', - E_USER_DEPRECATED - ); - $options['logout_target'] = LOGOUT_TARGET; - $this->logout_target = $options['logout_target']; - } + // defaults to '' + $options['logout_target'] = ''; + $this->logout_target = $options['logout_target']; } // *** PASSWORD SETTINGS @@ -568,6 +446,20 @@ class Login } $this->password_forgot = $options['forgot_flow']; + // sync _SESSION acl settings + if ( + !isset($options['auth_resync_interval']) || + !is_numeric($options['auth_resync_interval']) || + $options['auth_resync_interval'] < 0 || + $options['auth_resync_interval'] > $this->default_session_gc_maxlifetime + ) { + // default 5 minutues + $options['auth_resync_interval'] = self::DEFAULT_AUTH_RESYNC_INTERVAL; + } else { + $options['auth_resync_interval'] = (int)$options['auth_resync_interval']; + } + $this->auth_resync_interval = $options['auth_resync_interval']; + // *** LANGUAGE // LANG: LOCALE PATH if (empty($options['locale_path'])) { @@ -579,7 +471,6 @@ class Login // set path $options['locale_path'] = BASE . INCLUDES . LOCALE; } - $this->session->set('LOCALE_PATH', $options['locale_path']); // LANG: LOCALE if (empty($options['site_locale'])) { trigger_error( @@ -614,7 +505,6 @@ class Login $options['set_domain'] = str_replace(DIRECTORY_SEPARATOR, '', CONTENT_PATH); } } - $this->session->set('DEFAULT_DOMAIN', $options['site_domain']); // LANG: ENCODING if (empty($options['site_encoding'])) { trigger_error( @@ -624,12 +514,212 @@ class Login $options['site_encoding'] = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ? SITE_ENCODING : 'UTF-8'; } + // set enhancded security flag + if ( + empty($options['enhanced_security']) || + !is_bool($options['enhanced_security']) + ) { + $options['enhanced_security'] = true; + } + $this->header_enhance_security = $options['enhanced_security']; // write array to options $this->options = $options; return true; } + /** + * sets the login error message array + * + * @return void + */ + private function loginInitErrorMessages() + { + // string key, msg: string, flag: e (error), o (ok) + $this->login_error_msg = [ + '0' => [ + 'msg' => 'No error', + 'flag' => 'o' + ], + // actually obsolete + '100' => [ + 'msg' => '[EUCUUID] set from GET/POST!', + 'flag' => 'e', + ], + // query errors + '1009' => [ + 'msg' => 'Login query reading failed', + 'flag' => 'e', + ], + // user not found + '1010' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + // general login error + '1011' => [ + 'msg' => 'Login Failed - General authentication error', + 'flag' => 'e' + ], + // fallback md5 password wrong + '1012' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + // new password_hash wrong + '1013' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + '1101' => [ + 'msg' => 'Login Failed - Login User ID must be validated', + 'flag' => 'e' + ], + '1102' => [ + 'msg' => 'Login Failed - Login User ID is outside valid date range', + 'flag' => 'e' + ], + '102' => [ + 'msg' => 'Login Failed - Please enter username and password', + 'flag' => 'e' + ], + '103' => [ + 'msg' => 'You do not have the rights to access this Page', + 'flag' => 'e' + ], + '104' => [ + 'msg' => 'Login Failed - User not enabled', + 'flag' => 'e' + ], + '105' => [ + 'msg' => 'Login Failed - User is locked', + 'flag' => 'e' + ], + '106' => [ + 'msg' => 'Login Failed - User is deleted', + 'flag' => 'e' + ], + '107' => [ + 'msg' => 'Login Failed - User in locked via date period', + 'flag' => 'e' + ], + '108' => [ + 'msg' => 'Login Failed - User is locked via Login User ID', + 'flag' => 'e' + ], + '109' => [ + 'msg' => 'Check permission query reading failed', + 'flag' => 'e' + ], + '110' => [ + 'msg' => 'Forced logout', + 'flag' => '', + ], + // actually this is an illegal user, but I mask it + '220' => [ + 'msg' => 'Password change - The user could not be found', + 'flag' => 'e' + ], + '200' => [ + 'msg' => 'Password change - Please enter username and old password', + 'flag' => 'e' + ], + '201' => [ + 'msg' => 'Password change - The user could not be found', + 'flag' => 'e' + ], + '202' => [ + 'msg' => 'Password change - The old password is not correct', + 'flag' => 'e' + ], + '203' => [ + 'msg' => 'Password change - Please fill out both new password fields', + 'flag' => 'e' + ], + '204' => [ + 'msg' => 'Password change - The new passwords do not match', + 'flag' => 'e' + ], + // we should also not here WHAT is valid + '205' => [ + 'msg' => 'Password change - The new password is not in a valid format', + 'flag' => 'e' + ], + // for OK password change + '300' => [ + 'msg' => 'Password change successful', + 'flag' => 'o' + ], + // this is bad bad error + '9999' => [ + 'msg' => 'Necessary crypt engine could not be found. Login is impossible', + 'flag' => 'e' + ], + ]; + } + + /** + * loads the access right list from the database + * + * @return void + */ + private function loginLoadAccessRightList(): void + { + // read the current edit_access_right list into an array + $q = <<= 0 + ORDER BY + level + SQL; + while (is_array($res = $this->db->dbReturn($q))) { + // level to description format (numeric) + $this->default_acl_list[$res['level']] = [ + 'type' => $res['type'], + 'name' => $res['name'] + ]; + $this->default_acl_list_type[(string)$res['type']] = (int)$res['level']; + } + // write that into the session + $this->session->setMany([ + 'LOGIN_DEFAULT_ACL_LIST' => $this->default_acl_list, + 'LOGIN_DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type, + ]); + } + + /** + * Improves the application's security over HTTP(S) by setting specific headers + * + * @return void + */ + protected function loginEnhanceHttpSecurity(): void + { + // skip if not wanted + if (!$this->header_enhance_security) { + return; + } + // remove exposure of PHP version (at least where possible) + header_remove('X-Powered-By'); + // if the user is signed in + if ($this->permission_okay) { + // prevent clickjacking + header('X-Frame-Options: sameorigin'); + // prevent content sniffing (MIME sniffing) + header('X-Content-Type-Options: nosniff'); + + // disable caching of potentially sensitive data + header('Cache-Control: no-store, no-cache, must-revalidate', true); + header('Expires: Thu, 19 Nov 1981 00:00:00 GMT', true); + header('Pragma: no-cache', true); + } + } + + // MARK: validation checks + /** * Checks for all flags and sets error codes for each * In order: @@ -640,6 +730,7 @@ class Login * @param int $locked Locked because of too many invalid passwords * @param int $locked_period Locked because of time period set * @param int $login_user_id_locked Locked from using Login User Id + * @param int $force_logout Force logout counter, if higher than session, permission is false * @return bool */ private function loginValidationCheck( @@ -647,7 +738,8 @@ class Login int $enabled, int $locked, int $locked_period, - int $login_user_id_locked + int $login_user_id_locked, + int $force_logout ): bool { $validation = false; if ($deleted) { @@ -665,6 +757,8 @@ class Login } elseif ($login_user_id_locked) { // user is locked, either set or auto set $this->login_error = 108; + } elseif ($force_logout > $this->session->get('LOGIN_FORCE_LOGOUT')) { + $this->login_error = 110; } else { $validation = true; } @@ -748,6 +842,115 @@ class Login return $login_id_ok; } + /** + * write error data for login errors + * + * @param array $res + * @return void + */ + private function loginWriteLoginError(array $res) + { + if (!$this->login_error) { + return; + } + $login_error_date_first = ''; + if ($res['login_error_count'] == 0) { + $login_error_date_first = ", login_error_date_first = NOW()"; + } + // update login error count for this user + $q = <<db->dbExecParams( + str_replace('{LOGIN_ERROR_SQL}', $login_error_date_first, $q), + [$res['edit_user_id']] + ); + // totally lock the user if error max is reached + if ( + $this->max_login_error_count != -1 && + $res['login_error_count'] + 1 > $this->max_login_error_count + ) { + // do some alert reporting in case this error is too big + // if strict is set, lock this user + // this needs manual unlocking by an admin user + if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) { + $q = << $res + * @return void + */ + private function loginSetEditUserUidData(array $res) + { + // normal user processing + // set class var and session var + $this->edit_user_id = (int)$res['edit_user_id']; + $this->edit_user_cuid = (string)$res['cuid']; + $this->edit_user_cuuid = (string)$res['cuuid']; + $this->session->setMany([ + 'LOGIN_EUID' => $this->edit_user_id, + 'LOGIN_EUCUID' => $this->edit_user_cuid, + 'LOGIN_EUCUUID' => $this->edit_user_cuuid, + ]); + } + + /** + * check for re-loading of ACL data after a period of time + * or if any of the core session vars is not set + * + * @return void + */ + private function loginAuthResync() + { + if (!$this->session->get('LOGIN_LAST_AUTH_RESYNC')) { + $this->session->set('LOGIN_LAST_AUTH_RESYNC', 0); + } + // reauth on missing session vars and timed out re-sync interval + $mandatory_session_vars = [ + 'LOGIN_USER_NAME', 'LOGIN_GROUP_NAME', 'LOGIN_EUCUID', 'LOGIN_EUCUUID', + 'LOGIN_USER_ADDITIONAL_ACL', 'LOGIN_GROUP_ADDITIONAL_ACL', + 'LOGIN_ADMIN', 'LOGIN_GROUP_ACL_LEVEL', + 'LOGIN_PAGES', 'LOGIN_PAGES_LOOKUP', 'LOGIN_PAGES_ACL_LEVEL', + 'LOGIN_USER_ACL_LEVEL', + 'LOGIN_UNIT', 'LOGIN_UNIT_DEFAULT_EACUID' + ]; + $force_reauth = false; + foreach ($mandatory_session_vars as $_session_var) { + if (!isset($_SESSION[$_session_var])) { + $force_reauth = true; + break; + } + } + if ( + $this->session->get('LOGIN_LAST_AUTH_RESYNC') + $this->auth_resync_interval <= time() && + $force_reauth == false + ) { + return; + } + if (($res = $this->loginLoadUserData($this->edit_user_cuuid)) === false) { + return; + } + // set the session vars + $this->loginSetSession($res); + } + + // MARK: MAIN LOGIN ACTION + /** * if user pressed login button this script is called, * but only if there is no preview euid set @@ -757,7 +960,11 @@ class Login private function loginLoginUser(): void { // if pressed login at least and is not yet loggined in - if ($this->euid || (!$this->login && !$this->login_user_id)) { + if ($this->edit_user_cuuid || (!$this->login && !$this->login_user_id)) { + // run reload user data based on re-auth timeout, but only if we got a set cuuid + if ($this->edit_user_cuuid) { + $this->loginAuthResync(); + } return; } // if not username AND password where given @@ -767,86 +974,8 @@ class Login $this->permission_okay = false; return; } - // have to get the global stuff here for setting it later - // we have to get the themes in here too - $q = "SELECT eu.edit_user_id, eu.cuid, eu.cuuid, eu.username, eu.password, " - . "eu.edit_group_id, " - . "eg.name AS edit_group_name, eu.admin, " - // additinal acl lists - . "eu.additional_acl AS user_additional_acl, " - . "eg.additional_acl AS group_additional_acl, " - // login error + locked - . "eu.login_error_count, eu.login_error_date_last, " - . "eu.login_error_date_first, eu.strict, eu.locked, " - // date based lock - . "CASE WHEN (" - . "(eu.lock_until IS NULL " - . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " - . "AND (eu.lock_after IS NULL " - . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" - . ") THEN 0::INT ELSE 1::INT END locked_period, " - // debug (legacy) - . "eu.debug, eu.db_debug, " - // enabled - . "eu.enabled, eu.deleted, " - // for checks only - . "eu.login_user_id, " - // login id validation - . "CASE WHEN (" - . "(eu.login_user_id_valid_from IS NULL " - . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " - . "AND (eu.login_user_id_valid_until IS NULL " - . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" - . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " - // check if user must login - . "CASE WHEN eu.login_user_id_revalidate_after IS NOT NULL " - . "AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL " - . "AND (eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after)::DATE " - . "<= NOW()::DATE " - . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " - . "eu.login_user_id_locked, " - // language - . "el.short_name AS locale, el.iso_name AS encoding, " - // levels - . "eareu.level AS user_level, eareu.type AS user_type, " - . "eareg.level AS group_level, eareg.type AS group_type, " - // colors - . "first.header_color AS first_header_color, " - . "second.header_color AS second_header_color, second.template " - . "FROM edit_user eu " - . "LEFT JOIN edit_scheme second ON " - . "(second.edit_scheme_id = eu.edit_scheme_id AND second.enabled = 1), " - . "edit_language el, edit_group eg, " - . "edit_access_right eareu, " - . "edit_access_right eareg, " - . "edit_scheme first " - . "WHERE first.edit_scheme_id = eg.edit_scheme_id " - . "AND eu.edit_group_id = eg.edit_group_id " - . "AND eu.edit_language_id = el.edit_language_id " - . "AND eu.edit_access_right_id = eareu.edit_access_right_id " - . "AND eg.edit_access_right_id = eareg.edit_access_right_id " - . "AND " - // either login_user_id OR password must be given - . (!empty($this->login_user_id && empty($this->username)) ? - // check with login id if set and NO username - "eu.login_user_id = " . $this->db->dbEscapeLiteral($this->login_user_id) . " " : - // password match is done in script, against old plain or new blowfish encypted - "LOWER(username) = " . $this->db->dbEscapeLiteral(strtolower($this->username)) . " " - ); - // reset any query data that might exist - $this->db->dbCacheReset($q); - // never cache return data - $res = $this->db->dbReturn($q, $this->db::NO_CACHE); - // query was not run successful - if (!empty($this->db->dbGetLastError())) { - $this->login_error = 1009; - $this->permission_okay = false; - return; - } elseif (!is_array($res)) { - // username is wrong, but we throw for wrong username - // and wrong password the same error - $this->login_error = 1010; - $this->permission_okay = false; + // load user data, abort on error + if (($res = $this->loginLoadUserData()) === false) { return; } // if login errors is half of max errors and the last login error @@ -864,7 +993,8 @@ class Login (int)$res['enabled'], (int)$res['locked'], (int)$res['locked_period'], - (int)$res['login_user_id_locked'] + (int)$res['login_user_id_locked'], + (int)$res['force_logout'] ) ) { // error set in method (104, 105, 106, 107, 108) @@ -893,241 +1023,31 @@ class Login // .' => HASH: '.(Password::passwordRehashCheck($res['password']) ? 'NEW NEEDED' : 'OK')); if (Password::passwordRehashCheck($res['password'])) { // update password hash to new one now - $q = "UPDATE edit_user " - . "SET password = '" . $this->db->dbEscapeString(Password::passwordSet($this->password)) - . "' WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); + $q = <<db->dbExecParams($q, [ + Password::passwordSet($this->password), + $res['edit_user_id'] + ]); } // normal user processing // set class var and session var - $this->euid = (int)$res['edit_user_id']; - $this->ecuid = (string)$res['cuid']; - $this->ecuuid = (string)$res['cuuid']; - $this->session->setMany([ - 'EUID' => $this->euid, - 'ECUID' => $this->ecuid, - 'ECUUID' => $this->ecuuid, - ]); - // check if user is okay - $this->loginCheckPermissions(); - if ($this->login_error == 0) { - if ( - !empty($res['login_user_id']) && - !empty($this->username) && !empty($this->password) - ) { - $q = "UPDATE edit_user SET " - . "login_user_id_last_revalidate = NOW() " - . "WHERE edit_user_id = " . $this->euid; - $this->db->dbExec($q); - } - $locale = $res['locale'] ?? 'en'; - $encoding = $res['encoding'] ?? 'UTF-8'; - $this->session->setMany([ - // now set all session vars and read page permissions - 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']), - 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']), - // general info for user logged in - 'USER_NAME' => $res['username'], - 'ADMIN' => $res['admin'], - 'GROUP_NAME' => $res['edit_group_name'], - 'USER_ACL_LEVEL' => $res['user_level'], - 'USER_ACL_TYPE' => $res['user_type'], - 'USER_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['user_additional_acl']), - 'GROUP_ACL_LEVEL' => $res['group_level'], - 'GROUP_ACL_TYPE' => $res['group_type'], - 'GROUP_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['group_additional_acl']), - // deprecated TEMPLATE setting - 'TEMPLATE' => $res['template'] ? $res['template'] : '', - 'HEADER_COLOR' => !empty($res['second_header_color']) ? - $res['second_header_color'] : - $res['first_header_color'], - // LANGUAGE/LOCALE/ENCODING: - 'LANG' => $locale, - 'DEFAULT_CHARSET' => $encoding, - 'DEFAULT_LOCALE' => $locale . '.' . strtoupper($encoding), - 'DEFAULT_LANG' => $locale . '_' . strtolower(str_replace('-', '', $encoding)) - ]); - // missing # before, this is for legacy data, will be deprecated - if ( - !empty($this->session->get('HEADER_COLOR')) && - preg_match("/^[\dA-Fa-f]{6,8}$/", $this->session->get('HEADER_COLOR')) - ) { - $this->session->set('HEADER_COLOR', '#' . $this->session->get('HEADER_COLOR')); - } - // TODO: make sure that header color is valid: - // # + 6 hex - // # + 8 hex (alpha) - // rgb(), rgba(), hsl(), hsla() - // rgb: nnn.n for each - // hsl: nnn.n for first, nnn.n% for 2nd, 3rd - // Check\Colors::validateColor() - // reset any login error count for this user - if ($res['login_error_count'] > 0) { - $q = "UPDATE edit_user " - . "SET login_error_count = 0, login_error_date_last = NULL, " - . "login_error_date_first = NULL " - . "WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); - } - $edit_page_ids = []; - $pages = []; - $pages_acl = []; - // set pages access - $q = "SELECT ep.edit_page_id, ep.cuid, epca.cuid AS content_alias_uid, " - . "ep.hostname, ep.filename, ep.name AS edit_page_name, " - . "ep.order_number AS edit_page_order, ep.menu, " - . "ep.popup, ep.popup_x, ep.popup_y, ep.online, ear.level, ear.type " - . "FROM edit_page ep " - . "LEFT JOIN edit_page epca ON (epca.edit_page_id = ep.content_alias_edit_page_id)" - . ", edit_page_access epa, edit_access_right ear " - . "WHERE ep.edit_page_id = epa.edit_page_id " - . "AND ear.edit_access_right_id = epa.edit_access_right_id " - . "AND epa.enabled = 1 AND epa.edit_group_id = " . $res["edit_group_id"] . " " - . "ORDER BY ep.order_number"; - while (is_array($res = $this->db->dbReturn($q))) { - // page id array for sub data readout - $edit_page_ids[$res['edit_page_id']] = $res['cuid']; - // create the array for pages - $pages[$res['cuid']] = [ - 'edit_page_id' => $res['edit_page_id'], - 'cuid' => $res['cuid'], - // for reference of content data on a differen page - 'content_alias_uid' => $res['content_alias_uid'], - 'hostname' => $res['hostname'], - 'filename' => $res['filename'], - 'page_name' => $res['edit_page_name'], - 'order' => $res['edit_page_order'], - 'menu' => $res['menu'], - 'popup' => $res['popup'], - 'popup_x' => $res['popup_x'], - 'popup_y' => $res['popup_y'], - 'online' => $res['online'], - 'acl_level' => $res['level'], - 'acl_type' => $res['type'], - 'query' => [], - 'visible' => [] - ]; - // make reference filename -> level - $pages_acl[$res['filename']] = $res['level']; - } // for each page - // get the visible groups for all pages and write them to the pages - $q = "SELECT epvg.edit_page_id, name, flag " - . "FROM edit_visible_group evp, edit_page_visible_group epvg " - . "WHERE evp.edit_visible_group_id = epvg.edit_visible_group_id " - . "AND epvg.edit_page_id IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY epvg.edit_page_id"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['visible'][$res['name']] = $res['flag']; - } - // get the same for the query strings - $q = "SELECT eqs.edit_page_id, name, value, dynamic FROM edit_query_string eqs " - . "WHERE enabled = 1 AND edit_page_id " - . "IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY eqs.edit_page_id"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['query'][] = [ - 'name' => $res['name'], - 'value' => $res['value'], - 'dynamic' => $res['dynamic'] - ]; - } - // get the page content and add them to the page - $q = "SELECT epc.edit_page_id, epc.name, epc.uid, epc.order_number, " - . "epc.online, ear.level, ear.type " - . "FROM edit_page_content epc, edit_access_right ear " - . "WHERE epc.edit_access_right_id = ear.edit_access_right_id AND " - . "epc.edit_page_id IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY epc.order_number"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['content'][$res['uid']] = [ - 'name' => $res['name'], - 'uid' => $res['uid'], - 'online' => $res['online'], - 'order' => $res['order_number'], - // access name and level - 'acl_type' => $res['type'], - 'acl_level' => $res['level'] - ]; - } - // write back the pages data to the output array - $this->session->setMany([ - 'PAGES' => $pages, - 'PAGES_ACL_LEVEL' => $pages_acl, - ]); - // load the edit_access user rights - $q = "SELECT ea.edit_access_id, level, type, ea.name, " - . "ea.color, ea.uid, edit_default, ea.additional_acl " - . "FROM edit_access_user eau, edit_access_right ear, edit_access ea " - . "WHERE eau.edit_access_id = ea.edit_access_id " - . "AND eau.edit_access_right_id = ear.edit_access_right_id " - . "AND eau.enabled = 1 AND edit_user_id = " . $this->euid . " " - . "ORDER BY ea.name"; - $unit_access = []; - $eauid = []; - $unit_acl = []; - $unit_uid = []; - while (is_array($res = $this->db->dbReturn($q))) { - // read edit access data fields and drop them into the unit access array - $q_sub = "SELECT name, value " - . "FROM edit_access_data " - . "WHERE enabled = 1 AND edit_access_id = " . $res['edit_access_id']; - $ea_data = []; - while (is_array($res_sub = $this->db->dbReturn($q_sub))) { - $ea_data[$res_sub['name']] = $res_sub['value']; - } - // build master unit array - $unit_access[$res['edit_access_id']] = [ - 'id' => (int)$res['edit_access_id'], - 'acl_level' => $res['level'], - 'acl_type' => $res['type'], - 'name' => $res['name'], - 'uid' => $res['uid'], - 'color' => $res['color'], - 'default' => $res['edit_default'], - 'additional_acl' => Json::jsonConvertToArray($res['additional_acl']), - 'data' => $ea_data - ]; - // set the default unit - if ($res['edit_default']) { - $this->session->set('UNIT_DEFAULT', (int)$res['edit_access_id']); - } - $unit_uid[$res['uid']] = (int)$res['edit_access_id']; - // sub arrays for simple access - array_push($eauid, $res['edit_access_id']); - $unit_acl[$res['edit_access_id']] = $res['level']; - } - $this->session->setMany([ - 'UNIT_UID' => $unit_uid, - 'UNIT' => $unit_access, - 'UNIT_ACL_LEVEL' => $unit_acl, - 'EAID' => $eauid, - ]); - } // user has permission to THIS page + $this->loginSetEditUserUidData($res); + // set the last login time stamp for normal login only (not for reauthenticate) + $this->db->dbExecParams(<<edit_user_id]); + // set the session vars + $this->loginSetSession($res); } // user was not enabled or other login error - if ($this->login_error && is_array($res)) { - $login_error_date_first = ''; - if ($res['login_error_count'] == 0) { - $login_error_date_first = ", login_error_date_first = NOW()"; - } - // update login error count for this user - $q = "UPDATE edit_user " - . "SET login_error_count = login_error_count + 1, " - . "login_error_date_last = NOW() " . $login_error_date_first . " " - . "WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); - // totally lock the user if error max is reached - if ( - $this->max_login_error_count != -1 && - $res['login_error_count'] + 1 > $this->max_login_error_count - ) { - // do some alert reporting in case this error is too big - // if strict is set, lock this user - // this needs manual unlocking by an admin user - if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) { - $q = "UPDATE edit_user SET locked = 1 WHERE edit_user_id = " . $res['edit_user_id']; - } - } - } + // check for login error and write to the user + $this->loginWriteLoginError($res); // if there was an login error, show login screen if ($this->login_error) { // reset the perm var, to confirm logout @@ -1135,6 +1055,402 @@ class Login } } + /** + * load user data and all connect4ed settings + * + * @param ?string $edit_user_cuuid for re-auth + * @return array|false + */ + private function loginLoadUserData(?string $edit_user_cuuid = null): array|false + { + $q = <<= eu.lock_until) + ) + AND ( + eu.lock_after IS NULL + OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after) + ) + ) THEN 0::INT ELSE 1::INT END locked_period, + -- enabled + eu.enabled, eu.deleted, + -- for checks only + eu.login_user_id, + -- login id validation + CASE WHEN ( + ( + eu.login_user_id_valid_from IS NULL + OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from) + ) + AND ( + eu.login_user_id_valid_until IS NULL + OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until) + ) + ) THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, + -- check if user must login + CASE WHEN + eu.login_user_id_revalidate_after IS NOT NULL + AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL + AND (eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after)::DATE + <= NOW()::DATE + THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, + eu.login_user_id_locked, + -- language + el.short_name AS locale, el.iso_name AS encoding, + -- levels + eareu.level AS user_level, eareu.type AS user_type, + eareg.level AS group_level, eareg.type AS group_type, + -- colors + first.header_color AS first_header_color, + second.header_color AS second_header_color, second.template + FROM edit_user eu + LEFT JOIN edit_scheme second ON + (second.edit_scheme_id = eu.edit_scheme_id AND second.enabled = 1), + edit_language el, edit_group eg, + edit_access_right eareu, + edit_access_right eareg, + edit_scheme first + WHERE first.edit_scheme_id = eg.edit_scheme_id + AND eu.edit_group_id = eg.edit_group_id + AND eu.edit_language_id = el.edit_language_id + AND eu.edit_access_right_id = eareu.edit_access_right_id + AND eg.edit_access_right_id = eareg.edit_access_right_id + AND {SEARCH_QUERY} + SQL; + $params = []; + $replace_string = ''; + // if login is OK and we have edit_user_cuuid as parameter, then this is internal re-auth + // else login_user_id OR password must be given + if (!empty($edit_user_cuuid)) { + $replace_string = 'eu.cuuid = $1'; + $params = [$this->edit_user_cuuid]; + } elseif (!empty($this->login_user_id) && empty($this->username)) { + // check with login id if set and NO username + $replace_string = 'eu.login_user_id = $1'; + $params = [$this->login_user_id]; + } else { + // password match is done in script, against old plain or new blowfish encypted + $replace_string = 'LOWER(username) = $1'; + $params = [strtolower($this->username)]; + } + $q = str_replace( + '{SEARCH_QUERY}', + $replace_string, + $q + ); + // reset any query data that might exist + $this->db->dbCacheReset($q, $params, show_warning:false); + // never cache return data + $res = $this->db->dbReturnParams($q, $params, $this->db::NO_CACHE); + // query was not run successful + if (!empty($this->db->dbGetLastError())) { + $this->login_error = 1009; + $this->permission_okay = false; + return false; + } elseif (!is_array($res)) { + // username is wrong, but we throw for wrong username + // and wrong password the same error + // unless with have edit user cuuid set then we run an general ACL error + if (empty($edit_user_cuuid)) { + $this->login_error = 1010; + } else { + $this->login_error = 1011; + } + $this->permission_okay = false; + return false; + } + return $res; + } + + // MARK: login set all session variables + + /** + * set all the _SESSION variables + * + * @param array $res user data loaded query result + * @return void + */ + private function loginSetSession(array $res): void + { + // user has permission to THIS page + if ($this->login_error != 0) { + return; + } + // set the dit group id + $edit_group_id = $res["edit_group_id"]; + $edit_user_id = (int)$res['edit_user_id']; + // update last revalidate flag + if ( + !empty($res['login_user_id']) && + !empty($this->username) && !empty($this->password) + ) { + $q = <<db->dbExecParams($q, [$edit_user_id]); + } + $locale = $res['locale'] ?? 'en'; + $encoding = $res['encoding'] ?? 'UTF-8'; + $this->session->setMany([ + // now set all session vars and read page permissions + // DEBUG flag is deprecated + // 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']), + // 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']), + // login timestamp + 'LOGIN_LAST_AUTH_RESYNC' => time(), + // current forced logout counter + 'LOGIN_FORCE_LOGOUT' => $res['force_logout'], + // general info for user logged in + 'LOGIN_USER_NAME' => $res['username'], + 'LOGIN_EMAIL' => $res['email'], + 'LOGIN_ADMIN' => $res['admin'], + 'LOGIN_GROUP_NAME' => $res['edit_group_name'], + 'LOGIN_USER_ACL_LEVEL' => $res['user_level'], + 'LOGIN_USER_ACL_TYPE' => $res['user_type'], + 'LOGIN_USER_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['user_additional_acl']), + 'LOGIN_GROUP_ACL_LEVEL' => $res['group_level'], + 'LOGIN_GROUP_ACL_TYPE' => $res['group_type'], + 'LOGIN_GROUP_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['group_additional_acl']), + // deprecated TEMPLATE setting + // 'TEMPLATE' => $res['template'] ? $res['template'] : '', + 'LOGIN_HEADER_COLOR' => !empty($res['second_header_color']) ? + $res['second_header_color'] : + $res['first_header_color'], + // LANGUAGE/LOCALE/ENCODING: + // 'LOGIN_LANG' => $locale, + 'DEFAULT_CHARSET' => $encoding, + 'DEFAULT_LOCALE' => $locale . '.' . strtoupper($encoding), + 'DEFAULT_LANG' => $locale . '_' . strtolower(str_replace('-', '', $encoding)) + ]); + // missing # before, this is for legacy data, will be deprecated + if ( + !empty($this->session->get('LOGIN_HEADER_COLOR')) && + preg_match("/^[\dA-Fa-f]{6,8}$/", $this->session->get('LOGIN_HEADER_COLOR')) + ) { + $this->session->set('LOGIN_HEADER_COLOR', '#' . $this->session->get('LOGIN_HEADER_COLOR')); + } + // TODO: make sure that header color is valid: + // # + 6 hex + // # + 8 hex (alpha) + // rgb(), rgba(), hsl(), hsla() + // rgb: nnn.n for each + // hsl: nnn.n for first, nnn.n% for 2nd, 3rd + // Check\Colors::validateColor() + // reset any login error count for this user + if ($res['login_error_count'] > 0) { + $q = <<db->dbExecParams($q, [$edit_user_id]); + } + $edit_page_ids = []; + $pages = []; + $pages_lookup = []; + $pages_acl = []; + // set pages access + $q = <<db->dbReturnParams($q, [$edit_group_id]))) { + // page id array for sub data readout + $edit_page_ids[$res['edit_page_id']] = $res['cuid']; + // create the array for pages + $pages[$res['cuid']] = [ + 'edit_page_id' => $res['edit_page_id'], + 'cuid' => $res['cuid'], + 'cuuid' => $res['cuuid'], + // for reference of content data on a differen page + 'content_alias_uid' => $res['content_alias_uid'], + 'hostname' => $res['hostname'], + 'filename' => $res['filename'], + 'page_name' => $res['edit_page_name'], + 'order' => $res['edit_page_order'], + 'menu' => $res['menu'], + 'popup' => $res['popup'], + 'popup_x' => $res['popup_x'], + 'popup_y' => $res['popup_y'], + 'online' => $res['online'], + 'acl_level' => $res['level'], + 'acl_type' => $res['type'], + 'query' => [], + 'visible' => [] + ]; + $pages_lookup[$res['filename']] = $res['cuid']; + // make reference filename -> level + $pages_acl[$res['filename']] = $res['level']; + } // for each page + // edit page id params + $params = ['{' . join(',', array_keys($edit_page_ids)) . '}']; + // get the visible groups for all pages and write them to the pages + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['visible'][$res['name']] = $res['flag']; + } + // get the same for the query strings + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['query'][] = [ + 'name' => $res['name'], + 'value' => $res['value'], + 'dynamic' => $res['dynamic'] + ]; + } + // get the page content and add them to the page + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['content'][$res['uid']] = [ + 'name' => $res['name'], + 'uid' => $res['uid'], + 'cuid' => $res['cuid'], + 'cuuid' => $res['cuuid'], + 'online' => $res['online'], + 'order' => $res['order_number'], + // access name and level + 'acl_type' => $res['type'], + 'acl_level' => $res['level'] + ]; + } + // write back the pages data to the output array + $this->session->setMany([ + 'LOGIN_PAGES' => $pages, + 'LOGIN_PAGES_LOOKUP' => $pages_lookup, + 'LOGIN_PAGES_ACL_LEVEL' => $pages_acl, + ]); + // load the edit_access user rights + $q = <<db->dbReturnParams($q, [$edit_user_id]))) { + // read edit access data fields and drop them into the unit access array + $q_sub = <<db->dbReturnParams($q_sub, [$res['edit_access_id']]))) { + $ea_data[$res_sub['name']] = $res_sub['value']; + } + // build master unit array + $unit_access_cuid[$res['cuid']] = [ + 'id' => (int)$res['edit_access_id'], // DEPRECATED + 'cuuid' => $res['cuuid'], + 'acl_level' => $res['level'], + 'acl_type' => $res['type'], + 'name' => $res['name'], + 'uid' => $res['uid'], + 'color' => $res['color'], + 'default' => $res['edit_default'], + 'additional_acl' => Json::jsonConvertToArray($res['additional_acl']), + 'data' => $ea_data + ]; + // LEGACY LOOKUP + $unit_access_eaid[$res['edit_access_id']] = [ + 'cuid' => $res['cuid'], + ]; + // set the default unit + $this->session->setMany([ + 'LOGIN_UNIT_DEFAULT_EAID' => null, + 'LOGIN_UNIT_DEFAULT_EACUID' => null, + ]); + if ($res['edit_default']) { + $this->session->set('LOGIN_UNIT_DEFAULT_EAID', (int)$res['edit_access_id']); // DEPRECATED + $this->session->set('LOGIN_UNIT_DEFAULT_EACUID', (int)$res['cuid']); + } + $unit_uid_lookup[$res['uid']] = $res['edit_access_id']; // DEPRECATED + $unit_cuid_lookup[$res['uid']] = $res['cuid']; + // sub arrays for simple access + array_push($eaid, $res['edit_access_id']); + array_push($eacuid, $res['cuid']); + $unit_acl[$res['cuid']] = $res['level']; + } + $this->session->setMany([ + 'LOGIN_UNIT_UID' => $unit_uid_lookup, // DEPRECATED + 'LOGIN_UNIT_CUID' => $unit_cuid_lookup, + 'LOGIN_UNIT' => $unit_access_cuid, + 'LOGIN_UNIT_LEGACY' => $unit_access_eaid, // DEPRECATED + 'LOGIN_UNIT_ACL_LEVEL' => $unit_acl, + 'LOGIN_EAID' => $eaid, // DEPRECATED + 'LOGIN_EACUID' => $eacuid, + ]); + } + + // MARK: login set ACL + /** * sets all the basic ACLs * init set the basic acl the user has, based on the following rules @@ -1160,21 +1476,23 @@ class Login return; } // username (login), group name - $this->acl['user_name'] = $_SESSION['USER_NAME']; - $this->acl['group_name'] = $_SESSION['GROUP_NAME']; + $this->acl['user_name'] = $_SESSION['LOGIN_USER_NAME']; + $this->acl['group_name'] = $_SESSION['LOGIN_GROUP_NAME']; + // DEPRECATED + $this->acl['euid'] = $_SESSION['LOGIN_EUID']; // edit user cuid - $this->acl['ecuid'] = $_SESSION['ECUID']; - $this->acl['ecuuid'] = $_SESSION['ECUUID']; + $this->acl['eucuid'] = $_SESSION['LOGIN_EUCUID']; + $this->acl['eucuuid'] = $_SESSION['LOGIN_EUCUUID']; // set additional acl $this->acl['additional_acl'] = [ - 'user' => $_SESSION['USER_ADDITIONAL_ACL'], - 'group' => $_SESSION['GROUP_ADDITIONAL_ACL'], + 'user' => $_SESSION['LOGIN_USER_ADDITIONAL_ACL'], + 'group' => $_SESSION['LOGIN_GROUP_ADDITIONAL_ACL'], ]; // we start with the default acl $this->acl['base'] = $this->default_acl_level; // set admin flag and base to 100 - if (!empty($_SESSION['ADMIN'])) { + if (!empty($_SESSION['LOGIN_ADMIN'])) { $this->acl['admin'] = 1; $this->acl['base'] = 100; } else { @@ -1182,80 +1500,88 @@ class Login // now go throw the flow and set the correct ACL // user > page > group // group ACL 0 - if ($_SESSION['GROUP_ACL_LEVEL'] != -1) { - $this->acl['base'] = (int)$_SESSION['GROUP_ACL_LEVEL']; + if ($_SESSION['LOGIN_GROUP_ACL_LEVEL'] != -1) { + $this->acl['base'] = (int)$_SESSION['LOGIN_GROUP_ACL_LEVEL']; } // page ACL 1 if ( - isset($_SESSION['PAGES_ACL_LEVEL'][$this->page_name]) && - $_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1 + isset($_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]) && + $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name] != -1 ) { - $this->acl['base'] = (int)$_SESSION['PAGES_ACL_LEVEL'][$this->page_name]; + $this->acl['base'] = (int)$_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]; } // user ACL 2 - if ($_SESSION['USER_ACL_LEVEL'] != -1) { - $this->acl['base'] = (int)$_SESSION['USER_ACL_LEVEL']; + if ($_SESSION['LOGIN_USER_ACL_LEVEL'] != -1) { + $this->acl['base'] = (int)$_SESSION['LOGIN_USER_ACL_LEVEL']; } } - $this->session->set('BASE_ACL_LEVEL', $this->acl['base']); + $this->session->set('LOGIN_BASE_ACL_LEVEL', $this->acl['base']); // set the current page acl // start with base acl // set group if not -1, overrides default // set page if not -1, overrides group set $this->acl['page'] = $this->acl['base']; - if ($_SESSION['GROUP_ACL_LEVEL'] != -1) { - $this->acl['page'] = $_SESSION['GROUP_ACL_LEVEL']; + if ($_SESSION['LOGIN_GROUP_ACL_LEVEL'] != -1) { + $this->acl['page'] = $_SESSION['LOGIN_GROUP_ACL_LEVEL']; } if ( - isset($_SESSION['PAGES_ACL_LEVEL'][$this->page_name]) && - $_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1 + isset($_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]) && + $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name] != -1 ) { - $this->acl['page'] = $_SESSION['PAGES_ACL_LEVEL'][$this->page_name]; + $this->acl['page'] = $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]; } + $this->acl['pages_detail'] = $_SESSION['LOGIN_PAGES']; + $this->acl['pages_lookup_cuid'] = $_SESSION['LOGIN_PAGES_LOOKUP']; - $this->acl['unit_id'] = null; + $this->acl['unit_cuid'] = null; $this->acl['unit_name'] = null; $this->acl['unit_uid'] = null; $this->acl['unit'] = []; + $this->acl['unit_legacy'] = []; $this->acl['unit_detail'] = []; // PER ACCOUNT (UNIT/edit access)-> - foreach ($_SESSION['UNIT'] as $ea_id => $unit) { + foreach ($_SESSION['LOGIN_UNIT'] as $ea_cuid => $unit) { // if admin flag is set, all units are set to 100 if (!empty($this->acl['admin'])) { - $this->acl['unit'][$ea_id] = $this->acl['base']; + $this->acl['unit'][$ea_cuid] = $this->acl['base']; } else { if ($unit['acl_level'] != -1) { - $this->acl['unit'][$ea_id] = $unit['acl_level']; + $this->acl['unit'][$ea_cuid] = $unit['acl_level']; } else { - $this->acl['unit'][$ea_id] = $this->acl['base']; + $this->acl['unit'][$ea_cuid] = $this->acl['base']; } } + // legacy + $this->acl['unit_legacy'][$unit['id']] = $this->acl['unit'][$ea_cuid]; // detail name/level set - $this->acl['unit_detail'][$ea_id] = [ + $this->acl['unit_detail'][$ea_cuid] = [ + 'id' => $unit['id'], 'name' => $unit['name'], 'uid' => $unit['uid'], - 'level' => $this->default_acl_list[$this->acl['unit'][$ea_id]]['name'] ?? -1, + 'cuuid' => $unit['cuuid'], + 'level' => $this->default_acl_list[$this->acl['unit'][$ea_cuid]]['name'] ?? -1, + 'level_number' => $this->acl['unit'][$ea_cuid], 'default' => $unit['default'], 'data' => $unit['data'], 'additional_acl' => $unit['additional_acl'] ]; // set default if (!empty($unit['default'])) { - $this->acl['unit_id'] = $unit['id']; + $this->acl['unit_cuid'] = $ea_cuid; $this->acl['unit_name'] = $unit['name']; $this->acl['unit_uid'] = $unit['uid']; } } // flag if to show extra edit access drop downs (because user has multiple groups assigned) - if (count($_SESSION['UNIT']) > 1) { + if (count($_SESSION['LOGIN_UNIT']) > 1) { $this->acl['show_ea_extra'] = true; } else { $this->acl['show_ea_extra'] = false; } // set the default edit access - $this->acl['default_edit_access'] = $_SESSION['UNIT_DEFAULT'] ?? null; + $this->acl['default_edit_access'] = $_SESSION['LOGIN_UNIT_DEFAULT_EACUID']; // integrate the type acl list, but only for the keyword -> level $this->acl['min'] = $this->default_acl_list_type; // set the full acl list too (lookup level number and get level data) @@ -1264,6 +1590,8 @@ class Login // $this->debug('ACL', $this->print_ar($this->acl)); } + // MARK: login set locale + /** * set locale * if invalid, set to empty string @@ -1323,6 +1651,8 @@ class Login ]; } + // MARK: password handling + /** * checks if the password is in a valid format * @@ -1461,6 +1791,8 @@ class Login $this->writeEditLog($event, $data, $this->login_error, $this->pw_username); } + // MARK: set HTML login page + /** * creates the login html part if no permission (error) is set * this does not print anything yet @@ -1489,6 +1821,10 @@ class Login $this->login_template['strings']['LANGUAGE'] = $locales['lang'] ?? 'en'; // if password change is okay + // TODO: this should be a "forgot" password + // -> input email + // -> send to user with link + token + // -> validate token -> show change password if ($this->password_change) { $html_string_password_change = $this->login_template['password_change']; @@ -1505,14 +1841,14 @@ class Login // print error messagae if ($this->login_error) { $html_string_password_change = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg($this->login_error) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg($this->login_error)], $html_string_password_change ); } else { $html_string_password_change = str_replace( - '{ERROR_MSG}', - '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-hidden', ''], $html_string_password_change ); } @@ -1520,8 +1856,11 @@ class Login if ($this->change_password && !$this->password_change_ok) { $html_string_password_change = str_replace( '{PASSWORD_CHANGE_SHOW}', - '', + << + ShowHideDiv('login_pw_change_div'); + + HTML, $html_string_password_change ); } else { @@ -1548,18 +1887,22 @@ class Login // print error messagae if ($this->login_error) { $html_string = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg($this->login_error) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg($this->login_error)], $html_string ); } elseif ($this->password_change_ok && $this->password_change) { $html_string = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg(300) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg(300)], $html_string ); } else { - $html_string = str_replace('{ERROR_MSG}', '
', $html_string); + $html_string = str_replace( + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-hidden', ''], + $html_string + ); } // create the replace array context @@ -1570,6 +1913,8 @@ class Login return $html_string; } + // MARK: logout call + /** * last function called, writes log and prints out error msg and * exists script if permission 0 @@ -1591,11 +1936,15 @@ class Login $event = 'No Permission'; } // prepare for log - if ($this->euid) { + if ($this->edit_user_cuuid) { // get user from user table - $q = "SELECT username FROM edit_user WHERE edit_user_id = " . $this->euid; + $q = <<db->dbReturnRow($q))) { + if (is_array($res = $this->db->dbReturnRowParams($q, [$this->edit_user_cuuid]))) { $username = $res['username']; } } // if euid is set, get username (or try) @@ -1610,6 +1959,8 @@ class Login } } + // MARK: set template for login page + /** * checks if there are external templates, if not uses internal fallback ones * @@ -1636,39 +1987,93 @@ class Login 'NEW_PASSWORD' => $this->l->__('New Password'), 'NEW_PASSWORD_CONFIRM' => $this->l->__('New Password confirm'), 'CLOSE' => $this->l->__('Close'), - 'JS_SHOW_HIDE' => "function ShowHideDiv(id) { " - . "element = document.getElementById(id); " - . "if (element.className == 'visible' || !element.className) element.className = 'hidden'; " - . "else element.className = 'visible'; }", - 'PASSWORD_CHANGE_BUTTON' => '' + 'JS_SHOW_HIDE' => << str_replace( + '{PASSWORD_CHANGE_BUTTON_VALUE}', + $strings['PASSWORD_CHANGE_BUTTON_VALUE'], + // phpcs:disable Generic.Files.LineLength + << + HTML + // phpcs:enable Generic.Files.LineLength + ), ]); - // TODO: submit or JS to set target page as ajax call - // NOTE: for the HTML block I ignore line lengths - // phpcs:disable + // phpcs:disable Generic.Files.LineLength $this->login_template['password_change'] = << - - - - - - - - - - - - -

{TITLE_PASSWORD_CHANGE}

{ERROR_MSG}
{USERNAME}
{OLD_PASSWORD}
{NEW_PASSWORD}
{NEW_PASSWORD_CONFIRM}
-
+ {PASSWORD_CHANGE_SHOW} HTML; - // phpcs:enable + // phpcs:enable Generic.Files.LineLength } if ($this->password_forgot) { + // TODO: create a password forget request flow } if (!$this->password_change && !$this->password_forgot) { $strings = array_merge($strings, [ @@ -1687,71 +2092,162 @@ HTML; } // now check templates - // TODO: submit or JS to set target page as ajax call if (!$this->login_template['template']) { + // phpcs:disable Generic.Files.LineLength $this->login_template['template'] = << -{HTML_TITLE} - - + {LOGOUT_TARGET} -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-

{TITLE}

-
 
- {ERROR_MSG} -
{USERNAME}
{PASSWORD}
- - {PASSWORD_CHANGE_BUTTON} -
-

-
 
-{PASSWORD_CHANGE_DIV} +
HTML; + // phpcs:enable Generic.Files.LineLength } } @@ -1883,17 +2379,19 @@ HTML; } $q = <<db->dbExecParams( @@ -1904,13 +2402,14 @@ HTML; ), [ // row 1 - empty($username) ? $this->session->get('USER_NAME') ?? '' : $username, - is_numeric($this->session->get('EUID')) ? - $this->session->get('EUID') : null, - is_string($this->session->get('ECUID')) ? - $this->session->get('ECUID') : null, - !empty($this->session->get('ECUUID')) && Uids::validateUuuidv4($this->session->get('ECUUID')) ? - $this->session->get('ECUUID') : null, + empty($username) ? $this->session->get('LOGIN_USER_NAME') ?? '' : $username, + is_numeric($this->session->get('LOGIN_EUID')) ? + $this->session->get('LOGIN_EUID') : null, + is_string($this->session->get('LOGIN_EUCUID')) ? + $this->session->get('LOGIN_EUCUID') : null, + !empty($this->session->get('LOGIN_EUCUUID')) && + Uids::validateUuuidv4($this->session->get('LOGIN_EUCUUID')) ? + $this->session->get('LOGIN_EUCUUID') : null, (string)$event, (string)$error, $data_write, @@ -1918,29 +2417,45 @@ HTML; (string)$this->page_name, // row 2 $_SERVER["REMOTE_ADDR"] ?? null, + Json::jsonConvertArrayTo([ + 'REMOTE_ADDR' => $_SERVER["REMOTE_ADDR"] ?? null, + 'HTTP_X_FORWARDED_FOR' => !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? + explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']) + : [], + 'CLIENT_IP' => !empty($_SERVER['CLIENT_IP']) ? + explode(',', $_SERVER['CLIENT_IP']) + : [], + ]), $_SERVER['HTTP_USER_AGENT'] ?? null, $_SERVER['HTTP_REFERER'] ?? null, $_SERVER['SCRIPT_FILENAME'] ?? null, $_SERVER['QUERY_STRING'] ?? null, + $_SERVER['REQUEST_SCHEME'] ?? null, $_SERVER['SERVER_NAME'] ?? null, - $_SERVER['HTTP_HOST'] ?? null, // row 3 - $_SERVER['HTTP_ACCEPT'] ?? null, - $_SERVER['HTTP_ACCEPT_CHARSET'] ?? null, - $_SERVER['HTTP_ACCEPT_ENCODING'] ?? null, + $_SERVER['HTTP_HOST'] ?? null, + Json::jsonConvertArrayTo([ + 'HTTP_ACCEPT' => $_SERVER['HTTP_ACCEPT'] ?? null, + 'HTTP_ACCEPT_CHARSET' => $_SERVER['HTTP_ACCEPT_CHARSET'] ?? null, + 'HTTP_ACCEPT_LANGUAGE' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null, + 'HTTP_ACCEPT_ENCODING' => $_SERVER['HTTP_ACCEPT_ENCODING'] ?? null, + ]), $this->session->getSessionId() !== '' ? $this->session->getSessionId() : null, // row 4 - $action_set['action'] ?? null, - $action_set['action_id'] ?? null, - $action_set['action_sub_id'] ?? null, - $action_set['action_yes'] ?? null, - $action_set['action_flag'] ?? null, - $action_set['action_menu'] ?? null, - $action_set['action_loaded'] ?? null, - $action_set['action_value'] ?? null, - $action_set['action_type'] ?? null, - $action_set['action_error'] ?? null, + // action data as JSONB + Json::jsonConvertArrayTo([ + 'action' => $action_set['action'] ?? null, + 'action_id' => $action_set['action_id'] ?? null, + 'action_sub_id' => $action_set['action_sub_id'] ?? null, + 'action_yes' => $action_set['action_yes'] ?? null, + 'action_flag' => $action_set['action_flag'] ?? null, + 'action_menu' => $action_set['action_menu'] ?? null, + 'action_loaded' => $action_set['action_loaded'] ?? null, + 'action_value' => $action_set['action_value'] ?? null, + 'action_type' => $action_set['action_type'] ?? null, + 'action_error' => $action_set['action_error'] ?? null, + ]) ], 'NULL' ); @@ -1967,7 +2482,7 @@ HTML; // **** PUBLIC INTERNAL // ************************************************************************* - // MARK: LOGIN CALL + // MARK: MASTER PUBLIC LOGIN CALL /** * Main call that needs to be run to actaully check for login @@ -2026,7 +2541,7 @@ HTML; $this->login_user_id, -1, $login_user_id_changed - ); + ) ?? ''; // flag unclean input data if ($login_user_id_changed > 0) { $this->login_user_id_unclear = true; @@ -2037,10 +2552,7 @@ HTML; } } // if there is none, there is none, saves me POST/GET check - $this->euid = (int)($this->session->get('EUID') ?? 0); - // TODO: allow load from cuid - // $this->ecuid = (string)($this->session->get('ECUID') ?? ''); - // $this->ecuuid = (string)($this->session->get('ECUUID') ?? ''); + $this->edit_user_cuuid = (string)($this->session->get('LOGIN_EUCUUID') ?? ''); // get login vars, are so, can't be changed // prepare // pass on vars to Object vars @@ -2049,11 +2561,11 @@ HTML; $this->password = $_POST['login_password'] ?? ''; $this->logout = $_POST['login_logout'] ?? ''; // password change vars - $this->change_password = $_POST['change_password'] ?? ''; - $this->pw_username = $_POST['pw_username'] ?? ''; - $this->pw_old_password = $_POST['pw_old_password'] ?? ''; - $this->pw_new_password = $_POST['pw_new_password'] ?? ''; - $this->pw_new_password_confirm = $_POST['pw_new_password_confirm'] ?? ''; + $this->change_password = $_POST['login_change_password'] ?? ''; + $this->pw_username = $_POST['login_pw_username'] ?? ''; + $this->pw_old_password = $_POST['login_pw_old_password'] ?? ''; + $this->pw_new_password = $_POST['login_pw_new_password'] ?? ''; + $this->pw_new_password_confirm = $_POST['login_pw_new_password_confirm'] ?? ''; // disallow user list for password change $this->pw_change_deny_users = ['admin']; // max login counts before error reporting @@ -2063,10 +2575,12 @@ HTML; // if username & password & !$euid start login $this->loginLoginUser(); - // checks if $euid given check if user is okay for that side + // checks if $euid given check if user is okay for that site $this->loginCheckPermissions(); - // logsout user + // logout user $this->loginLogoutUser(); + // set headers for enhanced security + $this->loginEnhanceHttpSecurity(); // ** LANGUAGE SET AFTER LOGIN ** $this->loginSetLocale(); // load translator @@ -2153,6 +2667,144 @@ HTML; return $this->login_is_ajax_page; } + /** + * Returns current set loginUserId or empty if unset + * + * @return string loginUserId or empty string for not set + */ + public function loginGetLoginUserId(): string + { + return $this->login_user_id; + } + + /** + * Returns GET/POST for where the loginUserId was set + * + * @return string GET or POST or empty string for not set + */ + public function loginGetLoginUserIdSource(): string + { + return $this->login_user_id_source; + } + + /** + * Returns unclear login user id state. If true then illegal characters + * where present in the loginUserId parameter + * + * @return bool False for clear, True if illegal characters found + */ + public function loginGetLoginUserIdUnclean(): bool + { + return $this->login_user_id_unclear; + } + + /** + * Return locale settings with + * locale + * domain + * encoding + * path + * + * empty string if not set + * + * @return array Locale settings + */ + public function loginGetLocale(): array + { + return $this->locale; + } + + /** + * return header color or null for not set + * + * @return string|null Header color in RGB hex with leading sharp + */ + public function loginGetHeaderColor(): ?string + { + return $this->session->get('LOGIN_HEADER_COLOR'); + } + + /** + * Return the current loaded list of pages the user can access + * + * @return array + */ + public function loginGetPages(): array + { + + return $this->session->get('LOGIN_PAGES'); + } + + /** + * Return the current loaded list of pages the user can access + * + * @return array + */ + public function loginGetPageLookupList(): array + { + return $this->session->get('LOGIN_PAGES_LOOKUP'); + } + + /** + * Check access to a file in the pages list + * + * @param string $filename File name to check + * @return bool True if page in list and anything other than None access, False if failed + */ + public function loginPageAccessAllowed(string $filename): bool + { + return ( + $this->session->get('LOGIN_PAGES')[ + $this->session->get('LOGIN_PAGES_LOOKUP')[$filename] ?? '' + ] ?? 0 + ) != 0 ? true : false; + } + + // MARK: logged in uid(pk)/eucuid/eucuuid + + /** + * Get the current set EUID (edit user id) + * + * @return string EUID as string + */ + public function loginGetEuid(): string + { + return (string)$this->edit_user_id; + } + + /** + * Get the current set EUCUID (edit user cuid) + * + * @return string EUCUID as string + */ + public function loginGetEuCuid(): string + { + return (string)$this->edit_user_cuid; + } + + /** + * Get the current set EUCUUID (edit user cuuid) + * + * @return string EUCUUID as string + * @deprecated Wrong name, use ->loginGetEuCuuid + */ + public function loginGetEcuuid(): string + { + return (string)$this->edit_user_cuuid; + } + + /** + * Get the current set EUCUUID (edit user cuuid) + * + * @return string EUCUUID as string + */ + public function loginGetEuCuuid(): string + { + return (string)$this->edit_user_cuuid; + } + + // MARK: get error messages + /** * returns the last set error code * @@ -2199,6 +2851,8 @@ HTML; return $string; } + // MARK: password checks + /** * Sets the minium length and checks on valid. * Current max length is 255 characters @@ -2251,6 +2905,8 @@ HTML; return $value; } + // MARK: max login count + /** * Set the maximum login errors a user can have before getting locked * if the user has the strict lock setting turned on @@ -2277,6 +2933,8 @@ HTML; return $this->max_login_error_count; } + // MARK: LGOUT USER + /** * if a user pressed on logout, destroyes session and unsets all global vars * @@ -2291,13 +2949,15 @@ HTML; // unset session vars set/used in this login $this->session->sessionDestroy(); // unset euid - $this->euid = null; - $this->ecuid = null; - $this->ecuuid = null; + $this->edit_user_id = null; + $this->edit_user_cuid = null; + $this->edit_user_cuuid = null; // then prints the login screen again $this->permission_okay = false; } + // MARK: logged in user permssion check + /** * for every page the user access this script checks if he is allowed to do so * @@ -2308,47 +2968,61 @@ HTML; // start with not allowed $this->permission_okay = false; // bail for no euid (no login) - if (empty($this->euid)) { + if (empty($this->edit_user_cuuid)) { return $this->permission_okay; } - // euid must match ecuid and ecuuid + // euid must match eucuid and eucuuid // bail for previous wrong page match, eg if method is called twice if ($this->login_error == 103) { return $this->permission_okay; } - $q = "SELECT ep.filename, eu.cuid, eu.cuuid, " - // base lock flags - . "eu.deleted, eu.enabled, eu.locked, " - // date based lock - . "CASE WHEN (" - . "(eu.lock_until IS NULL " - . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " - . "AND (eu.lock_after IS NULL " - . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" - . ") THEN 0::INT ELSE 1::INT END locked_period, " - // login id validation - . "login_user_id, " - . "CASE WHEN (" - . "(eu.login_user_id_valid_from IS NULL " - . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " - . "AND (eu.login_user_id_valid_until IS NULL " - . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" - . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " - // check if user must login - . "CASE WHEN eu.login_user_id_revalidate_after IS NOT NULL " - . "AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL " - . "AND eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after <= NOW()::DATE " - . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " - . "eu.login_user_id_locked " - // - . "FROM edit_page ep, edit_page_access epa, edit_group eg, edit_user eu " - . "WHERE ep.edit_page_id = epa.edit_page_id " - . "AND eg.edit_group_id = epa.edit_group_id " - . "AND eg.edit_group_id = eu.edit_group_id " - . "AND eu.edit_user_id = " . $this->euid . " " - . "AND ep.filename = '" . $this->page_name . "' " - . "AND eg.enabled = 1 AND epa.enabled = 1"; - $res = $this->db->dbReturnRow($q); + $q = <<= eu.lock_until) + ) + AND ( + eu.lock_after IS NULL + OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after) + ) + ) THEN 0::INT ELSE 1::INT END locked_period, + -- login id validation + login_user_id, + CASE WHEN ( + ( + eu.login_user_id_valid_from IS NULL + OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from) + ) + AND ( + eu.login_user_id_valid_until IS NULL + OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until) + ) + ) THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, + -- check if user must login + CASE WHEN + eu.login_user_id_revalidate_after IS NOT NULL + AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL + AND eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after <= NOW()::DATE + THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, + eu.login_user_id_locked + -- + FROM + edit_page ep, edit_page_access epa, edit_group eg, edit_user eu + WHERE + ep.edit_page_id = epa.edit_page_id + AND eg.edit_group_id = epa.edit_group_id + AND eg.edit_group_id = eu.edit_group_id + AND eg.enabled = 1 AND epa.enabled = 1 + AND eu.cuuid = $1 + AND ep.filename = $2 + SQL; + $res = $this->db->dbReturnRowParams($q, [$this->edit_user_cuuid, $this->page_name]); if (!is_array($res)) { $this->login_error = 109; return $this->permission_okay; @@ -2359,7 +3033,8 @@ HTML; (int)$res['enabled'], (int)$res['locked'], (int)$res['locked_period'], - (int)$res['login_user_id_locked'] + (int)$res['login_user_id_locked'], + (int)$res['force_logout'] ) ) { // errors set in method @@ -2382,13 +3057,8 @@ HTML; } else { $this->login_error = 103; } - // set ECUID - $this->ecuid = (string)$res['cuid']; - $this->ecuuid = (string)$res['cuuid']; - $this->session->setMany([ - 'ECUID' => $this->ecuid, - 'ECUUID' => $this->ecuuid, - ]); + // set all the internal vars + $this->loginSetEditUserUidData($res); // if called from public, so we can check if the permissions are ok return $this->permission_okay; } @@ -2403,6 +3073,8 @@ HTML; return $this->permission_okay; } + // MARK: ACL acess check + /** * Check if source (page, base) is matching to the given min access string * min access string must be valid access level string (eg read, mod, write) @@ -2503,24 +3175,68 @@ HTML; return (int)$this->default_acl_list_type[$type]; } + // MARK: edit access helpers + /** * checks if this edit access id is valid * * @param int|null $edit_access_id access id pk to check * @return bool true/false: if the edit access is not * in the valid list: false + * @deprecated Please switch to using edit access cuid check with ->loginCheckEditAccessCuid() */ public function loginCheckEditAccess(?int $edit_access_id): bool { if ($edit_access_id === null) { return false; } - if (array_key_exists($edit_access_id, $this->acl['unit'])) { + if (array_key_exists($edit_access_id, $this->acl['unit_legacy'])) { return true; } return false; } + /** + * check if this edit access cuid is valid + * + * @param string|null $cuid + * @return bool + */ + public function loginCheckEditAccessCuid(?string $cuid): bool + { + if ($cuid === null) { + return false; + } + if (array_key_exists($cuid, $this->acl['unit'])) { + return true; + } + return false; + } + + /** + * checks that the given edit access id is valid for this user + * return null if nothing set, or the edit access id + * + * @param string|null $cuid edit access cuid to check + * @return string|null same edit access cuid if ok + * or the default edit access id + * if given one is not valid + */ + public function loginCheckEditAccessValidCuid(?string $cuid): ?string + { + if ( + $cuid !== null && + is_array($this->session->get('LOGIN_UNIT')) && + !array_key_exists($cuid, $this->session->get('LOGIN_UNIT')) + ) { + $cuid = null; + if (!empty($this->session->get('LOGIN_UNIT_DEFAULT_EACUID'))) { + $cuid = $this->session->get('LOGIN_UNIT_DEFAULT_EACUID'); + } + } + return $cuid; + } + /** * checks that the given edit access id is valid for this user * return null if nothing set, or the edit access id @@ -2529,38 +3245,39 @@ HTML; * @return int|null same edit access id if ok * or the default edit access id * if given one is not valid + * @#deprecated Please switch to using edit access cuid check with ->loginCheckEditAccessValidCuid() */ public function loginCheckEditAccessId(?int $edit_access_id): ?int { if ( $edit_access_id !== null && - is_array($this->session->get('UNIT')) && - !array_key_exists($edit_access_id, $this->session->get('UNIT')) + is_array($this->session->get('LOGIN_UNIT_LEGACY')) && + !array_key_exists($edit_access_id, $this->session->get('LOGIN_UNIT_LEGACY')) ) { $edit_access_id = null; - if (is_numeric($this->session->get('UNIT_DEFAULT'))) { - $edit_access_id = (int)$this->session->get('UNIT_DEFAULT'); + if (!empty($this->session->get('LOGIN_UNIT_DEFAULT_EAID'))) { + $edit_access_id = (int)$this->session->get('LOGIN_UNIT_DEFAULT_EAID'); } } return $edit_access_id; } /** - * return a set entry from the UNIT session for an edit access_id + * return a set entry from the UNIT session for an edit access cuid * if not found return false * - * @param int $edit_access_id edit access id - * @param string|int $data_key key value to search for - * @return bool|string false for not found or string for found data + * @param string $cuid edit access cuid + * @param string|int $data_key key value to search for + * @return false|string false for not found or string for found data */ public function loginGetEditAccessData( - int $edit_access_id, + string $cuid, string|int $data_key - ): bool|string { - if (!isset($_SESSION['UNIT'][$edit_access_id]['data'][$data_key])) { + ): false|string { + if (!isset($_SESSION['LOGIN_UNIT'][$cuid]['data'][$data_key])) { return false; } - return $_SESSION['UNIT'][$edit_access_id]['data'][$data_key]; + return $_SESSION['LOGIN_UNIT'][$cuid]['data'][$data_key]; } /** @@ -2568,14 +3285,57 @@ HTML; * false on not found * * @param string $uid Edit Access UID to look for - * @return int|bool Either primary key in int or false in bool for not found + * @return int|false Either primary key in int or false in bool for not found + * @deprecated use loginGetEditAccessCuidFromUid */ - public function loginGetEditAccessIdFromUid(string $uid): int|bool + public function loginGetEditAccessIdFromUid(string $uid): int|false { - if (!isset($_SESSION['UNIT_UID'][$uid])) { + if (!isset($_SESSION['LOGIN_UNIT_UID'][$uid])) { return false; } - return (int)$_SESSION['UNIT_UID'][$uid]; + return (int)$_SESSION['LOGIN_UNIT_UID'][$uid]; + } + + /** + * Get the edit access UID from the edit access CUID + * + * @param string $uid + * @return int|false + */ + public function loginGetEditAccessCuidFromUid(string $uid): int|false + { + if (!isset($_SESSION['LOGIN_UNIT_CUID'][$uid])) { + return false; + } + return (int)$_SESSION['LOGIN_UNIT_CUID'][$uid]; + } + + /** + * Legacy lookup for edit access id to cuid + * + * @param int $id edit access id PK + * @return string|false edit access cuid or false if not found + */ + public function loginGetEditAccessCuidFromId(int $id): string|false + { + if (!isset($_SESSION['LOGIN_UNIT_LEGACY'][$id])) { + return false; + } + return (string)$_SESSION['LOGIN_UNIT_LEGACY'][$id]['cuid']; + } + + /** + * This is a Legacy lookup from the edit access id to cuid for further lookups in the normal list + * + * @param string $cuid edit access cuid + * @return int|false false on not found or edit access id PK + */ + public function loginGetEditAccessIdFromCuid(string $cuid): int|false + { + if (!isset($_SESSION['LOGIN_UNIT'][$cuid])) { + return false; + } + return $_SESSION['LOGIN_UNIT'][$cuid]['id']; } /** @@ -2591,6 +3351,8 @@ HTML; return false; } + // MARK: various basic login id checks + /** * Returns true if login button was pressed * @@ -2600,119 +3362,6 @@ HTML; { return empty($this->login) ? false : true; } - - /** - * Returns current set loginUserId or empty if unset - * - * @return string loginUserId or empty string for not set - */ - public function loginGetLoginUserId(): string - { - return $this->login_user_id; - } - - /** - * Returns GET/POST for where the loginUserId was set - * - * @return string GET or POST or empty string for not set - */ - public function loginGetLoginUserIdSource(): string - { - return $this->login_user_id_source; - } - - /** - * Returns unclear login user id state. If true then illegal characters - * where present in the loginUserId parameter - * - * @return bool False for clear, True if illegal characters found - */ - public function loginGetLoginUserIdUnclean(): bool - { - return $this->login_user_id_unclear; - } - - /** - * old name for loginGetEditAccessData - * - * @deprecated Use $login->loginGetEditAccessData() - * @param int $edit_access_id - * @param string|int $data_key - * @return bool|string - */ - public function loginSetEditAccessData( - int $edit_access_id, - string|int $data_key - ): bool|string { - return $this->loginGetEditAccessData($edit_access_id, $data_key); - } - - /** - * Return locale settings with - * locale - * domain - * encoding - * path - * - * empty string if not set - * - * @return array Locale settings - */ - public function loginGetLocale(): array - { - return $this->locale; - } - - /** - * return header color or null for not set - * - * @return string|null Header color in RGB hex with leading sharp - */ - public function loginGetHeaderColor(): ?string - { - return $this->session->get('HEADER_COLOR'); - } - - /** - * Return the current loaded list of pages the user can access - * - * @return array - */ - public function loginGetPages(): array - { - - return $this->session->get('PAGES'); - } - - /** - * Get the current set EUID (edit user id) - * - * @return string EUID as string - */ - public function loginGetEuid(): string - { - return (string)$this->euid; - } - - /** - * Get the current set ECUID (edit user cuid) - * - * @return string ECUID as string - */ - public function loginGetEcuid(): string - { - return (string)$this->ecuid; - } - - /** - * Get the current set ECUUID (edit user cuuid) - * - * @return string ECUUID as string - */ - public function loginGetEcuuid(): string - { - return (string)$this->ecuuid; - } } // __END__ diff --git a/www/lib/CoreLibs/ACL/LoginUserStatus.php b/www/lib/CoreLibs/ACL/LoginUserStatus.php new file mode 100644 index 00000000..ddc5a84f --- /dev/null +++ b/www/lib/CoreLibs/ACL/LoginUserStatus.php @@ -0,0 +1,68 @@ + + */ + public static function getMap() + { + return array_flip((new \ReflectionClass(static::class))->getConstants()); + } + + /** + * Returns the descriptive role names + * + * @return string[] + */ + public static function getNames() + { + + return array_keys((new \ReflectionClass(static::class))->getConstants()); + } + + /** + * Returns the numerical role values + * + * @return int[] + */ + public static function getValues() + { + return array_values((new \ReflectionClass(static::class))->getConstants()); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Admin/Backend.php b/www/lib/CoreLibs/Admin/Backend.php index 6f02cd25..cd64e2e1 100644 --- a/www/lib/CoreLibs/Admin/Backend.php +++ b/www/lib/CoreLibs/Admin/Backend.php @@ -289,7 +289,7 @@ class Backend * JSON, STRING/SERIEAL, BINARY/BZIP or ZLIB * @param string|null $db_schema [default=null] override target schema * @return void - * @deprecated Use $login->writeLog() and set action_set from ->adbGetActionSet() + * @deprecated Use $login->writeLog($event, $data, action_set:$cms->adbGetActionSet(), write_type:$write_type) */ public function adbEditLog( string $event = '', @@ -358,7 +358,7 @@ class Backend } $q = << */ @@ -63,6 +60,7 @@ class EditBase // smarty template engine (extended Translation version) $this->smarty = new \CoreLibs\Template\SmartyExtend( $l10n, + $log, $options['cache_id'] ?? '', $options['compile_id'] ?? '', ); @@ -78,7 +76,7 @@ class EditBase ); if ($this->form->mobile_phone) { echo "I am sorry, but this page cannot be viewed by a mobile phone"; - exit; + exit(1); } // $this->log->debug('POST', $this->log->prAr($_POST)); } @@ -415,8 +413,6 @@ class EditBase $elements[] = $this->form->formCreateElement('lock_until'); $elements[] = $this->form->formCreateElement('lock_after'); $elements[] = $this->form->formCreateElement('admin'); - $elements[] = $this->form->formCreateElement('debug'); - $elements[] = $this->form->formCreateElement('db_debug'); $elements[] = $this->form->formCreateElement('edit_language_id'); $elements[] = $this->form->formCreateElement('edit_scheme_id'); $elements[] = $this->form->formCreateElementListTable('edit_access_user'); @@ -540,8 +536,7 @@ class EditBase * builds the smarty content and runs smarty display output * * @return void - * @throws Exception - * @throws SmartyException + * @throws \Smarty\Exception */ public function editBaseRun( ?string $template_dir = null, diff --git a/www/lib/CoreLibs/Basic.php b/www/lib/CoreLibs/Basic.php index db57672a..1eb3410f 100644 --- a/www/lib/CoreLibs/Basic.php +++ b/www/lib/CoreLibs/Basic.php @@ -103,11 +103,7 @@ class Basic 'VIDEOS', 'DOCUMENTS', 'PDFS', 'BINARIES', 'ICONS', 'UPLOADS', 'CSV', 'JS', 'CSS', 'TABLE_ARRAYS', 'SMARTY', 'LANG', 'CACHE', 'TMP', 'LOG', 'TEMPLATES', 'TEMPLATES_C', 'DEFAULT_LANG', 'DEFAULT_ENCODING', 'DEFAULT_HASH', - 'DEFAULT_ACL_LEVEL', 'LOGOUT_TARGET', 'PASSWORD_CHANGE', 'AJAX_REQUEST_TYPE', - 'USE_PROTOTYPE', 'USE_SCRIPTACULOUS', 'USE_JQUERY', 'PAGE_WIDTH', - 'MASTER_TEMPLATE_NAME', 'PUBLIC_SCHEMA', 'TEST_SCHEMA', 'DEV_SCHEMA', - 'LIVE_SCHEMA', 'DB_CONFIG_NAME', 'DB_CONFIG', 'TARGET', 'DEBUG', - 'SHOW_ALL_ERRORS' + 'DB_CONFIG_NAME', 'DB_CONFIG', 'TARGET' ] as $constant ) { if (!defined($constant)) { @@ -1028,8 +1024,12 @@ class Basic */ public function __sha1Short(string $string, bool $use_sha = false): string { - trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__sha1Short()', E_USER_DEPRECATED); - return \CoreLibs\Create\Hash::__sha1Short($string, $use_sha); + trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::sha1Short() or ::__crc32b()', E_USER_DEPRECATED); + if ($use_sha) { + return \CoreLibs\Create\Hash::sha1Short($string); + } else { + return \CoreLibs\Create\Hash::__crc32b($string); + } } /** @@ -1044,8 +1044,8 @@ class Basic */ public function __hash(string $string, string $hash_type = 'adler32'): string { - trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__hash()', E_USER_DEPRECATED); - return \CoreLibs\Create\Hash::__hash($string, $hash_type); + trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::hash()', E_USER_DEPRECATED); + return \CoreLibs\Create\Hash::hash($string, $hash_type); } // *** HASH FUNCTIONS END diff --git a/www/lib/CoreLibs/Combined/ArrayHandler.php b/www/lib/CoreLibs/Combined/ArrayHandler.php index b1002949..dae3e1e9 100644 --- a/www/lib/CoreLibs/Combined/ArrayHandler.php +++ b/www/lib/CoreLibs/Combined/ArrayHandler.php @@ -525,6 +525,62 @@ class ArrayHandler { return array_diff($array, $remove); } + + /** + * From the array with key -> mixed values, + * return only the entries where the key matches the key given in the key list parameter + * + * key list is a list[string] + * if key list is empty, return array as is + * + * @param array $array + * @param array $key_list + * @return array + */ + public static function arrayReturnMatchingKeyOnly( + array $array, + array $key_list + ): array { + // on empty return as is + if (empty($key_list)) { + return $array; + } + return array_filter( + $array, + fn($key) => in_array($key, $key_list), + ARRAY_FILTER_USE_KEY + ); + } + + /** + * Modifieds the key of an array with a prefix and/or suffix and returns it with the original value + * does not change order in array + * + * @param array $in_array + * @param string $key_mod_prefix [default=''] key prefix string + * @param string $key_mod_suffix [default=''] key suffix string + * @return array + */ + public static function arrayModifyKey( + array $in_array, + string $key_mod_prefix = '', + string $key_mod_suffix = '' + ): array { + // skip if array is empty or neither prefix or suffix are set + if ( + $in_array == [] || + ($key_mod_prefix == '' && $key_mod_suffix == '') + ) { + return $in_array; + } + return array_combine( + array_map( + fn($key) => $key_mod_prefix . $key . $key_mod_suffix, + array_keys($in_array) + ), + array_values($in_array) + ); + } } // __END__ diff --git a/www/lib/CoreLibs/Combined/DateTime.php b/www/lib/CoreLibs/Combined/DateTime.php index d46619c5..b86bdee2 100644 --- a/www/lib/CoreLibs/Combined/DateTime.php +++ b/www/lib/CoreLibs/Combined/DateTime.php @@ -639,16 +639,26 @@ class DateTime * * @param string $start_date valid start date (y/m/d) * @param string $end_date valid end date (y/m/d) - * @param bool $return_named return array type, false (default), true for named - * @return array 0/overall, 1/weekday, 2/weekend + * @param bool $return_named [default=false] return array type, false (default), true for named + * @param bool $include_end_date [default=true] include end date in calc + * @param bool $exclude_start_date [default=false] include end date in calc + * @return array{0:int,1:int,2:int,3:bool}|array{overall:int,weekday:int,weekend:int,reverse:bool} + * 0/overall, 1/weekday, 2/weekend, 3/reverse */ public static function calcDaysInterval( string $start_date, string $end_date, - bool $return_named = false + bool $return_named = false, + bool $include_end_date = true, + bool $exclude_start_date = false ): array { // pos 0 all, pos 1 weekday, pos 2 weekend - $days = []; + $days = [ + 0 => 0, + 1 => 0, + 2 => 0, + 3 => false, + ]; // if anything invalid, return 0,0,0 try { $start = new \DateTime($start_date); @@ -659,19 +669,30 @@ class DateTime 'overall' => 0, 'weekday' => 0, 'weekend' => 0, + 'reverse' => false ]; } else { - return [0, 0, 0]; + return $days; } } // so we include the last day too, we need to add +1 second in the time - $end->setTime(0, 0, 1); - // if end date before start date, only this will be filled - $days[0] = $end->diff($start)->days; - $days[1] = 0; - $days[2] = 0; + // if start is before end, switch dates and flag + $days[3] = false; + if ($start > $end) { + $new_start = $end; + $end = $start; + $start = $new_start; + $days[3] = true; + } // get period for weekends/weekdays - $period = new \DatePeriod($start, new \DateInterval('P1D'), $end); + $options = 0; + if ($include_end_date) { + $options |= \DatePeriod::INCLUDE_END_DATE; + } + if ($exclude_start_date) { + $options |= \DatePeriod::EXCLUDE_START_DATE; + } + $period = new \DatePeriod($start, new \DateInterval('P1D'), $end, $options); foreach ($period as $dt) { $curr = $dt->format('D'); if ($curr == 'Sat' || $curr == 'Sun') { @@ -679,18 +700,80 @@ class DateTime } else { $days[1]++; } + $days[0]++; } if ($return_named === true) { return [ 'overall' => $days[0], 'weekday' => $days[1], 'weekend' => $days[2], + 'reverse' => $days[3], ]; } else { return $days; } } + /** + * wrapper for calcDaysInterval with numeric return only + * + * @param string $start_date valid start date (y/m/d) + * @param string $end_date valid end date (y/m/d) + * @param bool $include_end_date [default=true] include end date in calc + * @param bool $exclude_start_date [default=false] include end date in calc + * @return array{0:int,1:int,2:int,3:bool} + */ + public static function calcDaysIntervalNumIndex( + string $start_date, + string $end_date, + bool $include_end_date = true, + bool $exclude_start_date = false + ): array { + $values = self::calcDaysInterval( + $start_date, + $end_date, + false, + $include_end_date, + $exclude_start_date + ); + return [ + $values[0] ?? 0, + $values[1] ?? 0, + $values[2] ?? 0, + $values[3] ?? false, + ]; + } + + /** + * wrapper for calcDaysInterval with named return only + * + * @param string $start_date valid start date (y/m/d) + * @param string $end_date valid end date (y/m/d) + * @param bool $include_end_date [default=true] include end date in calc + * @param bool $exclude_start_date [default=false] include end date in calc + * @return array{overall:int,weekday:int,weekend:int,reverse:bool} + */ + public static function calcDaysIntervalNamedIndex( + string $start_date, + string $end_date, + bool $include_end_date = true, + bool $exclude_start_date = false + ): array { + $values = self::calcDaysInterval( + $start_date, + $end_date, + true, + $include_end_date, + $exclude_start_date + ); + return [ + 'overall' => $values['overall'] ?? 0, + 'weekday' => $values['weekday'] ?? 0, + 'weekend' => $values['weekend'] ?? 0, + 'reverse' => $values['reverse'] ?? false, + ]; + } + /** * check if a weekend day (sat/sun) is in the given date range * Can have time too, but is not needed @@ -705,6 +788,13 @@ class DateTime ): bool { $dd_start = new \DateTime($start_date); $dd_end = new \DateTime($end_date); + // flip if start is after end + if ($dd_start > $dd_end) { + $new_start = $dd_end; + $dd_end = $dd_start; + $dd_start = $new_start; + } + // if start > end, flip if ( // starts with a weekend $dd_start->format('N') >= 6 || diff --git a/www/lib/CoreLibs/Convert/Html.php b/www/lib/CoreLibs/Convert/Html.php index 2094dc55..ae057662 100644 --- a/www/lib/CoreLibs/Convert/Html.php +++ b/www/lib/CoreLibs/Convert/Html.php @@ -10,9 +10,16 @@ namespace CoreLibs\Convert; class Html { + /** @var int */ public const SELECTED = 0; + /** @var int */ public const CHECKED = 1; + // TODO: check for not valid htmlentites encoding + // as of PHP 8.4: https://www.php.net/manual/en/function.htmlentities.php + /** @#var array */ + // public const VALID_HTMLENT_ENCODINGS = []; + /** * full wrapper for html entities * @@ -22,14 +29,19 @@ class Html * encodes in UTF-8 * does not double encode * - * @param mixed $string string to html encode - * @param int $flags [default: ENT_QUOTES | ENT_HTML5] + * @param mixed $string string to html encode + * @param int $flags [default=ENT_QUOTES | ENT_HTML5] + * @param string $encoding [default=UTF-8] * @return mixed if string, encoded, else as is (eg null) */ - public static function htmlent(mixed $string, int $flags = ENT_QUOTES | ENT_HTML5): mixed - { + public static function htmlent( + mixed $string, + int $flags = ENT_QUOTES | ENT_HTML5, + string $encoding = 'UTF-8' + ): mixed { if (is_string($string)) { - return htmlentities($string, $flags, 'UTF-8', false); + // if not a valid encoding this will throw a warning and use UTF-8 + return htmlentities($string, $flags, $encoding, false); } return $string; } @@ -37,7 +49,7 @@ class Html /** * strips out all line breaks or replaced with given string * @param string $string string - * @param string $replace replace character, default ' ' + * @param string $replace [default=' '] replace character * @return string cleaned string without any line breaks */ public static function removeLB(string $string, string $replace = ' '): string diff --git a/www/lib/CoreLibs/Create/Hash.php b/www/lib/CoreLibs/Create/Hash.php index bf83afeb..9ed734db 100644 --- a/www/lib/CoreLibs/Create/Hash.php +++ b/www/lib/CoreLibs/Create/Hash.php @@ -10,9 +10,14 @@ namespace CoreLibs\Create; class Hash { + /** @var string default short hash -> deprecated use STANDARD_HASH_SHORT */ public const DEFAULT_HASH = 'adler32'; + /** @var string default long hash (40 chars) */ public const STANDARD_HASH_LONG = 'ripemd160'; + /** @var string default short hash (8 chars) */ public const STANDARD_HASH_SHORT = 'adler32'; + /** @var string this is the standard hash to use hashStd and hash (64 chars) */ + public const STANDARD_HASH = 'sha256'; /** * checks php version and if >=5.2.7 it will flip the string @@ -20,6 +25,7 @@ class Hash * hash returns false * preg_replace fails for older php version * Use __hash with crc32b or hash('crc32b', ...) for correct output + * For future short hashes use hashShort() instead * * @param string $string string to crc * @return string crc32b hash (old type) @@ -43,19 +49,31 @@ class Hash * replacement for __crc32b call * * @param string $string string to hash - * @param bool $use_sha use sha instead of crc32b (default false) + * @param bool $use_sha [default=false] use sha1 instead of crc32b * @return string hash of the string + * @deprecated use __crc32b() for drop in replacement with default, or sha1Short() for use sha true */ public static function __sha1Short(string $string, bool $use_sha = false): string { if ($use_sha) { - // return only the first 9 characters - return substr(hash('sha1', $string), 0, 9); + return self::sha1Short($string); } else { return self::__crc32b($string); } } + /** + * returns a short sha1 + * + * @param string $string string to hash + * @return string hash of the string + */ + public static function sha1Short(string $string): string + { + // return only the first 9 characters + return substr(hash('sha1', $string), 0, 9); + } + /** * replacemend for __crc32b call (alternate) * defaults to adler 32 @@ -63,34 +81,135 @@ class Hash * all that create 8 char long hashes * * @param string $string string to hash - * @param string $hash_type hash type (default adler32) + * @param string $hash_type [default=STANDARD_HASH_SHORT] hash type (default adler32) * @return string hash of the string + * @deprecated use hashShort() of short hashes with adler 32 or hash() for other hash types */ public static function __hash( string $string, - string $hash_type = self::DEFAULT_HASH + string $hash_type = self::STANDARD_HASH_SHORT + ): string { + return self::hash($string, $hash_type); + } + + /** + * check if hash type is valid, returns false if not + * + * @param string $hash_type + * @return bool + */ + public static function isValidHashType(string $hash_type): bool + { + if (!in_array($hash_type, hash_algos())) { + return false; + } + return true; + } + + /** + * check if hash hmac type is valid, returns false if not + * + * @param string $hash_hmac_type + * @return bool + */ + public static function isValidHashHmacType(string $hash_hmac_type): bool + { + if (!in_array($hash_hmac_type, hash_hmac_algos())) { + return false; + } + return true; + } + + /** + * creates a hash over string if any valid hash given. + * if no hash type set use sha256 + * + * @param string $string string to hash + * @param string $hash_type [default=STANDARD_HASH] hash type (default sha256) + * @return string hash of the string + */ + public static function hash( + string $string, + string $hash_type = self::STANDARD_HASH ): string { - // if not empty, check if in valid list if ( empty($hash_type) || !in_array($hash_type, hash_algos()) ) { - // fallback to default hash type if none set or invalid - $hash_type = self::DEFAULT_HASH; + // fallback to default hash type if empty or invalid + $hash_type = self::STANDARD_HASH; } return hash($hash_type, $string); } /** - * Wrapper function for standard long hashd + * creates a hash mac key + * + * @param string $string string to hash mac + * @param string $key key to use + * @param string $hash_type [default=STANDARD_HASH] + * @return string hash mac string + */ + public static function hashHmac( + string $string, + #[\SensitiveParameter] + string $key, + string $hash_type = self::STANDARD_HASH + ): string { + if ( + empty($hash_type) || + !in_array($hash_type, hash_hmac_algos()) + ) { + // fallback to default hash type if e or invalid + $hash_type = self::STANDARD_HASH; + } + return hash_hmac($hash_type, $string, $key); + } + + /** + * short hash with max length of 8, uses adler32 + * + * @param string $string string to hash + * @return string hash of the string + */ + public static function hashShort(string $string): string + { + return hash(self::STANDARD_HASH_SHORT, $string); + } + + /** + * Wrapper function for standard long hash + * + * @param string $string String to be hashed + * @return string Hashed string + * @deprecated use hashLong() + */ + public static function __hashLong(string $string): string + { + return self::hashLong($string); + } + + /** + * Wrapper function for standard long hash, uses ripmd160 * * @param string $string String to be hashed * @return string Hashed string */ - public static function __hashLong(string $string): string + public static function hashLong(string $string): string { return hash(self::STANDARD_HASH_LONG, $string); } + + /** + * create standard hash basd on STANDAR_HASH, currently sha256 + * + * @param string $string string in + * @return string hash of the string + */ + public static function hashStd(string $string): string + { + return self::hash($string, self::STANDARD_HASH); + } } // __END__ diff --git a/www/lib/CoreLibs/Create/Session.php b/www/lib/CoreLibs/Create/Session.php index 6873ee27..4440b47e 100644 --- a/www/lib/CoreLibs/Create/Session.php +++ b/www/lib/CoreLibs/Create/Session.php @@ -21,21 +21,107 @@ class Session private string $session_id = ''; /** @var bool flag auto write close */ private bool $auto_write_close = false; + /** @var string regenerate option, default never */ + private string $regenerate = 'never'; + /** @var int regenerate interval either 1 to 100 for random or 0 to 3600 for interval */ + private int $regenerate_interval = 0; + + /** @var array allowed session id regenerate (rotate) options */ + private const ALLOWED_REGENERATE_OPTIONS = ['none', 'random', 'interval']; + /** @var int default random interval */ + public const DEFAULT_REGENERATE_RANDOM = 100; + /** @var int default rotate internval in minutes */ + public const DEFAULT_REGENERATE_INTERVAL = 5 * 60; + /** @var int maximum time for regenerate interval is one hour */ + public const MAX_REGENERATE_INTERAL = 60 * 60; /** * init a session, if array is empty or array does not have session_name set * then no auto init is run * * @param string $session_name if set and not empty, will start session + * @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options */ - public function __construct(string $session_name, bool $auto_write_close = false) - { + public function __construct( + string $session_name, + array $options = [] + ) { + $this->setOptions($options); $this->initSession($session_name); - $this->auto_write_close = $auto_write_close; } // MARK: private methods + /** + * set session class options + * + * @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options + * @return void + */ + private function setOptions(array $options): void + { + if ( + !isset($options['auto_write_close']) || + !is_bool($options['auto_write_close']) + ) { + $options['auto_write_close'] = false; + } + $this->auto_write_close = $options['auto_write_close']; + if ( + !isset($options['session_strict']) || + !is_bool($options['session_strict']) + ) { + $options['session_strict'] = true; + } + // set strict options, on not started sessiononly + if ( + $options['session_strict'] && + $this->getSessionStatus() === PHP_SESSION_NONE + ) { + // use cookies to store session IDs + ini_set('session.use_cookies', 1); + // use cookies only (do not send session IDs in URLs) + ini_set('session.use_only_cookies', 1); + // do not send session IDs in URLs + ini_set('session.use_trans_sid', 0); + } + // session regenerate id options + if ( + empty($options['regenerate']) || + !in_array($options['regenerate'], self::ALLOWED_REGENERATE_OPTIONS) + ) { + $options['regenerate'] = 'never'; + } + $this->regenerate = (string)$options['regenerate']; + // for regenerate: 'random' (default 100) + // regenerate_interval must be between (1 = always) and 100 (1 in 100) + // for regenerate: 'interval' (default 5min) + // regenerate_interval must be 0 = always, to 3600 (every hour) + if ( + $options['regenerate'] == 'random' && + ( + !isset($options['regenerate_interval']) || + !is_numeric($options['regenerate_interval']) || + $options['regenerate_interval'] < 0 || + $options['regenerate_interval'] > 100 + ) + ) { + $options['regenerate_interval'] = self::DEFAULT_REGENERATE_RANDOM; + } + if ( + $options['regenerate'] == 'interval' && + ( + !isset($options['regenerate_interval']) || + !is_numeric($options['regenerate_interval']) || + $options['regenerate_interval'] < 1 || + $options['regenerate_interval'] > self::MAX_REGENERATE_INTERAL + ) + ) { + $options['regenerate_interval'] = self::DEFAULT_REGENERATE_INTERVAL; + } + $this->regenerate_interval = (int)($options['regenerate_interval'] ?? 0); + } + /** * Start session * startSession should be called for complete check @@ -72,6 +158,72 @@ class Session return false; } + // MARK: regenerate session + + /** + * auto rotate session id + * + * @return void + * @throws \RuntimeException failure to regenerate session id + * @throws \UnexpectedValueException failed to get new session id + * @throws \RuntimeException failed to set new sesson id + * @throws \UnexpectedValueException new session id generated does not match the new set one + */ + private function sessionRegenerateSessionId() + { + // never + if ($this->regenerate == 'never') { + return; + } + // regenerate + if ( + !( + // is not session obsolete + empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) && + ( + ( + // random + $this->regenerate == 'random' && + mt_rand(1, $this->regenerate_interval) == 1 + ) || ( + // interval type + $this->regenerate == 'interval' && + ($_SESSION['SESSION_REGENERATE_TIMESTAMP'] ?? 0) + $this->regenerate_interval < time() + ) + ) + ) + ) { + return; + } + // Set current session to expire in 1 minute + $_SESSION['SESSION_REGENERATE_OBSOLETE'] = true; + $_SESSION['SESSION_REGENERATE_EXPIRES'] = time() + 60; + $_SESSION['SESSION_REGENERATE_TIMESTAMP'] = time(); + // Create new session without destroying the old one + if (session_regenerate_id(false) === false) { + throw new \RuntimeException('[SESSION] Session id regeneration failed', 1); + } + // Grab current session ID and close both sessions to allow other scripts to use them + if (false === ($new_session_id = $this->getSessionIdCall())) { + throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 2); + } + $this->writeClose(); + // Set session ID to the new one, and start it back up again + if (($get_new_session_id = session_id($new_session_id)) === false) { + throw new \RuntimeException('[SESSION] set session_id failed', 3); + } + if ($get_new_session_id != $new_session_id) { + throw new \UnexpectedValueException('[SESSION] new session id does not match the new set one', 4); + } + $this->session_id = $new_session_id; + $this->startSessionCall(); + // Don't want this one to expire + unset($_SESSION['SESSION_REGENERATE_OBSOLETE']); + unset($_SESSION['SESSION_REGENERATE_EXPIRES']); + } + + // MARK: session validation + /** * check if session name is valid * @@ -151,6 +303,13 @@ class Session if (!$this->checkActiveSession()) { throw new \RuntimeException('[SESSION] Failed to activate session', 5); } + if ( + !empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) && + !empty($_SESSION['SESSION_REGENERATE_EXPIRES']) && $_SESSION['SESSION_REGENERATE_EXPIRES'] < time() + ) { + $this->sessionDestroy(); + throw new \RuntimeException('[SESSION] Expired session found', 6); + } } elseif ($session_name != $this->getSessionName()) { throw new \UnexpectedValueException( '[SESSION] Another session exists with a different name: ' . $this->getSessionName(), @@ -159,10 +318,12 @@ class Session } // check session id if (false === ($session_id = $this->getSessionIdCall())) { - throw new \UnexpectedValueException('[SESSION] getSessionId did not return a session id', 6); + throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 7); } // set session id $this->session_id = $session_id; + // run session id re-create from time to time + $this->sessionRegenerateSessionId(); // if flagged auto close, write close session if ($this->auto_write_close) { $this->writeClose(); @@ -202,11 +363,12 @@ class Session * set the auto write close flag * * @param bool $flag - * @return void + * @return Session */ - public function setAutoWriteClose(bool $flag): void + public function setAutoWriteClose(bool $flag): Session { $this->auto_write_close = $flag; + return $this; } /** @@ -352,14 +514,15 @@ class Session * * @param string $name array name in _SESSION * @param mixed $value value to set (can be anything) - * @return void + * @return Session */ - public function set(string $name, mixed $value): void + public function set(string $name, mixed $value): Session { $this->checkValidSessionEntryKey($name); $this->restartSession(); $_SESSION[$name] = $value; $this->closeSessionCall(); + return $this; } /** @@ -416,16 +579,17 @@ class Session * unset one _SESSION entry 'name' if exists * * @param string $name _SESSION key name to remove - * @return void + * @return Session */ - public function unset(string $name): void + public function unset(string $name): Session { if (!isset($_SESSION[$name])) { - return; + return $this; } $this->restartSession(); unset($_SESSION[$name]); $this->closeSessionCall(); + return $this; } /** diff --git a/www/lib/CoreLibs/DB/Extended/ArrayIO.php b/www/lib/CoreLibs/DB/Extended/ArrayIO.php index ac98091c..50256c6a 100644 --- a/www/lib/CoreLibs/DB/Extended/ArrayIO.php +++ b/www/lib/CoreLibs/DB/Extended/ArrayIO.php @@ -39,9 +39,9 @@ class ArrayIO extends \CoreLibs\DB\IO { // main calss variables /** @var array */ - private array $table_array; // the array from the table to work on + private array $table_array = []; // the array from the table to work on /** @var string */ - private string $table_name; // the table_name + private string $table_name = ''; // the table_name /** @var string */ private string $pk_name = ''; // the primary key from this table /** @var int|string|null */ @@ -127,9 +127,9 @@ class ArrayIO extends \CoreLibs\DB\IO public function getTableArray(bool $reset = false): array { if (!$reset) { - return $this->table_array ?? []; + return $this->table_array; } - $table_array = $this->table_array ?? []; + $table_array = $this->table_array; reset($table_array); return $table_array; } @@ -194,7 +194,7 @@ class ArrayIO extends \CoreLibs\DB\IO */ public function getTableName(): string { - return $this->table_name ?? ''; + return $this->table_name; } /** diff --git a/www/lib/CoreLibs/DB/IO.php b/www/lib/CoreLibs/DB/IO.php index 4daad5b8..732d7a27 100644 --- a/www/lib/CoreLibs/DB/IO.php +++ b/www/lib/CoreLibs/DB/IO.php @@ -303,6 +303,8 @@ class IO private string $query = ''; /** @var array current params for query */ private array $params = []; + /** @var string current hash build from query and params */ + private string $query_hash = ''; // if we do have a convert call, store the convert data in here, else it will be empty /** @var array{}|array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int,matches:array,params_lookup:array,query:string,params:array} */ private array $placeholder_converted = []; @@ -500,7 +502,7 @@ class IO die(''); } // write to internal one, once OK - $this->db_functions = $db_functions; + $this->db_functions = $db_functions; /** @phan-suppress-current-line PhanPossiblyNullTypeMismatchProperty */ // connect to DB if (!$this->__connectToDB()) { @@ -1319,7 +1321,7 @@ class IO */ private function __dbCountQueryParams(string $query): int { - return $this->db_functions->__dbCountQueryParams($query); + return count($this->db_functions->__dbGetQueryParams($query)); } /** @@ -1382,6 +1384,8 @@ class IO $this->query = $query; // current params $this->params = $params; + // empty on new + $this->query_hash = ''; // no query set if (empty($this->query)) { $this->__dbError(11); @@ -1413,10 +1417,7 @@ class IO $this->pk_name_table[$table] ? $this->pk_name_table[$table] : 'NULL'; } - if ( - !preg_match(self::REGEX_RETURNING, $this->query) && - $this->pk_name && $this->pk_name != 'NULL' - ) { + if (!preg_match(self::REGEX_RETURNING, $this->query) && $this->pk_name != 'NULL') { // check if this query has a ; at the end and remove it $__query = preg_replace("/(;\s*)$/", '', $this->query); // must be query, if preg replace failed, use query as before @@ -1426,7 +1427,7 @@ class IO } elseif ( preg_match(self::REGEX_RETURNING, $this->query, $matches) ) { - if ($this->pk_name && $this->pk_name != 'NULL') { + if ($this->pk_name != 'NULL') { // add the primary key if it is not in the returning set if (!preg_match("/$this->pk_name/", $matches[1])) { $this->query .= " , " . $this->pk_name; @@ -1444,7 +1445,7 @@ class IO $this->returning_id = true; } // import protection, hash needed - $query_hash = $this->dbGetQueryHash($this->query, $this->params); + $query_hash = $this->dbBuildQueryHash($this->query, $this->params); // QUERY PARAMS: run query params check and rewrite if ($this->dbGetConvertPlaceholder() === true) { try { @@ -1478,7 +1479,8 @@ class IO return false; } } - + // set query hash + $this->query_hash = $query_hash; // $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id); // for DEBUG, only on first time ;) $this->__dbDebug( @@ -1962,7 +1964,7 @@ class IO { // set start array if ($query) { - $array = $this->cursor_ext[$this->dbGetQueryHash($query)] ?? []; + $array = $this->cursor_ext[$this->dbBuildQueryHash($query)] ?? []; } else { $array = $this->cursor_ext; } @@ -2364,7 +2366,7 @@ class IO return false; } // create hash from query ... - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); // pre declare array if (!isset($this->cursor_ext[$query_hash])) { $this->cursor_ext[$query_hash] = [ @@ -2542,7 +2544,10 @@ class IO } // only go if NO cursor exists // if cursor exists ... - if ($this->cursor_ext[$query_hash]['cursor']) { + if ( + $this->cursor_ext[$query_hash]['cursor'] instanceof \PgSql\Result || + $this->cursor_ext[$query_hash]['cursor'] == 1 + ) { if ($first_call === true) { $this->cursor_ext[$query_hash]['log'][] = 'First call'; // count the rows returned (if select) @@ -2940,13 +2945,15 @@ class IO * data to create a unique call one, optional * @return bool False if query not found, true if success */ - public function dbCacheReset(string $query, array $params = []): bool + public function dbCacheReset(string $query, array $params = [], bool $show_warning = true): bool { - $this->__dbErrorReset(); - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); // clears cache for this query - if (empty($this->cursor_ext[$query_hash]['query'])) { - $this->__dbError(18, context: [ + if ( + $show_warning && + empty($this->cursor_ext[$query_hash]['query']) + ) { + $this->__dbWarning(18, context: [ 'query' => $query, 'params' => $params, 'hash' => $query_hash, @@ -2985,7 +2992,7 @@ class IO if ($query === null) { return $this->cursor_ext; } - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); if ( !empty($this->cursor_ext) && isset($this->cursor_ext[$query_hash]) @@ -3015,7 +3022,7 @@ class IO $this->__dbError(11); return false; } - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); if ( !empty($this->cursor_ext) && isset($this->cursor_ext[$query_hash]) @@ -3041,7 +3048,7 @@ class IO $this->__dbError(11); return false; } - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); if ( !empty($this->cursor_ext) && isset($this->cursor_ext[$query_hash]) @@ -3067,7 +3074,7 @@ class IO */ public function dbResetQueryCalled(string $query, array $params = []): void { - $this->query_called[$this->dbGetQueryHash($query, $params)] = 0; + $this->query_called[$this->dbBuildQueryHash($query, $params)] = 0; } /** @@ -3080,7 +3087,7 @@ class IO */ public function dbGetQueryCalled(string $query, array $params = []): int { - $query_hash = $this->dbGetQueryHash($query, $params); + $query_hash = $this->dbBuildQueryHash($query, $params); if (!empty($this->query_called[$query_hash])) { return $this->query_called[$query_hash]; } else { @@ -3141,6 +3148,7 @@ class IO 'pk_name' => '', 'count' => 0, 'query' => '', + 'query_raw' => $query, 'result' => null, 'returning_id' => false, 'placeholder_converted' => [], @@ -3237,11 +3245,12 @@ class IO } } else { // if we try to use the same statement name for a differnt query, error abort - if ($this->prepare_cursor[$stm_name]['query'] != $query) { + if ($this->prepare_cursor[$stm_name]['query_raw'] != $query) { // thrown error $this->__dbError(26, false, context: [ 'statement_name' => $stm_name, 'prepared_query' => $this->prepare_cursor[$stm_name]['query'], + 'prepared_query_raw' => $this->prepare_cursor[$stm_name]['query_raw'], 'query' => $query, 'pk_name' => $pk_name, ]); @@ -4047,7 +4056,7 @@ class IO } /** - * Returns hash for query + * Creates hash for query and parameters * Hash is used in all internal storage systems for return data * * @param string $query The query to create the hash from @@ -4055,9 +4064,9 @@ class IO * data to create a unique call one, optional * @return string Hash, as set by hash long */ - public function dbGetQueryHash(string $query, array $params = []): string + public function dbBuildQueryHash(string $query, array $params = []): string { - return Hash::__hashLong( + return Hash::hashLong( $query . ( $params !== [] ? '#' . json_encode($params) : '' @@ -4105,6 +4114,26 @@ class IO $this->params = []; } + /** + * get the current set query hash + * + * @return string Current Query hash + */ + public function dbGetQueryHash(): string + { + return $this->query_hash; + } + + /** + * reset query hash + * + * @return void + */ + public function dbResetQueryHash(): void + { + $this->query_hash = ''; + } + /** * Returns the placeholder convert set or empty * @@ -4284,6 +4313,17 @@ class IO return $this->field_names[$pos] ?? false; } + /** + * get all the $ placeholders + * + * @param string $query + * @return array + */ + public function dbGetQueryParamPlaceholders(string $query): array + { + return $this->db_functions->__dbGetQueryParams($query); + } + /** * Return a field type for a field name or pos, * will return false if field is not found in list @@ -4364,6 +4404,37 @@ class IO return $this->prepare_cursor[$stm_name][$key]; } + /** + * Checks if a prepared query eixsts + * + * @param string $stm_name Statement to check + * @param string $query [default=''] If set then query must also match + * @return false|int<0,2> False on missing stm_name + * 0: ok, 1: stm_name matchin, 2: stm_name and query matching + */ + public function dbPreparedCursorStatus(string $stm_name, string $query = ''): false|int + { + if (empty($stm_name)) { + $this->__dbError( + 101, + false, + 'No statement name given' + ); + return false; + } + // does not exist + $return_value = 0; + if (!empty($this->prepare_cursor[$stm_name]['query_raw'])) { + // statement name eixts + $return_value = 1; + if ($this->prepare_cursor[$stm_name]['query_raw'] == $query) { + // query also matches + $return_value = 2; + } + } + return $return_value; + } + // *************************** // ERROR AND WARNING DATA // *************************** diff --git a/www/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php b/www/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php index 8ccfa9a6..5dda8fa9 100644 --- a/www/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php +++ b/www/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php @@ -379,9 +379,9 @@ interface SqlFunctions * Undocumented function * * @param string $query - * @return int + * @return array */ - public function __dbCountQueryParams(string $query): int; + public function __dbGetQueryParams(string $query): array; } // __END__ diff --git a/www/lib/CoreLibs/DB/SQL/PgSQL.php b/www/lib/CoreLibs/DB/SQL/PgSQL.php index a343c8bc..c05195c2 100644 --- a/www/lib/CoreLibs/DB/SQL/PgSQL.php +++ b/www/lib/CoreLibs/DB/SQL/PgSQL.php @@ -978,12 +978,12 @@ class PgSQL implements Interface\SqlFunctions } /** - * Count placeholder queries. $ only + * Get the all the $ params, as a unique list * * @param string $query - * @return int + * @return array */ - public function __dbCountQueryParams(string $query): int + public function __dbGetQueryParams(string $query): array { $matches = []; // regex for params: only stand alone $number allowed @@ -998,11 +998,11 @@ class PgSQL implements Interface\SqlFunctions // Matches in 1:, must be array_filtered to remove empty, count with array_unique // Regex located in the ConvertPlaceholder class preg_match_all( - ConvertPlaceholder::REGEX_LOOKUP_PLACEHOLDERS, + ConvertPlaceholder::REGEX_LOOKUP_NUMBERED, $query, $matches ); - return count(array_unique(array_filter($matches[3]))); + return array_unique(array_filter($matches[ConvertPlaceholder::MATCHING_POS])); } } diff --git a/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php index 0b9542b2..9fbbf876 100644 --- a/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php +++ b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php @@ -14,56 +14,57 @@ namespace CoreLibs\DB\Support; class ConvertPlaceholder { - /** @var string split regex */ - private const PATTERN_QUERY_SPLIT = '[(<>=,?-]|->|->>|#>|#>>|@>|<@|\?\|\?\&|\|\||#-'; - /** @var string the main regex including the pattern query split */ - private const PATTERN_ELEMENT = '(?:\'.*?\')?\s*(?:\?\?|' . self::PATTERN_QUERY_SPLIT . ')\s*'; - /** @var string parts to ignore in the SQL */ - private const PATTERN_IGNORE = - // digit -> ignore - '\d+|' - // other string -> ignore - . '(?:\'.*?\')|'; - /** @var string named parameters */ - private const PATTERN_NAMED = '(:\w+)'; - /** @var string question mark parameters */ - private const PATTERN_QUESTION_MARK = '(?:(?:\?\?)?\s*(\?{1}))'; - /** @var string numbered parameters */ + /** @var string text block in SQL, single quited + * Note that does not include $$..$$ strings or anything with token name or nested ones + */ + private const PATTERN_TEXT_BLOCK_SINGLE_QUOTE = '(?:\'(?:[^\'\\\\]|\\\\.)*\')'; + /** @var string text block in SQL, dollar quoted + * NOTE: if this is added everything shifts by one lookup number + */ + private const PATTERN_TEXT_BLOCK_DOLLAR = '(?:\$(\w*)\$.*?\$\1\$)'; + /** @var string comment regex + * anything that starts with -- and ends with a line break but any character that is not line break inbetween + * this is the FIRST thing in the line and will skip any further lookups */ + private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)'; + // below are the params lookups + /** @var string named parameters, must start with single : */ + private const PATTERN_NAMED = '((? add line break to matches in "." . '/s'; + /** @var string lookup for only numbered placeholders */ + public const REGEX_LOOKUP_NUMBERED = '/' + . self::PATTERN_COMMENT . '|' + . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|' + . self::PATTERN_TEXT_BLOCK_DOLLAR . '|' + // match for replace part + . '(?:' + // $n numbered part (\PG php) [1] + . self::PATTERN_NUMBERED + // end match + . ')' + . '/s'; + /** @var int position for regex in full placeholder lookup: named */ + public const LOOOKUP_NAMED_POS = 2; + /** @var int position for regex in full placeholder lookup: question mark */ + public const LOOOKUP_QUESTION_MARK_POS = 3; + /** @var int position for regex in full placeholder lookup: numbered */ + public const LOOOKUP_NUMBERED_POS = 4; + /** @var int matches position for replacement and single lookup */ + public const MATCHING_POS = 2; /** * Convert PDO type query with placeholders to \PG style and vica versa @@ -112,11 +133,12 @@ class ConvertPlaceholder $found = -1; } /** @var array 1: named */ - $named_matches = array_filter($matches[1]); + $named_matches = array_filter($matches[self::LOOOKUP_NAMED_POS]); /** @var array 2: open ? */ - $qmark_matches = array_filter($matches[2]); + $qmark_matches = array_filter($matches[self::LOOOKUP_QUESTION_MARK_POS]); /** @var array 3: $n matches */ - $numbered_matches = array_filter($matches[3]); + $numbered_matches = array_filter($matches[self::LOOOKUP_NUMBERED_POS]); + // print "**MATCHES**:
" . print_r($matches, true) . "
"; // count matches $count_named = count(array_unique($named_matches)); $count_qmark = count($qmark_matches); @@ -215,38 +237,37 @@ class ConvertPlaceholder $empty_params = $converted_placeholders['original']['empty_params']; switch ($converted_placeholders['type']) { case 'named': - // 0: full - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part :named + // 1: replace part :named $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_NAMED, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { - // only count up if $match[3] is not yet in lookup table - if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 209 + ); + } + $match = $matches[self::MATCHING_POS]; + // only count up if $match[1] is not yet in lookup table + if (empty($params_lookup[$match])) { $pos++; - $params_lookup[$matches[3]] = '$' . $pos; + $params_lookup[$match] = '$' . $pos; // skip params setup if param list is empty if (!$empty_params) { - $params_new[] = $params[$matches[3]] ?? + $params_new[] = $params[$match] ?? throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params list', + 'Cannot lookup ' . $match . ' in params list', 210 ); } } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - $params_lookup[$matches[3]] ?? - throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params lookup list', - 211 - ) - ); + return $params_lookup[$match] ?? + throw new \RuntimeException( + 'Cannot lookup ' . $match . ' in params lookup list', + 211 + ); }, $converted_placeholders['original']['query'] ); @@ -256,61 +277,61 @@ class ConvertPlaceholder // order and data stays the same $params_new = $params ?? []; } - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part ? + // 1: replace part ? $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_QUESTION_MARK, function ($matches) use (&$pos, &$params_lookup) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 229 + ); + } + $match = $matches[self::MATCHING_POS]; // only count pos up for actual replacements we will do - if (!empty($matches[3])) { + if (!empty($match)) { $pos++; $params_lookup[] = '$' . $pos; } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - '$' . $pos - ); + return '$' . $pos; }, $converted_placeholders['original']['query'] ); break; case 'numbered': - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part $numbered + // 1: replace part $numbered $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_NUMBERED, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { - // only count up if $match[3] is not yet in lookup table - if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 239 + ); + } + $match = $matches[self::MATCHING_POS]; + // only count up if $match[1] is not yet in lookup table + if (empty($params_lookup[$match])) { $pos++; - $params_lookup[$matches[3]] = ':' . $pos . '_named'; + $params_lookup[$match] = ':' . $pos . '_named'; // skip params setup if param list is empty if (!$empty_params) { $params_new[] = $params[($pos - 1)] ?? throw new \RuntimeException( 'Cannot lookup ' . ($pos - 1) . ' in params list', - 220 + 230 ); } } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - $params_lookup[$matches[3]] ?? - throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params lookup list', - 221 - ) - ); + return $params_lookup[$match] ?? + throw new \RuntimeException( + 'Cannot lookup ' . $match . ' in params lookup list', + 231 + ); }, $converted_placeholders['original']['query'] ); diff --git a/www/lib/CoreLibs/DeprecatedHelper/Deprecated84.php b/www/lib/CoreLibs/DeprecatedHelper/Deprecated84.php new file mode 100644 index 00000000..c9de92c7 --- /dev/null +++ b/www/lib/CoreLibs/DeprecatedHelper/Deprecated84.php @@ -0,0 +1,95 @@ + $fields + * @param string $separator + * @param string $enclosure + * @param string $escape + * @param string $eol + * @return int|false + * @throws InvalidArgumentException + */ + public static function fputcsv( + mixed $stream, + array $fields, + string $separator = ",", + string $enclosure = '"', + string $escape = '', // set to empty for future compatible + string $eol = PHP_EOL + ): int | false { + if (!is_resource($stream)) { + throw new \InvalidArgumentException("fputcsv stream parameter must be a resrouce"); + } + return fputcsv($stream, $fields, $separator, $enclosure, $escape, $eol); + } + + /** + * This is a wrapper for fgetcsv to fix deprecated warning for $escape parameter + * See: https://www.php.net/manual/en/function.fgetcsv.php + * escape parameter deprecation and recommend to set to "" for compatible with PHP 9.0 + * + * @param mixed $stream + * @param null|int<0,max> $length + * @param string $separator + * @param string $enclosure + * @param string $escape + * @return array|false + * @throws InvalidArgumentException + */ + public static function fgetcsv( + mixed $stream, + ?int $length = null, + string $separator = ',', + string $enclosure = '"', + string $escape = '' // set to empty for future compatible + ): array | false { + if (!is_resource($stream)) { + throw new \InvalidArgumentException("fgetcsv stream parameter must be a resrouce"); + } + return fgetcsv($stream, $length, $separator, $enclosure, $escape); + } + + /** + * This is a wrapper for str_getcsv to fix deprecated warning for $escape parameter + * See: https://www.php.net/manual/en/function.str-getcsv.php + * escape parameter deprecation and recommend to set to "" for compatible with PHP 9.0 + * + * @param string $string + * @param string $separator + * @param string $enclosure + * @param string $escape + * @return array + */ + // phpcs:disable PSR1.Methods.CamelCapsMethodName + public static function str_getcsv( + string $string, + string $separator = ",", + string $enclosure = '"', + string $escape = '' // set to empty for future compatible + ): array { + return str_getcsv($string, $separator, $enclosure, $escape); + } + // phpcs:enable PSR1.Methods.CamelCapsMethodName +} + +// __END__ diff --git a/www/lib/CoreLibs/Language/GetLocale.php b/www/lib/CoreLibs/Language/GetLocale.php index d0e6f9e1..2953621b 100644 --- a/www/lib/CoreLibs/Language/GetLocale.php +++ b/www/lib/CoreLibs/Language/GetLocale.php @@ -50,7 +50,6 @@ class GetLocale $locale = defined('SITE_LOCALE') && !empty(SITE_LOCALE) ? SITE_LOCALE : // else parse from default, if not 'en' - /** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */ (defined('DEFAULT_LOCALE') && !empty(DEFAULT_LOCALE) ? DEFAULT_LOCALE : 'en'); } @@ -97,8 +96,7 @@ class GetLocale $encoding = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ? SITE_ENCODING : // or default encoding, if not 'UTF-8' - /** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */ - (defined('DEFAULT_ENCODING') && !empty(DEFAULT_ENCODING) ? + (defined('DEFAULT_ENCODING') ? DEFAULT_ENCODING : 'UTF-8'); } } diff --git a/www/lib/CoreLibs/Logging/Logging.php b/www/lib/CoreLibs/Logging/Logging.php index d672d9a7..7476ef56 100644 --- a/www/lib/CoreLibs/Logging/Logging.php +++ b/www/lib/CoreLibs/Logging/Logging.php @@ -30,6 +30,10 @@ class Logging { /** @var int minimum size for a max file size, so we don't set 1 byte, 10kb */ public const MIN_LOG_MAX_FILESIZE = 10 * 1024; + /** @var string log file extension, not changeable */ + private const LOG_FILE_NAME_EXT = "log"; + /** @var string log file block separator, not changeable */ + private const LOG_FILE_BLOCK_SEPARATOR = '.'; // NOTE: the second party array{} hs some errors /** @var array>|array{string:array{type:string,type_info?:string,mandatory:true,alias?:string,default:string|bool|Level,deprecated:bool,use?:string}} */ @@ -104,8 +108,6 @@ class Logging private string $log_folder = ''; /** @var string a alphanumeric name that has to be set as global definition */ private string $log_file_id = ''; - /** @var string log file name extension */ - private string $log_file_name_ext = 'log'; /** @var string log file name with folder, for actual writing */ private string $log_file_name = ''; /** @var int set in bytes */ @@ -431,7 +433,7 @@ class Logging private function buildLogFileName(Level $level, string $group_id = ''): string { // init base file path - $fn = $this->log_print_file . '.' . $this->log_file_name_ext; + $fn = $this->log_print_file . '.' . self::LOG_FILE_NAME_EXT; // log ID prefix settings, if not valid, replace with empty if (!empty($this->log_file_id)) { $rpl_string = $this->log_file_id; @@ -440,14 +442,15 @@ class Logging } $fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix) - $rpl_string = !$this->getLogFlag(Flag::per_level) ? '' : - '_' . $level->getName(); + $rpl_string = $this->getLogFlag(Flag::per_level) ? + self::LOG_FILE_BLOCK_SEPARATOR . $level->getName() : + ''; $fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename // write per level - $rpl_string = !$this->getLogFlag(Flag::per_group) ? '' : + $rpl_string = $this->getLogFlag(Flag::per_group) ? // normalize level, replace all non alphanumeric characters with - - '_' . ( + self::LOG_FILE_BLOCK_SEPARATOR . ( // if return is only - then set error string preg_match( "/^-+$/", @@ -455,25 +458,29 @@ class Logging ) ? 'INVALID-LEVEL-STRING' : $level_string - ); + ) : + ''; $fn = str_replace('{GROUP}', $rpl_string, $fn); // create output filename // set per class, but don't use get_class as we will only get self - $rpl_string = !$this->getLogFlag(Flag::per_class) ? '' : '_' - // set sub class settings - . str_replace('\\', '-', Support::getCallerTopLevelClass()); + $rpl_string = $this->getLogFlag(Flag::per_class) ? + // set sub class settings + self::LOG_FILE_BLOCK_SEPARATOR . str_replace('\\', '-', Support::getCallerTopLevelClass()) : + ''; $fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename // if request to write to one file - $rpl_string = !$this->getLogFlag(Flag::per_page) ? - '' : - '_' . System::getPageName(System::NO_EXTENSION); + $rpl_string = $this->getLogFlag(Flag::per_page) ? + self::LOG_FILE_BLOCK_SEPARATOR . System::getPageName(System::NO_EXTENSION) : + ''; $fn = str_replace('{PAGENAME}', $rpl_string, $fn); // create output filename // if run id, we auto add ymd, so we ignore the log file date if ($this->getLogFlag(Flag::per_run)) { - $rpl_string = '_' . $this->getLogUniqueId(); // add 8 char unique string + // add 8 char unique string and date block with time + $rpl_string = self::LOG_FILE_BLOCK_SEPARATOR . $this->getLogUniqueId(); } elseif ($this->getLogFlag(Flag::per_date)) { - $rpl_string = '_' . $this->getLogDate(); // add date to file + // add date to file + $rpl_string = self::LOG_FILE_BLOCK_SEPARATOR . $this->getLogDate(); } else { $rpl_string = ''; } @@ -739,7 +746,10 @@ class Logging { if (empty($this->log_file_unique_id) || $override == true) { $this->log_file_unique_id = - date('Y-m-d_His') . '_U_' + date('Y-m-d_His') + . self::LOG_FILE_BLOCK_SEPARATOR + . 'U_' + // this doesn't have to be unique for everything, just for this logging purpose . substr(hash( 'sha1', random_bytes(63) diff --git a/www/lib/CoreLibs/Output/Form/Generate.php b/www/lib/CoreLibs/Output/Form/Generate.php index 68e7a1df..b2262c70 100644 --- a/www/lib/CoreLibs/Output/Form/Generate.php +++ b/www/lib/CoreLibs/Output/Form/Generate.php @@ -1371,7 +1371,7 @@ class Generate ) { $this->msg .= sprintf( $this->l->__('Please enter a valid (%s) input for the %s Field!
'), - $this->dba->getTableArray()[$key]['error_example'], + $this->dba->getTableArray()[$key]['error_example'] ?? '[MISSING]', $this->dba->getTableArray()[$key]['output_name'] ); } @@ -2602,7 +2602,7 @@ class Generate } } // add lost error ones - $this->log->error('P: ' . $data['prefix'] . ', ' + $this->log->error('Prefix: ' . $data['prefix'] . ', ' . Support::prAr($_POST['ERROR'][$data['prefix']] ?? [])); if ($this->error && !empty($_POST['ERROR'][$data['prefix']])) { $prfx = $data['prefix']; // short diff --git a/www/lib/CoreLibs/Output/Form/TableArrays/EditUsers.php b/www/lib/CoreLibs/Output/Form/TableArrays/EditUsers.php index 6d87753a..fce998ed 100644 --- a/www/lib/CoreLibs/Output/Form/TableArrays/EditUsers.php +++ b/www/lib/CoreLibs/Output/Form/TableArrays/EditUsers.php @@ -50,7 +50,8 @@ class EditUsers implements Interface\TableArraysInterface 'HIDDEN_value' => $_POST['HIDDEN_password'] ?? '', 'CONFIRM_value' => $_POST['CONFIRM_password'] ?? '', 'output_name' => 'Password', - 'mandatory' => 1, + // make it not mandatory to create dummy accounts that can only login via login url id + 'mandatory' => 0, 'type' => 'password', // later has to be password for encryption in database 'update' => [ // connected field updates, and update data 'password_change_date' => [ // db row to update @@ -135,30 +136,6 @@ class EditUsers implements Interface\TableArraysInterface 'min_edit_acl' => '100', 'min_show_acl' => '100', ], - 'debug' => [ - 'value' => $_POST['debug'] ?? '', - 'output_name' => 'Debug', - 'type' => 'binary', - 'int' => 1, - 'element_list' => [ - '1' => 'Yes', - '0' => 'No' - ], - 'min_edit_acl' => '100', - 'min_show_acl' => '100', - ], - 'db_debug' => [ - 'value' => $_POST['db_debug'] ?? '', - 'output_name' => 'DB Debug', - 'type' => 'binary', - 'int' => 1, - 'element_list' => [ - '1' => 'Yes', - '0' => 'No' - ], - 'min_edit_acl' => '100', - 'min_show_acl' => '100', - ], 'email' => [ 'value' => $_POST['email'] ?? '', 'output_name' => 'E-Mail', @@ -206,6 +183,7 @@ class EditUsers implements Interface\TableArraysInterface 'type' => 'text', 'error_check' => 'unique|custom', 'error_regex' => "/^[A-Za-z0-9]+$/", + 'error_example' => "ABCdef123", 'emptynull' => 1,'min_edit_acl' => '100', 'min_show_acl' => '100', ], diff --git a/www/lib/CoreLibs/Output/ProgressBar.php b/www/lib/CoreLibs/Output/ProgressBar.php index 5c4f9c76..c64b6995 100644 --- a/www/lib/CoreLibs/Output/ProgressBar.php +++ b/www/lib/CoreLibs/Output/ProgressBar.php @@ -418,9 +418,7 @@ class ProgressBar // if this is percent, we ignore anything, it is auto positioned if ($this->label[$name]['type'] != 'percent') { foreach (['top', 'left', 'width', 'height'] as $pos_name) { - if ($$pos_name !== false) { - $this->label[$name][$pos_name] = intval($$pos_name); - } + $this->label[$name][$pos_name] = intval($$pos_name); } if ($align != '') { diff --git a/www/lib/CoreLibs/Security/AsymmetricAnonymousEncryption.php b/www/lib/CoreLibs/Security/AsymmetricAnonymousEncryption.php new file mode 100644 index 00000000..a30cb3b1 --- /dev/null +++ b/www/lib/CoreLibs/Security/AsymmetricAnonymousEncryption.php @@ -0,0 +1,408 @@ +setPublicKey($public_key); + } + if ($key_pair !== null) { + $this->setKeyPair($key_pair); + if (empty($public_key)) { + $public_key = CreateKey::getPublicKey($key_pair); + $this->setPublicKey($public_key); + } + } + } + + /** + * Returns the singleton self object. + * For function wrapper use + * + * @param string|null $key_pair + * @param string|null $public_key + * @return AsymmetricAnonymousEncryption object + */ + public static function getInstance( + #[\SensitiveParameter] + string|null $key_pair = null, + string|null $public_key = null + ): self { + // new if no instsance or key is different + if ( + empty(self::$instance) || + self::$instance->key_pair != $key_pair || + self::$instance->public_key != $public_key + ) { + self::$instance = new self($key_pair, $public_key); + } + return self::$instance; + } + + /** + * clean up + */ + public function __destruct() + { + if (empty($this->key_pair)) { + return; + } + try { + // would set it to null, but we we do not want to make key null + sodium_memzero($this->key_pair); + return; + } catch (SodiumException) { + // empty catch + } + if (is_null($this->key_pair)) { + return; + } + $zero = str_repeat("\0", mb_strlen($this->key_pair, '8bit')); + $this->key_pair = $this->key_pair ^ ( + $zero ^ $this->key_pair + ); + unset($zero); + unset($this->key_pair); /** @phan-suppress-current-line PhanTypeObjectUnsetDeclaredProperty */ + } + + /* ************************************************************************ + * MARK: PRIVATE + * *************************************************************************/ + + /** + * Create the internal key pair in binary + * + * @param ?string $key_pair + * @return string + * @throws \UnexpectedValueException key pair empty + * @throws \UnexpectedValueException invalid hex key pair + * @throws \RangeException key pair not correct size + */ + private function createKeyPair( + #[\SensitiveParameter] + ?string $key_pair + ): string { + if (empty($key_pair)) { + throw new \UnexpectedValueException('Key pair cannot be empty'); + } + try { + $key_pair = CreateKey::hex2bin($key_pair); + } catch (SodiumException $e) { + sodium_memzero($key_pair); + throw new \UnexpectedValueException('Invalid hex key pair: ' . $e->getMessage()); + } + if (mb_strlen($key_pair, '8bit') !== SODIUM_CRYPTO_BOX_KEYPAIRBYTES) { + sodium_memzero($key_pair); + throw new \RangeException( + 'Key pair is not the correct size (must be ' + . SODIUM_CRYPTO_BOX_KEYPAIRBYTES . ' bytes long).' + ); + } + return $key_pair; + } + + /** + * create the internal public key in binary + * + * @param ?string $public_key + * @return string + * @throws \UnexpectedValueException public key empty + * @throws \UnexpectedValueException invalid hex key + * @throws \RangeException invalid key length + */ + private function createPublicKey(?string $public_key): string + { + if (empty($public_key)) { + throw new \UnexpectedValueException('Public key cannot be empty'); + } + try { + $public_key = CreateKey::hex2bin($public_key); + } catch (SodiumException $e) { + sodium_memzero($public_key); + throw new \UnexpectedValueException('Invalid hex public key: ' . $e->getMessage()); + } + if (mb_strlen($public_key, '8bit') !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { + sodium_memzero($public_key); + throw new \RangeException( + 'Public key is not the correct size (must be ' + . SODIUM_CRYPTO_BOX_PUBLICKEYBYTES . ' bytes long).' + ); + } + return $public_key; + } + + /** + * encrypt a message asymmetric with a bpulic key + * + * @param string $message + * @param ?string $public_key + * @return string + * @throws \UnexpectedValueException create encryption failed + * @throws \UnexpectedValueException convert to base64 failed + */ + private function asymmetricEncryption( + #[\SensitiveParameter] + string $message, + ?string $public_key + ): string { + $public_key = $this->createPublicKey($public_key); + try { + $encrypted = sodium_crypto_box_seal($message, $public_key); + } catch (SodiumException $e) { + sodium_memzero($message); + throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage()); + } + sodium_memzero($message); + try { + $result = sodium_bin2base64($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL); + } catch (SodiumException $e) { + sodium_memzero($encrypted); + throw new \UnexpectedValueException("bin2base64 failed: " . $e->getMessage()); + } + sodium_memzero($encrypted); + return $result; + } + + /** + * decrypt a message that is asymmetric encrypted with a key pair + * + * @param string $message + * @param ?string $key_pair + * @return string + * @throws \UnexpectedValueException message string empty + * @throws \UnexpectedValueException base64 decoding failed + * @throws \UnexpectedValueException decryption failed + * @throws \UnexpectedValueException could not decrypt message + */ + private function asymmetricDecryption( + #[\SensitiveParameter] + string $message, + #[\SensitiveParameter] + ?string $key_pair + ): string { + if (empty($message)) { + throw new \UnexpectedValueException('Encrypted string cannot be empty'); + } + $key_pair = $this->createKeyPair($key_pair); + try { + $result = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL); + } catch (SodiumException $e) { + sodium_memzero($message); + sodium_memzero($key_pair); + throw new \UnexpectedValueException("base642bin failed: " . $e->getMessage()); + } + sodium_memzero($message); + $plaintext = false; + try { + $plaintext = sodium_crypto_box_seal_open($result, $key_pair); + } catch (SodiumException $e) { + sodium_memzero($message); + sodium_memzero($key_pair); + sodium_memzero($result); + throw new \UnexpectedValueException("Decrypting message failed: " . $e->getMessage()); + } + sodium_memzero($key_pair); + sodium_memzero($result); + if (!is_string($plaintext)) { + throw new \UnexpectedValueException('Invalid key pair'); + } + return $plaintext; + } + + /* ************************************************************************ + * MARK: PUBLIC + * *************************************************************************/ + + /** + * sets the private key for encryption + * + * @param string $key_pair Key pair in hex + * @return AsymmetricAnonymousEncryption + * @throws \UnexpectedValueException key pair empty + */ + public function setKeyPair( + #[\SensitiveParameter] + string $key_pair + ): AsymmetricAnonymousEncryption { + if (empty($key_pair)) { + throw new \UnexpectedValueException('Key pair cannot be empty'); + } + // check if valid; + $this->createKeyPair($key_pair); + // set new key pair + $this->key_pair = $key_pair; + sodium_memzero($key_pair); + // set public key if not set + if (empty($this->public_key)) { + $this->public_key = CreateKey::getPublicKey($this->key_pair); + // check if valid + $this->createPublicKey($this->public_key); + } + return $this; + } + + /** + * check if set key pair matches given one + * + * @param string $key_pair + * @return bool + */ + public function compareKeyPair( + #[\SensitiveParameter] + string $key_pair + ): bool { + return $this->key_pair === $key_pair; + } + + /** + * get the current set key pair, null if not set + * + * @return string|null + */ + public function getKeyPair(): ?string + { + return $this->key_pair; + } + + /** + * sets the public key for decryption + * if only key pair exists Security\Create::getPublicKey() can be used to + * extract the public key from the key pair + * + * @param string $public_key Public Key in hex + * @return AsymmetricAnonymousEncryption + * @throws \UnexpectedValueException public key empty + */ + public function setPublicKey(string $public_key): AsymmetricAnonymousEncryption + { + if (empty($public_key)) { + throw new \UnexpectedValueException('Public key cannot be empty'); + } + // check if valid + $this->createPublicKey($public_key); + $this->public_key = $public_key; + sodium_memzero($public_key); + return $this; + } + + /** + * check if the set public key matches the given one + * + * @param string $public_key + * @return bool + */ + public function comparePublicKey(string $public_key): bool + { + return $this->public_key === $public_key; + } + + /** + * get the current set public key, null if not set + * + * @return string|null + */ + public function getPublicKey(): ?string + { + return $this->public_key; + } + + /** + * Encrypt a message with a public key + * static version + * + * @param string $message Message to encrypt + * @param string $public_key Public key in hex to encrypt message with + * @return string Encrypted message as hex string + */ + public static function encryptKey( + #[\SensitiveParameter] + string $message, + string $public_key + ): string { + return self::getInstance()->asymmetricEncryption($message, $public_key); + } + + /** + * Encrypt a message + * + * @param string $message Message to ecnrypt + * @return string Encrypted message as hex string + */ + public function encrypt( + #[\SensitiveParameter] + string $message + ): string { + return $this->asymmetricEncryption($message, $this->public_key); + } + + /** + * decrypt a message with a key pair + * static version + * + * @param string $message Message to decrypt in hex + * @param string $key_pair Key pair in hex to decrypt the message with + * @return string Decrypted message + */ + public static function decryptKey( + #[\SensitiveParameter] + string $message, + #[\SensitiveParameter] + string $key_pair + ): string { + return self::getInstance()->asymmetricDecryption($message, $key_pair); + } + + /** + * decrypt a message + * + * @param string $message Message to decrypt in hex + * @return string Decrypted message + */ + public function decrypt( + #[\SensitiveParameter] + string $message + ): string { + return $this->asymmetricDecryption($message, $this->key_pair); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Security/CreateKey.php b/www/lib/CoreLibs/Security/CreateKey.php index add2773e..e9f7c53c 100644 --- a/www/lib/CoreLibs/Security/CreateKey.php +++ b/www/lib/CoreLibs/Security/CreateKey.php @@ -35,14 +35,39 @@ class CreateKey return random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); } + /** + * creates a sodium cyptobox keypair as hex string + * + * @return string hex string for the keypair + */ + public static function createKeyPair(): string + { + return self::bin2hex(sodium_crypto_box_keypair()); + } + + /** + * extracts the public key and returns it as hex string from the hex keypari + * + * @param string $hex_keypair hex encoded keypair + * @return string hex encoded public key + */ + public static function getPublicKey( + #[\SensitiveParameter] + string $hex_keypair + ): string { + return self::bin2hex(sodium_crypto_box_publickey(self::hex2bin($hex_keypair))); + } + /** * convert binary key to hex string * * @param string $hex_key Convert binary key string to hex * @return string */ - public static function bin2hex(string $hex_key): string - { + public static function bin2hex( + #[\SensitiveParameter] + string $hex_key + ): string { return sodium_bin2hex($hex_key); } @@ -52,8 +77,10 @@ class CreateKey * @param string $string_key Convery hex key string to binary * @return string */ - public static function hex2bin(string $string_key): string - { + public static function hex2bin( + #[\SensitiveParameter] + string $string_key + ): string { return sodium_hex2bin($string_key); } } diff --git a/www/lib/CoreLibs/Security/Password.php b/www/lib/CoreLibs/Security/Password.php index 984fa5cb..8c64228e 100644 --- a/www/lib/CoreLibs/Security/Password.php +++ b/www/lib/CoreLibs/Security/Password.php @@ -16,8 +16,10 @@ class Password * @param string $password password * @return string hashed password */ - public static function passwordSet(string $password): string - { + public static function passwordSet( + #[\SensitiveParameter] + string $password + ): string { // always use the PHP default for the password // password options ca be set in the password init, // but should be kept as default @@ -31,8 +33,11 @@ class Password * @param string $hash password hash * @return bool true or false */ - public static function passwordVerify(string $password, string $hash): bool - { + public static function passwordVerify( + #[\SensitiveParameter] + string $password, + string $hash + ): bool { if (password_verify($password, $hash)) { return true; } else { diff --git a/www/lib/CoreLibs/Security/SymmetricEncryption.php b/www/lib/CoreLibs/Security/SymmetricEncryption.php index c12f4b3f..08826212 100644 --- a/www/lib/CoreLibs/Security/SymmetricEncryption.php +++ b/www/lib/CoreLibs/Security/SymmetricEncryption.php @@ -24,19 +24,19 @@ class SymmetricEncryption /** @var SymmetricEncryption self instance */ private static SymmetricEncryption $instance; - /** @var string bin hex key */ - private string $key = ''; + /** @var ?string bin hex key */ + private ?string $key = null; /** * init class * if key not passed, key must be set with createKey * - * @param string|null|null $key + * @param string|null $key encryption key */ public function __construct( - string|null $key = null + ?string $key = null ) { - if ($key != null) { + if ($key !== null) { $this->setKey($key); } } @@ -45,16 +45,49 @@ class SymmetricEncryption * Returns the singleton self object. * For function wrapper use * + * @param string|null $key encryption key * @return SymmetricEncryption object */ - public static function getInstance(string|null $key = null): self + public static function getInstance(?string $key = null): self { - if (empty(self::$instance)) { + // new if no instsance or key is different + if ( + empty(self::$instance) || + self::$instance->key != $key + ) { self::$instance = new self($key); } return self::$instance; } + /** + * clean up + * + * @return void + */ + public function __deconstruct() + { + if (empty($this->key)) { + return; + } + try { + // would set it to null, but we we do not want to make key null + sodium_memzero($this->key); + return; + } catch (SodiumException) { + // empty catch + } + if (is_null($this->key)) { + return; + } + $zero = str_repeat("\0", mb_strlen($this->key, '8bit')); + $this->key = $this->key ^ ( + $zero ^ $this->key + ); + unset($zero); + unset($this->key); /** @phan-suppress-current-line PhanTypeObjectUnsetDeclaredProperty */ + } + /* ************************************************************************ * MARK: PRIVATE * *************************************************************************/ @@ -62,11 +95,19 @@ class SymmetricEncryption /** * create key and check validity * - * @param string $key The key from which the binary key will be created - * @return string Binary key string + * @param ?string $key The key from which the binary key will be created + * @return string Binary key string + * @throws \UnexpectedValueException empty key + * @throws \UnexpectedValueException invalid hex key + * @throws \RangeException invalid length */ - private function createKey(string $key): string - { + private function createKey( + #[\SensitiveParameter] + ?string $key + ): string { + if (empty($key)) { + throw new \UnexpectedValueException('Key cannot be empty'); + } try { $key = CreateKey::hex2bin($key); } catch (SodiumException $e) { @@ -87,36 +128,42 @@ class SymmetricEncryption * @param string $encrypted Text to decrypt * @param ?string $key Mandatory encryption key, will throw exception if empty * @return string Plain text - * @throws \RangeException - * @throws \UnexpectedValueException - * @throws \UnexpectedValueException + * @throws \UnexpectedValueException key cannot be empty + * @throws \UnexpectedValueException decipher message failed + * @throws \UnexpectedValueException invalid key */ - private function decryptData(string $encrypted, ?string $key): string - { - if (empty($key)) { - throw new \UnexpectedValueException('Key not set'); + private function decryptData( + #[\SensitiveParameter] + string $encrypted, + #[\SensitiveParameter] + ?string $key + ): string { + if (empty($encrypted)) { + throw new \UnexpectedValueException('Encrypted string cannot be empty'); } $key = $this->createKey($key); $decoded = base64_decode($encrypted); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); - $plain = false; + $plaintext = false; try { - $plain = sodium_crypto_secretbox_open( + $plaintext = sodium_crypto_secretbox_open( $ciphertext, $nonce, $key ); } catch (SodiumException $e) { + sodium_memzero($ciphertext); + sodium_memzero($key); throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage()); } - if (!is_string($plain)) { - throw new \UnexpectedValueException('Invalid Key'); - } sodium_memzero($ciphertext); sodium_memzero($key); - return $plain; + if (!is_string($plaintext)) { + throw new \UnexpectedValueException('Invalid Key'); + } + return $plaintext; } /** @@ -124,15 +171,15 @@ class SymmetricEncryption * * @param string $message Message to encrypt * @param ?string $key Mandatory encryption key, will throw exception if empty - * @return string - * @throws \Exception - * @throws \RangeException + * @return string Ciphered text + * @throws \UnexpectedValueException create message failed */ - private function encryptData(string $message, ?string $key): string - { - if (empty($this->key) || $key === null) { - throw new \UnexpectedValueException('Key not set'); - } + private function encryptData( + #[\SensitiveParameter] + string $message, + #[\SensitiveParameter] + ?string $key + ): string { $key = $this->createKey($key); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); try { @@ -145,6 +192,8 @@ class SymmetricEncryption ) ); } catch (SodiumException $e) { + sodium_memzero($message); + sodium_memzero($key); throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage()); } sodium_memzero($message); @@ -156,19 +205,49 @@ class SymmetricEncryption * MARK: PUBLIC * *************************************************************************/ - /** * set a new key for encryption * * @param string $key - * @return void + * @return SymmetricEncryption + * @throws \UnexpectedValueException key cannot be empty */ - public function setKey(string $key) - { + public function setKey( + #[\SensitiveParameter] + string $key + ): SymmetricEncryption { if (empty($key)) { throw new \UnexpectedValueException('Key cannot be empty'); } + // check that this is a valid key + $this->createKey($key); + // set key $this->key = $key; + sodium_memzero($key); + return $this; + } + + /** + * Checks if set key is equal to parameter key + * + * @param string $key + * @return bool + */ + public function compareKey( + #[\SensitiveParameter] + string $key + ): bool { + return $key === $this->key; + } + + /** + * returns the current set key, null if not set + * + * @return ?string + */ + public function getKey(): ?string + { + return $this->key; } /** @@ -178,13 +257,13 @@ class SymmetricEncryption * @param string $encrypted Message encrypted with safeEncrypt() * @param string $key Encryption key (as hex string) * @return string - * @throws \Exception - * @throws \RangeException - * @throws \UnexpectedValueException - * @throws \UnexpectedValueException */ - public static function decryptKey(string $encrypted, string $key): string - { + public static function decryptKey( + #[\SensitiveParameter] + string $encrypted, + #[\SensitiveParameter] + string $key + ): string { return self::getInstance()->decryptData($encrypted, $key); } @@ -193,12 +272,11 @@ class SymmetricEncryption * * @param string $encrypted Message encrypted with safeEncrypt() * @return string - * @throws \RangeException - * @throws \UnexpectedValueException - * @throws \UnexpectedValueException */ - public function decrypt(string $encrypted): string - { + public function decrypt( + #[\SensitiveParameter] + string $encrypted + ): string { return $this->decryptData($encrypted, $this->key); } @@ -209,11 +287,13 @@ class SymmetricEncryption * @param string $message Message to encrypt * @param string $key Encryption key (as hex string) * @return string - * @throws \Exception - * @throws \RangeException */ - public static function encryptKey(string $message, string $key): string - { + public static function encryptKey( + #[\SensitiveParameter] + string $message, + #[\SensitiveParameter] + string $key + ): string { return self::getInstance()->encryptData($message, $key); } @@ -222,11 +302,11 @@ class SymmetricEncryption * * @param string $message Message to encrypt * @return string - * @throws \Exception - * @throws \RangeException */ - public function encrypt(string $message): string - { + public function encrypt( + #[\SensitiveParameter] + string $message + ): string { return $this->encryptData($message, $this->key); } } diff --git a/www/lib/CoreLibs/Template/SmartyExtend.php b/www/lib/CoreLibs/Template/SmartyExtend.php index e9b906d6..10eaa981 100644 --- a/www/lib/CoreLibs/Template/SmartyExtend.php +++ b/www/lib/CoreLibs/Template/SmartyExtend.php @@ -19,12 +19,13 @@ declare(strict_types=1); namespace CoreLibs\Template; -// leading slash if this is in lib\Smarty -class SmartyExtend extends \Smarty +class SmartyExtend extends \Smarty\Smarty { // internal translation engine - /** @var \CoreLibs\Language\L10n */ + /** @var \CoreLibs\Language\L10n language class */ public \CoreLibs\Language\L10n $l10n; + /** @var \CoreLibs\Logging\Logging $log logging class */ + public \CoreLibs\Logging\Logging $log; // lang & encoding /** @var string */ @@ -157,14 +158,18 @@ class SmartyExtend extends \Smarty * calls L10 for pass on internaly in smarty * also registers the getvar caller plugin * - * @param \CoreLibs\Language\L10n $l10n l10n language class - * @param string|null $cache_id - * @param string|null $compile_id + * @param \CoreLibs\Language\L10n $l10n l10n language class + * @param \CoreLibs\Logging\Logging $log Logger class + * @param string|null $cache_id [default=null] + * @param string|null $compile_id [default=null] + * @param array $options [default=[]] */ public function __construct( \CoreLibs\Language\L10n $l10n, + \CoreLibs\Logging\Logging $log, ?string $cache_id = null, - ?string $compile_id = null + ?string $compile_id = null, + array $options = [] ) { // trigger deprecation if ( @@ -177,14 +182,33 @@ class SmartyExtend extends \Smarty E_USER_DEPRECATED ); } - // set variables (to be deprecated) - $cache_id = $cache_id ?? - (defined('CACHE_ID') ? CACHE_ID : ''); - $compile_id = $compile_id ?? - (defined('COMPILE_ID') ? COMPILE_ID : ''); + // set variables from global constants (deprecated) + if ($cache_id === null && defined('CACHE_ID')) { + trigger_error( + 'SmartyExtended: No cache_id set and CACHE_ID constant set, this is deprecated', + E_USER_DEPRECATED + ); + $cache_id = CACHE_ID; + } + if ($compile_id === null && defined('COMPILE_ID')) { + trigger_error( + 'SmartyExtended: No compile_id set and COMPILE_ID constant set, this is deprecated', + E_USER_DEPRECATED + ); + $compile_id = COMPILE_ID; + } + if (empty($cache_id)) { + throw new \BadMethodCallException('cache_id parameter is not set'); + } + if (empty($compile_id)) { + throw new \BadMethodCallException('compile_id parameter is not set'); + } + // call basic smarty - // or Smarty::__construct(); parent::__construct(); + + $this->log = $log; + // init lang $this->l10n = $l10n; // parse and read, legacy stuff @@ -194,7 +218,6 @@ class SmartyExtend extends \Smarty $this->lang_short = $locale['lang_short']; $this->domain = $locale['domain']; $this->lang_dir = $locale['path']; - // opt load functions so we can use legacy init for smarty run perhaps \CoreLibs\Language\L10n::loadFunctions(); _setlocale(LC_MESSAGES, $locale['locale']); @@ -203,7 +226,6 @@ class SmartyExtend extends \Smarty _bind_textdomain_codeset($this->domain, $this->encoding); // register smarty variable - // $this->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'getvar', [&$this, 'getTemplateVars']); $this->registerPlugin(self::PLUGIN_MODIFIER, 'getvar', [&$this, 'getTemplateVars']); $this->page_name = \CoreLibs\Get\System::getPageName(); @@ -211,6 +233,77 @@ class SmartyExtend extends \Smarty // set internal settings $this->CACHE_ID = $cache_id; $this->COMPILE_ID = $compile_id; + // set options + $this->setOptions($options); + } + + /** + * set options + * + * @param array $options + * @return void + */ + private function setOptions(array $options): void + { + // set escape html if option is set + if (!empty($options['escape_html'])) { + $this->setEscapeHtml(true); + } + // load plugins + // plugin array: + // 'file': string, path to plugin content to load + // 'type': a valid smarty type see Smarty PLUGIN_ constants for correct names + // 'tag': the smarty tag + // 'callback': the function to call in 'file' + if (!empty($options['plugins'])) { + foreach ($options['plugins'] as $plugin) { + // file is readable + if ( + empty($plugin['file']) || + !is_file($plugin['file']) || + !is_readable($plugin['file']) + ) { + $this->log->warning('SmartyExtended plugin load failed, file not accessable', [ + 'plugin' => $plugin, + ]); + continue; + } + // tag is alphanumeric + if (!preg_match("/^\w+$/", $plugin['tag'] ?? '')) { + $this->log->warning('SmartyExtended plugin load failed, invalid tag', [ + 'plugin' => $plugin, + ]); + continue; + } + // callback is alphanumeric + if (!preg_match("/^\w+$/", $plugin['callback'] ?? '')) { + $this->log->warning('SmartyExtended plugin load failed, invalid callback', [ + 'plugin' => $plugin, + ]); + continue; + } + try { + /** @phan-suppress-next-line PhanNoopNew */ + new \ReflectionClassConstant($this, $plugin['type']); + } catch (\ReflectionException $e) { + $this->log->error('SmartyExtended plugin load failed, type is not valid', [ + 'message' => $e->getMessage(), + 'plugin' => $plugin, + ]); + continue; + } + try { + require $plugin['file']; + $this->registerPlugin($plugin['type'], $plugin['tag'], $plugin['callback']); + } catch (\Smarty\Exception $e) { + $this->log->error('SmartyExtended plugin load failed with exception', [ + 'message' => $e->getMessage(), + 'plugin' => $plugin, + ]); + continue; + } + } + } } /** diff --git a/www/lib/FileUpload/Core/qqUploadedFileXhr.php b/www/lib/FileUpload/Core/qqUploadedFileXhr.php index 98494145..c1a359cc 100644 --- a/www/lib/FileUpload/Core/qqUploadedFileXhr.php +++ b/www/lib/FileUpload/Core/qqUploadedFileXhr.php @@ -46,19 +46,19 @@ class qqUploadedFileXhr implements qqUploadedFile // phpcs:ignore Squiz.Classes. */ public function getName(): string { - return $_GET['qqfile'] ?? ''; + return !empty($_GET['qqfile']) && is_string($_GET['qqfile']) ? $_GET['qqfile'] : ''; } /** * Get file size from _SERVERa array, throws an error if not possible * - * @return int + * @return int size of the file * * @throws \Exception */ public function getSize(): int { - if (isset($_SERVER['CONTENT_LENGTH'])) { + if (isset($_SERVER['CONTENT_LENGTH']) && is_numeric($_SERVER['CONTENT_LENGTH'])) { return (int)$_SERVER['CONTENT_LENGTH']; } else { throw new \Exception('Getting content length is not supported.');