Compare commits

...

80 Commits

Author SHA1 Message Date
Clemens Schwaighofer
7a4c905ae3 Bug fix in RandomKey validateRandomKeyData() 2026-01-14 15:10:12 +09:00
Clemens Schwaighofer
0531b29749 Release: v9.39.0 2026-01-14 13:23:21 +09:00
Clemens Schwaighofer
5cf06f5b51 Update DateTime stringToTime with better regex and bug fix for millisecond parsing 2026-01-14 13:22:24 +09:00
Clemens Schwaighofer
bb23e760d0 Release: v9.38.1 2026-01-14 10:56:25 +09:00
Clemens Schwaighofer
45487b0039 Update parseCharacterRanges with dashless 2026-01-14 10:54:54 +09:00
Clemens Schwaighofer
bfc82e12a5 Release: v9.38.0 2026-01-14 10:43:30 +09:00
Clemens Schwaighofer
8bd14b4385 Add parseCharacterRanges function to Strings.php and tests 2026-01-14 10:42:16 +09:00
Clemens Schwaighofer
cfb6f14c09 Add color output for phan 2026-01-07 13:21:17 +09:00
Clemens Schwaighofer
969aa684d9 Indent fixes for publish script 2026-01-07 12:50:37 +09:00
Clemens Schwaighofer
74999e6f6b Update phpstan to v2 in phive 2026-01-06 18:44:38 +09:00
Clemens Schwaighofer
787468e67c Release: v9.37.0 2026-01-06 18:38:08 +09:00
Clemens Schwaighofer
936b424065 PHP 8.5 updates 2026-01-06 18:37:00 +09:00
Clemens Schwaighofer
f765f50350 Release: v9.36.0 2025-11-27 18:09:21 +09:00
Clemens Schwaighofer
3b1f8745c2 v9.36.0: Bug for call stack in logging and ErrorMessage class 2025-11-27 18:08:15 +09:00
Clemens Schwaighofer
9079d3120f Release: v9.35.1 2025-10-09 15:11:18 +09:00
Clemens Schwaighofer
e945eac122 v9.35.1: Fix phpunit tests with error_log 2025-10-09 15:10:21 +09:00
Clemens Schwaighofer
352e3dca1f Release: v9.35.0 2025-10-09 12:03:02 +09:00
Clemens Schwaighofer
fb9e04fe55 v9.35.0: Logging datetime to iso date format with time zone, unit checks fixes 2025-10-09 12:01:57 +09:00
Clemens Schwaighofer
1b46b378a3 Update publish script with minor output updates 2025-07-15 14:13:08 +09:00
Clemens Schwaighofer
4dda0937e5 Update pulish script with functions and after publish report 2025-07-15 13:43:01 +09:00
Clemens Schwaighofer
935d5420a1 Release: v9.34.0 2025-06-05 18:15:16 +09:00
Clemens Schwaighofer
dbbe6c263b RandomKey class update, add methods to strings, json, array class, fix phpstan errors in other classes, add tests 2025-06-05 18:13:59 +09:00
Clemens Schwaighofer
ec1fb72ba9 Release: v9.33.0 2025-04-22 11:14:19 +09:00
Clemens Schwaighofer
e0003beae2 phpstan fixes for DB IO, add array key modify method that keeps value 2025-04-22 11:11:55 +09:00
Clemens Schwaighofer
bacd31d0e3 Release: v9.32.1 2025-04-16 17:46:03 +09:00
Clemens Schwaighofer
ae125ea45e DB IO cache reset flag add to ignore warning, and ignore cache reset warning in Login class 2025-04-16 17:44:42 +09:00
Clemens Schwaighofer
94eb1c7697 Release: v9.32.0 2025-04-15 18:43:49 +09:00
Clemens Schwaighofer
aff4944ffd ACL Login update with pages in acl array and file in allowed access list lookup 2025-04-15 18:42:55 +09:00
Clemens Schwaighofer
1a4c8e188f Release: v9.31.1 2025-04-15 17:49:25 +09:00
Clemens Schwaighofer
c603922fca Bug fix in edit user control array for custom error example data missing 2025-04-15 17:48:35 +09:00
Clemens Schwaighofer
7ac13c2ba6 Release: v9.31.0 2025-04-09 11:43:26 +09:00
Clemens Schwaighofer
1c66ee34a1 DB IO rename dbGetQueryHash to dbBuildQueryHash, store last query hash 2025-04-09 11:40:07 +09:00
Clemens Schwaighofer
2e101d55d2 Release: v9.30.0 2025-04-07 19:54:30 +09:00
Clemens Schwaighofer
4b699d753d DB placeholder comment fix, add hash hmac to Hashlib 2025-04-07 19:52:01 +09:00
Clemens Schwaighofer
254a0e4802 Release: v9.29.0 2025-04-04 16:03:34 +09:00
Clemens Schwaighofer
82f35535ae Class Hash update 2025-04-04 15:59:59 +09:00
Clemens Schwaighofer
c41796a478 Release: v9.28.1 2025-04-01 11:29:56 +09:00
Clemens Schwaighofer
a310fab3ee Release: v9.28.0 2025-04-01 11:28:58 +09:00
Clemens Schwaighofer
9914815285 htmlent add encoding, date combined add wrapper for calc date interval for numeric and named index return 2025-04-01 11:27:57 +09:00
Clemens Schwaighofer
969467fa15 Release: v9.27.0 2025-02-17 11:27:12 +09:00
Clemens Schwaighofer
f4dd78fff2 Merge branch 'development' 2025-02-17 11:26:16 +09:00
Clemens Schwaighofer
ba5e78e839 Config errors throw exception, bug fixes for date interval, eslint update, Login ACL number to unit detail 2025-02-17 11:25:36 +09:00
Clemens Schwaighofer
1a5ee2e16d Release: v9.26.8 2025-01-20 10:51:05 +09:00
Clemens Schwaighofer
e1d9985ec8 DB IO cache reset query not found is warning and not error 2025-01-20 10:50:07 +09:00
Clemens Schwaighofer
2316c151ac Release: v9.26.7.1 2025-01-17 18:12:33 +09:00
Clemens Schwaighofer
8ff8aa195b Merge branch 'master' into development 2025-01-17 18:10:07 +09:00
Clemens Schwaighofer
f176d12a1e Release: v9.26.7 2025-01-17 18:03:29 +09:00
Clemens Schwaighofer
f974b15f78 Smarty Extended update 2025-01-17 18:01:21 +09:00
Clemens Schwaighofer
91fad09367 DB IO prepare query fix for INSERT types 2025-01-17 17:56:43 +09:00
Clemens Schwaighofer
e8fe1feda5 Release: v9.26.6.1 2025-01-17 14:46:42 +09:00
Clemens Schwaighofer
23fd78e5c8 ACL Login depricate edit access id check 2025-01-17 14:45:54 +09:00
Clemens Schwaighofer
6cdede2997 Release: v9.26.6 2025-01-17 14:40:21 +09:00
Clemens Schwaighofer
ace02b14d8 Merge branch 'development' 2025-01-17 14:39:05 +09:00
Clemens Schwaighofer
58e916d314 Fix ACL Login edit access cuid <-> id lookup 2025-01-17 14:38:41 +09:00
Clemens Schwaighofer
4f6d85f4da Release: v9.26.5 2025-01-17 12:52:30 +09:00
Clemens Schwaighofer
cd45590a72 ACL Login add lookup edit access id to cuid 2025-01-17 12:51:25 +09:00
Clemens Schwaighofer
4d42da201c Release: v9.26.4 2025-01-17 10:06:57 +09:00
Clemens Schwaighofer
e310cb626a Logging file block separator character change, deprecated php 8.4 helpers 2025-01-17 10:05:54 +09:00
Clemens Schwaighofer
c04c71d755 Release: v9.26.3 2025-01-16 14:52:16 +09:00
Clemens Schwaighofer
9fc40a6629 ACL Login add edit access id to acl array 2025-01-16 14:51:29 +09:00
Clemens Schwaighofer
6362e7f2f0 Release: v9.26.2 2025-01-16 14:40:08 +09:00
Clemens Schwaighofer
50dfc10d31 Merge branch 'development' 2025-01-16 14:38:59 +09:00
Clemens Schwaighofer
24077e483f ACL Login add edit access id to cuid lookup 2025-01-16 14:38:49 +09:00
Clemens Schwaighofer
6585c6bfef Release: v9.26.1 2025-01-16 14:11:41 +09:00
Clemens Schwaighofer
f180046283 ACL Login unit detail info update, deprecated message fix 2025-01-16 14:10:46 +09:00
Clemens Schwaighofer
b64d0ce5f0 Release: v9.26.0 2025-01-16 10:27:00 +09:00
Clemens Schwaighofer
bab8460f80 PHP 8.4 compatible release 2025-01-16 10:25:58 +09:00
Clemens Schwaighofer
a092217201 Release: v9.25.3 2024-12-24 12:52:33 +09:00
Clemens Schwaighofer
e286d7f913 DB IO placeholder counter fix 2024-12-24 12:49:49 +09:00
Clemens Schwaighofer
e148a39902 Release: v9.25.2 2024-12-23 11:37:26 +09:00
Clemens Schwaighofer
b7d5a79c3a Allow method chaining in Session and encryption class
For session set/unset/auto write close flag

In the encryption classes for setting keys
2024-12-23 11:36:06 +09:00
Clemens Schwaighofer
9f8a86b4b0 Release:v 9.25.1.1 2024-12-18 10:59:47 +09:00
Clemens Schwaighofer
50e593789e Asyemmetric Anonymous Encryption 2024-12-18 10:55:16 +09:00
Clemens Schwaighofer
4ee141f8df Release: v9.24.1 2024-12-13 11:47:05 +09:00
Clemens Schwaighofer
9ee8f43478 Rename all table columns from ecuid and ecuuid to eucuid and eucuuid 2024-12-13 11:45:16 +09:00
Clemens Schwaighofer
2c75dbdf6c Release: v9.24.0 2024-12-13 11:12:39 +09:00
Clemens Schwaighofer
5fe61388fc phpunit xml update 2024-12-13 11:11:42 +09:00
Clemens Schwaighofer
a03c7e7319 Class ACL Login and Session update
Session:
- can recreate session id periodic (Default never)
- options are set via array like in other classes
- checks for strict session settings on default

ACL Login:
- remove all DEBUG/DB_DEBUG variables, calls, etc
	- removed from the EditBase/EditUsers classes too
- switch to UUIDv4 as the session lookup variable
- all session vars are prefixed with "LOGIN_"
	- the charset ones are left as DEFAULT_CHARSET, DEFAULT_LOCALE, DEFAULT_LANG
	- the old LOGIN_LANG has been removed (deprecated)
	- TEMPLATE session has been removed, there is no template data in the edit class
- session is resynced (ACL lookup), default 5min, adjustable via option
- sets strict header options as default
- moves several methods parts into their own classes
	- plan to split up class into sub classes for certain actions
- new force logout counter in DB
- edit logger is moved into this class
	- plan to move logging into sub class
- all SQL calls user heredoc and params
- update login/change password to new layout for pc/smartphone compatible
	- change password will be replaced with reset password in future
- last login success is now set as timestamp
- all old PK lookups for edit access etc are deprecated and replaced with cuid lookups

ArrayHandling:
- add array return matching key
Give any array with key values and a list of keys and only return matching keys
Wrapper for array_filter call
2024-12-13 10:54:20 +09:00
Clemens Schwaighofer
7e01152bb4 Release: v9.23.3 2024-12-12 21:12:24 +09:00
Clemens Schwaighofer
fbea8f4aca Fix for Symmetric encryption key handling 2024-12-12 21:11:25 +09:00
76 changed files with 10028 additions and 2167 deletions

View File

@@ -56,6 +56,8 @@ return [
// Automatically inferred from composer.json requirement for "php" of ">=8.2" // Automatically inferred from composer.json requirement for "php" of ">=8.2"
'target_php_version' => '8.2', 'target_php_version' => '8.2',
"minimum_target_php_version" => "8.2", "minimum_target_php_version" => "8.2",
// turn color on (-C)
"color_issue_messages_if_supported" => true,
// If enabled, missing properties will be created when // If enabled, missing properties will be created when
// they are first seen. If false, we'll report an // they are first seen. If false, we'll report an

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive"> <phive xmlns="https://phar.io/phive">
<phar name="phpunit" version="^9.6" installed="9.6.19" location="./tools/phpunit" copy="false"/> <phar name="phpunit" version="^9.6" installed="9.6.31" location="./tools/phpunit" copy="false"/>
<phar name="phpcs" version="^3.7.2" installed="3.10.0" location="./tools/phpcs" copy="false"/> <phar name="phpcs" version="^4.0.0" installed="4.0.1" location="./tools/phpcs" copy="false"/>
<phar name="phpcbf" version="^3.7.2" installed="3.10.0" location="./tools/phpcbf" copy="false"/> <phar name="phpcbf" version="^4.0.0" installed="4.0.1" location="./tools/phpcbf" copy="false"/>
<phar name="psalm" version="^5.15.0" installed="5.24.0" location="./tools/psalm" copy="false"/> <phar name="psalm" version="^5.15.0" installed="5.26.1" location="./tools/psalm" copy="false"/>
<phar name="phpstan" version="^1.10.37" installed="1.11.1" location="./tools/phpstan" copy="false"/> <phar name="phpstan" version="^2.0.0" installed="2.1.33" location="./tools/phpstan" copy="false"/>
<phar name="phan" version="^5.4.2" installed="5.4.3" location="./tools/phan" copy="false"/> <phar name="phan" version="^5.4.2" installed="5.5.2" location="./tools/phan" copy="false"/>
</phive> </phive>

View File

@@ -25,7 +25,7 @@
"phpstan/phpdoc-parser": "^2.0", "phpstan/phpdoc-parser": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-deprecation-rules": "^2.0",
"phan/phan": "^5.4", "phan/phan": "^5.4",
"egrajp/smarty-extended": "^4.3", "egrajp/smarty-extended": "^5.4",
"gullevek/dotenv": "dev-master", "gullevek/dotenv": "dev-master",
"phpunit/phpunit": "^9" "phpunit/phpunit": "^9"
}, },

View File

@@ -22,6 +22,9 @@ parameters:
# - vendor # - vendor
# ignore errores with # ignore errores with
ignoreErrors: ignoreErrors:
-
message: '#Expression in empty\(\) is not falsy.#'
path: %currentWorkingDirectory%/src/Language/GetLocale.php
#- # this error is ignore because of the PHP 8.0 to 8.1 change for pg_*, only for 8.0 or lower #- # this error is ignore because of the PHP 8.0 to 8.1 change for pg_*, only for 8.0 or lower
# message: "#^Parameter \\#1 \\$(result|connection) of function pg_\\w+ expects resource(\\|null)?, object\\|resource(\\|bool)? given\\.$#" # message: "#^Parameter \\#1 \\$(result|connection) of function pg_\\w+ expects resource(\\|null)?, object\\|resource(\\|bool)? given\\.$#"
# path: %currentWorkingDirectory%/www/lib/CoreLibs/DB/SQL/PgSQL.php # path: %currentWorkingDirectory%/www/lib/CoreLibs/DB/SQL/PgSQL.php

View File

@@ -4,4 +4,9 @@
verbose="true" verbose="true"
bootstrap="test/phpunit/bootstrap.php" bootstrap="test/phpunit/bootstrap.php"
> >
<testsuites>
<testsuite name="deploy">
<directory>test/phpunit</directory>
</testsuite>
</testsuites>
</phpunit> </phpunit>

View File

@@ -1 +1 @@
9.23.2 9.39.0

View File

@@ -3,23 +3,92 @@
BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/"; BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/";
PACKAGE_DOWNLOAD="${BASE_FOLDER}package-download/"; PACKAGE_DOWNLOAD="${BASE_FOLDER}package-download/";
if [ ! -d "${PACKAGE_DOWNLOAD}" ]; then if [ ! -d "${PACKAGE_DOWNLOAD}" ]; then
mkdir "${PACKAGE_DOWNLOAD}"; mkdir "${PACKAGE_DOWNLOAD}";
fi; fi;
VERSION=$(git tag --list | sort -V | tail -n1 | sed -e "s/^v//"); VERSION=$(git tag --list | sort -V | tail -n1 | sed -e "s/^v//");
file_last_published="${BASE_FOLDER}last.published"; file_last_published="${BASE_FOLDER}last.published";
go_flag="$1"; go_flag="$1";
function gitea_publish
{
_GITEA_PUBLISH="${1}"
_GITEA_UPLOAD_FILENAME="${2}"
_GITEA_URL_DL="${3}"
_GITEA_URL_PUSH="${4}"
_GITEA_USER="${5}"
_GITEA_TOKEN="${6}"
_PACKAGE_DOWNLOAD="${7}"
_VERSION="${8}"
_file_last_published="${9}"
if [ -z "${_GITEA_PUBLISH}" ]; then
return
fi;
if [ -n "${_GITEA_UPLOAD_FILENAME}" ] &&
[ -n "${_GITEA_URL_DL}" ] && [ -n "${_GITEA_URL_PUSH}" ] &&
[ -n "${_GITEA_USER}" ] && [ -n "${_GITEA_TOKEN}" ]; then
echo "> Publish ${_GITEA_UPLOAD_FILENAME} with ${_VERSION} to: ${_GITEA_URL_PUSH}";
if [ ! -f "${_PACKAGE_DOWNLOAD}${_GITEA_UPLOAD_FILENAME}-v${_VERSION}.zip" ]; then
echo "> Download: ${_GITEA_UPLOAD_FILENAME}-v${_VERSION}.zip";
curl -LJO \
--output-dir "${_PACKAGE_DOWNLOAD}" \
"${_GITEA_URL_DL}"/v"${_VERSION}".zip;
fi;
if [ ! -f "${_PACKAGE_DOWNLOAD}${_GITEA_UPLOAD_FILENAME}-v${_VERSION}.zip" ]; then
echo "[!] Package file does not exist for version: ${_VERSION}";
else
response=$(curl --user "${_GITEA_USER}":"${_GITEA_TOKEN}" \
--upload-file "${_PACKAGE_DOWNLOAD}${_GITEA_UPLOAD_FILENAME}-v${_VERSION}.zip" \
"${_GITEA_URL_PUSH}"?version="${_VERSION}");
status=$(echo "${response}" | jq .errors[].status);
message=$(echo "${response}" | jq .errors[].message);
if [ -n "${status}" ]; then
echo "[!] Error ${status}: ${message}";
else
echo "> Publish completed";
fi;
echo "${_VERSION}" > "${_file_last_published}";
fi;
else
echo "[!] Missing either GITEA_UPLOAD_FILENAME, GITEA_URL_DL, GITEA_URL_PUSH, GITEA_USER or GITEA_TOKEN environment variable";
fi;
}
function gitlab_publish
{
_GITLAB_PUBLISH="${1}";
_GITLAB_URL="${2}";
_GITLAB_DEPLOY_TOKEN="${3}";
_PACKAGE_DOWNLOAD="${4}"
_VERSION="${5}"
_file_last_published="${6}"
if [ -z "${_GITLAB_PUBLISH}" ]; then
return;
fi;
if [ -n "${_GITLAB_URL}" ] && [ -n "${_GITLAB_DEPLOY_TOKEN}" ]; then
curl --data tag=v"${_VERSION}" \
--header "Deploy-Token: ${_GITLAB_DEPLOY_TOKEN}" \
"${_GITLAB_URL}";
curl --data branch=master \
--header "Deploy-Token: ${_GITLAB_DEPLOY_TOKEN}" \
"${_GITLAB_URL}";
echo "${_VERSION}" > "${_file_last_published}";
else
echo "[!] Missing GITLAB_URL or GITLAB_DEPLOY_TOKEN environment variable";
fi;
}
if [ -z "${VERSION}" ]; then if [ -z "${VERSION}" ]; then
echo "Version must be set in the form x.y.z without any leading characters"; echo "[!] Version must be set in the form x.y.z without any leading characters";
exit; exit;
fi; fi;
# compare version, if different or newer, deploy # compare version, if different or newer, deploy
if [ -f "${file_last_published}" ]; then if [ -f "${file_last_published}" ]; then
LAST_PUBLISHED_VERSION=$(cat "${file_last_published}"); LAST_PUBLISHED_VERSION=$(cat "${file_last_published}");
if dpkg --compare-versions "${VERSION}" le "${LAST_PUBLISHED_VERSION}"; then if dpkg --compare-versions "${VERSION}" le "${LAST_PUBLISHED_VERSION}"; then
echo "git tag version ${VERSION} is not newer than previous published version ${LAST_PUBLISHED_VERSION}"; echo "[!] git tag version ${VERSION} is not newer than previous published version ${LAST_PUBLISHED_VERSION}";
exit; fi;
fi;
fi; fi;
# read in the .env.deploy file and we must have # read in the .env.deploy file and we must have
@@ -36,62 +105,30 @@ fi;
# GITLAB_TOKEN # GITLAB_TOKEN
# GITLAB_URL # GITLAB_URL
if [ ! -f "${BASE_FOLDER}.env.deploy" ]; then if [ ! -f "${BASE_FOLDER}.env.deploy" ]; then
echo "Deploy enviroment file .env.deploy is missing"; echo "[!] Deploy enviroment file .env.deploy is missing";
exit; exit;
fi; fi;
set -o allexport; set -o allexport;
cd "${BASE_FOLDER}" || exit; cd "${BASE_FOLDER}" || exit
# shellcheck source=.env.deploy # shellcheck source=.env.deploy
source .env.deploy; source .env.deploy;
cd - || exit; cd - >/dev/null 2>&1 || exit;
set +o allexport; set +o allexport;
if [ "${go_flag}" != "go" ]; then if [ "${go_flag}" != "go" ]; then
echo "No go flag given"; echo "[!] No go flag given";
echo "Would publish ${VERSION}"; echo "> Would publish ${VERSION}";
echo "[END]"; echo "[END]";
exit; exit;
fi; fi;
echo "[START]"; echo "[START]";
# gitea # gitea
# skip iof gitea_publish "${GITEA_PUBLISH}" "${GITEA_UPLOAD_FILENAME}" "${GITEA_URL_DL}" "${GITEA_URL_PUSH}" "${GITEA_USER}" "${GITEA_TOKEN}" "${PACKAGE_DOWNLOAD}" "${VERSION}" "${file_last_published}";
if [ -n "${GITEA_PUBLISH}" ]; then gitea_publish "${PR_GITEA_PUBLISH}" "${PR_GITEA_UPLOAD_FILENAME}" "${PR_GITEA_URL_DL}" "${PR_GITEA_URL_PUSH}" "${PR_GITEA_USER}" "${PR_GITEA_TOKEN}" "${PACKAGE_DOWNLOAD}" "${VERSION}" "${file_last_published}";
if [ -n "${GITEA_UPLOAD_FILENAME}" ] &&
[ -n "${GITEA_URL_DL}" ] && [ -n "${GITEA_URL_PUSH}" ] &&
[ -n "${GITEA_USER}" ] && [ -n "${GITEA_TOKEN}" ]; then
if [ ! -f "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" ]; then
curl -LJO \
--output-dir "${PACKAGE_DOWNLOAD}" \
"${GITEA_URL_DL}"/v"${VERSION}".zip;
fi;
if [ ! -f "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" ]; then
echo "Version file does not exist for ${VERSION}";
else
curl --user "${GITEA_USER}":"${GITEA_TOKEN}" \
--upload-file "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" \
"${GITEA_URL_PUSH}"?version="${VERSION}";
echo "${VERSION}" > "${file_last_published}";
fi;
else
echo "Missing either GITEA_UPLOAD_FILENAME, GITEA_URL_DL, GITEA_URL_PUSH, GITEA_USER or GITEA_TOKEN environment variable";
fi;
fi;
# gitlab # gitlab
if [ -n "${GITLAB_PUBLISH}" ]; then # gitlab_publish "${GITLAB_PUBLISH}" "${GITLAB_URL}" "${GITLAB_DEPLOY_TOKEN}" "${PACKAGE_DOWNLOAD}" "${VERSION}" "${file_last_published}";
if [ -n "${GITLAB_URL}" ] && [ -n "${GITLAB_DEPLOY_TOKEN}" ]; then
curl --data tag=v"${VERSION}" \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"${GITLAB_URL}";
curl --data branch=master \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"${GITLAB_URL}";
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing GITLAB_URL or GITLAB_DEPLOY_TOKEN environment variable";
fi;
fi;
echo ""; echo "";
echo "[DONE]"; echo "[DONE]";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/12/12
* DESCRIPTION:
* ACL Login user status bitmap list
*/
declare(strict_types=1);
namespace CoreLibs\ACL;
final class LoginUserStatus
{
// lock status bitmap (smallint, 256)
/** @var int enabled flag */
public const ENABLED = 1;
/** @var int deleted flag */
public const DELETED = 2;
/** @var int locked flag */
public const LOCKED = 4;
/** @var int banned/suspened flag [not implemented] */
public const BANNED = 8;
/** @var int password reset in progress [not implemented] */
public const RESET = 16;
/** @var int confirm/paending, eg waiting for confirm of email [not implemented] */
public const CONFIRM = 32;
/** @var int strict, on error lock */
public const STRICT = 64;
/** @var int proected, cannot delete */
public const PROTECTED = 128;
/** @var int master admin flag */
public const ADMIN = 256;
/**
* Returns an array mapping the numerical role values to their descriptive names
*
* @return array<int|string,string>
*/
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__

View File

@@ -289,7 +289,7 @@ class Backend
* JSON, STRING/SERIEAL, BINARY/BZIP or ZLIB * JSON, STRING/SERIEAL, BINARY/BZIP or ZLIB
* @param string|null $db_schema [default=null] override target schema * @param string|null $db_schema [default=null] override target schema
* @return void * @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( public function adbEditLog(
string $event = '', string $event = '',
@@ -358,7 +358,7 @@ class Backend
} }
$q = <<<SQL $q = <<<SQL
INSERT INTO {DB_SCHEMA}.edit_log ( INSERT INTO {DB_SCHEMA}.edit_log (
username, euid, ecuid, ecuuid, event_date, event, error, data, data_binary, page, username, euid, eucuid, eucuuid, event_date, event, error, data, data_binary, page,
ip, user_agent, referer, script_name, query_string, server_name, http_host, ip, user_agent, referer, script_name, query_string, server_name, http_host,
http_accept, http_accept_charset, http_accept_encoding, session_id, http_accept, http_accept_charset, http_accept_encoding, session_id,
action, action_id, action_sub_id, action_yes, action_flag, action_menu, action_loaded, action, action_id, action_sub_id, action_yes, action_flag, action_menu, action_loaded,

View File

@@ -14,9 +14,6 @@ declare(strict_types=1);
namespace CoreLibs\Admin; namespace CoreLibs\Admin;
use Exception;
use SmartyException;
class EditBase class EditBase
{ {
/** @var array<mixed> */ /** @var array<mixed> */
@@ -63,6 +60,7 @@ class EditBase
// smarty template engine (extended Translation version) // smarty template engine (extended Translation version)
$this->smarty = new \CoreLibs\Template\SmartyExtend( $this->smarty = new \CoreLibs\Template\SmartyExtend(
$l10n, $l10n,
$log,
$options['cache_id'] ?? '', $options['cache_id'] ?? '',
$options['compile_id'] ?? '', $options['compile_id'] ?? '',
); );
@@ -78,7 +76,7 @@ class EditBase
); );
if ($this->form->mobile_phone) { if ($this->form->mobile_phone) {
echo "I am sorry, but this page cannot be viewed by a 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)); // $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_until');
$elements[] = $this->form->formCreateElement('lock_after'); $elements[] = $this->form->formCreateElement('lock_after');
$elements[] = $this->form->formCreateElement('admin'); $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_language_id');
$elements[] = $this->form->formCreateElement('edit_scheme_id'); $elements[] = $this->form->formCreateElement('edit_scheme_id');
$elements[] = $this->form->formCreateElementListTable('edit_access_user'); $elements[] = $this->form->formCreateElementListTable('edit_access_user');
@@ -540,8 +536,7 @@ class EditBase
* builds the smarty content and runs smarty display output * builds the smarty content and runs smarty display output
* *
* @return void * @return void
* @throws Exception * @throws \Smarty\Exception
* @throws SmartyException
*/ */
public function editBaseRun( public function editBaseRun(
?string $template_dir = null, ?string $template_dir = null,

View File

@@ -103,11 +103,7 @@ class Basic
'VIDEOS', 'DOCUMENTS', 'PDFS', 'BINARIES', 'ICONS', 'UPLOADS', 'CSV', 'JS', 'VIDEOS', 'DOCUMENTS', 'PDFS', 'BINARIES', 'ICONS', 'UPLOADS', 'CSV', 'JS',
'CSS', 'TABLE_ARRAYS', 'SMARTY', 'LANG', 'CACHE', 'TMP', 'LOG', 'TEMPLATES', 'CSS', 'TABLE_ARRAYS', 'SMARTY', 'LANG', 'CACHE', 'TMP', 'LOG', 'TEMPLATES',
'TEMPLATES_C', 'DEFAULT_LANG', 'DEFAULT_ENCODING', 'DEFAULT_HASH', 'TEMPLATES_C', 'DEFAULT_LANG', 'DEFAULT_ENCODING', 'DEFAULT_HASH',
'DEFAULT_ACL_LEVEL', 'LOGOUT_TARGET', 'PASSWORD_CHANGE', 'AJAX_REQUEST_TYPE', 'DB_CONFIG_NAME', 'DB_CONFIG', 'TARGET'
'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'
] as $constant ] as $constant
) { ) {
if (!defined($constant)) { if (!defined($constant)) {
@@ -387,7 +383,8 @@ class Basic
public function initRandomKeyLength(int $key_length): bool public function initRandomKeyLength(int $key_length): bool
{ {
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\RandomKey::setRandomKeyLength()', E_USER_DEPRECATED); trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\RandomKey::setRandomKeyLength()', E_USER_DEPRECATED);
return \CoreLibs\Create\RandomKey::setRandomKeyLength($key_length); // no op, we do no longer pre set the random key length
return true;
} }
/** /**
@@ -992,10 +989,10 @@ class Basic
* @param bool $auto_check default true, if source encoding is set * @param bool $auto_check default true, if source encoding is set
* check that the source is actually matching * check that the source is actually matching
* to what we sav the source is * to what we sav the source is
* @return string encoding converted string * @return string|false encoding converted string
* @deprecated use \CoreLibs\Convert\Encoding::convertEncoding() instead * @deprecated use \CoreLibs\Convert\Encoding::convertEncoding() instead
*/ */
public static function convertEncoding(string $string, string $to_encoding, string $source_encoding = '', bool $auto_check = true): string public static function convertEncoding(string $string, string $to_encoding, string $source_encoding = '', bool $auto_check = true): string|false
{ {
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Encoding::convertEncoding()', E_USER_DEPRECATED); trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Encoding::convertEncoding()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Encoding::convertEncoding($string, $to_encoding, $source_encoding, $auto_check); return \CoreLibs\Convert\Encoding::convertEncoding($string, $to_encoding, $source_encoding, $auto_check);
@@ -1028,8 +1025,12 @@ class Basic
*/ */
public function __sha1Short(string $string, bool $use_sha = false): string public function __sha1Short(string $string, bool $use_sha = false): string
{ {
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__sha1Short()', E_USER_DEPRECATED); trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::sha1Short() or ::__crc32b()', E_USER_DEPRECATED);
return \CoreLibs\Create\Hash::__sha1Short($string, $use_sha); if ($use_sha) {
return \CoreLibs\Create\Hash::sha1Short($string);
} else {
return \CoreLibs\Create\Hash::__crc32b($string);
}
} }
/** /**
@@ -1044,8 +1045,8 @@ class Basic
*/ */
public function __hash(string $string, string $hash_type = 'adler32'): string public function __hash(string $string, string $hash_type = 'adler32'): string
{ {
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__hash()', E_USER_DEPRECATED); trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::hash()', E_USER_DEPRECATED);
return \CoreLibs\Create\Hash::__hash($string, $hash_type); return \CoreLibs\Create\Hash::hash($string, $hash_type);
} }
// *** HASH FUNCTIONS END // *** HASH FUNCTIONS END

View File

@@ -10,12 +10,16 @@ class Email
/** @var array<int,string> */ /** @var array<int,string> */
private static array $email_regex_check = [ private static array $email_regex_check = [
0 => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@" 0 => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@"
. "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$", // MASTER // . "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$", // MASTER
// fixed pattern matching for domain
. "(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$", // MASTER
1 => "@(.*)@(.*)", // double @ 1 => "@(.*)@(.*)", // double @
2 => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@", // wrong part before @ 2 => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@", // wrong part before @
3 => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.([a-zA-Z]{2,}){1}$", // wrong part after @ // 3 => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.([a-zA-Z]{2,}){1}$", // wrong part after @
4 => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.", // wrong domain name part 3 => "@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$", // wrong part after @
5 => "\.([a-zA-Z]{2,6}){1}$", // wrong top level part // 4 => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.", // wrong domain name part
4 => "@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.", // wrong domain name part
5 => "\.[a-zA-Z]{2,6}$", // wrong top level part
6 => "@(.*)\.{2,}", // double .. in domain name part 6 => "@(.*)\.{2,}", // double .. in domain name part
7 => "@.*\.$" // ends with a dot, top level, domain missing 7 => "@.*\.$" // ends with a dot, top level, domain missing
]; ];

View File

@@ -56,7 +56,11 @@ class Encoding
{ {
// return mb_substitute_character(); // return mb_substitute_character();
if ($return_substitute_func === true) { if ($return_substitute_func === true) {
return mb_substitute_character(); // if false abort with error
if (($return = mb_substitute_character()) === false) {
return self::$mb_error_char;
}
return $return;
} else { } else {
return self::$mb_error_char; return self::$mb_error_char;
} }
@@ -88,7 +92,13 @@ class Encoding
): array|false { ): array|false {
// convert to target encoding and convert back // convert to target encoding and convert back
$temp = mb_convert_encoding($string, $to_encoding, $from_encoding); $temp = mb_convert_encoding($string, $to_encoding, $from_encoding);
if ($temp === false) {
return false;
}
$compare = mb_convert_encoding($temp, $from_encoding, $to_encoding); $compare = mb_convert_encoding($temp, $from_encoding, $to_encoding);
if ($compare === false) {
return false;
}
// if string does not match anymore we have a convert problem // if string does not match anymore we have a convert problem
if ($string == $compare) { if ($string == $compare) {
return false; return false;
@@ -104,7 +114,7 @@ class Encoding
(($char != $r_char && (!self::$mb_error_char || (($char != $r_char && (!self::$mb_error_char ||
in_array(self::$mb_error_char, ['none', 'long', 'entity']))) || in_array(self::$mb_error_char, ['none', 'long', 'entity']))) ||
($char != $r_char && $r_char == self::$mb_error_char && self::$mb_error_char)) && ($char != $r_char && $r_char == self::$mb_error_char && self::$mb_error_char)) &&
ord($char) != 194 ord($char[0]) != 194
) { ) {
$failed[] = $char; $failed[] = $char;
} }

View File

@@ -10,6 +10,8 @@ namespace CoreLibs\Combined;
class ArrayHandler class ArrayHandler
{ {
public const string DATA_SEPARATOR = ':';
/** /**
* searches key = value in an array / array * searches key = value in an array / array
* only returns the first one found * only returns the first one found
@@ -148,28 +150,32 @@ class ArrayHandler
* array search simple. looks for key, value combination, if found, returns true * array search simple. looks for key, value combination, if found, returns true
* on default does not strict check, so string '4' will match int 4 and vica versa * on default does not strict check, so string '4' will match int 4 and vica versa
* *
* @param array<mixed> $array search in as array * @param array<mixed> $in_array search in as array
* @param string|int $key key (key to search in) * @param string|int $key key (key to search in)
* @param string|int|bool $value value (what to find) * @param string|int|bool|array<string|int|bool> $value values list (what to find)
* @param bool $strict [false], if set to true, will strict check key/value * @param bool $strict [false], if set to true, will strict check key/value
* @return bool true on found, false on not found * @return bool true on found, false on not found
*/ */
public static function arraySearchSimple( public static function arraySearchSimple(
array $array, array $in_array,
string|int $key, string|int $key,
string|int|bool $value, string|int|bool|array $value,
bool $strict = false bool $strict = false
): bool { ): bool {
foreach ($array as $_key => $_value) { // convert to array
if (!is_array($value)) {
$value = [$value];
}
foreach ($in_array as $_key => $_value) {
// if value is an array, we search // if value is an array, we search
if (is_array($_value)) { if (is_array($_value)) {
// call recursive, and return result if it is true, else continue // call recursive, and return result if it is true, else continue
if (($result = self::arraySearchSimple($_value, $key, $value, $strict)) !== false) { if (($result = self::arraySearchSimple($_value, $key, $value, $strict)) !== false) {
return $result; return $result;
} }
} elseif ($strict === false && $_key == $key && $_value == $value) { } elseif ($strict === false && $_key == $key && in_array($_value, $value)) {
return true; return true;
} elseif ($strict === true && $_key === $key && $_value === $value) { } elseif ($strict === true && $_key === $key && in_array($_value, $value, true)) {
return true; return true;
} }
} }
@@ -183,19 +189,19 @@ class ArrayHandler
* If prefix is turned on each found group will be prefixed with the * If prefix is turned on each found group will be prefixed with the
* search key * search key
* *
* @param array<mixed> $array array to search in * @param array<mixed> $in_array array to search in
* @param array<mixed> $needles keys to find in array * @param array<mixed> $needles keys to find in array
* @param bool $flat [false] Turn on flat output * @param bool $flat [false] Turn on flat output
* @param bool $prefix [false] Prefix found with needle key * @param bool $prefix [false] Prefix found with needle key
* @return array<mixed> Found values * @return array<mixed> Found values
*/ */
public static function arraySearchKey( public static function arraySearchKey(
array $array, array $in_array,
array $needles, array $needles,
bool $flat = false, bool $flat = false,
bool $prefix = false bool $prefix = false
): array { ): array {
$iterator = new \RecursiveArrayIterator($array); $iterator = new \RecursiveArrayIterator($in_array);
$recursive = new \RecursiveIteratorIterator( $recursive = new \RecursiveIteratorIterator(
$iterator, $iterator,
\RecursiveIteratorIterator::SELF_FIRST \RecursiveIteratorIterator::SELF_FIRST
@@ -236,17 +242,171 @@ class ArrayHandler
return $hit_list; return $hit_list;
} }
/**
* Search in an array for value with or without key and
* check in the same array block for the required key
* If not found return an array with the array block there the required key is missing,
* the path as string with seperator block set and the missing key entry
*
* @param array<mixed> $in_array
* @param string|int|float|bool $search_value
* @param string|array<string> $required_key
* @param ?string $search_key [null]
* @param string $path_separator [DATA_SEPARATOR]
* @param string $current_path
* @return array<array{content?:array<mixed>,path?:string,missing_key?:array<string>}>
*/
public static function findArraysMissingKey(
array $in_array,
string|int|float|bool $search_value,
string|array $required_key,
?string $search_key = null,
string $path_separator = self::DATA_SEPARATOR,
string $current_path = ''
): array {
$results = [];
foreach ($in_array as $key => $value) {
$path = $current_path ? $current_path . $path_separator . $key : $key;
if (is_array($value)) {
// Check if this array contains the search value
// either any value match or with key
if ($search_key === null) {
$containsValue = in_array($search_value, $value, true);
} else {
$containsValue = array_key_exists($search_key, $value) && $value[$search_key] === $search_value;
}
// If it contains the value but doesn't have the required key
if (
$containsValue &&
(
(
is_string($required_key) &&
!array_key_exists($required_key, $value)
) || (
is_array($required_key) &&
count(array_intersect($required_key, array_keys($value))) !== count($required_key)
)
)
) {
$results[] = [
'content' => $value,
'path' => $path,
'missing_key' => is_array($required_key) ?
array_values(array_diff($required_key, array_keys($value))) :
[$required_key]
];
}
// Recursively search nested arrays
$results = array_merge(
$results,
self::findArraysMissingKey(
$value,
$search_value,
$required_key,
$search_key,
$path_separator,
$path
)
);
}
}
return $results;
}
/**
* Find key => value entry and return set with key for all matching
* Can search recursively through nested arrays if recursive flag is set
*
* @param array<mixed> $in_array
* @param string $lookup
* @param int|string|float|bool $search
* @param bool $strict [false]
* @param bool $case_insensitive [false]
* @param bool $recursive [false]
* @param bool $flat_result [true] If set to false and recursive is on the result is a nested array
* @param string $flat_separator [DATA_SEPARATOR] if flat result is true, can be any string
* @return array<mixed>
*/
public static function selectArrayFromOption(
array $in_array,
string $lookup,
int|string|float|bool $search,
bool $strict = false,
bool $case_insensitive = false,
bool $recursive = false,
bool $flat_result = true,
string $flat_separator = self::DATA_SEPARATOR
): array {
// skip on empty
if ($in_array == []) {
return [];
}
// init return result
$result = [];
// case sensitive convert if string
if ($case_insensitive && is_string($search)) {
$search = strtolower($search);
}
foreach ($in_array as $key => $value) {
// Handle current level search
if (isset($value[$lookup])) {
$compareValue = $value[$lookup];
if ($case_insensitive && is_string($compareValue)) {
$compareValue = strtolower($compareValue);
}
if (
($strict && $search === $compareValue) ||
(!$strict && $search == $compareValue)
) {
$result[$key] = $value;
}
}
// Handle recursive search if flag is set
if ($recursive && is_array($value)) {
$recursiveResults = self::selectArrayFromOption(
$value,
$lookup,
$search,
$strict,
$case_insensitive,
true,
$flat_result,
$flat_separator
);
// Merge recursive results with current results
// Preserve keys by using array_merge with string keys or + operator
foreach ($recursiveResults as $recKey => $recValue) {
if ($flat_result) {
$result[$key . $flat_separator . $recKey] = $recValue;
} else {
$result[$key][$recKey] = $recValue;
}
}
}
}
return $result;
}
/** /**
* main wrapper function for next/prev key * main wrapper function for next/prev key
* *
* @param array<mixed> $array array to search in * @param array<mixed> $in_array array to search in
* @param int|string $key key for next/prev * @param int|string $key key for next/prev
* @param bool $next [=true] if to search next or prev * @param bool $next [=true] if to search next or prev
* @return int|string|null Next/prev key or null for end/first * @return int|string|null Next/prev key or null for end/first
*/ */
private static function arrayGetKey(array $array, int|string $key, bool $next = true): int|string|null private static function arrayGetKey(array $in_array, int|string $key, bool $next = true): int|string|null
{ {
$keys = array_keys($array); $keys = array_keys($in_array);
if (($position = array_search($key, $keys, true)) === false) { if (($position = array_search($key, $keys, true)) === false) {
return null; return null;
} }
@@ -262,26 +422,26 @@ class ArrayHandler
* Get previous array key from an array * Get previous array key from an array
* null on not found * null on not found
* *
* @param array<mixed> $array * @param array<mixed> $in_array
* @param int|string $key * @param int|string $key
* @return int|string|null Next key, or null for not found * @return int|string|null Next key, or null for not found
*/ */
public static function arrayGetPrevKey(array $array, int|string $key): int|string|null public static function arrayGetPrevKey(array $in_array, int|string $key): int|string|null
{ {
return self::arrayGetKey($array, $key, false); return self::arrayGetKey($in_array, $key, false);
} }
/** /**
* Get next array key from an array * Get next array key from an array
* null on not found * null on not found
* *
* @param array<mixed> $array * @param array<mixed> $in_array
* @param int|string $key * @param int|string $key
* @return int|string|null Next key, or null for not found * @return int|string|null Next key, or null for not found
*/ */
public static function arrayGetNextKey(array $array, int|string $key): int|string|null public static function arrayGetNextKey(array $in_array, int|string $key): int|string|null
{ {
return self::arrayGetKey($array, $key, true); return self::arrayGetKey($in_array, $key, true);
} }
/** /**
@@ -303,27 +463,27 @@ class ArrayHandler
} }
// default key is not string // default key is not string
$key_is_string = false; $key_is_string = false;
$arrays = func_get_args(); $in_arrays = func_get_args();
// if last is not array, then assume it is trigger for key is always string // if last is not array, then assume it is trigger for key is always string
if (!is_array(end($arrays))) { if (!is_array(end($in_arrays))) {
if (array_pop($arrays)) { if (array_pop($in_arrays)) {
$key_is_string = true; $key_is_string = true;
} }
} }
// check that arrays count is at least two, else we don't have enough to do anything // check that arrays count is at least two, else we don't have enough to do anything
if (count($arrays) < 2) { if (count($in_arrays) < 2) {
throw new \ArgumentCountError(__FUNCTION__ . ' needs two or more array arguments'); throw new \ArgumentCountError(__FUNCTION__ . ' needs two or more array arguments');
} }
$merged = []; $merged = [];
while ($arrays) { while ($in_arrays) {
$array = array_shift($arrays); $in_array = array_shift($in_arrays);
if (!is_array($array)) { if (!is_array($in_array)) {
throw new \TypeError(__FUNCTION__ . ' encountered a non array argument'); throw new \TypeError(__FUNCTION__ . ' encountered a non array argument');
} }
if (!$array) { if (!$in_array) {
continue; continue;
} }
foreach ($array as $key => $value) { foreach ($in_array as $key => $value) {
// if string or if key is assumed to be string do key match // if string or if key is assumed to be string do key match
// else add new entry // else add new entry
if (is_string($key) || $key_is_string === false) { if (is_string($key) || $key_is_string === false) {
@@ -429,14 +589,14 @@ class ArrayHandler
* converts multi dimensional array to a flat array * converts multi dimensional array to a flat array
* does NOT preserve keys * does NOT preserve keys
* *
* @param array<mixed> $array multi dimensionial array * @param array<mixed> $in_array multi dimensionial array
* @return array<mixed> flattened array * @return array<mixed> flattened array
*/ */
public static function flattenArray(array $array): array public static function flattenArray(array $in_array): array
{ {
$return = []; $return = [];
array_walk_recursive( array_walk_recursive(
$array, $in_array,
function ($value) use (&$return) { function ($value) use (&$return) {
$return[] = $value; $return[] = $value;
} }
@@ -447,13 +607,13 @@ class ArrayHandler
/** /**
* will loop through an array recursivly and write the array keys back * will loop through an array recursivly and write the array keys back
* *
* @param array<mixed> $array multidemnsional array to flatten * @param array<mixed> $in_array multidemnsional array to flatten
* @param array<mixed> $return recoursive pass on array of keys * @param array<mixed> $return recoursive pass on array of keys
* @return array<mixed> flattened keys array * @return array<mixed> flattened keys array
*/ */
public static function flattenArrayKey(array $array, array $return = []): array public static function flattenArrayKey(array $in_array, array $return = []): array
{ {
foreach ($array as $key => $sub) { foreach ($in_array as $key => $sub) {
$return[] = $key; $return[] = $key;
if (is_array($sub) && count($sub) > 0) { if (is_array($sub) && count($sub) > 0) {
$return = self::flattenArrayKey($sub, $return); $return = self::flattenArrayKey($sub, $return);
@@ -466,14 +626,14 @@ class ArrayHandler
* as above will flatten an array, but in this case only the outmost * as above will flatten an array, but in this case only the outmost
* leave nodes, all other keyswill be skipped * leave nodes, all other keyswill be skipped
* *
* @param array<mixed> $array multidemnsional array to flatten * @param array<mixed> $in_array multidemnsional array to flatten
* @return array<mixed> flattened keys array * @return array<mixed> flattened keys array
*/ */
public static function flattenArrayKeyLeavesOnly(array $array): array public static function flattenArrayKeyLeavesOnly(array $in_array): array
{ {
$return = []; $return = [];
array_walk_recursive( array_walk_recursive(
$array, $in_array,
function ($value, $key) use (&$return) { function ($value, $key) use (&$return) {
$return[] = $key; $return[] = $key;
} }
@@ -485,14 +645,14 @@ class ArrayHandler
* searches for key -> value in an array tree and writes the value one level up * searches for key -> value in an array tree and writes the value one level up
* this will remove this leaf will all other values * this will remove this leaf will all other values
* *
* @param array<mixed> $array nested array * @param array<mixed> $in_array nested array
* @param string|int $search key to find that has no sub leaf * @param string|int $search key to find that has no sub leaf
* and will be pushed up * and will be pushed up
* @return array<mixed> modified, flattened array * @return array<mixed> modified, flattened array
*/ */
public static function arrayFlatForKey(array $array, string|int $search): array public static function arrayFlatForKey(array $in_array, string|int $search): array
{ {
foreach ($array as $key => $value) { foreach ($in_array as $key => $value) {
// if it is not an array do just nothing // if it is not an array do just nothing
if (!is_array($value)) { if (!is_array($value)) {
continue; continue;
@@ -500,14 +660,14 @@ class ArrayHandler
// probe it has search key // probe it has search key
if (isset($value[$search])) { if (isset($value[$search])) {
// set as current // set as current
$array[$key] = $value[$search]; $in_array[$key] = $value[$search];
} else { } else {
// call up next node down // call up next node down
// $array[$key] = call_user_func(__METHOD__, $value, $search); // $in_array[$key] = call_user_func(__METHOD__, $value, $search);
$array[$key] = self::arrayFlatForKey($value, $search); $in_array[$key] = self::arrayFlatForKey($value, $search);
} }
} }
return $array; return $in_array;
} }
/** /**
@@ -517,13 +677,139 @@ class ArrayHandler
* *
* https://stackoverflow.com/a/369608 * https://stackoverflow.com/a/369608
* *
* @param array<mixed> $array Array where elements are located * @param array<mixed> $in_array Array where elements are located
* @param array<mixed> $remove Elements to remove * @param array<mixed> $remove Elements to remove
* @return array<mixed> Array with $remove elements removed * @return array<mixed> Array with $remove elements removed
*/ */
public static function arrayRemoveEntry(array $array, array $remove): array public static function arrayRemoveEntry(array $in_array, array $remove): array
{ {
return array_diff($array, $remove); return array_diff($in_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<string,mixed> $in_array
* @param array<string> $key_list
* @return array<string,mixed>
*/
public static function arrayReturnMatchingKeyOnly(
array $in_array,
array $key_list
): array {
// on empty return as is
if (empty($key_list)) {
return $in_array;
}
return array_filter(
$in_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<string|int,mixed> $in_array
* @param string $key_mod_prefix [''] key prefix string
* @param string $key_mod_suffix [''] key suffix string
* @return array<string|int,mixed>
*/
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)
);
}
/**
* sort array and return in same call
* sort ascending or descending with or without lower case convert
* value only, will loose key connections unless preserve_keys is set to true
*
* @param array<mixed> $in_array Array to sort by values
* @param bool $case_insensitive [false] Sort case insensitive
* @param bool $reverse [false] Reverse sort
* @param bool $maintain_keys [false] Maintain keys
* @param int $flag [SORT_REGULAR] Sort flags
* @return array<mixed>
*/
public static function sortArray(
array $in_array,
bool $case_insensitive = false,
bool $reverse = false,
bool $maintain_keys = false,
int $flag = SORT_REGULAR
): array {
$fk_sort_lower_case = function (string $a, string $b): int {
return strtolower($a) <=> strtolower($b);
};
$fk_sort_lower_case_reverse = function (string $a, string $b): int {
return strtolower($b) <=> strtolower($a);
};
$case_insensitive ? (
$maintain_keys ?
(uasort($in_array, $reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case)) :
(usort($in_array, $reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case))
) :
(
$maintain_keys ?
($reverse ? arsort($in_array, $flag) : asort($in_array, $flag)) :
($reverse ? rsort($in_array, $flag) : sort($in_array, $flag))
);
return $in_array;
}
/**
* sort by key ascending or descending and return
*
* @param array<mixed> $in_array Array to srt
* @param bool $case_insensitive [false] Sort keys case insenstive
* @param bool $reverse [false] Reverse key sort
* @return array<mixed>
*/
public static function ksortArray(array $in_array, bool $case_insensitive = false, bool $reverse = false): array
{
$fk_sort_lower_case = function (string $a, string $b): int {
return strtolower($a) <=> strtolower($b);
};
$fk_sort_lower_case_reverse = function (string $a, string $b): int {
return strtolower($b) <=> strtolower($a);
};
$fk_sort = function (string $a, string $b): int {
return $a <=> $b;
};
$fk_sort_reverse = function (string $a, string $b): int {
return $b <=> $a;
};
uksort(
$in_array,
$case_insensitive ?
($reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case) :
($reverse ? $fk_sort_reverse : $fk_sort)
);
return $in_array;
} }
} }

View File

@@ -395,39 +395,68 @@ class DateTime
* does a reverse of the timeStringFormat and converts the string from * does a reverse of the timeStringFormat and converts the string from
* xd xh xm xs xms to a timestamp.microtime format * xd xh xm xs xms to a timestamp.microtime format
* *
* @param string|int|float $timestring formatted interval * @param string|int|float $timestring formatted interval
* @return string|int|float converted float interval, or string as is * @param bool $throw_exception [default=false] if set to true will throw exception
* instead of returning input value as is
* @return string|int|float converted float interval, or string as is
*/ */
public static function stringToTime(string|int|float $timestring): string|int|float public static function stringToTime(
{ string|int|float $timestring,
bool $throw_exception = false
): string|int|float {
$timestamp = 0; $timestamp = 0;
if (!preg_match("/(d|h|m|s|ms)/", (string)$timestring)) {
return $timestring;
}
$timestring = (string)$timestring;
// pos for preg match read + multiply factor
$timegroups = [2 => 86400, 4 => 3600, 6 => 60, 8 => 1];
$matches = []; $matches = [];
// if start with -, strip and set negative
$negative = false;
if (preg_match("/^-/", $timestring)) {
$negative = true;
$timestring = substr($timestring, 1);
}
// preg match: 0: full string // preg match: 0: full string
// 2, 4, 6, 8 are the to need values // 2, 4, 6, 8 are the to need values
preg_match("/^((\d+)d ?)?((\d+)h ?)?((\d+)m ?)?((\d+)s ?)?((\d+)ms)?$/", $timestring, $matches); if (
!preg_match(
"/^\s*(-)?\s*"
. "((\d+)\s*d(?:ay(?:s)?)?)?\s*"
. "((\d+)\s*h(?:our(?:s)?)?)?\s*"
. "((\d+)\s*m(?:in(?:ute)?(?:s)?)?)?\s*"
. "((\d+)\s*s(?:ec(?:ond)?(?:s)?)?)?\s*"
. "((\d+)\s*m(?:illi)?s(?:ec(?:ond)?(?:s)?)?)?\s*"
. "$/",
(string)$timestring,
$matches
)
) {
if ($throw_exception) {
throw new \InvalidArgumentException(
'Invalid time string format, cannot parse: "' . (string)$timestring . '"',
1
);
}
return $timestring;
}
if (count($matches) < 2) {
if ($throw_exception) {
throw new \InvalidArgumentException(
'Invalid time string format, no interval value found: "' . (string)$timestring . '"',
2
);
}
return $timestring;
}
// pos for preg match read + multiply factor
$timegroups = [3 => 86400, 5 => 3600, 7 => 60, 9 => 1];
// if start with -, strip and set negative
$negative = false;
if (!empty($matches[1])) {
$negative = true;
}
// multiply the returned matches and sum them up. the last one (ms) is added with . // multiply the returned matches and sum them up. the last one (ms) is added with .
foreach ($timegroups as $i => $time_multiply) { foreach ($timegroups as $i => $time_multiply) {
if (isset($matches[$i]) && is_numeric($matches[$i])) { if (isset($matches[$i]) && is_numeric($matches[$i])) {
$timestamp += (float)$matches[$i] * $time_multiply; $timestamp += (float)$matches[$i] * $time_multiply;
} }
} }
if (isset($matches[10]) && is_numeric($matches[10])) { if (isset($matches[11]) && is_numeric($matches[11])) {
$timestamp .= '.' . $matches[10]; // for milliseconds, we need to divide by 1000 and add them
$timestamp += (float)($matches[11] / 1000);
} }
if ($negative) { if ($negative) {
// cast to flaot so we can do a negative multiplication // cast to float so we can do a negative multiplication
$timestamp = (float)$timestamp * -1; $timestamp = (float)$timestamp * -1;
} }
return $timestamp; return $timestamp;
@@ -639,16 +668,26 @@ class DateTime
* *
* @param string $start_date valid start date (y/m/d) * @param string $start_date valid start date (y/m/d)
* @param string $end_date valid end 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 * @param bool $return_named [default=false] return array type, false (default), true for named
* @return array<mixed> 0/overall, 1/weekday, 2/weekend * @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( public static function calcDaysInterval(
string $start_date, string $start_date,
string $end_date, string $end_date,
bool $return_named = false bool $return_named = false,
bool $include_end_date = true,
bool $exclude_start_date = false
): array { ): array {
// pos 0 all, pos 1 weekday, pos 2 weekend // 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 // if anything invalid, return 0,0,0
try { try {
$start = new \DateTime($start_date); $start = new \DateTime($start_date);
@@ -659,19 +698,30 @@ class DateTime
'overall' => 0, 'overall' => 0,
'weekday' => 0, 'weekday' => 0,
'weekend' => 0, 'weekend' => 0,
'reverse' => false
]; ];
} else { } else {
return [0, 0, 0]; return $days;
} }
} }
// so we include the last day too, we need to add +1 second in the time // so we include the last day too, we need to add +1 second in the time
$end->setTime(0, 0, 1); // if start is before end, switch dates and flag
// if end date before start date, only this will be filled $days[3] = false;
$days[0] = $end->diff($start)->days; if ($start > $end) {
$days[1] = 0; $new_start = $end;
$days[2] = 0; $end = $start;
$start = $new_start;
$days[3] = true;
}
// get period for weekends/weekdays // 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) { foreach ($period as $dt) {
$curr = $dt->format('D'); $curr = $dt->format('D');
if ($curr == 'Sat' || $curr == 'Sun') { if ($curr == 'Sat' || $curr == 'Sun') {
@@ -679,18 +729,80 @@ class DateTime
} else { } else {
$days[1]++; $days[1]++;
} }
$days[0]++;
} }
if ($return_named === true) { if ($return_named === true) {
return [ return [
'overall' => $days[0], 'overall' => $days[0],
'weekday' => $days[1], 'weekday' => $days[1],
'weekend' => $days[2], 'weekend' => $days[2],
'reverse' => $days[3],
]; ];
} else { } else {
return $days; 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 * check if a weekend day (sat/sun) is in the given date range
* Can have time too, but is not needed * Can have time too, but is not needed
@@ -705,6 +817,13 @@ class DateTime
): bool { ): bool {
$dd_start = new \DateTime($start_date); $dd_start = new \DateTime($start_date);
$dd_end = new \DateTime($end_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 ( if (
// starts with a weekend // starts with a weekend
$dd_start->format('N') >= 6 || $dd_start->format('N') >= 6 ||

View File

@@ -14,6 +14,7 @@ class Byte
public const BYTE_FORMAT_NOSPACE = 1; public const BYTE_FORMAT_NOSPACE = 1;
public const BYTE_FORMAT_ADJUST = 2; public const BYTE_FORMAT_ADJUST = 2;
public const BYTE_FORMAT_SI = 4; public const BYTE_FORMAT_SI = 4;
public const RETURN_AS_STRING = 8;
/** /**
* This function replaces the old byteStringFormat * This function replaces the old byteStringFormat
@@ -77,7 +78,7 @@ class Byte
// labels in order of size [Y, Z] // labels in order of size [Y, Z]
$labels = ['', 'K', 'M', 'G', 'T', 'P', 'E']; $labels = ['', 'K', 'M', 'G', 'T', 'P', 'E'];
// exp position calculation // exp position calculation
$exp = floor(log($abs_bytes, $unit)); $exp = (int)floor(log($abs_bytes, $unit));
// avoid printing out anything larger than max labels // avoid printing out anything larger than max labels
if ($exp >= count($labels)) { if ($exp >= count($labels)) {
$exp = count($labels) - 1; $exp = count($labels) - 1;
@@ -119,7 +120,9 @@ class Byte
* @param int $flags bitwise flag with use space turned on * @param int $flags bitwise flag with use space turned on
* BYTE_FORMAT_SI: use 1000 instead of 1024 * BYTE_FORMAT_SI: use 1000 instead of 1024
* @return string|int|float converted value or original value * @return string|int|float converted value or original value
* @throws \InvalidArgumentException 1: no valid flag set * @throws \InvalidArgumentException no valid flag set
* @throws \LengthException number too large to convert to int
* @throws \RuntimeException BCMath extension not loaded if flag is set to string
*/ */
public static function stringByteFormat(string|int|float $number, int $flags = 0): string|int|float public static function stringByteFormat(string|int|float $number, int $flags = 0): string|int|float
{ {
@@ -129,7 +132,12 @@ class Byte
} else { } else {
$si = false; $si = false;
} }
if ($flags != 0 && $flags != 4) { if ($flags & self::RETURN_AS_STRING) {
$return_as_string = true;
} else {
$return_as_string = false;
}
if ($flags != 0 && $flags != 4 && $flags != 8 && $flags != 12) {
throw new \InvalidArgumentException("Invalid flags parameter: $flags", 1); throw new \InvalidArgumentException("Invalid flags parameter: $flags", 1);
} }
// matches in regex // matches in regex
@@ -142,6 +150,10 @@ class Byte
strtolower((string)$number), strtolower((string)$number),
$matches $matches
); );
$number_negative = false;
if (!empty($matches[1])) {
$number_negative = true;
}
if (isset($matches[2]) && isset($matches[3])) { if (isset($matches[2]) && isset($matches[3])) {
// remove all non valid characters from the number // remove all non valid characters from the number
$number = preg_replace('/[^0-9\.]/', '', $matches[2]); $number = preg_replace('/[^0-9\.]/', '', $matches[2]);
@@ -152,12 +164,48 @@ class Byte
if ($unit) { if ($unit) {
$number = $number * pow($si ? 1000 : 1024, stripos($valid_units_, $unit[0]) ?: 0); $number = $number * pow($si ? 1000 : 1024, stripos($valid_units_, $unit[0]) ?: 0);
} }
// if the number is too large, we cannot convert to int directly
if ($number <= PHP_INT_MIN || $number >= PHP_INT_MAX) {
// if we do not want to convert to string
if (!$return_as_string) {
throw new \LengthException(
'Number too large be converted to int: ' . (string)$number
);
}
// for string, check if bcmath is loaded, if not this will not work
if (!extension_loaded('bcmath')) {
throw new \RuntimeException(
'Number too large be converted to int and BCMath extension not loaded: ' . (string)$number
);
}
}
// string return
if ($return_as_string) {
// return as string to avoid overflow
// $number = (string)round($number);
$number = bcmul(number_format(
$number,
12,
'.',
''
), "1");
if ($number_negative) {
$number = '-' . $number;
}
return $number;
}
// convert to INT to avoid +E output // convert to INT to avoid +E output
$number = (int)round($number); $number = (int)round($number);
// if negative input, keep nnegative // if negative input, keep nnegative
if (!empty($matches[1])) { if ($number_negative) {
$number *= -1; $number *= -1;
} }
// check if number is negative but should be, this is Lenght overflow
if (!$number_negative && $number < 0) {
throw new \LengthException(
'Number too large be converted to int: ' . (string)$number
);
}
} }
// if not matching return as is // if not matching return as is
return $number; return $number;

View File

@@ -23,14 +23,14 @@ class Encoding
* @param bool $auto_check default true, if source encoding is set * @param bool $auto_check default true, if source encoding is set
* check that the source is actually matching * check that the source is actually matching
* to what we sav the source is * to what we sav the source is
* @return string encoding converted string * @return string|false encoding converted string or false on error
*/ */
public static function convertEncoding( public static function convertEncoding(
string $string, string $string,
string $to_encoding, string $to_encoding,
string $source_encoding = '', string $source_encoding = '',
bool $auto_check = true bool $auto_check = true
): string { ): string|false {
// set if not given // set if not given
if (!$source_encoding) { if (!$source_encoding) {
$source_encoding = mb_detect_encoding($string); $source_encoding = mb_detect_encoding($string);

View File

@@ -10,9 +10,16 @@ namespace CoreLibs\Convert;
class Html class Html
{ {
/** @var int */
public const SELECTED = 0; public const SELECTED = 0;
/** @var int */
public const CHECKED = 1; 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<string> */
// public const VALID_HTMLENT_ENCODINGS = [];
/** /**
* full wrapper for html entities * full wrapper for html entities
* *
@@ -22,14 +29,19 @@ class Html
* encodes in UTF-8 * encodes in UTF-8
* does not double encode * does not double encode
* *
* @param mixed $string string to html encode * @param mixed $string string to html encode
* @param int $flags [default: ENT_QUOTES | ENT_HTML5] * @param int $flags [default=ENT_QUOTES | ENT_HTML5]
* @param string $encoding [default=UTF-8]
* @return mixed if string, encoded, else as is (eg null) * @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)) { 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; return $string;
} }
@@ -37,7 +49,7 @@ class Html
/** /**
* strips out all line breaks or replaced with given string * strips out all line breaks or replaced with given string
* @param string $string 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 * @return string cleaned string without any line breaks
*/ */
public static function removeLB(string $string, string $replace = ' '): string public static function removeLB(string $string, string $replace = ' '): string

View File

@@ -55,10 +55,10 @@ class Json
* Deos not throw errors * Deos not throw errors
* *
* @param array<mixed> $data * @param array<mixed> $data
* @param int $flags json_encode flags as is * @param int $flags [JSON_UNESCAPED_UNICODE] json_encode flags as is
* @return string JSON string or '{}' if false * @return string JSON string or '{}' if false
*/ */
public static function jsonConvertArrayTo(array $data, int $flags = 0): string public static function jsonConvertArrayTo(array $data, int $flags = JSON_UNESCAPED_UNICODE): string
{ {
$json_string = json_encode($data, $flags) ?: '{}'; $json_string = json_encode($data, $flags) ?: '{}';
self::$json_last_error = json_last_error(); self::$json_last_error = json_last_error();
@@ -119,6 +119,23 @@ class Json
} }
return $return_string === true ? $json_error_string : self::$json_last_error; return $return_string === true ? $json_error_string : self::$json_last_error;
} }
/**
* wrapper to call convert array to json with pretty print
*
* @param array<mixed> $data
* @return string
*/
public static function jsonPrettyPrint(array $data): string
{
return self::jsonConvertArrayTo(
$data,
JSON_PRETTY_PRINT |
JSON_UNESCAPED_LINE_TERMINATORS |
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE
);
}
} }
// __END__ // __END__

View File

@@ -62,10 +62,15 @@ class Math
* *
* @param float $number Number to cubic root * @param float $number Number to cubic root
* @return float Calculated value * @return float Calculated value
* @throws \InvalidArgumentException if $number is negative
*/ */
public static function cbrt(float|int $number): float public static function cbrt(float|int $number): float
{ {
return pow((float)$number, 1.0 / 3); $value = pow((float)$number, 1.0 / 3);
if (is_nan($value)) {
throw new \InvalidArgumentException('cube root from this number is not supported: ' . $number);
}
return $value;
} }
/** /**
@@ -199,15 +204,17 @@ class Math
callback: fn ($col) => is_array($row) ? callback: fn ($col) => is_array($row) ?
array_reduce( array_reduce(
array: $row, array: $row,
callback: fn ($a, $v, $i = null) => $a + $v * ( // TODO check that v is not an array
callback: fn ($a, $v, $i = null) => $a + $v * ( /** @phpstan-ignore-line Possible array + int */
// if last entry missing for full copy add a 0 to it // if last entry missing for full copy add a 0 to it
$col[$i ?? array_search($v, $row, true)] ?? 0 /** @phpstan-ignore-line */ $col[$i ?? array_search($v, $row, true)] ?? 0
), ),
initial: 0, initial: 0,
) : ) :
array_reduce( array_reduce(
array: $col, array: $col,
callback: fn ($a, $v) => $a + $v * $row, // TODO check that v is not an array
callback: fn ($a, $v) => $a + $v * $row, /** @phpstan-ignore-line Possible array + int */
initial: 0, initial: 0,
), ),
array: $bCols, array: $bCols,

View File

@@ -8,8 +8,20 @@ declare(strict_types=1);
namespace CoreLibs\Convert; namespace CoreLibs\Convert;
use CoreLibs\Combined\ArrayHandler;
class Strings class Strings
{ {
/** @var array<int,string> all the preg error messages */
public const array PREG_ERROR_MESSAGES = [
PREG_NO_ERROR => 'No error',
PREG_INTERNAL_ERROR => 'Internal PCRE error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
PREG_BAD_UTF8_OFFSET_ERROR => 'Bad UTF-8 offset',
PREG_JIT_STACKLIMIT_ERROR => 'JIT stack limit exhausted'
];
/** /**
* return the number of elements in the split list * return the number of elements in the split list
* 0 if nothing / invalid split * 0 if nothing / invalid split
@@ -52,29 +64,42 @@ class Strings
* Note a string LONGER then the maxium will be attached with the LAST * Note a string LONGER then the maxium will be attached with the LAST
* split character. In above exmaple * split character. In above exmaple
* ABCD1234EFGHTOOLONG will be ABCD-1234-EFGH-TOOLONG * ABCD1234EFGHTOOLONG will be ABCD-1234-EFGH-TOOLONG
* If the characters are NOT ASCII it will return the string as is
* *
* @param string $value string value to split * @param string $string string value to split
* @param string $split_format split format * @param string $split_format split format
* @param string $split_characters list of charcters with which we split
* if not set uses dash ('-')
* @return string split formatted string or original value if not chnaged * @return string split formatted string or original value if not chnaged
* @throws \InvalidArgumentException for empty split format, invalid values, split characters or split format
*/ */
public static function splitFormatString( public static function splitFormatString(
string $value, string $string,
string $split_format, string $split_format,
string $split_characters = '-'
): string { ): string {
if ( // skip if string or split format is empty is empty
// abort if split format is empty if (empty($string) || empty($split_format)) {
empty($split_format) || return $string;
// if not in the valid ASCII character range for any of the strings }
preg_match('/[^\x20-\x7e]/', $value) || if (preg_match('/[^\x20-\x7e]/', $string)) {
// preg_match('/[^\x20-\x7e]/', $split_format) || throw new \InvalidArgumentException(
preg_match('/[^\x20-\x7e]/', $split_characters) || "The string to split can only be ascii characters: " . $string
// only numbers and split characters in split_format );
!preg_match("/[0-9" . $split_characters . "]/", $split_format) }
) { // get the split characters that are not numerical and check they are ascii
return $value; $split_characters = self::removeDuplicates(preg_replace('/[0-9]/', '', $split_format) ?: '');
if (empty($split_characters)) {
throw new \InvalidArgumentException(
"A split character must exist in the format string: " . $split_format
);
}
if (preg_match('/[^\x20-\x7e]/', $split_characters)) {
throw new \InvalidArgumentException(
"The split character has to be a valid ascii character: " . $split_characters
);
}
if (!preg_match("/^[0-9" . $split_characters . "]+$/", $split_format)) {
throw new \InvalidArgumentException(
"The split format can only be numbers and the split characters: " . $split_format
);
} }
// split format list // split format list
$split_list = preg_split( $split_list = preg_split(
@@ -86,14 +111,14 @@ class Strings
); );
// if this is false, or only one array, abort split // if this is false, or only one array, abort split
if (!is_array($split_list) || count($split_list) == 1) { if (!is_array($split_list) || count($split_list) == 1) {
return $value; return $string;
} }
$out = ''; $out = '';
$pos = 0; $pos = 0;
$last_split = ''; $last_split = '';
foreach ($split_list as $offset) { foreach ($split_list as $offset) {
if (is_numeric($offset)) { if (is_numeric($offset)) {
$_part = substr($value, $pos, (int)$offset); $_part = substr($string, $pos, (int)$offset);
if (empty($_part)) { if (empty($_part)) {
break; break;
} }
@@ -104,8 +129,8 @@ class Strings
$last_split = $offset; $last_split = $offset;
} }
} }
if (!empty($out) && $pos < strlen($value)) { if (!empty($out) && $pos < strlen($string)) {
$out .= $last_split . substr($value, $pos); $out .= $last_split . substr($string, $pos);
} }
// if last is not alphanumeric remove, remove // if last is not alphanumeric remove, remove
if (!strcspn(substr($out, -1, 1), $split_characters)) { if (!strcspn(substr($out, -1, 1), $split_characters)) {
@@ -115,10 +140,49 @@ class Strings
if (!empty($out)) { if (!empty($out)) {
return $out; return $out;
} else { } else {
return $value; return $string;
} }
} }
/**
* Split a string into n-length blocks with a split character inbetween
* This is simplified version from splitFormatString that uses
* fixed split length with a characters, this evenly splits the string out into the
* given length
* This works with non ASCII characters too
*
* @param string $string string to split
* @param int $split_length split length, must be smaller than string and larger than 0
* @param string $split_characters [default=-] the character to split, can be more than one
* @return string
* @throws \InvalidArgumentException Thrown if split length style is invalid
*/
public static function splitFormatStringFixed(
string $string,
int $split_length,
string $split_characters = '-'
): string {
// if empty string or if split lenght is 0 or empty split characters
// then we skip any splitting
if (empty($string) || $split_length == 0 || empty($split_characters)) {
return $string;
}
$return_string = '';
$string_length = mb_strlen($string);
// check that the length is not too short
if ($split_length < 1 || $split_length >= $string_length) {
throw new \InvalidArgumentException(
"The split length must be at least 1 character and less than the string length to split. "
. "Split length: " . $split_length . ", string length: " . $string_length
);
}
for ($i = 0; $i < $string_length; $i += $split_length) {
$return_string .= mb_substr($string, $i, $split_length) . $split_characters;
}
// remove last trailing character which is always the split char length
return mb_substr($return_string, 0, -1 * mb_strlen($split_characters));
}
/** /**
* Strip any duplicated slahes from a path * Strip any duplicated slahes from a path
* eg: //foo///bar/foo.inc -> /foo/bar/foo.inc * eg: //foo///bar/foo.inc -> /foo/bar/foo.inc
@@ -146,6 +210,165 @@ class Strings
{ {
return trim($text, pack('H*', 'EFBBBF')); return trim($text, pack('H*', 'EFBBBF'));
} }
/**
* Make as string of characters unique
*
* @param string $string
* @return string
*/
public static function removeDuplicates(string $string): string
{
// combine again
$result = implode(
'',
// unique list
array_unique(
// split into array
mb_str_split($string)
)
);
return $result;
}
/**
* check if all characters are in set
*
* @param string $needle Needle to search
* @param string $haystack Haystack to search in
* @return bool True on found, False if not in haystack
*/
public static function allCharsInSet(string $needle, string $haystack): bool
{
$input_length = strlen($needle);
for ($i = 0; $i < $input_length; $i++) {
if (strpos($haystack, $needle[$i]) === false) {
return false;
}
}
return true;
}
/**
* converts a list of arrays of strings into a string of unique entries
* input arrays can be nested, only values are used
*
* @param array<mixed> ...$char_lists
* @return string
*/
public static function buildCharStringFromLists(array ...$char_lists): string
{
return implode('', array_unique(
ArrayHandler::flattenArray(
array_merge(...$char_lists)
)
));
}
/**
* Split up character ranges in format A-Z, a-z, 0-9
*
* @param string $input
* @return string[]
*/
public static function parseCharacterRanges(string $input): array
{
// if not alphanumeric, throw value error
if (!preg_match("/^[A-Za-z0-9\-\s]+$/u", $input)) {
throw new \InvalidArgumentException(
"The input string contains invalid characters, "
. "only alphanumeric, dash (-), space and 'or' are allowed: "
. $input
);
}
// Remove all spaces
$input = str_replace(' ', '', $input);
$result = [];
// if there is no - inside, return unique characters as array
if (strpos($input, '-') === false) {
return array_unique(mb_str_split($input));
}
// Find all patterns like "A-Z" (character-dash-character)
preg_match_all('/(.)-(.)/u', $input, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$start = $match[1];
$end = $match[2];
// Get ASCII/Unicode values
$startOrd = ord($start[0]);
$endOrd = ord($end[0]);
// make sure start is before end
if ($startOrd > $endOrd) {
[$startOrd, $endOrd] = [$endOrd, $startOrd];
}
// Generate range of characters
for ($i = $startOrd; $i <= $endOrd; $i++) {
$char = chr($i);
if (!in_array($char, $result)) {
$result[] = $char;
}
}
}
// make the result unique
$result = array_unique($result);
return $result;
}
/**
* Check if a regex is valid. Does not return the detail regex parser error
*
* @param string $pattern Any regex string
* @return bool False on invalid regex
*/
public static function isValidRegex(string $pattern): bool
{
preg_last_error();
try {
$var = '';
@preg_match($pattern, $var);
return preg_last_error() === PREG_NO_ERROR;
} catch (\Error $e) {
return false;
}
}
/**
* Returns the last preg error messages as string
* all messages are defined in PREG_ERROR_MESSAGES
*
* @return string
*/
public static function getLastRegexErrorString(): string
{
return self::PREG_ERROR_MESSAGES[preg_last_error()] ?? 'Unknown error';
}
/**
* check if a regex is invalid, returns array with flag and error string
*
* @param string $pattern
* @return array{valid:bool,preg_error:int,error:null|string,pcre_error:null|string}
*/
public static function validateRegex(string $pattern): array
{
// Clear any previous PCRE errors
preg_last_error();
$var = '';
if (@preg_match($pattern, $var) === false) {
$error = preg_last_error();
return [
'valid' => false,
'preg_error' => $error,
'error' => self::PREG_ERROR_MESSAGES[$error] ?? 'Unknown error',
'pcre_error' => preg_last_error_msg(),
];
}
return ['valid' => true, 'preg_error' => PREG_NO_ERROR, 'error' => null, 'pcre_error' => null];
}
} }
// __END__ // __END__

View File

@@ -38,6 +38,7 @@ class Email
* @param string $encoding Encoding, if not set UTF-8 * @param string $encoding Encoding, if not set UTF-8
* @param bool $kv_folding If set to true and a valid encoding, do KV folding * @param bool $kv_folding If set to true and a valid encoding, do KV folding
* @return string Correctly encoded and build email string * @return string Correctly encoded and build email string
* @throws \IntlException if email name cannot be converted to UTF-8
*/ */
public static function encodeEmailName( public static function encodeEmailName(
string $email, string $email,
@@ -52,6 +53,10 @@ class Email
if ($encoding != 'UTF-8') { if ($encoding != 'UTF-8') {
$email_name = mb_convert_encoding($email_name, $encoding, 'UTF-8'); $email_name = mb_convert_encoding($email_name, $encoding, 'UTF-8');
} }
// if we cannot transcode the name, return just the email
if ($email_name === false) {
throw new \IntlException('Cannot convert email_name to UTF-8');
}
$email_name = $email_name =
mb_encode_mimeheader( mb_encode_mimeheader(
in_array($encoding, self::$encoding_kv_allowed) && $kv_folding ? in_array($encoding, self::$encoding_kv_allowed) && $kv_folding ?
@@ -77,6 +82,8 @@ class Email
* @param bool $kv_folding If set to true and a valid encoding, * @param bool $kv_folding If set to true and a valid encoding,
* do KV folding * do KV folding
* @return array<string> Pos 0: Subject, Pos 1: Body * @return array<string> Pos 0: Subject, Pos 1: Body
* @throws \IntlException if subject cannot be converted to UTF-8
* @throws \IntlException if body cannot be converted to UTF-8
*/ */
private static function replaceContent( private static function replaceContent(
string $subject, string $subject,
@@ -102,6 +109,12 @@ class Email
$subject = mb_convert_encoding($subject, $encoding, 'UTF-8'); $subject = mb_convert_encoding($subject, $encoding, 'UTF-8');
$body = mb_convert_encoding($body, $encoding, 'UTF-8'); $body = mb_convert_encoding($body, $encoding, 'UTF-8');
} }
if ($subject === false) {
throw new \IntlException('Cannot convert subject to UTF-8');
}
if ($body === false) {
throw new \IntlException('Cannot convert body to UTF-8');
}
// we need to encodde the subject // we need to encodde the subject
$subject = mb_encode_mimeheader( $subject = mb_encode_mimeheader(
in_array($encoding, self::$encoding_kv_allowed) && $kv_folding ? in_array($encoding, self::$encoding_kv_allowed) && $kv_folding ?

View File

@@ -10,9 +10,14 @@ namespace CoreLibs\Create;
class Hash class Hash
{ {
/** @var string default short hash -> deprecated use STANDARD_HASH_SHORT */
public const DEFAULT_HASH = 'adler32'; public const DEFAULT_HASH = 'adler32';
/** @var string default long hash (40 chars) */
public const STANDARD_HASH_LONG = 'ripemd160'; public const STANDARD_HASH_LONG = 'ripemd160';
/** @var string default short hash (8 chars) */
public const STANDARD_HASH_SHORT = 'adler32'; 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 * checks php version and if >=5.2.7 it will flip the string
@@ -20,6 +25,7 @@ class Hash
* hash returns false * hash returns false
* preg_replace fails for older php version * preg_replace fails for older php version
* Use __hash with crc32b or hash('crc32b', ...) for correct output * Use __hash with crc32b or hash('crc32b', ...) for correct output
* For future short hashes use hashShort() instead
* *
* @param string $string string to crc * @param string $string string to crc
* @return string crc32b hash (old type) * @return string crc32b hash (old type)
@@ -43,19 +49,31 @@ class Hash
* replacement for __crc32b call * replacement for __crc32b call
* *
* @param string $string string to hash * @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 * @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 public static function __sha1Short(string $string, bool $use_sha = false): string
{ {
if ($use_sha) { if ($use_sha) {
// return only the first 9 characters return self::sha1Short($string);
return substr(hash('sha1', $string), 0, 9);
} else { } else {
return self::__crc32b($string); 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) * replacemend for __crc32b call (alternate)
* defaults to adler 32 * defaults to adler 32
@@ -63,34 +81,135 @@ class Hash
* all that create 8 char long hashes * all that create 8 char long hashes
* *
* @param string $string string to hash * @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 * @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( public static function __hash(
string $string, 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 { ): string {
// if not empty, check if in valid list
if ( if (
empty($hash_type) || empty($hash_type) ||
!in_array($hash_type, hash_algos()) !in_array($hash_type, hash_algos())
) { ) {
// fallback to default hash type if none set or invalid // fallback to default hash type if empty or invalid
$hash_type = self::DEFAULT_HASH; $hash_type = self::STANDARD_HASH;
} }
return hash($hash_type, $string); 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 * @param string $string String to be hashed
* @return string Hashed string * @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); 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__ // __END__

View File

@@ -8,39 +8,97 @@ declare(strict_types=1);
namespace CoreLibs\Create; namespace CoreLibs\Create;
use CoreLibs\Convert\Strings;
class RandomKey class RandomKey
{ {
/** @var int set the default key length it nothing else is set */
public const int KEY_LENGTH_DEFAULT = 4;
/** @var int the maximum key length allowed */
public const int KEY_LENGTH_MAX = 256;
/** @var string the default characters in the key range */
public const string KEY_CHARACTER_RANGE_DEFAULT =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
. 'abcdefghijklmnopqrstuvwxyz'
. '0123456789';
// key generation // key generation
/** @var string */ /** @var string all the characters that are int he current radnom key range */
private static string $key_range = ''; private static string $key_character_range = '';
/** @var int */ /** @var int character count in they key character range */
private static int $one_key_length; private static int $key_character_range_length = 0;
/** @var int */ /** @var int default key lenghth */
private static int $key_length = 4; // default key length /** @deprecated Will be removed, as setting has moved to randomKeyGen */
/** @var int */ private static int $key_length = 4;
private static int $max_key_length = 256; // max allowed length
/** /**
* if launched as class, init random key data first * if launched as class, init random key data first
*
* @param array<string> ...$key_range
*/ */
public function __construct() public function __construct(array ...$key_range)
{ {
$this->initRandomKeyData(); $this->setRandomKeyData(...$key_range);
} }
/**
* internal key range validation
*
* @param array<string> ...$key_range
* @return string
*/
private static function validateRandomKeyData(array ...$key_range): string
{
$key_character_range = Strings::buildCharStringFromLists(...$key_range);
if (strlen($key_character_range) <= 1) {
return '';
}
return $key_character_range;
}
/** /**
* sets the random key range with the default values * sets the random key range with the default values
* *
* @param array<string> $key_range a list of key ranges as array
* @return void has no return * @return void has no return
* @throws \LengthException If the string length is only 1 abort
*/ */
private static function initRandomKeyData(): void public static function setRandomKeyData(array ...$key_range): void
{ {
// random key generation base string // if key range is not set
self::$key_range = join('', array_merge( if (!count($key_range)) {
range('A', 'Z'), self::$key_character_range = self::KEY_CHARACTER_RANGE_DEFAULT;
range('a', 'z'), } else {
range('0', '9') self::$key_character_range = self::validateRandomKeyData(...$key_range);
)); // random key generation base string
self::$one_key_length = strlen(self::$key_range); }
self::$key_character_range_length = strlen(self::$key_character_range);
if (self::$key_character_range_length <= 1) {
throw new \LengthException(
"The given key character range '" . self::$key_character_range . "' "
. "is too small, must be at lest two characters: "
. self::$key_character_range_length
);
}
}
/**
* get the characters for the current key characters
*
* @return string
*/
public static function getRandomKeyData(): string
{
return self::$key_character_range;
}
/**
* get the length of all random characters
*
* @return int
*/
public static function getRandomKeyDataLength(): int
{
return self::$key_character_range_length;
} }
/** /**
@@ -49,11 +107,11 @@ class RandomKey
* @param int $key_length key length * @param int $key_length key length
* @return bool true for valid, false for invalid length * @return bool true for valid, false for invalid length
*/ */
private static function validateRandomKeyLenght(int $key_length): bool private static function validateRandomKeyLength(int $key_length): bool
{ {
if ( if (
$key_length > 0 && $key_length > 0 &&
$key_length <= self::$max_key_length $key_length <= self::KEY_LENGTH_MAX
) { ) {
return true; return true;
} else { } else {
@@ -67,11 +125,12 @@ class RandomKey
* *
* @param int $key_length key length * @param int $key_length key length
* @return bool true/false for set status * @return bool true/false for set status
* @deprecated This function does no longer set the key length, the randomKeyGen parameter has to be used
*/ */
public static function setRandomKeyLength(int $key_length): bool public static function setRandomKeyLength(int $key_length): bool
{ {
// only if valid int key with valid length // only if valid int key with valid length
if (self::validateRandomKeyLenght($key_length) === true) { if (self::validateRandomKeyLength($key_length) === true) {
self::$key_length = $key_length; self::$key_length = $key_length;
return true; return true;
} else { } else {
@@ -83,6 +142,7 @@ class RandomKey
* get the current set random key length * get the current set random key length
* *
* @return int Current set key length * @return int Current set key length
* @deprecated Key length is set during randomKeyGen call, this nethid is deprecated
*/ */
public static function getRandomKeyLength(): int public static function getRandomKeyLength(): int
{ {
@@ -94,28 +154,37 @@ class RandomKey
* if override key length is set, it will check on valid key and use this * if override key length is set, it will check on valid key and use this
* this will not set the class key length variable * this will not set the class key length variable
* *
* @param int $key_length key length override, -1 for use default * @param int $key_length [default=-1] key length override,
* @return string random key * if not set use default [LEGACY]
* @param array<string> $key_range a list of key ranges as array,
* if not set use previous set data
* @return string random key
*/ */
public static function randomKeyGen(int $key_length = -1): string public static function randomKeyGen(
{ int $key_length = self::KEY_LENGTH_DEFAULT,
// init random key strings if not set array ...$key_range
if ( ): string {
!isset(self::$one_key_length) $key_character_range = '';
) { if (count($key_range)) {
self::initRandomKeyData(); $key_character_range = self::validateRandomKeyData(...$key_range);
} $key_character_range_length = strlen($key_character_range);
$use_key_length = 0;
// only if valid int key with valid length
if (self::validateRandomKeyLenght($key_length) === true) {
$use_key_length = $key_length;
} else { } else {
$use_key_length = self::$key_length; if (!self::$key_character_range_length) {
self::setRandomKeyData();
}
$key_character_range = self::getRandomKeyData();
$key_character_range_length = self::getRandomKeyDataLength();
}
// if not valid key length, fallback to default
if (!self::validateRandomKeyLength($key_length)) {
$key_length = self::KEY_LENGTH_DEFAULT;
} }
// create random string // create random string
$random_string = ''; $random_string = '';
for ($i = 1; $i <= $use_key_length; $i++) { for ($i = 1; $i <= $key_length; $i++) {
$random_string .= self::$key_range[random_int(0, self::$one_key_length - 1)]; $random_string .= $key_character_range[
random_int(0, $key_character_range_length - 1)
];
} }
return $random_string; return $random_string;
} }

View File

@@ -21,21 +21,107 @@ class Session
private string $session_id = ''; private string $session_id = '';
/** @var bool flag auto write close */ /** @var bool flag auto write close */
private bool $auto_write_close = false; 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<string> 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 * init a session, if array is empty or array does not have session_name set
* then no auto init is run * then no auto init is run
* *
* @param string $session_name if set and not empty, will start session * @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->initSession($session_name);
$this->auto_write_close = $auto_write_close;
} }
// MARK: private methods // 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 * Start session
* startSession should be called for complete check * startSession should be called for complete check
@@ -72,6 +158,72 @@ class Session
return false; 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 * check if session name is valid
* *
@@ -151,6 +303,13 @@ class Session
if (!$this->checkActiveSession()) { if (!$this->checkActiveSession()) {
throw new \RuntimeException('[SESSION] Failed to activate session', 5); 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()) { } elseif ($session_name != $this->getSessionName()) {
throw new \UnexpectedValueException( throw new \UnexpectedValueException(
'[SESSION] Another session exists with a different name: ' . $this->getSessionName(), '[SESSION] Another session exists with a different name: ' . $this->getSessionName(),
@@ -159,10 +318,12 @@ class Session
} }
// check session id // check session id
if (false === ($session_id = $this->getSessionIdCall())) { 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 // set session id
$this->session_id = $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 flagged auto close, write close session
if ($this->auto_write_close) { if ($this->auto_write_close) {
$this->writeClose(); $this->writeClose();
@@ -202,11 +363,12 @@ class Session
* set the auto write close flag * set the auto write close flag
* *
* @param bool $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; $this->auto_write_close = $flag;
return $this;
} }
/** /**
@@ -352,14 +514,15 @@ class Session
* *
* @param string $name array name in _SESSION * @param string $name array name in _SESSION
* @param mixed $value value to set (can be anything) * @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->checkValidSessionEntryKey($name);
$this->restartSession(); $this->restartSession();
$_SESSION[$name] = $value; $_SESSION[$name] = $value;
$this->closeSessionCall(); $this->closeSessionCall();
return $this;
} }
/** /**
@@ -416,16 +579,17 @@ class Session
* unset one _SESSION entry 'name' if exists * unset one _SESSION entry 'name' if exists
* *
* @param string $name _SESSION key name to remove * @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])) { if (!isset($_SESSION[$name])) {
return; return $this;
} }
$this->restartSession(); $this->restartSession();
unset($_SESSION[$name]); unset($_SESSION[$name]);
$this->closeSessionCall(); $this->closeSessionCall();
return $this;
} }
/** /**

View File

@@ -81,7 +81,7 @@ class Uids
*/ */
public static function validateUuuidv4(string $uuidv4): bool public static function validateUuuidv4(string $uuidv4): bool
{ {
if (!preg_match("/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/", $uuidv4)) { if (!preg_match("/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i", $uuidv4)) {
return false; return false;
} }
return true; return true;

View File

@@ -39,9 +39,9 @@ class ArrayIO extends \CoreLibs\DB\IO
{ {
// main calss variables // main calss variables
/** @var array<mixed> */ /** @var array<mixed> */
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 */ /** @var string */
private string $table_name; // the table_name private string $table_name = ''; // the table_name
/** @var string */ /** @var string */
private string $pk_name = ''; // the primary key from this table private string $pk_name = ''; // the primary key from this table
/** @var int|string|null */ /** @var int|string|null */
@@ -127,9 +127,9 @@ class ArrayIO extends \CoreLibs\DB\IO
public function getTableArray(bool $reset = false): array public function getTableArray(bool $reset = false): array
{ {
if (!$reset) { if (!$reset) {
return $this->table_array ?? []; return $this->table_array;
} }
$table_array = $this->table_array ?? []; $table_array = $this->table_array;
reset($table_array); reset($table_array);
return $table_array; return $table_array;
} }
@@ -194,7 +194,7 @@ class ArrayIO extends \CoreLibs\DB\IO
*/ */
public function getTableName(): string public function getTableName(): string
{ {
return $this->table_name ?? ''; return $this->table_name;
} }
/** /**

View File

@@ -303,6 +303,8 @@ class IO
private string $query = ''; private string $query = '';
/** @var array<mixed> current params for query */ /** @var array<mixed> current params for query */
private array $params = []; 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 // 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<mixed>},type:''|'named'|'numbered'|'question_mark',found:int,matches:array<string>,params_lookup:array<mixed>,query:string,params:array<mixed>} */ /** @var array{}|array{original:array{query:string,params:array<mixed>},type:''|'named'|'numbered'|'question_mark',found:int,matches:array<string>,params_lookup:array<mixed>,query:string,params:array<mixed>} */
private array $placeholder_converted = []; private array $placeholder_converted = [];
@@ -500,7 +502,7 @@ class IO
die('<!-- Cannot load db functions class for: ' . $this->db_type . ' -->'); die('<!-- Cannot load db functions class for: ' . $this->db_type . ' -->');
} }
// write to internal one, once OK // write to internal one, once OK
$this->db_functions = $db_functions; $this->db_functions = $db_functions; /** @phan-suppress-current-line PhanPossiblyNullTypeMismatchProperty */
// connect to DB // connect to DB
if (!$this->__connectToDB()) { if (!$this->__connectToDB()) {
@@ -1319,7 +1321,7 @@ class IO
*/ */
private function __dbCountQueryParams(string $query): int 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; $this->query = $query;
// current params // current params
$this->params = $params; $this->params = $params;
// empty on new
$this->query_hash = '';
// no query set // no query set
if (empty($this->query)) { if (empty($this->query)) {
$this->__dbError(11); $this->__dbError(11);
@@ -1413,10 +1417,7 @@ class IO
$this->pk_name_table[$table] ? $this->pk_name_table[$table] ?
$this->pk_name_table[$table] : 'NULL'; $this->pk_name_table[$table] : 'NULL';
} }
if ( if (!preg_match(self::REGEX_RETURNING, $this->query) && $this->pk_name != 'NULL') {
!preg_match(self::REGEX_RETURNING, $this->query) &&
$this->pk_name && $this->pk_name != 'NULL'
) {
// check if this query has a ; at the end and remove it // check if this query has a ; at the end and remove it
$__query = preg_replace("/(;\s*)$/", '', $this->query); $__query = preg_replace("/(;\s*)$/", '', $this->query);
// must be query, if preg replace failed, use query as before // must be query, if preg replace failed, use query as before
@@ -1426,7 +1427,7 @@ class IO
} elseif ( } elseif (
preg_match(self::REGEX_RETURNING, $this->query, $matches) 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 // add the primary key if it is not in the returning set
if (!preg_match("/$this->pk_name/", $matches[1])) { if (!preg_match("/$this->pk_name/", $matches[1])) {
$this->query .= " , " . $this->pk_name; $this->query .= " , " . $this->pk_name;
@@ -1444,7 +1445,7 @@ class IO
$this->returning_id = true; $this->returning_id = true;
} }
// import protection, hash needed // 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 // QUERY PARAMS: run query params check and rewrite
if ($this->dbGetConvertPlaceholder() === true) { if ($this->dbGetConvertPlaceholder() === true) {
try { try {
@@ -1478,7 +1479,8 @@ class IO
return false; return false;
} }
} }
// set query hash
$this->query_hash = $query_hash;
// $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id); // $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id);
// for DEBUG, only on first time ;) // for DEBUG, only on first time ;)
$this->__dbDebug( $this->__dbDebug(
@@ -1962,7 +1964,7 @@ class IO
{ {
// set start array // set start array
if ($query) { if ($query) {
$array = $this->cursor_ext[$this->dbGetQueryHash($query)] ?? []; $array = $this->cursor_ext[$this->dbBuildQueryHash($query)] ?? [];
} else { } else {
$array = $this->cursor_ext; $array = $this->cursor_ext;
} }
@@ -2364,7 +2366,7 @@ class IO
return false; return false;
} }
// create hash from query ... // create hash from query ...
$query_hash = $this->dbGetQueryHash($query, $params); $query_hash = $this->dbBuildQueryHash($query, $params);
// pre declare array // pre declare array
if (!isset($this->cursor_ext[$query_hash])) { if (!isset($this->cursor_ext[$query_hash])) {
$this->cursor_ext[$query_hash] = [ $this->cursor_ext[$query_hash] = [
@@ -2542,7 +2544,10 @@ class IO
} // only go if NO cursor exists } // only go if NO cursor exists
// if 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) { if ($first_call === true) {
$this->cursor_ext[$query_hash]['log'][] = 'First call'; $this->cursor_ext[$query_hash]['log'][] = 'First call';
// count the rows returned (if select) // count the rows returned (if select)
@@ -2940,13 +2945,15 @@ class IO
* data to create a unique call one, optional * data to create a unique call one, optional
* @return bool False if query not found, true if success * @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->dbBuildQueryHash($query, $params);
$query_hash = $this->dbGetQueryHash($query, $params);
// clears cache for this query // clears cache for this query
if (empty($this->cursor_ext[$query_hash]['query'])) { if (
$this->__dbError(18, context: [ $show_warning &&
empty($this->cursor_ext[$query_hash]['query'])
) {
$this->__dbWarning(18, context: [
'query' => $query, 'query' => $query,
'params' => $params, 'params' => $params,
'hash' => $query_hash, 'hash' => $query_hash,
@@ -2985,7 +2992,7 @@ class IO
if ($query === null) { if ($query === null) {
return $this->cursor_ext; return $this->cursor_ext;
} }
$query_hash = $this->dbGetQueryHash($query, $params); $query_hash = $this->dbBuildQueryHash($query, $params);
if ( if (
!empty($this->cursor_ext) && !empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash]) isset($this->cursor_ext[$query_hash])
@@ -3015,7 +3022,7 @@ class IO
$this->__dbError(11); $this->__dbError(11);
return false; return false;
} }
$query_hash = $this->dbGetQueryHash($query, $params); $query_hash = $this->dbBuildQueryHash($query, $params);
if ( if (
!empty($this->cursor_ext) && !empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash]) isset($this->cursor_ext[$query_hash])
@@ -3041,7 +3048,7 @@ class IO
$this->__dbError(11); $this->__dbError(11);
return false; return false;
} }
$query_hash = $this->dbGetQueryHash($query, $params); $query_hash = $this->dbBuildQueryHash($query, $params);
if ( if (
!empty($this->cursor_ext) && !empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash]) isset($this->cursor_ext[$query_hash])
@@ -3067,7 +3074,7 @@ class IO
*/ */
public function dbResetQueryCalled(string $query, array $params = []): void 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 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])) { if (!empty($this->query_called[$query_hash])) {
return $this->query_called[$query_hash]; return $this->query_called[$query_hash];
} else { } else {
@@ -3141,6 +3148,7 @@ class IO
'pk_name' => '', 'pk_name' => '',
'count' => 0, 'count' => 0,
'query' => '', 'query' => '',
'query_raw' => $query,
'result' => null, 'result' => null,
'returning_id' => false, 'returning_id' => false,
'placeholder_converted' => [], 'placeholder_converted' => [],
@@ -3237,11 +3245,12 @@ class IO
} }
} else { } else {
// if we try to use the same statement name for a differnt query, error abort // 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 // thrown error
$this->__dbError(26, false, context: [ $this->__dbError(26, false, context: [
'statement_name' => $stm_name, 'statement_name' => $stm_name,
'prepared_query' => $this->prepare_cursor[$stm_name]['query'], 'prepared_query' => $this->prepare_cursor[$stm_name]['query'],
'prepared_query_raw' => $this->prepare_cursor[$stm_name]['query_raw'],
'query' => $query, 'query' => $query,
'pk_name' => $pk_name, '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 * Hash is used in all internal storage systems for return data
* *
* @param string $query The query to create the hash from * @param string $query The query to create the hash from
@@ -4055,9 +4064,9 @@ class IO
* data to create a unique call one, optional * data to create a unique call one, optional
* @return string Hash, as set by hash long * @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 . ( $query . (
$params !== [] ? $params !== [] ?
'#' . json_encode($params) : '' '#' . json_encode($params) : ''
@@ -4105,6 +4114,26 @@ class IO
$this->params = []; $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 * Returns the placeholder convert set or empty
* *
@@ -4284,6 +4313,17 @@ class IO
return $this->field_names[$pos] ?? false; return $this->field_names[$pos] ?? false;
} }
/**
* get all the $ placeholders
*
* @param string $query
* @return array<string>
*/
public function dbGetQueryParamPlaceholders(string $query): array
{
return $this->db_functions->__dbGetQueryParams($query);
}
/** /**
* Return a field type for a field name or pos, * Return a field type for a field name or pos,
* will return false if field is not found in list * will return false if field is not found in list
@@ -4364,6 +4404,37 @@ class IO
return $this->prepare_cursor[$stm_name][$key]; 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 // ERROR AND WARNING DATA
// *************************** // ***************************

View File

@@ -379,9 +379,9 @@ interface SqlFunctions
* Undocumented function * Undocumented function
* *
* @param string $query * @param string $query
* @return int * @return array<string>
*/ */
public function __dbCountQueryParams(string $query): int; public function __dbGetQueryParams(string $query): array;
} }
// __END__ // __END__

View File

@@ -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 * @param string $query
* @return int * @return array<string>
*/ */
public function __dbCountQueryParams(string $query): int public function __dbGetQueryParams(string $query): array
{ {
$matches = []; $matches = [];
// regex for params: only stand alone $number allowed // 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 // Matches in 1:, must be array_filtered to remove empty, count with array_unique
// Regex located in the ConvertPlaceholder class // Regex located in the ConvertPlaceholder class
preg_match_all( preg_match_all(
ConvertPlaceholder::REGEX_LOOKUP_PLACEHOLDERS, ConvertPlaceholder::REGEX_LOOKUP_NUMBERED,
$query, $query,
$matches $matches
); );
return count(array_unique(array_filter($matches[3]))); return array_unique(array_filter($matches[ConvertPlaceholder::MATCHING_POS]));
} }
} }

View File

@@ -14,74 +14,57 @@ namespace CoreLibs\DB\Support;
class ConvertPlaceholder class ConvertPlaceholder
{ {
// NOTE for missing: range */+ are not iplemented in the regex below, but - is for now /** @var string text block in SQL, single quited
// NOTE some combinations are allowed, but the query will fail before this * Note that does not include $$..$$ strings or anything with token name or nested ones
/** @var string split regex, entries before $ group */ */
private const PATTERN_QUERY_SPLIT = private const PATTERN_TEXT_BLOCK_SINGLE_QUOTE = '(?:\'(?:[^\'\\\\]|\\\\.)*\')';
'\?\?|' // UNKNOWN: double ??, is this to avoid something? /** @var string text block in SQL, dollar quoted
. '[\(,]|' // for ',' and '(' mostly in INSERT or ANY() * NOTE: if this is added everything shifts by one lookup number
. '[<>=]|' // general set for <, >, = in any query with any combination */
. '\^@|' // text search for start from text with ^@ private const PATTERN_TEXT_BLOCK_DOLLAR = '(?:\$(\w*)\$.*?\$\1\$)';
. '\|\||' // concats two elements
. '&&|' // array overlap
. '\-\|\-|' // range overlap for array
. '[^-]-{1}|' // single -, used in JSON too
. '->|->>|#>|#>>|@>|<@|@@|@\?|\?{1}|\?\||\?&|#-'; //JSON searches, Array searchs, etc
/** @var string the main regex including the pattern query split */
private const PATTERN_ELEMENT = '(?:\'.*?\')?\s*(?:' . self::PATTERN_QUERY_SPLIT . ')\s*';
/** @var string comment regex /** @var string comment regex
* anything that starts with -- and ends with a line break but any character that is not line break inbetween */ * anything that starts with -- and ends with a line break but any character that is not line break inbetween
private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)*\s*'; * this is the FIRST thing in the line and will skip any further lookups */
/** @var string parts to ignore in the SQL */ private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)';
private const PATTERN_IGNORE = // below are the params lookups
// digit -> ignore /** @var string named parameters, must start with single : */
'\d+|' private const PATTERN_NAMED = '((?<!:):(?:\w+))';
// other string -> ignore /** @var string question mark parameters, will catch any */
. '(?:\'.*?\')|'; private const PATTERN_QUESTION_MARK = '(\?{1})';
/** @var string named parameters */ /** @var string numbered parameters, can only start 1 to 9, second and further digits can be 0-9
private const PATTERN_NAMED = '(:\w+)'; * This ignores the $$ ... $$ escape syntax. If we find something like this will fail
/** @var string question mark parameters */ * It is recommended to use proper string escape quiting for writing data to the DB
private const PATTERN_QUESTION_MARK = '(?:(?:\?\?)?\s*(\?{1}))'; */
/** @var string numbered parameters */
private const PATTERN_NUMBERED = '(\$[1-9]{1}(?:[0-9]{1,})?)'; private const PATTERN_NUMBERED = '(\$[1-9]{1}(?:[0-9]{1,})?)';
// below here are full regex that will be used // below here are full regex that will be used
/** @var string replace regex for named (:...) entries */ /** @var string replace regex for named (:...) entries */
public const REGEX_REPLACE_NAMED = '/' public const REGEX_REPLACE_NAMED = '/'
. '(' . self::PATTERN_ELEMENT . ')' . self::PATTERN_COMMENT . '|'
. self::PATTERN_COMMENT . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. '(' . self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_IGNORE
. self::PATTERN_NAMED . self::PATTERN_NAMED
. ')'
. '/s'; . '/s';
/** @var string replace regex for question mark (?) entries */ /** @var string replace regex for question mark (?) entries */
public const REGEX_REPLACE_QUESTION_MARK = '/' public const REGEX_REPLACE_QUESTION_MARK = '/'
. '(' . self::PATTERN_ELEMENT . ')' . self::PATTERN_COMMENT . '|'
. self::PATTERN_COMMENT . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. '(' . self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_IGNORE
. self::PATTERN_QUESTION_MARK . self::PATTERN_QUESTION_MARK
. ')'
. '/s'; . '/s';
/** @var string replace regex for numbered ($n) entries */ /** @var string replace regex for numbered ($n) entries */
public const REGEX_REPLACE_NUMBERED = '/' public const REGEX_REPLACE_NUMBERED = '/'
. '(' . self::PATTERN_ELEMENT . ')' . self::PATTERN_COMMENT . '|'
. self::PATTERN_COMMENT . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. '(' . self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_IGNORE
. self::PATTERN_NUMBERED . self::PATTERN_NUMBERED
. ')'
. '/s'; . '/s';
/** @var string the main lookup query for all placeholders */ /** @var string the main lookup query for all placeholders */
public const REGEX_LOOKUP_PLACEHOLDERS = '/' public const REGEX_LOOKUP_PLACEHOLDERS = '/'
// prefix string part, must match towards . self::PATTERN_COMMENT . '|'
// seperator for ( = , ? - [and json/jsonb in pg doc section 9.15] . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_ELEMENT . self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_COMMENT
// match for replace part // match for replace part
. '(?:' . '(?:'
// ignore parts
. self::PATTERN_IGNORE
// :name named part (PDO) [1] // :name named part (PDO) [1]
. self::PATTERN_NAMED . '|' . self::PATTERN_NAMED . '|'
// ? question mark part (PDO) [2] // ? question mark part (PDO) [2]
@@ -92,6 +75,26 @@ class ConvertPlaceholder
. ')' . ')'
// single line -> add line break to matches in "." // single line -> add line break to matches in "."
. '/s'; . '/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 * Convert PDO type query with placeholders to \PG style and vica versa
@@ -130,11 +133,12 @@ class ConvertPlaceholder
$found = -1; $found = -1;
} }
/** @var array<string> 1: named */ /** @var array<string> 1: named */
$named_matches = array_filter($matches[1]); $named_matches = array_filter($matches[self::LOOOKUP_NAMED_POS]);
/** @var array<string> 2: open ? */ /** @var array<string> 2: open ? */
$qmark_matches = array_filter($matches[2]); $qmark_matches = array_filter($matches[self::LOOOKUP_QUESTION_MARK_POS]);
/** @var array<string> 3: $n matches */ /** @var array<string> 3: $n matches */
$numbered_matches = array_filter($matches[3]); $numbered_matches = array_filter($matches[self::LOOOKUP_NUMBERED_POS]);
// print "**MATCHES**: <pre>" . print_r($matches, true) . "</pre>";
// count matches // count matches
$count_named = count(array_unique($named_matches)); $count_named = count(array_unique($named_matches));
$count_qmark = count($qmark_matches); $count_qmark = count($qmark_matches);
@@ -233,38 +237,37 @@ class ConvertPlaceholder
$empty_params = $converted_placeholders['original']['empty_params']; $empty_params = $converted_placeholders['original']['empty_params'];
switch ($converted_placeholders['type']) { switch ($converted_placeholders['type']) {
case 'named': case 'named':
// 0: full // 1: replace part :named
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part :named
$pos = 0; $pos = 0;
$query_new = preg_replace_callback( $query_new = preg_replace_callback(
self::REGEX_REPLACE_NAMED, self::REGEX_REPLACE_NAMED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
// only count up if $match[3] is not yet in lookup table if (!isset($matches[self::MATCHING_POS])) {
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { 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++; $pos++;
$params_lookup[$matches[3]] = '$' . $pos; $params_lookup[$match] = '$' . $pos;
// skip params setup if param list is empty // skip params setup if param list is empty
if (!$empty_params) { if (!$empty_params) {
$params_new[] = $params[$matches[3]] ?? $params_new[] = $params[$match] ??
throw new \RuntimeException( throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params list', 'Cannot lookup ' . $match . ' in params list',
210 210
); );
} }
} }
// add the connectors back (1), and the data sets only if no replacement will be done // add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . ( return $params_lookup[$match]/* ??
empty($matches[3]) ? throw new \RuntimeException(
$matches[2] : 'Cannot lookup ' . $match . ' in params lookup list',
$params_lookup[$matches[3]] ?? 211
throw new \RuntimeException( )*/;
'Cannot lookup ' . $matches[3] . ' in params lookup list',
211
)
);
}, },
$converted_placeholders['original']['query'] $converted_placeholders['original']['query']
); );
@@ -274,61 +277,61 @@ class ConvertPlaceholder
// order and data stays the same // order and data stays the same
$params_new = $params ?? []; $params_new = $params ?? [];
} }
// 0: full // 1: replace part ?
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part ?
$pos = 0; $pos = 0;
$query_new = preg_replace_callback( $query_new = preg_replace_callback(
self::REGEX_REPLACE_QUESTION_MARK, self::REGEX_REPLACE_QUESTION_MARK,
function ($matches) use (&$pos, &$params_lookup) { 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 // only count pos up for actual replacements we will do
if (!empty($matches[3])) { if (!empty($match)) {
$pos++; $pos++;
$params_lookup[] = '$' . $pos; $params_lookup[] = '$' . $pos;
} }
// add the connectors back (1), and the data sets only if no replacement will be done // add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . ( return '$' . $pos;
empty($matches[3]) ?
$matches[2] :
'$' . $pos
);
}, },
$converted_placeholders['original']['query'] $converted_placeholders['original']['query']
); );
break; break;
case 'numbered': case 'numbered':
// 0: full // 1: replace part $numbered
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part $numbered
$pos = 0; $pos = 0;
$query_new = preg_replace_callback( $query_new = preg_replace_callback(
self::REGEX_REPLACE_NUMBERED, self::REGEX_REPLACE_NUMBERED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
// only count up if $match[3] is not yet in lookup table if (!isset($matches[self::MATCHING_POS])) {
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { 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++; $pos++;
$params_lookup[$matches[3]] = ':' . $pos . '_named'; $params_lookup[$match] = ':' . $pos . '_named';
// skip params setup if param list is empty // skip params setup if param list is empty
if (!$empty_params) { if (!$empty_params) {
$params_new[] = $params[($pos - 1)] ?? $params_new[] = $params[($pos - 1)] ??
throw new \RuntimeException( throw new \RuntimeException(
'Cannot lookup ' . ($pos - 1) . ' in params list', '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 // add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . ( return $params_lookup[$match]/* ??
empty($matches[3]) ? throw new \RuntimeException(
$matches[2] : 'Cannot lookup ' . $match . ' in params lookup list',
$params_lookup[$matches[3]] ?? 231
throw new \RuntimeException( )*/;
'Cannot lookup ' . $matches[3] . ' in params lookup list',
221
)
);
}, },
$converted_placeholders['original']['query'] $converted_placeholders['original']['query']
); );

View File

@@ -33,6 +33,36 @@ class Support
return $string; return $string;
} }
/**
* print ISO type datetime with microseconds and timezone
* Y-m-dTH:i:s.uP
* if no micro time the ".u" part is omitted
*
* @param bool $set_micro_time
* @return string
*/
public static function printIsoTime(bool $set_micro_time = true): string
{
$datetime = new \DateTime();
// Format the DateTime object to ISO 8601 with microseconds
// 'Y-m-d\TH:i:s.uP' is the format string:
// Y: Full year (e.g., 2025)
// m: Month (01-12)
// d: Day of the month (01-31)
// T: Literal 'T' to separate date and time (escaped with a backslash)
// H: Hour (00-23)
// i: Minute (00-59)
// s: Second (00-59)
// u: Microseconds (e.g., 654321)
// P: Difference to Greenwich time (GMT) with colon (e.g., +09:00)
if ($set_micro_time) {
return $datetime->format('Y-m-d\TH:i:s.uP');
} else {
return $datetime->format('Y-m-d\TH:i:sP');
}
}
/** /**
* prints a html formatted (pre) data * prints a html formatted (pre) data
* *

View File

@@ -0,0 +1,95 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2025/1/17
* DESCRIPTION:
* Deprecated helper for fputcsv
*/
declare(strict_types=1);
namespace CoreLibs\DeprecatedHelper;
use InvalidArgumentException;
class Deprecated84
{
/**
* This is a wrapper for fputcsv to fix deprecated warning for $escape parameter
* See: https://www.php.net/manual/en/function.fputcsv.php
* escape parameter deprecation and recommend to set to "" for compatible with PHP 9.0
*
* @param mixed $stream
* @param array<mixed> $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<mixed>|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<mixed>
*/
// 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__

View File

@@ -50,7 +50,6 @@ class GetLocale
$locale = defined('SITE_LOCALE') && !empty(SITE_LOCALE) ? $locale = defined('SITE_LOCALE') && !empty(SITE_LOCALE) ?
SITE_LOCALE : SITE_LOCALE :
// else parse from default, if not 'en' // else parse from default, if not 'en'
/** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */
(defined('DEFAULT_LOCALE') && !empty(DEFAULT_LOCALE) ? (defined('DEFAULT_LOCALE') && !empty(DEFAULT_LOCALE) ?
DEFAULT_LOCALE : 'en'); DEFAULT_LOCALE : 'en');
} }
@@ -97,8 +96,7 @@ class GetLocale
$encoding = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ? $encoding = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ?
SITE_ENCODING : SITE_ENCODING :
// or default encoding, if not 'UTF-8' // or default encoding, if not 'UTF-8'
/** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */ (defined('DEFAULT_ENCODING') ?
(defined('DEFAULT_ENCODING') && !empty(DEFAULT_ENCODING) ?
DEFAULT_ENCODING : 'UTF-8'); DEFAULT_ENCODING : 'UTF-8');
} }
} }

View File

@@ -131,6 +131,7 @@ class ErrorMessage
// set a jump target // set a jump target
$this->setJumpTarget($jump_target['target'] ?? null, $jump_target['info'] ?? null, $level); $this->setJumpTarget($jump_target['target'] ?? null, $jump_target['info'] ?? null, $level);
// write to log for abort/crash // write to log for abort/crash
$this->log->setErrorMessageCallSetErrorMsg();
switch ($level) { switch ($level) {
case 'notice': case 'notice':
$this->log->notice($message ?? $str, array_merge([ $this->log->notice($message ?? $str, array_merge([
@@ -210,6 +211,7 @@ class ErrorMessage
?bool $log_error = null, ?bool $log_error = null,
?bool $log_warning = null, ?bool $log_warning = null,
): void { ): void {
$this->log->setErrorMessageCallSetMessage();
$this->setErrorMsg( $this->setErrorMsg(
$error_id ?? '', $error_id ?? '',
$level, $level,
@@ -289,7 +291,7 @@ class ErrorMessage
*/ */
public function getLastErrorMsg(): array public function getLastErrorMsg(): array
{ {
return $this->error_str[array_key_last($this->error_str)] ?? [ return $this->error_str[array_key_last($this->error_str) ?? -1] ?? [
'level' => '', 'level' => '',
'str' => '', 'str' => '',
'id' => '', 'id' => '',

View File

@@ -112,7 +112,7 @@ enum Level: int
} }
/** /**
* Returns true if the passed $level is higher or equal to $this * Returns true if the passed $level is included in set level
* *
* @param Level $level * @param Level $level
* @return bool * @return bool

View File

@@ -13,6 +13,7 @@ namespace CoreLibs\Logging\Logger;
enum MessageLevel: int enum MessageLevel: int
{ {
case noset = 0;
case ok = 100; case ok = 100;
case success = 150; // special for file uploads case success = 150; // special for file uploads
case info = 200; case info = 200;

View File

@@ -29,8 +29,21 @@ use Stringable;
class Logging class Logging
{ {
/** @var int minimum size for a max file size, so we don't set 1 byte, 10kb */ /** @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; public const int MIN_LOG_MAX_FILESIZE = 10 * 1024;
/** @var string log file extension, not changeable */
private const string LOG_FILE_NAME_EXT = "log";
/** @var string log file block separator, not changeable */
private const string LOG_FILE_BLOCK_SEPARATOR = '.';
/** @var int the base stack trace level for the line number */
private const int DEFAULT_STACK_TRACE_LEVEL_LINE = 1;
/** @var array<string,int> */
private const array STACK_OVERRIDE_CHECK = [
'setErrorMsg' => 2,
'setMessage' => 3,
];
// MARK: OPTION array
// NOTE: the second party array{} hs some errors // NOTE: the second party array{} hs some errors
/** @var array<string,array<string,string|bool|Level>>|array{string:array{type:string,type_info?:string,mandatory:true,alias?:string,default:string|bool|Level,deprecated:bool,use?:string}} */ /** @var array<string,array<string,string|bool|Level>>|array{string:array{type:string,type_info?:string,mandatory:true,alias?:string,default:string|bool|Level,deprecated:bool,use?:string}} */
private const OPTIONS = [ private const OPTIONS = [
@@ -46,6 +59,7 @@ class Logging
'type' => 'string', 'mandatory' => false, 'type' => 'string', 'mandatory' => false,
'default' => '', 'deprecated' => true, 'use' => 'log_file_id' 'default' => '', 'deprecated' => true, 'use' => 'log_file_id'
], ],
// log level
'log_level' => [ 'log_level' => [
'type' => 'instance', 'type' => 'instance',
'type_info' => '\CoreLibs\Logging\Logger\Level', 'type_info' => '\CoreLibs\Logging\Logger\Level',
@@ -53,6 +67,14 @@ class Logging
'default' => Level::Debug, 'default' => Level::Debug,
'deprecated' => false 'deprecated' => false
], ],
// level to trigger write to error_log
'error_log_write_level' => [
'type' => 'instance',
'type_info' => '\CoreLibs\Logging\Logger\Level',
'mandatory' => false,
'default' => Level::Emergency,
'deprecated' => false,
],
// options // options
'log_per_run' => [ 'log_per_run' => [
'type' => 'bool', 'mandatory' => false, 'type' => 'bool', 'mandatory' => false,
@@ -82,14 +104,21 @@ class Logging
'type' => 'bool', 'mandatory' => false, 'type' => 'bool', 'mandatory' => false,
'default' => false, 'deprecated' => true, 'use' => 'log_per_date' 'default' => false, 'deprecated' => true, 'use' => 'log_per_date'
], ],
// if turned off uses old time format without time zone
'log_time_format_iso' => [
'type' => 'bool', 'mandatory' => false,
'default' => true, 'deprecated' => false
]
]; ];
// options // options
/** @var array<mixed> */ /** @var array<mixed> */
private array $options = []; private array $options = [];
/** @var Level set level */ /** @var Level set logging level */
private Level $log_level; private Level $log_level;
/** @var Level set level for writing to error_log, will not write if log level lower than error log write level */
private Level $error_log_write_level;
// page and host name // page and host name
/** @var string */ /** @var string */
@@ -104,8 +133,6 @@ class Logging
private string $log_folder = ''; private string $log_folder = '';
/** @var string a alphanumeric name that has to be set as global definition */ /** @var string a alphanumeric name that has to be set as global definition */
private string $log_file_id = ''; 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 */ /** @var string log file name with folder, for actual writing */
private string $log_file_name = ''; private string $log_file_name = '';
/** @var int set in bytes */ /** @var int set in bytes */
@@ -119,6 +146,12 @@ class Logging
/** @var string Y-m-d file in file name */ /** @var string Y-m-d file in file name */
private string $log_file_date = ''; private string $log_file_date = '';
// speical flags for ErrorMessage calls
/** @var bool Flag to set if called from ErrorMessage::setErrorMsg */
private bool $error_message_call_set_error_msg = false;
/** @var bool Flag to set if called from ErrorMessage::setMessage */
private bool $error_message_call_set_message = false;
/** /**
* 1: create a new log file per run (time stamp + unique ID) * 1: create a new log file per run (time stamp + unique ID)
* 2: add Y-m-d and do automatic daily rotation * 2: add Y-m-d and do automatic daily rotation
@@ -143,12 +176,13 @@ class Logging
]; ];
/** /**
* Init logger * MARK: Init logger
* *
* options array layout * options array layout
* - log_folder: * - log_folder:
* - log_file_id / file_id (will be deprecated): * - log_file_id / file_id (will be deprecated):
* - log_level: * - log_level:
* - error_log_write_level: at what level we write to error_log
* *
* - log_per_run: * - log_per_run:
* - log_per_date: (was print_file_date) * - log_per_date: (was print_file_date)
@@ -170,6 +204,8 @@ class Logging
// set log level // set log level
$this->initLogLevel(); $this->initLogLevel();
// set error log write level
$this->initErrorLogWriteLevel();
// set log folder from options // set log folder from options
$this->initLogFolder(); $this->initLogFolder();
// set per run UID for logging // set per run UID for logging
@@ -188,8 +224,10 @@ class Logging
// PRIVATE METHODS // PRIVATE METHODS
// ********************************************************************* // *********************************************************************
// MARK: options check
/** /**
* Undocumented function * validate options
* *
* @param array<mixed> $options * @param array<mixed> $options
* @return bool * @return bool
@@ -261,6 +299,8 @@ class Logging
return true; return true;
} }
// MARK: init log elvels
/** /**
* init log level, just a wrapper to auto set from options * init log level, just a wrapper to auto set from options
* *
@@ -278,6 +318,24 @@ class Logging
$this->setLoggingLevel($this->options['log_level']); $this->setLoggingLevel($this->options['log_level']);
} }
/**
* init error log write level
*
* @return void
*/
private function initErrorLogWriteLevel()
{
if (
empty($this->options['error_log_write_level']) ||
!$this->options['error_log_write_level'] instanceof Level
) {
$this->options['error_log_write_level'] = Level::Emergency;
}
$this->setErrorLogWriteLevel($this->options['error_log_write_level']);
}
// MARK: set log folder
/** /**
* Set the log folder * Set the log folder
* If folder is not writeable the script will throw an E_USER_ERROR * If folder is not writeable the script will throw an E_USER_ERROR
@@ -319,6 +377,8 @@ class Logging
return $status; return $status;
} }
// MARK: set host name
/** /**
* Set the hostname and port * Set the hostname and port
* If port is not defaul 80 it will be added to the host name * If port is not defaul 80 it will be added to the host name
@@ -335,6 +395,8 @@ class Logging
} }
} }
// MARK: set log file id (file)
/** /**
* set log file prefix id * set log file prefix id
* *
@@ -393,6 +455,8 @@ class Logging
return $status; return $status;
} }
// MARK init log flags and levels
/** /**
* set flags from options and option flags connection internal settings * set flags from options and option flags connection internal settings
* *
@@ -421,6 +485,19 @@ class Logging
return $this->log_level->includes($level); return $this->log_level->includes($level);
} }
/**
* Checks that given level is matchins error_log write level
*
* @param Level $level
* @return bool
*/
private function checkErrorLogWriteLevel(Level $level): bool
{
return $this->error_log_write_level->includes($level);
}
// MARK: build log ifle name
/** /**
* Build the file name for writing * Build the file name for writing
* *
@@ -431,7 +508,7 @@ class Logging
private function buildLogFileName(Level $level, string $group_id = ''): string private function buildLogFileName(Level $level, string $group_id = ''): string
{ {
// init base file path // 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 // log ID prefix settings, if not valid, replace with empty
if (!empty($this->log_file_id)) { if (!empty($this->log_file_id)) {
$rpl_string = $this->log_file_id; $rpl_string = $this->log_file_id;
@@ -440,14 +517,15 @@ class Logging
} }
$fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix) $fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix)
$rpl_string = !$this->getLogFlag(Flag::per_level) ? '' : $rpl_string = $this->getLogFlag(Flag::per_level) ?
'_' . $level->getName(); self::LOG_FILE_BLOCK_SEPARATOR . $level->getName() :
'';
$fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename $fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename
// write per level // 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 - // normalize level, replace all non alphanumeric characters with -
'_' . ( self::LOG_FILE_BLOCK_SEPARATOR . (
// if return is only - then set error string // if return is only - then set error string
preg_match( preg_match(
"/^-+$/", "/^-+$/",
@@ -455,25 +533,29 @@ class Logging
) ? ) ?
'INVALID-LEVEL-STRING' : 'INVALID-LEVEL-STRING' :
$level_string $level_string
); ) :
'';
$fn = str_replace('{GROUP}', $rpl_string, $fn); // create output filename $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 // set per class, but don't use get_class as we will only get self
$rpl_string = !$this->getLogFlag(Flag::per_class) ? '' : '_' $rpl_string = $this->getLogFlag(Flag::per_class) ?
// set sub class settings // set sub class settings
. str_replace('\\', '-', Support::getCallerTopLevelClass()); self::LOG_FILE_BLOCK_SEPARATOR . str_replace('\\', '-', Support::getCallerTopLevelClass()) :
'';
$fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename $fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename
// if request to write to one file // if request to write to one file
$rpl_string = !$this->getLogFlag(Flag::per_page) ? $rpl_string = $this->getLogFlag(Flag::per_page) ?
'' : self::LOG_FILE_BLOCK_SEPARATOR . System::getPageName(System::NO_EXTENSION) :
'_' . System::getPageName(System::NO_EXTENSION); '';
$fn = str_replace('{PAGENAME}', $rpl_string, $fn); // create output filename $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 run id, we auto add ymd, so we ignore the log file date
if ($this->getLogFlag(Flag::per_run)) { 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)) { } 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 { } else {
$rpl_string = ''; $rpl_string = '';
} }
@@ -483,6 +565,8 @@ class Logging
return $fn; return $fn;
} }
// MARK: master write log to file
/** /**
* writes error msg data to file for current level * writes error msg data to file for current level
* *
@@ -500,6 +584,10 @@ class Logging
if (!$this->checkLogLevel($level)) { if (!$this->checkLogLevel($level)) {
return false; return false;
} }
// if we match level then write to error_log
if ($this->checkErrorLogWriteLevel($level)) {
error_log((string)$message);
}
// build logging file name // build logging file name
// fn is log folder + file name // fn is log folder + file name
@@ -524,6 +612,8 @@ class Logging
return true; return true;
} }
// MARK: master prepare log
/** /**
* Prepare the log message with all needed info blocks: * Prepare the log message with all needed info blocks:
* [timestamp] [host name] [file path + file::row number] [running uid] {class::/->method} * [timestamp] [host name] [file path + file::row number] [running uid] {class::/->method}
@@ -551,31 +641,65 @@ class Logging
$file_line = ''; $file_line = '';
$caller_class_method = '-'; $caller_class_method = '-';
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print "[" . $level->getName() . "] [$message] prepareLog:<br>" . Support::printAr($traces); $stack_trace_start_level_line = self::DEFAULT_STACK_TRACE_LEVEL_LINE;
// file + line: call not this but one before (the one that calls this) // set stack trace level +1 if called from ErrorMessage::setMessage
// start from this level, if unset fall down until we are at null if ($this->error_message_call_set_message) {
$start_trace_level = 2; $stack_trace_start_level_line = 3;
for ($trace_level = $start_trace_level; $trace_level >= 0; $trace_level--) { } elseif ($this->error_message_call_set_error_msg) {
if (isset($traces[$trace_level])) { $stack_trace_start_level_line = 2;
$file_line = ($traces[$trace_level]['file'] ?? $traces[$trace_level]['function']) }
. ':' . ($traces[$trace_level]['line'] ?? '-'); // if we have line > default, then check if valid, else reset to default
// as namespace\class->method if ($stack_trace_start_level_line > self::DEFAULT_STACK_TRACE_LEVEL_LINE) {
$caller_class_method = // check if function at level is one of the override checks
// get the last call before we are in the Logging class $fn_check = $traces[$stack_trace_start_level_line]['function'] ?? '';
($traces[$trace_level]['class'] ?? '') if (
// connector, if unkown use == !isset(self::STACK_OVERRIDE_CHECK[$fn_check]) ||
. ($traces[$trace_level]['type'] ?? '') self::STACK_OVERRIDE_CHECK[$fn_check] != $stack_trace_start_level_line
// method/function: prepareLog->(debug|info|...)->[THIS] ) {
. $traces[$trace_level]['function']; $stack_trace_start_level_line = self::DEFAULT_STACK_TRACE_LEVEL_LINE;
break;
} }
} }
$this->error_message_call_set_message = false;
$this->error_message_call_set_error_msg = false;
// set stack trace level +1 if called from ErrorMessage::setMessage
// print "[" . $level->getName() . "] [$message] [" . $stack_trace_start_level_line . "] "
// . "prepareLog:<br>" . Support::printAr($traces);
// file + line: call not this but one before (the one that calls this)
// start from this level, if unset fall down until we are at null
// NOTE this has to be pushed to 3 for setMessage wrap calls
for ($trace_level = $stack_trace_start_level_line; $trace_level >= 0; $trace_level--) {
if (!isset($traces[$trace_level])) {
continue;
}
$file_line = ($traces[$trace_level]['file'] ?? $traces[$trace_level]['function'])
. ':' . ($traces[$trace_level]['line'] ?? '-');
// call function is one stack level above
$trace_level++;
// skip setting if we are in the top level already
if (!isset($traces[$trace_level])) {
break;
}
// as namespace\class->method
$caller_class_method =
// get the last call before we are in the Logging class
($traces[$trace_level]['class'] ?? '')
// connector, if unkown use ==
. ($traces[$trace_level]['type'] ?? '')
// method/function: prepareLog->(debug|info|...)->[THIS]
. $traces[$trace_level]['function'];
break;
}
// if not line is set
if (empty($file_line)) { if (empty($file_line)) {
$file_line = System::getPageName(System::FULL_PATH); $file_line = System::getPageName(System::FULL_PATH);
} }
// print "CLASS: " . $class . "<br>"; // print "CLASS: " . $class . "<br>";
// get timestamp // get timestamp
$timestamp = Support::printTime(); if (!empty($this->options['log_time_format_iso'])) {
$timestamp = Support::printIsoTime();
} else {
$timestamp = Support::printTime();
}
// if group id is empty replace it with current level // if group id is empty replace it with current level
$group_str = $level->getName(); $group_str = $level->getName();
@@ -603,6 +727,7 @@ class Logging
// PUBLIC STATIC METHJODS // PUBLIC STATIC METHJODS
// ********************************************************************* // *********************************************************************
// MARK: set log level
/** /**
* set the log level * set the log level
* *
@@ -663,7 +788,7 @@ class Logging
// **** GET/SETTER // **** GET/SETTER
// log level set and get // MARK: log level
/** /**
* set new log level * set new log level
@@ -698,7 +823,30 @@ class Logging
); );
} }
// log file id set (file name prefix) // MARK: error log write level
/**
* set the error_log write level
*
* @param string|int|Level $level
* @return void
*/
public function setErrorLogWriteLevel(string|int|Level $level): void
{
$this->error_log_write_level = $this->processLogLevel($level);
}
/**
* get the current level for error_log write
*
* @return Level
*/
public function getErrorLogWriteLevel(): Level
{
return $this->error_log_write_level;
}
// MARK: log file id set (file name prefix)
/** /**
* sets the internal log file prefix id * sets the internal log file prefix id
@@ -726,7 +874,7 @@ class Logging
return $this->log_file_id; return $this->log_file_id;
} }
// log unique id set (for per run) // MARK: log unique id set (for per run)
/** /**
* Sets a unique id based on current date (y/m/d, h:i:s) and a unique id (8 chars) * Sets a unique id based on current date (y/m/d, h:i:s) and a unique id (8 chars)
@@ -739,7 +887,10 @@ class Logging
{ {
if (empty($this->log_file_unique_id) || $override == true) { if (empty($this->log_file_unique_id) || $override == true) {
$this->log_file_unique_id = $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( . substr(hash(
'sha1', 'sha1',
random_bytes(63) random_bytes(63)
@@ -758,7 +909,7 @@ class Logging
return $this->log_file_unique_id; return $this->log_file_unique_id;
} }
// general log date // MARK: general log date
/** /**
* set the log file date to Y-m-d * set the log file date to Y-m-d
@@ -781,7 +932,7 @@ class Logging
return $this->log_file_date; return $this->log_file_date;
} }
// general flag set // MARK: general flag set
/** /**
* set one of the basic flags * set one of the basic flags
@@ -836,7 +987,7 @@ class Logging
return $this->log_flags; return $this->log_flags;
} }
// log folder/file // MARK: log folder/file
/** /**
* set new log folder, check that folder is writeable * set new log folder, check that folder is writeable
@@ -880,7 +1031,7 @@ class Logging
return $this->log_file_name; return $this->log_file_name;
} }
// max log file size // MARK: max log file size
/** /**
* set mag log file size * set mag log file size
@@ -911,7 +1062,31 @@ class Logging
} }
// ********************************************************************* // *********************************************************************
// OPTIONS CALLS // MARK: ErrorMessage class overrides
// *********************************************************************
/**
* call if called from Error Message setMessage wrapper
*
* @return void
*/
public function setErrorMessageCallSetMessage(): void
{
$this->error_message_call_set_message = true;
}
/**
* call if called from Error Message setMessage wrapper
*
* @return void
*/
public function setErrorMessageCallSetErrorMsg(): void
{
$this->error_message_call_set_error_msg = true;
}
// *********************************************************************
// MARK: OPTIONS CALLS
// ********************************************************************* // *********************************************************************
/** /**
@@ -929,6 +1104,8 @@ class Logging
// MAIN CALLS // MAIN CALLS
// ********************************************************************* // *********************************************************************
// MARK: main log call
/** /**
* Commong log interface * Commong log interface
* *
@@ -966,7 +1143,7 @@ class Logging
} }
/** /**
* DEBUG: 100 * MARK: DEBUG: 100
* *
* write debug data to error_msg array * write debug data to error_msg array
* *
@@ -998,7 +1175,7 @@ class Logging
} }
/** /**
* INFO: 200 * MARK: INFO: 200
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1017,7 +1194,7 @@ class Logging
} }
/** /**
* NOTICE: 250 * MARK: NOTICE: 250
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1036,7 +1213,7 @@ class Logging
} }
/** /**
* WARNING: 300 * MARK: WARNING: 300
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1055,7 +1232,7 @@ class Logging
} }
/** /**
* ERROR: 400 * MARK: ERROR: 400
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1074,7 +1251,7 @@ class Logging
} }
/** /**
* CTRITICAL: 500 * MARK: CTRITICAL: 500
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1093,7 +1270,7 @@ class Logging
} }
/** /**
* ALERT: 550 * MARK: ALERT: 550
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1112,7 +1289,7 @@ class Logging
} }
/** /**
* EMERGENCY: 600 * MARK: EMERGENCY: 600
* *
* @param string|Stringable $message * @param string|Stringable $message
* @param mixed[] $context * @param mixed[] $context
@@ -1131,7 +1308,7 @@ class Logging
} }
// ********************************************************************* // *********************************************************************
// DEPRECATED SUPPORT CALLS // MARK: DEPRECATED SUPPORT CALLS
// ********************************************************************* // *********************************************************************
// legacy, but there are too many implemented // legacy, but there are too many implemented
@@ -1189,7 +1366,7 @@ class Logging
} }
// ********************************************************************* // *********************************************************************
// DEPRECATED METHODS // MARK: DEPRECATED METHODS
// ********************************************************************* // *********************************************************************
/** /**
@@ -1355,7 +1532,7 @@ class Logging
} }
// ********************************************************************* // *********************************************************************
// DEBUG METHODS // MARK: DEBUG METHODS
// ********************************************************************* // *********************************************************************
/** /**
@@ -1375,19 +1552,21 @@ class Logging
Level::Error, Level::Critical, Level::Alert, Level::Emergency Level::Error, Level::Critical, Level::Alert, Level::Emergency
] as $l ] as $l
) { ) {
print "Check: " . $this->log_level->getName() . " | " . $l->getName() . "<br>";
if ($this->log_level->isHigherThan($l)) { if ($this->log_level->isHigherThan($l)) {
print "L: " . $this->log_level->getName() . " > " . $l->getName() . "<br>"; print "L(gt): " . $this->log_level->getName() . " > " . $l->getName() . "<br>";
} }
if ($this->log_level->includes($l)) { if ($this->log_level->includes($l)) {
print "L: " . $this->log_level->getName() . " <= " . $l->getName() . "<br>"; print "L(le): " . $this->log_level->getName() . " <= " . $l->getName() . "<br>";
} }
if ($this->log_level->isLowerThan($l)) { if ($this->log_level->isLowerThan($l)) {
print "L: " . $this->log_level->getName() . " < " . $l->getName() . "<br>"; print "L(lt): " . $this->log_level->getName() . " < " . $l->getName() . "<br>";
} }
echo "<br>"; echo "<br>";
} }
// back to options level // back to options level
$this->initLogLevel(); $this->initLogLevel();
$this->initErrorLogWriteLevel();
print "OPT set level: " . $this->getLoggingLevel()->getName() . "<br>"; print "OPT set level: " . $this->getLoggingLevel()->getName() . "<br>";
} }
} }

View File

@@ -1371,7 +1371,7 @@ class Generate
) { ) {
$this->msg .= sprintf( $this->msg .= sprintf(
$this->l->__('Please enter a valid (%s) input for the <b>%s</b> Field!<br>'), $this->l->__('Please enter a valid (%s) input for the <b>%s</b> Field!<br>'),
$this->dba->getTableArray()[$key]['error_example'], $this->dba->getTableArray()[$key]['error_example'] ?? '[MISSING]',
$this->dba->getTableArray()[$key]['output_name'] $this->dba->getTableArray()[$key]['output_name']
); );
} }
@@ -2602,7 +2602,7 @@ class Generate
} }
} }
// add lost error ones // add lost error ones
$this->log->error('P: ' . $data['prefix'] . ', ' $this->log->error('Prefix: ' . $data['prefix'] . ', '
. Support::prAr($_POST['ERROR'][$data['prefix']] ?? [])); . Support::prAr($_POST['ERROR'][$data['prefix']] ?? []));
if ($this->error && !empty($_POST['ERROR'][$data['prefix']])) { if ($this->error && !empty($_POST['ERROR'][$data['prefix']])) {
$prfx = $data['prefix']; // short $prfx = $data['prefix']; // short

View File

@@ -50,7 +50,8 @@ class EditUsers implements Interface\TableArraysInterface
'HIDDEN_value' => $_POST['HIDDEN_password'] ?? '', 'HIDDEN_value' => $_POST['HIDDEN_password'] ?? '',
'CONFIRM_value' => $_POST['CONFIRM_password'] ?? '', 'CONFIRM_value' => $_POST['CONFIRM_password'] ?? '',
'output_name' => '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 'type' => 'password', // later has to be password for encryption in database
'update' => [ // connected field updates, and update data 'update' => [ // connected field updates, and update data
'password_change_date' => [ // db row to update 'password_change_date' => [ // db row to update
@@ -135,30 +136,6 @@ class EditUsers implements Interface\TableArraysInterface
'min_edit_acl' => '100', 'min_edit_acl' => '100',
'min_show_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' => [ 'email' => [
'value' => $_POST['email'] ?? '', 'value' => $_POST['email'] ?? '',
'output_name' => 'E-Mail', 'output_name' => 'E-Mail',
@@ -206,6 +183,7 @@ class EditUsers implements Interface\TableArraysInterface
'type' => 'text', 'type' => 'text',
'error_check' => 'unique|custom', 'error_check' => 'unique|custom',
'error_regex' => "/^[A-Za-z0-9]+$/", 'error_regex' => "/^[A-Za-z0-9]+$/",
'error_example' => "ABCdef123",
'emptynull' => 1,'min_edit_acl' => '100', 'emptynull' => 1,'min_edit_acl' => '100',
'min_show_acl' => '100', 'min_show_acl' => '100',
], ],

View File

@@ -365,9 +365,6 @@ class Image
imagepng($thumb, $thumbnail_write_path . $thumbnail); imagepng($thumb, $thumbnail_write_path . $thumbnail);
break; break;
} }
// free up resources (in case we are called in a loop)
imagedestroy($source);
imagedestroy($thumb);
} else { } else {
throw new \RuntimeException( throw new \RuntimeException(
'Invalid source image file. Only JPEG/PNG are allowed: ' . $filename, 'Invalid source image file. Only JPEG/PNG are allowed: ' . $filename,
@@ -543,8 +540,6 @@ class Image
imagepng($img, $filename); imagepng($img, $filename);
break; break;
} }
// clean up image if we have an image
imagedestroy($img);
} }
} }

View File

@@ -418,9 +418,7 @@ class ProgressBar
// if this is percent, we ignore anything, it is auto positioned // if this is percent, we ignore anything, it is auto positioned
if ($this->label[$name]['type'] != 'percent') { if ($this->label[$name]['type'] != 'percent') {
foreach (['top', 'left', 'width', 'height'] as $pos_name) { 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 != '') { if ($align != '') {

View File

@@ -0,0 +1,408 @@
<?php
/**
* very simple asymmetric encryption
* Better use:
* https://paragonie.com/project/halite
* https://github.com/paragonie/halite
*
* current code is just to encrypt and decrypt
*
* must use a valid encryption key created with
* Secruty\CreateKey class
*/
declare(strict_types=1);
namespace CoreLibs\Security;
use CoreLibs\Security\CreateKey;
use SodiumException;
class AsymmetricAnonymousEncryption
{
/** @var AsymmetricAnonymousEncryption self instance */
private static AsymmetricAnonymousEncryption $instance;
/** @var ?string key pair which holds secret and public key, needed for encryption */
private ?string $key_pair = null;
/** @var ?string public key, needed for decryption
* if not set but key_pair set, this will be extracted from key pair */
private ?string $public_key = null;
/**
* init class
* if key not passed, key must be set with createKey
*
* @param string|null $key_pair
* @param string|null $public_key
*/
public function __construct(
#[\SensitiveParameter]
string|null $key_pair = null,
string|null $public_key = null
) {
if ($public_key !== null) {
$this->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__

View File

@@ -35,14 +35,39 @@ class CreateKey
return random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES); 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 * convert binary key to hex string
* *
* @param string $hex_key Convert binary key string to hex * @param string $hex_key Convert binary key string to hex
* @return string * @return string
*/ */
public static function bin2hex(string $hex_key): string public static function bin2hex(
{ #[\SensitiveParameter]
string $hex_key
): string {
return sodium_bin2hex($hex_key); return sodium_bin2hex($hex_key);
} }
@@ -52,8 +77,10 @@ class CreateKey
* @param string $string_key Convery hex key string to binary * @param string $string_key Convery hex key string to binary
* @return string * @return string
*/ */
public static function hex2bin(string $string_key): string public static function hex2bin(
{ #[\SensitiveParameter]
string $string_key
): string {
return sodium_hex2bin($string_key); return sodium_hex2bin($string_key);
} }
} }

View File

@@ -16,8 +16,10 @@ class Password
* @param string $password password * @param string $password password
* @return string hashed 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 // always use the PHP default for the password
// password options ca be set in the password init, // password options ca be set in the password init,
// but should be kept as default // but should be kept as default
@@ -31,8 +33,11 @@ class Password
* @param string $hash password hash * @param string $hash password hash
* @return bool true or false * @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)) { if (password_verify($password, $hash)) {
return true; return true;
} else { } else {

View File

@@ -24,19 +24,19 @@ class SymmetricEncryption
/** @var SymmetricEncryption self instance */ /** @var SymmetricEncryption self instance */
private static SymmetricEncryption $instance; private static SymmetricEncryption $instance;
/** @var string bin hex key */ /** @var ?string bin hex key */
private string $key = ''; private ?string $key = null;
/** /**
* init class * init class
* if key not passed, key must be set with createKey * if key not passed, key must be set with createKey
* *
* @param string|null|null $key * @param string|null $key encryption key
*/ */
public function __construct( public function __construct(
string|null $key = null ?string $key = null
) { ) {
if ($key != null) { if ($key !== null) {
$this->setKey($key); $this->setKey($key);
} }
} }
@@ -45,16 +45,49 @@ class SymmetricEncryption
* Returns the singleton self object. * Returns the singleton self object.
* For function wrapper use * For function wrapper use
* *
* @param string|null $key encryption key
* @return SymmetricEncryption object * @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); self::$instance = new self($key);
} }
return self::$instance; 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 * MARK: PRIVATE
* *************************************************************************/ * *************************************************************************/
@@ -62,11 +95,19 @@ class SymmetricEncryption
/** /**
* create key and check validity * create key and check validity
* *
* @param string $key The key from which the binary key will be created * @param ?string $key The key from which the binary key will be created
* @return string Binary key string * @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 { try {
$key = CreateKey::hex2bin($key); $key = CreateKey::hex2bin($key);
} catch (SodiumException $e) { } catch (SodiumException $e) {
@@ -87,36 +128,42 @@ class SymmetricEncryption
* @param string $encrypted Text to decrypt * @param string $encrypted Text to decrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty * @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string Plain text * @return string Plain text
* @throws \RangeException * @throws \UnexpectedValueException key cannot be empty
* @throws \UnexpectedValueException * @throws \UnexpectedValueException decipher message failed
* @throws \UnexpectedValueException * @throws \UnexpectedValueException invalid key
*/ */
private function decryptData(string $encrypted, ?string $key): string private function decryptData(
{ #[\SensitiveParameter]
if (empty($key)) { string $encrypted,
throw new \UnexpectedValueException('Key not set'); #[\SensitiveParameter]
?string $key
): string {
if (empty($encrypted)) {
throw new \UnexpectedValueException('Encrypted string cannot be empty');
} }
$key = $this->createKey($key); $key = $this->createKey($key);
$decoded = base64_decode($encrypted); $decoded = base64_decode($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = false; $plaintext = false;
try { try {
$plain = sodium_crypto_secretbox_open( $plaintext = sodium_crypto_secretbox_open(
$ciphertext, $ciphertext,
$nonce, $nonce,
$key $key
); );
} catch (SodiumException $e) { } catch (SodiumException $e) {
sodium_memzero($ciphertext);
sodium_memzero($key);
throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage()); throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage());
} }
if (!is_string($plain)) {
throw new \UnexpectedValueException('Invalid Key');
}
sodium_memzero($ciphertext); sodium_memzero($ciphertext);
sodium_memzero($key); 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 $message Message to encrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty * @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string * @return string Ciphered text
* @throws \Exception * @throws \UnexpectedValueException create message failed
* @throws \RangeException
*/ */
private function encryptData(string $message, ?string $key): string private function encryptData(
{ #[\SensitiveParameter]
if (empty($this->key) || $key === null) { string $message,
throw new \UnexpectedValueException('Key not set'); #[\SensitiveParameter]
} ?string $key
): string {
$key = $this->createKey($key); $key = $this->createKey($key);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
try { try {
@@ -145,6 +192,8 @@ class SymmetricEncryption
) )
); );
} catch (SodiumException $e) { } catch (SodiumException $e) {
sodium_memzero($message);
sodium_memzero($key);
throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage()); throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage());
} }
sodium_memzero($message); sodium_memzero($message);
@@ -156,19 +205,49 @@ class SymmetricEncryption
* MARK: PUBLIC * MARK: PUBLIC
* *************************************************************************/ * *************************************************************************/
/** /**
* set a new key for encryption * set a new key for encryption
* *
* @param string $key * @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)) { if (empty($key)) {
throw new \UnexpectedValueException('Key cannot be empty'); throw new \UnexpectedValueException('Key cannot be empty');
} }
// check that this is a valid key
$this->createKey($key);
// set key
$this->key = $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 $encrypted Message encrypted with safeEncrypt()
* @param string $key Encryption key (as hex string) * @param string $key Encryption key (as hex string)
* @return 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); return self::getInstance()->decryptData($encrypted, $key);
} }
@@ -193,12 +272,11 @@ class SymmetricEncryption
* *
* @param string $encrypted Message encrypted with safeEncrypt() * @param string $encrypted Message encrypted with safeEncrypt()
* @return string * @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); return $this->decryptData($encrypted, $this->key);
} }
@@ -209,11 +287,13 @@ class SymmetricEncryption
* @param string $message Message to encrypt * @param string $message Message to encrypt
* @param string $key Encryption key (as hex string) * @param string $key Encryption key (as hex string)
* @return 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); return self::getInstance()->encryptData($message, $key);
} }
@@ -222,11 +302,11 @@ class SymmetricEncryption
* *
* @param string $message Message to encrypt * @param string $message Message to encrypt
* @return string * @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); return $this->encryptData($message, $this->key);
} }
} }

View File

@@ -19,12 +19,13 @@ declare(strict_types=1);
namespace CoreLibs\Template; namespace CoreLibs\Template;
// leading slash if this is in lib\Smarty class SmartyExtend extends \Smarty\Smarty
class SmartyExtend extends \Smarty
{ {
// internal translation engine // internal translation engine
/** @var \CoreLibs\Language\L10n */ /** @var \CoreLibs\Language\L10n language class */
public \CoreLibs\Language\L10n $l10n; public \CoreLibs\Language\L10n $l10n;
/** @var \CoreLibs\Logging\Logging $log logging class */
public \CoreLibs\Logging\Logging $log;
// lang & encoding // lang & encoding
/** @var string */ /** @var string */
@@ -157,14 +158,18 @@ class SmartyExtend extends \Smarty
* calls L10 for pass on internaly in smarty * calls L10 for pass on internaly in smarty
* also registers the getvar caller plugin * also registers the getvar caller plugin
* *
* @param \CoreLibs\Language\L10n $l10n l10n language class * @param \CoreLibs\Language\L10n $l10n l10n language class
* @param string|null $cache_id * @param \CoreLibs\Logging\Logging $log Logger class
* @param string|null $compile_id * @param string|null $cache_id [default=null]
* @param string|null $compile_id [default=null]
* @param array<string,mixed> $options [default=[]]
*/ */
public function __construct( public function __construct(
\CoreLibs\Language\L10n $l10n, \CoreLibs\Language\L10n $l10n,
\CoreLibs\Logging\Logging $log,
?string $cache_id = null, ?string $cache_id = null,
?string $compile_id = null ?string $compile_id = null,
array $options = []
) { ) {
// trigger deprecation // trigger deprecation
if ( if (
@@ -177,14 +182,33 @@ class SmartyExtend extends \Smarty
E_USER_DEPRECATED E_USER_DEPRECATED
); );
} }
// set variables (to be deprecated) // set variables from global constants (deprecated)
$cache_id = $cache_id ?? if ($cache_id === null && defined('CACHE_ID')) {
(defined('CACHE_ID') ? CACHE_ID : ''); trigger_error(
$compile_id = $compile_id ?? 'SmartyExtended: No cache_id set and CACHE_ID constant set, this is deprecated',
(defined('COMPILE_ID') ? COMPILE_ID : ''); 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 // call basic smarty
// or Smarty::__construct();
parent::__construct(); parent::__construct();
$this->log = $log;
// init lang // init lang
$this->l10n = $l10n; $this->l10n = $l10n;
// parse and read, legacy stuff // parse and read, legacy stuff
@@ -194,7 +218,6 @@ class SmartyExtend extends \Smarty
$this->lang_short = $locale['lang_short']; $this->lang_short = $locale['lang_short'];
$this->domain = $locale['domain']; $this->domain = $locale['domain'];
$this->lang_dir = $locale['path']; $this->lang_dir = $locale['path'];
// opt load functions so we can use legacy init for smarty run perhaps // opt load functions so we can use legacy init for smarty run perhaps
\CoreLibs\Language\L10n::loadFunctions(); \CoreLibs\Language\L10n::loadFunctions();
_setlocale(LC_MESSAGES, $locale['locale']); _setlocale(LC_MESSAGES, $locale['locale']);
@@ -203,7 +226,6 @@ class SmartyExtend extends \Smarty
_bind_textdomain_codeset($this->domain, $this->encoding); _bind_textdomain_codeset($this->domain, $this->encoding);
// register smarty variable // register smarty variable
// $this->registerPlugin(\Smarty\Smarty::PLUGIN_MODIFIER, 'getvar', [&$this, 'getTemplateVars']);
$this->registerPlugin(self::PLUGIN_MODIFIER, 'getvar', [&$this, 'getTemplateVars']); $this->registerPlugin(self::PLUGIN_MODIFIER, 'getvar', [&$this, 'getTemplateVars']);
$this->page_name = \CoreLibs\Get\System::getPageName(); $this->page_name = \CoreLibs\Get\System::getPageName();
@@ -211,6 +233,77 @@ class SmartyExtend extends \Smarty
// set internal settings // set internal settings
$this->CACHE_ID = $cache_id; $this->CACHE_ID = $cache_id;
$this->COMPILE_ID = $compile_id; $this->COMPILE_ID = $compile_id;
// set options
$this->setOptions($options);
}
/**
* set options
*
* @param array<string,mixed> $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;
}
}
}
} }
/** /**

View File

@@ -599,7 +599,7 @@ class Curl implements Interface\RequestsInterface
// for post we set POST option // for post we set POST option
if ($type == "post") { if ($type == "post") {
curl_setopt($handle, CURLOPT_POST, true); curl_setopt($handle, CURLOPT_POST, true);
} elseif (!empty($type) && in_array($type, self::CUSTOM_REQUESTS)) { } elseif (in_array($type, self::CUSTOM_REQUESTS)) {
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type)); curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type));
} }
// set body data if not null, will send empty [] for empty data // set body data if not null, will send empty [] for empty data
@@ -614,8 +614,6 @@ class Curl implements Interface\RequestsInterface
// print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>"; // print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>";
// get response code and bail on not authorized // get response code and bail on not authorized
$http_response = $this->handleCurlResponse($handle, $http_result, $options['http_errors']); $http_response = $this->handleCurlResponse($handle, $http_result, $options['http_errors']);
// close handler
$this->handleCurlClose($handle);
// return response and result // return response and result
return [ return [
'code' => (string)$http_response, 'code' => (string)$http_response,
@@ -838,17 +836,6 @@ class Curl implements Interface\RequestsInterface
); );
} }
/**
* close the current curl handle
*
* @param \CurlHandle $handle
* @return void
*/
private function handleCurlClose(\CurlHandle $handle): void
{
curl_close($handle);
}
// ********************************************************************* // *********************************************************************
// MARK: PUBLIC METHODS // MARK: PUBLIC METHODS
// ********************************************************************* // *********************************************************************

View File

@@ -183,8 +183,9 @@ list($HOST_NAME) = array_pad(explode(':', $_SERVER['HTTP_HOST'], 2), 2, null);
define('HOST_NAME', $HOST_NAME); define('HOST_NAME', $HOST_NAME);
// BAIL ON MISSING MASTER SITE CONFIG // BAIL ON MISSING MASTER SITE CONFIG
if (!isset($SITE_CONFIG[HOST_NAME]['location'])) { if (!isset($SITE_CONFIG[HOST_NAME]['location'])) {
echo 'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator'; throw new \InvalidArgumentException(
exit; 'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator'
);
} }
// BAIL ON MISSING DB CONFIG: // BAIL ON MISSING DB CONFIG:
// we have either no db selction for this host but have db config entries // we have either no db selction for this host but have db config entries
@@ -200,8 +201,9 @@ if (
empty($DB_CONFIG[$SITE_CONFIG[HOST_NAME]['db_host']])) empty($DB_CONFIG[$SITE_CONFIG[HOST_NAME]['db_host']]))
) )
) { ) {
echo 'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator'; throw new \InvalidArgumentException(
exit; 'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator'
);
} }
// set SSL on // set SSL on
$is_secure = false; $is_secure = false;

View File

@@ -48,7 +48,7 @@ header("Content-Type: application/json; charset=UTF-8");
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) { if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized"); header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}'); 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 // 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) { } elseif (($file_get = file_get_contents('php://input')) === false) {
header("HTTP/1.1 404 Not Found"); header("HTTP/1.1 404 Not Found");
print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}'); print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}');
exit; exit(1);
} }
print buildContent($http_headers, $file_get); print buildContent($http_headers, $file_get);

View File

@@ -12,6 +12,8 @@ Not yet covered tests:
- loginGetLocale - loginGetLocale
- loginGetHeaderColor - loginGetHeaderColor
- loginGetPages - loginGetPages
- loginGetPageLookupList
- loginPageAccessAllowed
- loginGetEuid - loginGetEuid
*/ */
@@ -22,8 +24,12 @@ Not yet covered tests:
*/ */
final class CoreLibsACLLoginTest extends TestCase final class CoreLibsACLLoginTest extends TestCase
{ {
private static $db; private static \CoreLibs\DB\IO $db;
private static $log; 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 * start DB conneciton, setup DB, etc
@@ -108,21 +114,46 @@ final class CoreLibsACLLoginTest extends TestCase
self::$db->dbSetMaxQueryCall(-1); self::$db->dbSetMaxQueryCall(-1);
// insert additional content for testing (locked user, etc) // insert additional content for testing (locked user, etc)
$queries = [ $queries = [
"INSERT INTO edit_access_data " <<<SQL
. "(edit_access_id, name, value, enabled) VALUES " INSERT INTO edit_access_data (
. "((SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'), " edit_access_id, name, value, enabled
. "'test', 'value', 1)" ) VALUES (
(SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'),
'test', 'value', 1
)
SQL
]; ];
foreach ($queries as $query) { foreach ($queries as $query) {
self::$db->dbExec($query); self::$db->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 // define mandatory constant
// must set // must set
// TARGET // TARGET
define('TARGET', 'test'); define('TARGET', 'test');
// LOGIN DB SCHEMA // LOGIN DB SCHEMA
// define('LOGIN_DB_SCHEMA', '');
// SHOULD SET // SHOULD SET
// DEFAULT_ACL_LEVEL (d80) // DEFAULT_ACL_LEVEL (d80)
@@ -235,24 +266,25 @@ final class CoreLibsACLLoginTest extends TestCase
'ajax_post_action' => 'login', 'ajax_post_action' => 'login',
], ],
], ],
'load, session euid set only, php error' => [ 'load, session eucuuid set only, php error' => [
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
], ],
[], [],
[], [],
[ [
'EUID' => 1, 'LOGIN_EUID' => 1,
'ECUID' => 'abc', 'LOGIN_EUCUID' => 'abc',
'ECUUID' => '1233456-1234-1234-1234-123456789012', 'LOGIN_EUCUUID' => '1233456-1234-1234-1234-123456789012',
], ],
2, 2,
[], [],
], ],
'load, session euid set, all set' => [ 'load, session eucuuid set, all set' => [
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -261,22 +293,23 @@ final class CoreLibsACLLoginTest extends TestCase
[], [],
[], [],
[ [
'EUID' => 1, 'LOGIN_EUID' => 1,
'ECUID' => 'abc', 'LOGIN_EUCUID' => 'abc',
'ECUUID' => '1233456-1234-1234-1234-123456789012', 'LOGIN_EUCUUID' => 'SET_EUCUUID_IN_TEST',
'USER_NAME' => '', 'LOGIN_USER_NAME' => '',
'GROUP_NAME' => '', 'LOGIN_GROUP_NAME' => '',
'ADMIN' => 1, 'LOGIN_ADMIN' => 1,
'GROUP_ACL_LEVEL' => -1, 'LOGIN_GROUP_ACL_LEVEL' => -1,
'PAGES_ACL_LEVEL' => [], 'LOGIN_PAGES_ACL_LEVEL' => [],
'USER_ACL_LEVEL' => -1, 'LOGIN_USER_ACL_LEVEL' => -1,
'USER_ADDITIONAL_ACL' => [], 'LOGIN_USER_ADDITIONAL_ACL' => [],
'GROUP_ADDITIONAL_ACL' => [], 'LOGIN_GROUP_ADDITIONAL_ACL' => [],
'UNIT_UID' => [ 'LOGIN_UNIT_UID' => [
'AdminAccess' => 1, 'AdminAccess' => '123456789012',
], ],
'UNIT' => [ 'LOGIN_UNIT' => [
1 => [ '123456789012' => [
'id' => 1,
'acl_level' => 80, 'acl_level' => 80,
'name' => 'Admin Access', 'name' => 'Admin Access',
'uid' => 'AdminAccess', 'uid' => 'AdminAccess',
@@ -288,8 +321,8 @@ final class CoreLibsACLLoginTest extends TestCase
'additional_acl' => [] 'additional_acl' => []
], ],
], ],
// 'UNIT_DEFAULT' => '', // 'LOGIN_UNIT_DEFAULT' => '',
// 'DEFAULT_ACL_LIST' => [], // 'LOGIN_DEFAULT_ACL_LIST' => [],
], ],
0, 0,
[ [
@@ -297,6 +330,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -416,6 +450,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_deleted' => true 'test_deleted' => true
@@ -441,6 +476,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_enabled' => true 'test_enabled' => true
@@ -466,6 +502,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_locked' => true 'test_locked' => true
@@ -491,6 +528,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_get_locked' => true, 'test_get_locked' => true,
@@ -515,6 +553,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_locked_period_until' => 'on' 'test_locked_period_until' => 'on'
@@ -540,6 +579,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -559,6 +599,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -569,6 +610,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_locked_period_after' => 'on' 'test_locked_period_after' => 'on'
@@ -594,6 +636,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_locked_period_until' => 'on', 'test_locked_period_until' => 'on',
@@ -620,6 +663,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_login_user_id_locked' => true 'test_login_user_id_locked' => true
@@ -645,6 +689,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -663,6 +708,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -673,6 +719,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -692,6 +739,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -702,6 +750,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -721,6 +770,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -731,6 +781,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -750,6 +801,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -781,6 +833,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -804,6 +857,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -814,6 +868,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -837,6 +892,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -847,6 +903,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_login_user_id_revalidate_after' => 'on', 'test_login_user_id_revalidate_after' => 'on',
@@ -873,6 +930,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -893,6 +951,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -903,6 +962,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_login_user_id_valid_from' => 'on', 'test_login_user_id_valid_from' => 'on',
@@ -929,6 +989,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -949,6 +1010,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -959,6 +1021,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_login_user_id_valid_until' => 'on', 'test_login_user_id_valid_until' => 'on',
@@ -985,6 +1048,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list', 'base_access' => 'list',
'page_access' => 'list', 'page_access' => 'list',
'test_login_user_id_valid_from' => 'on', 'test_login_user_id_valid_from' => 'on',
@@ -1012,6 +1076,7 @@ final class CoreLibsACLLoginTest extends TestCase
[ [
'page_name' => 'edit_users.php', 'page_name' => 'edit_users.php',
'edit_access_id' => 1, 'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess', 'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test', 'edit_access_data' => 'test',
'base_access' => 'list', 'base_access' => 'list',
@@ -1042,6 +1107,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true, 'admin_flag' => true,
'check_access' => true, 'check_access' => true,
'check_access_id' => 1, 'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value', 'check_access_data' => 'value',
'base_access' => true, 'base_access' => true,
'page_access' => true, 'page_access' => true,
@@ -1111,11 +1177,15 @@ final class CoreLibsACLLoginTest extends TestCase
$_POST[$post_var] = $post_value; $_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 // set _SESSION data
foreach ($session as $session_var => $session_value) { foreach ($session as $session_var => $session_value) {
$_SESSION[$session_var] = $session_value; $_SESSION[$session_var] = $session_value;
} }
/** @var \CoreLibs\ACL\Login&MockObject */ /** @var \CoreLibs\ACL\Login&MockObject */
$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
->setConstructorArgs([ ->setConstructorArgs([
@@ -1134,7 +1204,7 @@ final class CoreLibsACLLoginTest extends TestCase
. 'locale' . DIRECTORY_SEPARATOR, . 'locale' . DIRECTORY_SEPARATOR,
] ]
]) ])
->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin']) ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity'])
->getMock(); ->getMock();
$login_mock->expects($this->any()) $login_mock->expects($this->any())
->method('loginTerminate') ->method('loginTerminate')
@@ -1152,6 +1222,10 @@ final class CoreLibsACLLoginTest extends TestCase
->method('loginPrintLogin') ->method('loginPrintLogin')
->willReturnCallback(function () { ->willReturnCallback(function () {
}); });
$login_mock->expects($this->any())
->method('loginEnhanceHttpSecurity')
->willReturnCallback(function () {
});
// if mock_settings: enabled OFF // if mock_settings: enabled OFF
// run DB update and set off // run DB update and set off
@@ -1369,6 +1443,19 @@ final class CoreLibsACLLoginTest extends TestCase
// run test // run test
try { 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 // if ajax call
// check if parameter, or globals (old type) // check if parameter, or globals (old type)
// else normal call // else normal call
@@ -1427,6 +1514,31 @@ final class CoreLibsACLLoginTest extends TestCase
$login_mock->loginCheckAccessPage($mock_settings['page_access']), $login_mock->loginCheckAccessPage($mock_settings['page_access']),
'Assert 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 // - loginCheckEditAccess
$this->assertEquals( $this->assertEquals(
$expected['check_access'], $expected['check_access'],
@@ -1449,7 +1561,7 @@ final class CoreLibsACLLoginTest extends TestCase
$this->assertEquals( $this->assertEquals(
$expected['check_access_data'], $expected['check_access_data'],
$login_mock->loginGetEditAccessData( $login_mock->loginGetEditAccessData(
$mock_settings['edit_access_id'], $mock_settings['edit_access_uid'],
$mock_settings['edit_access_data'] $mock_settings['edit_access_data']
), ),
'Assert check access id data value valid' 'Assert check access id data value valid'
@@ -1480,11 +1592,12 @@ final class CoreLibsACLLoginTest extends TestCase
// - loginCheckPermissions // - loginCheckPermissions
// - loginGetPermissionOkay // - loginGetPermissionOkay
} catch (\Exception $e) { } catch (\Exception $e) {
// print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" /* print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/"
// . ($expected['login_error'] ?? 0) . "\n"; . ($expected['login_error'] ?? 0) . "\n";
// print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n"; print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n";
// print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n"; print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n";
// print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\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 this is 100, then we do further error checks
if ( if (
$e->getCode() == 100 || $e->getCode() == 100 ||

View File

@@ -30,11 +30,11 @@ DECLARE
random_length INT = 12; -- that should be long enough random_length INT = 12; -- that should be long enough
BEGIN BEGIN
IF TG_OP = 'INSERT' THEN IF TG_OP = 'INSERT' THEN
NEW.date_created := 'now'; NEW.date_created := clock_timestamp();
NEW.cuid := random_string(random_length); NEW.cuid := random_string(random_length);
NEW.cuuid := gen_random_uuid(); NEW.cuuid := gen_random_uuid();
ELSIF TG_OP = 'UPDATE' THEN ELSIF TG_OP = 'UPDATE' THEN
NEW.date_updated := 'now'; NEW.date_updated := clock_timestamp();
END IF; END IF;
RETURN NEW; RETURN NEW;
END; END;
@@ -579,11 +579,10 @@ CREATE TABLE edit_user (
strict SMALLINT DEFAULT 0, strict SMALLINT DEFAULT 0,
locked SMALLINT DEFAULT 0, locked SMALLINT DEFAULT 0,
protected SMALLINT NOT NULL 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 -- is admin user
admin SMALLINT NOT NULL DEFAULT 0, admin SMALLINT NOT NULL DEFAULT 0,
-- forced logout counter
force_logout INT DEFAULT 0,
-- last login log -- last login log
last_login TIMESTAMP WITHOUT TIME ZONE, last_login TIMESTAMP WITHOUT TIME ZONE,
-- login error -- 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.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.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.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.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.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_count IS 'Number of failed logins, reset on successful login';
@@ -655,37 +652,53 @@ CREATE TABLE edit_log (
edit_log_id INT GENERATED ALWAYS AS IDENTITY 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 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, FOREIGN KEY (euid) REFERENCES edit_user (edit_user_id) MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL,
ecuid VARCHAR, eucuid VARCHAR,
ecuuid UUID, eucuuid UUID, -- this is the one we want to use, full UUIDv4 from the edit user table
username VARCHAR, -- date_created equal, but can be overridden
password VARCHAR,
event_date TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, 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, error TEXT,
event TEXT, event TEXT,
-- bytea or string type storage of any data
data_binary BYTEA, data_binary BYTEA,
data TEXT, data TEXT,
-- set page name only
page VARCHAR, page VARCHAR,
action VARCHAR, -- various info data sets
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,
user_agent VARCHAR, user_agent VARCHAR,
referer VARCHAR, referer VARCHAR,
script_name VARCHAR, script_name VARCHAR,
query_string VARCHAR, query_string VARCHAR,
request_scheme VARCHAR, -- http or https
server_name VARCHAR, server_name VARCHAR,
http_host VARCHAR, http_host VARCHAR,
http_accept VARCHAR, http_data JSONB,
http_accept_charset VARCHAR, -- DEPRECATED [http*]
http_accept_encoding VARCHAR, http_accept VARCHAR, -- in http_data
session_id VARCHAR 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; ) INHERITS (edit_generic) WITHOUT OIDS;
-- END: table/edit_log.sql -- END: table/edit_log.sql
-- START: table/edit_log_overflow.sql -- START: table/edit_log_overflow.sql
@@ -1015,7 +1028,7 @@ INSERT INTO edit_page_access (enabled, edit_group_id, edit_page_id, edit_access_
-- edit user -- edit user
-- inserts admin user so basic users can be created -- inserts admin user so basic users can be created
DELETE FROM edit_user; 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_language_id FROM edit_language WHERE short_name = 'en_US'),
(SELECT edit_group_id FROM edit_group WHERE name = 'Admin'), (SELECT edit_group_id FROM edit_group WHERE name = 'Admin'),
(SELECT edit_scheme_id FROM edit_scheme WHERE name = 'Admin'), (SELECT edit_scheme_id FROM edit_scheme WHERE name = 'Admin'),

View File

@@ -24,12 +24,12 @@ final class CoreLibsCheckEmailTest extends TestCase
'get email regex invalid -1, will be 0' => [ 'get email regex invalid -1, will be 0' => [
-1, -1,
"^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@" "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@"
. "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$" . "(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$"
], ],
'get email regex invalid 10, will be 0' => [ 'get email regex invalid 10, will be 0' => [
10, 10,
"^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@" "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@"
. "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$" . "(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$"
], ],
'get email regex valid 1, will be 1' => [ 'get email regex valid 1, will be 1' => [
1, 1,
@@ -157,7 +157,7 @@ final class CoreLibsCheckEmailTest extends TestCase
'error' => 0, 'error' => 0,
'message' => 'Invalid email address', 'message' => 'Invalid email address',
'regex' => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@" 'regex' => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@"
. "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$" . "(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$"
] ]
], ],
'error 1 will return double @ error' => [ 'error 1 will return double @ error' => [
@@ -181,7 +181,7 @@ final class CoreLibsCheckEmailTest extends TestCase
[ [
'error' => 3, 'error' => 3,
'message' => 'Invalid domain part after @ sign', 'message' => 'Invalid domain part after @ sign',
'regex' => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.([a-zA-Z]{2,}){1}$" 'regex' => "@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$"
] ]
], ],
'error 4 will be invalid domain' => [ 'error 4 will be invalid domain' => [
@@ -189,7 +189,7 @@ final class CoreLibsCheckEmailTest extends TestCase
[ [
'error' => 4, 'error' => 4,
'message' => 'Invalid domain name part', 'message' => 'Invalid domain name part',
'regex' => "@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\." 'regex' => "@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\."
] ]
], ],
'error 5 will be invalid domain top level only' => [ 'error 5 will be invalid domain top level only' => [
@@ -197,7 +197,7 @@ final class CoreLibsCheckEmailTest extends TestCase
[ [
'error' => 5, 'error' => 5,
'message' => 'Wrong domain top level part', 'message' => 'Wrong domain top level part',
'regex' => "\.([a-zA-Z]{2,6}){1}$" 'regex' => "\.[a-zA-Z]{2,6}$"
] ]
], ],
'error 6 will be domain double dot' => [ 'error 6 will be domain double dot' => [

View File

@@ -0,0 +1,404 @@
<?php
// This code was created by Claude Sonnet 4
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Combined\ArrayHandler;
class CoreLibsCombinedArrayHandlerFindArraysMissingKeyTest extends TestCase
{
private const DATA_SEPARATOR = ':'; // Updated to match your class's separator
/**
* Test finding missing single key when searching by value without specific key
*/
public function testFindMissingSingleKeyWithValueSearch()
{
$array = [
'item1' => [
'name' => 'John',
'age' => 25
// missing 'email' key
],
'item2' => [
'name' => 'Jane',
'age' => 30,
'email' => 'jane@example.com'
],
'item3' => [
'name' => 'John', // same value as item1
'age' => 35,
'email' => 'john2@example.com'
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'John', 'email');
$this->assertCount(1, $result);
$this->assertEquals($array['item1'], $result[0]['content']);
$this->assertEquals('item1', $result[0]['path']);
$this->assertEquals(['email'], $result[0]['missing_key']);
}
/**
* Test finding missing single key when searching by specific key-value pair
*/
public function testFindMissingSingleKeyWithKeyValueSearch()
{
$array = [
'user1' => [
'id' => 1,
'name' => 'Alice'
// missing 'status' key
],
'user2' => [
'id' => 2,
'name' => 'Bob',
'status' => 'active'
],
'user3' => [
'id' => 1, // same id as user1
'name' => 'Charlie',
'status' => 'inactive'
]
];
$result = ArrayHandler::findArraysMissingKey($array, 1, 'status', 'id');
$this->assertCount(1, $result);
$this->assertEquals($array['user1'], $result[0]['content']);
$this->assertEquals('user1', $result[0]['path']);
$this->assertEquals(['status'], $result[0]['missing_key']);
}
/**
* Test finding missing multiple keys
*/
public function testFindMissingMultipleKeys()
{
$array = [
'record1' => [
'name' => 'Test',
'value' => 100
// missing both 'date' and 'status' keys
],
'record2' => [
'name' => 'Test',
'value' => 200,
'date' => '2023-01-01'
// missing 'status' key
],
'record3' => [
'name' => 'Test',
'value' => 300,
'date' => '2023-01-02',
'status' => 'complete'
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'Test', ['date', 'status']);
$this->assertCount(2, $result);
// First result should be record1 missing both keys
$this->assertEquals($array['record1'], $result[0]['content']);
$this->assertEquals('record1', $result[0]['path']);
$this->assertContains('date', $result[0]['missing_key']);
$this->assertContains('status', $result[0]['missing_key']);
$this->assertCount(2, $result[0]['missing_key']);
// Second result should be record2 missing status key
$this->assertEquals($array['record2'], $result[1]['content']);
$this->assertEquals('record2', $result[1]['path']);
$this->assertEquals(['status'], $result[1]['missing_key']);
}
/**
* Test with nested arrays
*/
public function testFindMissingKeyInNestedArrays()
{
$array = [
'section1' => [
'items' => [
'item1' => [
'name' => 'Product A',
'price' => 99.99
// missing 'category' key
],
'item2' => [
'name' => 'Product B',
'price' => 149.99,
'category' => 'electronics'
]
]
],
'section2' => [
'data' => [
'name' => 'Product A', // same name as nested item
'category' => 'books'
]
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'Product A', 'category');
$this->assertCount(1, $result);
$this->assertEquals($array['section1']['items']['item1'], $result[0]['content']);
$this->assertEquals('section1:items:item1', $result[0]['path']);
$this->assertEquals(['category'], $result[0]['missing_key']);
}
/**
* Test when no arrays are missing the required key
*/
public function testNoMissingKeys()
{
$array = [
'item1' => [
'name' => 'John',
'email' => 'john@example.com'
],
'item2' => [
'name' => 'Jane',
'email' => 'jane@example.com'
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'John', 'email');
$this->assertEmpty($result);
}
/**
* Test when search value is not found in any array
*/
public function testSearchValueNotFound()
{
$array = [
'item1' => [
'name' => 'John',
'age' => 25
],
'item2' => [
'name' => 'Jane',
'age' => 30
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'Bob', 'email');
$this->assertEmpty($result);
}
/**
* Test with different data types for search value
*/
public function testDifferentSearchValueTypes()
{
$array = [
'item1' => [
'active' => true,
'count' => 5
// missing 'label' key
],
'item2' => [
'active' => false,
'count' => 10,
'label' => 'test'
],
'item3' => [
'active' => true, // same boolean as item1
'count' => 15,
'label' => 'another'
]
];
// Test with boolean
$result = ArrayHandler::findArraysMissingKey($array, true, 'label', 'active');
$this->assertCount(1, $result);
$this->assertEquals('item1', $result[0]['path']);
// Test with integer
$result = ArrayHandler::findArraysMissingKey($array, 5, 'label', 'count');
$this->assertCount(1, $result);
$this->assertEquals('item1', $result[0]['path']);
}
/**
* Test with empty array
*/
public function testEmptyArray()
{
$array = [];
$result = ArrayHandler::findArraysMissingKey($array, 'test', 'key');
$this->assertEmpty($result);
}
/**
* Test with array containing non-array values
*/
public function testMixedArrayTypes()
{
$array = [
'string_value' => 'hello',
'numeric_value' => 123,
'array_value' => [
'name' => 'test',
// missing 'type' key
],
'another_array' => [
'name' => 'test',
'type' => 'example'
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'test', 'type');
$this->assertCount(1, $result);
$this->assertEquals($array['array_value'], $result[0]['content']);
$this->assertEquals('array_value', $result[0]['path']);
$this->assertEquals(['type'], $result[0]['missing_key']);
}
/**
* Test path building with deeper nesting
*/
public function testDeepNestingPathBuilding()
{
$array = [
'level1' => [
'level2' => [
'level3' => [
'items' => [
'target_item' => [
'name' => 'deep_test',
// missing 'required_field'
]
]
]
]
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'deep_test', 'required_field');
$this->assertCount(1, $result);
$this->assertEquals('level1:level2:level3:items:target_item', $result[0]['path']);
}
/**
* Test with custom path separator
*/
public function testCustomPathSeparator()
{
$array = [
'level1' => [
'level2' => [
'item' => [
'name' => 'test',
// missing 'type' key
]
]
]
];
$result = ArrayHandler::findArraysMissingKey($array, 'test', 'type', null, '/');
$this->assertCount(1, $result);
$this->assertEquals('level1/level2/item', $result[0]['path']);
}
/**
* Test default path separator behavior
*/
public function testDefaultPathSeparator()
{
$array = [
'parent' => [
'child' => [
'name' => 'test',
// missing 'value' key
]
]
];
// Using default separator (should be ':')
$result = ArrayHandler::findArraysMissingKey($array, 'test', 'value');
$this->assertCount(1, $result);
$this->assertEquals('parent:child', $result[0]['path']);
}
/**
* Test different path separators don't affect search logic
*/
public function testPathSeparatorDoesNotAffectSearchLogic()
{
$array = [
'section' => [
'data' => [
'id' => 123,
'name' => 'item'
// missing 'status'
]
]
];
// Test with different separators - results should be identical except for path
$result1 = ArrayHandler::findArraysMissingKey($array, 123, 'status', 'id', ':');
$result2 = ArrayHandler::findArraysMissingKey($array, 123, 'status', 'id', '.');
$result3 = ArrayHandler::findArraysMissingKey($array, 123, 'status', 'id', '/');
$this->assertCount(1, $result1);
$this->assertCount(1, $result2);
$this->assertCount(1, $result3);
// Content and missing_key should be the same
$this->assertEquals($result1[0]['content'], $result2[0]['content']);
$this->assertEquals($result1[0]['content'], $result3[0]['content']);
$this->assertEquals($result1[0]['missing_key'], $result2[0]['missing_key']);
$this->assertEquals($result1[0]['missing_key'], $result3[0]['missing_key']);
// Paths should be different based on separator
$this->assertEquals('section:data', $result1[0]['path']);
$this->assertEquals('section.data', $result2[0]['path']);
$this->assertEquals('section/data', $result3[0]['path']);
}
/**
* test type checking
*/
public function testStrictTypeChecking()
{
$array = [
'item1' => [
'id' => '123', // string
'name' => 'test'
// missing 'status'
],
'item2' => [
'id' => 123, // integer
'name' => 'test2',
'status' => 'active'
]
];
// Search for integer 123 - should only match item2
$result = ArrayHandler::findArraysMissingKey($array, 123, 'status', 'id');
$this->assertEmpty($result); // item2 has the status key
// Search for string '123' - should only match item1
$result = ArrayHandler::findArraysMissingKey($array, '123', 'status', 'id');
$this->assertCount(1, $result);
$this->assertEquals('item1', $result[0]['path']);
}
}
// __END__

View File

@@ -0,0 +1,333 @@
<?php
// This code was created by Claude Sonnet 4
// modification for value checks with assertEqualsCanonicalizing
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Combined\ArrayHandler;
class CoreLibsCombinedArrayHandlerKsortArrayTest extends TestCase
{
/**
* Test basic ascending sort (default behavior)
*/
public function testKsortArrayBasicAscending(): void
{
$input = [
'zebra' => 'value1',
'apple' => 'value2',
'banana' => 'value3',
'cherry' => 'value4'
];
$expected = [
'apple' => 'value2',
'banana' => 'value3',
'cherry' => 'value4',
'zebra' => 'value1'
];
$result = ArrayHandler::ksortArray($input);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test descending sort with reverse=true
*/
public function testKsortArrayDescending(): void
{
$input = [
'zebra' => 'value1',
'apple' => 'value2',
'banana' => 'value3',
'cherry' => 'value4'
];
$expected = [
'zebra' => 'value1',
'cherry' => 'value4',
'banana' => 'value3',
'apple' => 'value2'
];
$result = ArrayHandler::ksortArray($input, false, true);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test case-insensitive ascending sort
*/
public function testKsortArrayCaseInsensitiveAscending(): void
{
$input = [
'Zebra' => 'value1',
'apple' => 'value2',
'Banana' => 'value3',
'cherry' => 'value4'
];
$expected = [
'apple' => 'value2',
'Banana' => 'value3',
'cherry' => 'value4',
'Zebra' => 'value1'
];
$result = ArrayHandler::ksortArray($input, true);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test case-insensitive descending sort
*/
public function testKsortArrayCaseInsensitiveDescending(): void
{
$input = [
'Zebra' => 'value1',
'apple' => 'value2',
'Banana' => 'value3',
'cherry' => 'value4'
];
$expected = [
'Zebra' => 'value1',
'cherry' => 'value4',
'Banana' => 'value3',
'apple' => 'value2'
];
$result = ArrayHandler::ksortArray($input, true, true);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test with mixed case keys to verify case sensitivity behavior
*/
public function testKsortArrayCaseSensitivityComparison(): void
{
$input = [
'B' => 'value1',
'a' => 'value2',
'C' => 'value3',
'b' => 'value4'
];
// Case-sensitive sort (uppercase comes before lowercase in ASCII)
$expectedCaseSensitive = [
'B' => 'value1',
'C' => 'value3',
'a' => 'value2',
'b' => 'value4'
];
// Case-insensitive sort
$expectedCaseInsensitive = [
'a' => 'value2',
'B' => 'value1',
'b' => 'value4',
'C' => 'value3'
];
$resultCaseSensitive = ArrayHandler::ksortArray($input, false);
$resultCaseInsensitive = ArrayHandler::ksortArray($input, true);
$this->assertEquals($expectedCaseSensitive, $resultCaseSensitive);
$this->assertEquals($expectedCaseInsensitive, $resultCaseInsensitive);
}
/**
* Test with numeric string keys
*/
public function testKsortArrayNumericStringKeys(): void
{
$input = [
'10' => 'value1',
'2' => 'value2',
'1' => 'value3',
'20' => 'value4'
];
// String comparison, not numeric
$expected = [
'1' => 'value3',
'10' => 'value1',
'2' => 'value2',
'20' => 'value4'
];
$result = ArrayHandler::ksortArray($input);
$this->assertEquals($expected, $result);
}
/**
* Test with special characters in keys
*/
public function testKsortArraySpecialCharacters(): void
{
$input = [
'key_with_underscore' => 'value1',
'key-with-dash' => 'value2',
'key.with.dot' => 'value3',
'key with space' => 'value4',
'keyWithCamelCase' => 'value5'
];
$result = ArrayHandler::ksortArray($input);
// Verify it doesn't throw an error and maintains all keys
$this->assertCount(5, $result);
$this->assertArrayHasKey('key_with_underscore', $result);
$this->assertArrayHasKey('key-with-dash', $result);
$this->assertArrayHasKey('key.with.dot', $result);
$this->assertArrayHasKey('key with space', $result);
$this->assertArrayHasKey('keyWithCamelCase', $result);
}
/**
* Test with empty array
*/
public function testKsortArrayEmpty(): void
{
$input = [];
$result = ArrayHandler::ksortArray($input);
$this->assertEquals([], $result);
$this->assertIsArray($result);
}
/**
* Test with single element array
*/
public function testKsortArraySingleElement(): void
{
$input = ['onlykey' => 'onlyvalue'];
$result = ArrayHandler::ksortArray($input);
$this->assertEquals($input, $result);
}
/**
* Test that original array is not modified (function returns new array)
*/
public function testKsortArrayDoesNotModifyOriginal(): void
{
$original = [
'zebra' => 'value1',
'apple' => 'value2',
'banana' => 'value3'
];
$originalCopy = $original; // Keep a copy for comparison
$result = ArrayHandler::ksortArray($original);
// Original array should remain unchanged
$this->assertEquals($originalCopy, $original);
$this->assertNotEquals(array_keys($original), array_keys($result));
}
/**
* Test with complex mixed data types as values
*/
public function testKsortArrayMixedValueTypes(): void
{
$input = [
'string_key' => 'string_value',
'array_key' => ['nested', 'array'],
'int_key' => 42,
'bool_key' => true,
'null_key' => null
];
$result = ArrayHandler::ksortArray($input);
// Check that all keys are preserved and sorted
$expectedKeys = ['array_key', 'bool_key', 'int_key', 'null_key', 'string_key'];
$this->assertEquals($expectedKeys, array_keys($result));
// Check that values are preserved correctly
$this->assertEquals('string_value', $result['string_key']);
$this->assertEquals(['nested', 'array'], $result['array_key']);
$this->assertEquals(42, $result['int_key']);
$this->assertTrue($result['bool_key']);
$this->assertNull($result['null_key']);
}
/**
* Test all parameter combinations
*/
public function testKsortArrayAllParameterCombinations(): void
{
$input = [
'Delta' => 'value1',
'alpha' => 'value2',
'Charlie' => 'value3',
'bravo' => 'value4'
];
// Test all 4 combinations
$result1 = ArrayHandler::ksortArray($input, false, false); // default
$result2 = ArrayHandler::ksortArray($input, false, true); // reverse only
$result3 = ArrayHandler::ksortArray($input, true, false); // lowercase only
$result4 = ArrayHandler::ksortArray($input, true, true); // both
// Each should produce different ordering
$this->assertNotEquals(array_keys($result1), array_keys($result2));
$this->assertNotEquals(array_keys($result1), array_keys($result3));
$this->assertNotEquals(array_keys($result1), array_keys($result4));
$this->assertNotEquals(array_keys($result2), array_keys($result3));
$this->assertNotEquals(array_keys($result2), array_keys($result4));
$this->assertNotEquals(array_keys($result3), array_keys($result4));
// But all should have same keys and values, just different order
$this->assertEqualsCanonicalizing(array_values($input), array_values($result1));
$this->assertEqualsCanonicalizing(array_values($input), array_values($result2));
$this->assertEqualsCanonicalizing(array_values($input), array_values($result3));
$this->assertEqualsCanonicalizing(array_values($input), array_values($result4));
}
/**
* Data provider for comprehensive testing
*/
public function sortingParametersProvider(): array
{
return [
'default' => [false, false],
'reverse' => [false, true],
'lowercase' => [true, false],
'lowercase_reverse' => [true, true],
];
}
/**
* Test that function works with all parameter combinations using data provider
*
* @dataProvider sortingParametersProvider
*/
public function testKsortArrayWithDataProvider(bool $lowerCase, bool $reverse): void
{
$input = [
'Zebra' => 'animal1',
'apple' => 'fruit1',
'Banana' => 'fruit2',
'cat' => 'animal2'
];
$result = ArrayHandler::ksortArray($input, $lowerCase, $reverse);
// Basic assertions that apply to all combinations
$this->assertIsArray($result);
$this->assertCount(4, $result);
$this->assertArrayHasKey('Zebra', $result);
$this->assertArrayHasKey('apple', $result);
$this->assertArrayHasKey('Banana', $result);
$this->assertArrayHasKey('cat', $result);
}
}
// __END__

View File

@@ -0,0 +1,383 @@
<?php
// created by Claude Sonnet 4
// testRecursiveSearchWithFlatResult had wrong retunr count
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Combined\ArrayHandler;
class CoreLibsCombinedArrayHandlerSelectArrayFromOptionTest extends TestCase
{
private array $testData;
private array $nestedTestData;
protected function setUp(): void
{
$this->testData = [
'item1' => [
'name' => 'John',
'age' => 25,
'status' => 'active',
'score' => 85.5
],
'item2' => [
'name' => 'jane',
'age' => 30,
'status' => 'inactive',
'score' => 92.0
],
'item3' => [
'name' => 'Bob',
'age' => 25,
'status' => 'active',
'score' => 78.3
],
'item4' => [
'name' => 'Alice',
'age' => 35,
'status' => 'pending',
'score' => 88.7
]
];
$this->nestedTestData = [
'level1_a' => [
'name' => 'Level1A',
'type' => 'parent',
'children' => [
'child1' => [
'name' => 'Child1',
'type' => 'child',
'active' => true
],
'child2' => [
'name' => 'Child2',
'type' => 'child',
'active' => false
]
]
],
'level1_b' => [
'name' => 'Level1B',
'type' => 'parent',
'children' => [
'child3' => [
'name' => 'Child3',
'type' => 'child',
'active' => true,
'nested' => [
'deep1' => [
'name' => 'Deep1',
'type' => 'deep',
'active' => true
]
]
]
]
],
'item5' => [
'name' => 'Direct',
'type' => 'child',
'active' => false
]
];
}
public function testEmptyArrayReturnsEmpty(): void
{
$result = ArrayHandler::selectArrayFromOption([], 'name', 'John');
$this->assertEmpty($result);
}
public function testBasicStringSearch(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'name', 'John');
$this->assertCount(1, $result);
$this->assertArrayHasKey('item1', $result);
$this->assertEquals('John', $result['item1']['name']);
}
public function testBasicIntegerSearch(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'age', 25);
$this->assertCount(2, $result);
$this->assertArrayHasKey('item1', $result);
$this->assertArrayHasKey('item3', $result);
}
public function testBasicFloatSearch(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'score', 85.5);
$this->assertCount(1, $result);
$this->assertArrayHasKey('item1', $result);
$this->assertEquals(85.5, $result['item1']['score']);
}
public function testBasicBooleanSearch(): void
{
$data = [
'item1' => ['enabled' => true, 'name' => 'Test1'],
'item2' => ['enabled' => false, 'name' => 'Test2'],
'item3' => ['enabled' => true, 'name' => 'Test3']
];
$result = ArrayHandler::selectArrayFromOption($data, 'enabled', true);
$this->assertCount(2, $result);
$this->assertArrayHasKey('item1', $result);
$this->assertArrayHasKey('item3', $result);
}
public function testStrictComparison(): void
{
$data = [
'item1' => ['value' => '25', 'name' => 'String25'],
'item2' => ['value' => 25, 'name' => 'Int25'],
'item3' => ['value' => 25.0, 'name' => 'Float25']
];
// Non-strict should match all
$nonStrictResult = ArrayHandler::selectArrayFromOption($data, 'value', 25, false);
$this->assertCount(3, $nonStrictResult);
// Strict should only match exact type
$strictResult = ArrayHandler::selectArrayFromOption($data, 'value', 25, true);
$this->assertCount(1, $strictResult);
$this->assertArrayHasKey('item2', $strictResult);
}
public function testCaseInsensitiveSearch(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'name', 'JANE', false, true);
$this->assertCount(1, $result);
$this->assertArrayHasKey('item2', $result);
$this->assertEquals('jane', $result['item2']['name']);
}
public function testCaseSensitiveSearch(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'name', 'JANE', false, false);
$this->assertEmpty($result);
}
public function testRecursiveSearchWithFlatResult(): void
{
$result = ArrayHandler::selectArrayFromOption(
$this->nestedTestData,
'type',
'child',
false,
false,
true,
true,
':*'
);
$this->assertCount(4, $result);
$this->assertArrayHasKey('level1_a:*children:*child1', $result);
$this->assertArrayHasKey('level1_a:*children:*child2', $result);
$this->assertArrayHasKey('level1_b:*children:*child3', $result);
$this->assertArrayHasKey('item5', $result);
}
public function testRecursiveSearchWithNestedResult(): void
{
$result = ArrayHandler::selectArrayFromOption(
$this->nestedTestData,
'type',
'child',
false,
false,
true,
false
);
$this->assertCount(3, $result);
$this->assertArrayHasKey('level1_a', $result);
$this->assertArrayHasKey('level1_b', $result);
$this->assertArrayHasKey('item5', $result);
// Check nested structure is preserved
$this->assertArrayHasKey('children', $result['level1_a']);
$this->assertArrayHasKey('child1', $result['level1_a']['children']);
$this->assertArrayHasKey('child2', $result['level1_a']['children']);
}
public function testRecursiveSearchDeepNesting(): void
{
$result = ArrayHandler::selectArrayFromOption(
$this->nestedTestData,
'type',
'deep',
false,
false,
true,
true,
':*'
);
$this->assertCount(1, $result);
$this->assertArrayHasKey('level1_b:*children:*child3:*nested:*deep1', $result);
$this->assertEquals('Deep1', $result['level1_b:*children:*child3:*nested:*deep1']['name']);
}
public function testCustomFlatSeparator(): void
{
$result = ArrayHandler::selectArrayFromOption(
$this->nestedTestData,
'type',
'child',
false,
false,
true,
true,
'|'
);
$this->assertArrayHasKey('level1_a|children|child1', $result);
$this->assertArrayHasKey('level1_a|children|child2', $result);
$this->assertArrayHasKey('level1_b|children|child3', $result);
}
public function testNonRecursiveSearch(): void
{
$result = ArrayHandler::selectArrayFromOption(
$this->nestedTestData,
'type',
'child',
false,
false,
false
);
// Should only find direct matches, not nested ones
$this->assertCount(1, $result);
$this->assertArrayHasKey('item5', $result);
}
public function testNoMatchesFound(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'name', 'NonExistent');
$this->assertEmpty($result);
}
public function testMissingLookupKey(): void
{
$result = ArrayHandler::selectArrayFromOption($this->testData, 'nonexistent_key', 'value');
$this->assertEmpty($result);
}
public function testCombinedStrictAndCaseInsensitive(): void
{
$data = [
'item1' => ['name' => 'Test', 'id' => '123'],
'item2' => ['name' => 'test', 'id' => 123],
'item3' => ['name' => 'TEST', 'id' => '123']
];
// Case insensitive but strict type matching
$result = ArrayHandler::selectArrayFromOption($data, 'id', '123', true, true);
$this->assertCount(2, $result);
$this->assertArrayHasKey('item1', $result);
$this->assertArrayHasKey('item3', $result);
}
public function testBooleanWithCaseInsensitive(): void
{
$data = [
'item1' => ['active' => true, 'name' => 'Test1'],
'item2' => ['active' => false, 'name' => 'Test2']
];
// Case insensitive flag should not affect boolean comparison
$result = ArrayHandler::selectArrayFromOption($data, 'active', true, false, true);
$this->assertCount(1, $result);
$this->assertArrayHasKey('item1', $result);
}
public function testArrayWithNumericKeys(): void
{
$data = [
0 => ['name' => 'First', 'type' => 'test'],
1 => ['name' => 'Second', 'type' => 'test'],
2 => ['name' => 'Third', 'type' => 'other']
];
$result = ArrayHandler::selectArrayFromOption($data, 'type', 'test');
$this->assertCount(2, $result);
$this->assertArrayHasKey(0, $result);
$this->assertArrayHasKey(1, $result);
}
public function testRecursiveWithMixedKeyTypes(): void
{
$data = [
'string_key' => [
'name' => 'Parent',
'type' => 'parent',
0 => [
'name' => 'Child0',
'type' => 'child'
],
'child_key' => [
'name' => 'ChildKey',
'type' => 'child'
]
]
];
$result = ArrayHandler::selectArrayFromOption($data, 'type', 'child', false, false, true, true, ':*');
$this->assertCount(2, $result);
$this->assertArrayHasKey('string_key:*0', $result);
$this->assertArrayHasKey('string_key:*child_key', $result);
}
public function testAllParametersCombined(): void
{
$data = [
'parent1' => [
'name' => 'Parent1',
'status' => 'ACTIVE',
'children' => [
'child1' => [
'name' => 'Child1',
'status' => 'active'
]
]
]
];
$result = ArrayHandler::selectArrayFromOption(
$data,
'status',
'active',
false, // not strict
true, // case insensitive
true, // recursive
true, // flat result
'|' // custom separator
);
$this->assertCount(2, $result);
$this->assertArrayHasKey('parent1', $result);
$this->assertArrayHasKey('parent1|children|child1', $result);
}
}
// __END__

View File

@@ -0,0 +1,328 @@
<?php
// This code was created by Claude Sonnet 4
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Combined\ArrayHandler;
class CoreLibsCombinedArrayHandlerSortArrayTest extends TestCase
{
/**
* Test basic ascending sort without maintaining keys
*/
public function testBasicAscendingSort()
{
$input = [3, 1, 4, 1, 5, 9];
$expected = [1, 1, 3, 4, 5, 9];
$result = ArrayHandler::sortArray($input);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test basic descending sort without maintaining keys
*/
public function testBasicDescendingSort()
{
$input = [3, 1, 4, 1, 5, 9];
$expected = [9, 5, 4, 3, 1, 1];
$result = ArrayHandler::sortArray($input, false, true);
$this->assertEquals($expected, $result);
$this->assertEquals(array_keys($expected), array_keys($result));
}
/**
* Test ascending sort with key maintenance
*/
public function testAscendingSortWithKeyMaintenance()
{
$input = ['c' => 3, 'a' => 1, 'd' => 4, 'b' => 1, 'e' => 5];
$expected = ['a' => 1, 'b' => 1, 'c' => 3, 'd' => 4, 'e' => 5];
$result = ArrayHandler::sortArray($input, false, false, true);
$this->assertEquals($expected, $result);
}
/**
* Test descending sort with key maintenance
*/
public function testDescendingSortWithKeyMaintenance()
{
$input = ['c' => 3, 'a' => 1, 'd' => 4, 'b' => 1, 'e' => 5];
$expected = ['e' => 5, 'd' => 4, 'c' => 3, 'a' => 1, 'b' => 1];
$result = ArrayHandler::sortArray($input, false, true, true);
$this->assertEquals($expected, $result);
}
/**
* Test string sorting with lowercase conversion
*/
public function testStringLowerCaseSort()
{
$input = ['Banana', 'apple', 'Cherry', 'date'];
$expected = ['apple', 'Banana', 'Cherry', 'date'];
$result = ArrayHandler::sortArray($input, true);
$this->assertEquals($expected, $result);
}
/**
* Test string sorting with lowercase conversion in reverse
*/
public function testStringLowerCaseSortReverse()
{
$input = ['Banana', 'apple', 'Cherry', 'date'];
$expected = ['date', 'Cherry', 'Banana', 'apple'];
$result = ArrayHandler::sortArray($input, true, true);
$this->assertEquals($expected, $result);
}
/**
* Test string sorting with lowercase conversion and key maintenance
*/
public function testStringLowerCaseSortWithKeys()
{
$input = ['b' => 'Banana', 'a' => 'apple', 'c' => 'Cherry', 'd' => 'date'];
$expected = ['a' => 'apple', 'b' => 'Banana', 'c' => 'Cherry', 'd' => 'date'];
$result = ArrayHandler::sortArray($input, true, false, true);
$this->assertEquals($expected, $result);
}
/**
* Test string sorting with lowercase conversion, reverse, and key maintenance
*/
public function testStringLowerCaseSortReverseWithKeys()
{
$input = ['b' => 'Banana', 'a' => 'apple', 'c' => 'Cherry', 'd' => 'date'];
$expected = ['d' => 'date', 'c' => 'Cherry', 'b' => 'Banana', 'a' => 'apple'];
$result = ArrayHandler::sortArray($input, true, true, true);
$this->assertEquals($expected, $result);
}
/**
* Test numeric string sorting with SORT_NUMERIC flag
*/
public function testNumericStringSorting()
{
$input = ['10', '2', '1', '20'];
$expected = ['1', '2', '10', '20'];
$result = ArrayHandler::sortArray($input, false, false, false, SORT_NUMERIC);
$this->assertEquals($expected, $result);
}
/**
* Test natural string sorting with SORT_NATURAL flag
*/
public function testNaturalStringSorting()
{
$input = ['img1.png', 'img10.png', 'img2.png', 'img20.png'];
$expected = ['img1.png', 'img2.png', 'img10.png', 'img20.png'];
$result = ArrayHandler::sortArray($input, false, false, false, SORT_NATURAL);
$this->assertEquals($expected, $result);
}
/**
* Test with empty array
*/
public function testEmptyArray()
{
$input = [];
$expected = [];
$result = ArrayHandler::sortArray($input);
$this->assertEquals($expected, $result);
}
/**
* Test with single element array
*/
public function testSingleElementArray()
{
$input = [42];
$expected = [42];
$result = ArrayHandler::sortArray($input);
$this->assertEquals($expected, $result);
}
/**
* Test with array containing null values
*/
public function testArrayWithNullValues()
{
$input = [3, null, 1, null, 2];
$expected = [null, null, 1, 2, 3];
$result = ArrayHandler::sortArray($input);
$this->assertEquals($expected, $result);
}
/**
* Test with mixed data types
*/
public function testMixedDataTypes()
{
$input = [3, '1', 4.5, '2', 1];
$result = ArrayHandler::sortArray($input);
// Should sort according to PHP's natural comparison rules
$this->assertIsArray($result);
$this->assertCount(5, $result);
}
/**
* Test that original array is not modified (immutability)
*/
public function testOriginalArrayNotModified()
{
$original = [3, 1, 4, 1, 5, 9];
$input = $original;
$result = ArrayHandler::sortArray($input);
$this->assertEquals($original, $input);
$this->assertNotEquals($input, $result);
}
/**
* Test case sensitivity without lowercase flag
*/
public function testCaseSensitivityWithoutLowercase()
{
$input = ['Banana', 'apple', 'Cherry'];
$result = ArrayHandler::sortArray($input);
// Capital letters should come before lowercase in ASCII sort
$this->assertEquals('Banana', $result[0]);
$this->assertEquals('Cherry', $result[1]);
$this->assertEquals('apple', $result[2]);
}
/**
* Test all parameters combination
*/
public function testAllParametersCombination()
{
$input = ['z' => 'Zebra', 'a' => 'apple', 'b' => 'Banana'];
$result = ArrayHandler::sortArray($input, true, true, true, SORT_REGULAR);
// Should be sorted by lowercase, reversed, with keys maintained
$keys = array_keys($result);
$values = array_values($result);
$this->assertEquals(['z', 'b', 'a'], $keys);
$this->assertEquals(['Zebra', 'Banana', 'apple'], $values);
}
/**
* Test floating point numbers
*/
public function testFloatingPointNumbers()
{
$input = [3.14, 2.71, 1.41, 1.73];
$expected = [1.41, 1.73, 2.71, 3.14];
$result = ArrayHandler::sortArray($input);
$this->assertEquals($expected, $result);
}
/**
* Test with duplicate values and key maintenance
*/
public function testDuplicateValuesWithKeyMaintenance()
{
$input = ['first' => 1, 'second' => 2, 'third' => 1, 'fourth' => 2];
$result = ArrayHandler::sortArray($input, false, false, true);
$this->assertCount(4, $result);
$this->assertEquals([1, 1, 2, 2], array_values($result));
// Keys should be preserved
$this->assertArrayHasKey('first', $result);
$this->assertArrayHasKey('second', $result);
$this->assertArrayHasKey('third', $result);
$this->assertArrayHasKey('fourth', $result);
}
/**
* Data provider for comprehensive parameter testing
*/
public function sortParameterProvider(): array
{
return [
'basic_ascending' => [
[3, 1, 4, 2],
false, false, false, SORT_REGULAR,
[1, 2, 3, 4]
],
'basic_descending' => [
[3, 1, 4, 2],
false, true, false, SORT_REGULAR,
[4, 3, 2, 1]
],
'lowercase_ascending' => [
['Banana', 'apple', 'Cherry'],
true, false, false, SORT_REGULAR,
['apple', 'Banana', 'Cherry']
],
'lowercase_descending' => [
['Banana', 'apple', 'Cherry'],
true, true, false, SORT_REGULAR,
['Cherry', 'Banana', 'apple']
]
];
}
/**
* Test various parameter combinations using data provider
*
* @dataProvider sortParameterProvider
*/
public function testSortParameterCombinations(
array $input,
bool $lowercase,
bool $reverse,
bool $maintainKeys,
int $params,
array $expected
) {
$result = ArrayHandler::sortArray($input, $lowercase, $reverse, $maintainKeys, $params);
if (!$maintainKeys) {
$this->assertEquals($expected, $result);
} else {
$this->assertEquals($expected, array_values($result));
}
}
}
// __END__

File diff suppressed because it is too large Load Diff

View File

@@ -490,11 +490,11 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
], ],
'micro interval with microtime' => [ 'micro interval with microtime' => [
'18999d 0h 38m 10s 1235ms', '18999d 0h 38m 10s 1235ms',
1641515890.1235, 1641515891.235,
], ],
'micro interval with microtime' => [ 'micro interval with microtime' => [
'18999d 0h 38m 10s 1234567890ms', '18999d 0h 38m 10s 1234567890ms',
1641515890.1234567, 1642750457.89,
], ],
'negative interval no microtime' => [ 'negative interval no microtime' => [
'-18999d 0h 38m 10s', '-18999d 0h 38m 10s',
@@ -503,23 +503,246 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
// short for mini tests // short for mini tests
'microtime only' => [ 'microtime only' => [
'0s 1235ms', '0s 1235ms',
0.1235, 1.235,
], ],
'seconds only' => [ 'seconds only' => [
'30s 1235ms', '30s 1235ms',
30.1235, 31.235,
], ],
'minutes only' => [ 'minutes only' => [
'1m 30s 1235ms', '1m 30s 1235ms',
90.1235, 91.235,
], ],
'hours only' => [ 'hours only' => [
'1h 1m 30s 1235ms', '1h 1m 30s 1235ms',
3690.1235, 3691.235,
], ],
'days only' => [ 'days only' => [
'1d 1h 1m 30s 1235ms', '1d 1h 1m 30s 1235ms',
90090.1235, 90091.235,
],
'days only with long name' => [
'1day 1hour 1min 30second 1235millisecond',
90091.235,
],
// Test day variations
'day singular' => [
'5day',
432000,
],
'days plural' => [
'3days',
259200,
],
'days with space' => [
'2days 5h',
190800,
],
'day without space' => [
'1day1h',
90000,
],
// Test hour variations
'hour singular' => [
'2hour',
7200,
],
'hours plural' => [
'4hours',
14400,
],
'hours with space' => [
'3hours 30m',
12600,
],
'hour without space' => [
'1hour30m',
5400,
],
// Test minute variations
'min short' => [
'45min',
2700,
],
'minute singular' => [
'1minute',
60,
],
'minutes plural' => [
'10minutes',
600,
],
'minutes with space' => [
'5minutes 20s',
320,
],
'min without space' => [
'2min30s',
150,
],
// Test second variations
'sec short' => [
'30sec',
30,
],
'second singular' => [
'1second',
1,
],
'seconds plural' => [
'45seconds',
45,
],
'seconds with space' => [
'15seconds 500ms',
15.5,
],
'sec without space' => [
'10sec250ms',
10.25,
],
// Test millisecond variations
'ms short' => [
'500ms',
0.5,
],
'millis short' => [
'250millis',
0.25,
],
'millisec medium singular' => [
'250millisec',
0.25,
],
'millisecs medium plural' => [
'250millisecs',
0.25,
],
'misec medium singular' => [
'250millisec',
0.25,
],
'msecs medium plural' => [
'250millisecs',
0.25,
],
'millisecond long singular' => [
'1millisecond',
0.001,
],
'milliseconds long plural' => [
'999milliseconds',
0.999,
],
// Test negative values
'negative days' => [
'-5d',
-432000,
],
'negative hours' => [
'-3h',
-10800,
],
'negative minutes' => [
'-45m',
-2700,
],
'negative seconds' => [
'-30s',
-30,
],
'negative milliseconds' => [
'-500ms',
-0.5,
],
'negative complex' => [
'-2days 3hours 15minutes 30seconds 250milliseconds',
-184530.25,
],
// Test combined formats
'all components short' => [
'1d 2h 3m 4s 5ms',
93784.005,
],
'all components long' => [
'2days 3hours 4minutes 5seconds 678milliseconds',
183845.678,
],
'mixed short and long' => [
'1day 2h 3minutes 4sec 100ms',
93784.1,
],
'no spaces between components' => [
'1d2h3m4s5ms',
93784.005,
],
'only days and milliseconds' => [
'5d 123ms',
432000.123,
],
'only hours and seconds' => [
'2h 45s',
7245,
],
'only minutes and milliseconds' => [
'30m 500ms',
1800.5,
],
// Test zero values
'zero seconds' => [
'0s',
0,
],
'zero with milliseconds' => [
'0s 123ms',
0.123,
],
// Test large values
'large days' => [
'365days',
31536000,
],
'large hours' => [
'48hours',
172800,
],
'large minutes' => [
'1440minutes',
86400,
],
'large seconds' => [
'86400seconds',
86400,
],
// Test edge cases with spaces
'extra spaces' => [
'1d 2h 3m 4s 5ms',
93784.005,
],
'mixed spaces and no spaces' => [
'1d 2h3m 4s5ms',
93784.005,
],
// Test single component each
'only days short' => [
'7d',
604800,
],
'only hours short' => [
'12h',
43200,
],
'only minutes short' => [
'90m',
5400,
],
'only seconds short' => [
'120s',
120,
],
'only milliseconds short' => [
'1500ms',
1.5,
], ],
'already set' => [ 'already set' => [
1641515890, 1641515890,
@@ -529,10 +752,18 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
'xyz', 'xyz',
'xyz', 'xyz',
], ],
'empty data' => [
' ',
' ',
],
'out of bound data' => [ 'out of bound data' => [
'99999999999999999999d', '99999999999999999999d',
8.64E+24 8.64E+24
], ],
'spaces inbetween' => [
' - 9 d 2h 58minutes 35 seconds 123 ms ',
-788315.123,
]
]; ];
} }
@@ -555,6 +786,36 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
); );
} }
/**
* Undocumented function
*
* @covers ::stringToTime
* @testdox stringToTime invalid input will throw exception if requested
*
* @return void
*/
public function testStringToTimeException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageMatches("/^Invalid time string format, cannot parse: /");
\CoreLibs\Combined\DateTime::stringToTime('1x 2y 3z', true);
}
/**
* Undocumented function
*
* @covers ::stringToTime
* @testdox stringToTime empty input will throw exception if requested
*
* @return void
*/
public function testStringToTimeExceptionEmpty(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageMatches("/^Invalid time string format, no interval value found: /");
\CoreLibs\Combined\DateTime::stringToTime(' ', true);
}
/** /**
* Undocumented function * Undocumented function
* *
@@ -926,48 +1187,114 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
public function daysIntervalProvider(): array public function daysIntervalProvider(): array
{ {
return [ return [
'valid interval /, not named array' => [ // normal and format tests
'2020/1/1', 'valid interval / not named array' => [
'2020/1/30', 'input_a' => '2020/1/1',
false, 'input_b' => '2020/1/30',
[29, 22, 8], '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' => [ 'valid interval / named array' => [
'2020/1/1', 'input_a' => '2020/1/1',
'2020/1/30', 'input_b' => '2020/1/30',
true, 'return_named' => true,
['overall' => 29, 'weekday' => 22, 'weekend' => 8], 'include_end_date' => true,
'exclude_start_date' => false,
'expected' => ['overall' => 30, 'weekday' => 22, 'weekend' => 8, 'reverse' => false],
], ],
'valid interval -' => [ 'valid interval with "-"' => [
'2020-1-1', 'input_a' => '2020-1-1',
'2020-1-30', 'input_b' => '2020-1-30',
false, 'return_named' => false,
[29, 22, 8], 'include_end_date' => true,
], 'exclude_start_date' => false,
'valid interval switched' => [ 'expected' => [30, 22, 8, false],
'2020/1/30',
'2020/1/1',
false,
[28, 0, 0],
], ],
'valid interval with time' => [ 'valid interval with time' => [
'2020/1/1 12:12:12', 'input_a' => '2020/1/1 12:12:12',
'2020/1/30 13:13:13', 'input_b' => '2020/1/30 13:13:13',
false, 'return_named' => false,
[28, 21, 8], 'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [30, 22, 8, false],
], ],
// invalid
'invalid dates' => [ 'invalid dates' => [
'abc', 'input_a' => 'abc',
'xyz', 'input_b' => 'xyz',
false, 'return_named' => false,
[0, 0, 0] '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' => [ 'out of bound dates' => [
'1900-1-1', 'input_a' => '1900-1-1',
'9999-12-31', 'input_b' => '9999-12-31',
false, 'return_named' => false,
[2958463,2113189,845274], '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 +1309,52 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
* *
* @param string $input_a * @param string $input_a
* @param string $input_b * @param string $input_b
* @param bool $flag * @param bool $return_named
* @param array $expected * @param array $expected
* @return void * @return void
*/ */
public function testCalcDaysInterval( public function testCalcDaysInterval(
string $input_a, string $input_a,
string $input_b, string $input_b,
bool $flag, bool $return_named,
bool $include_end_date,
bool $exclude_start_date,
$expected $expected
): void { ): void {
$this->assertEquals( $this->assertEquals(
$expected, $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 +1546,38 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
'2023-07-03', '2023-07-03',
'2023-07-27', '2023-07-27',
true 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
],
]; ];
} }

View File

@@ -40,7 +40,7 @@ final class CoreLibsConvertByteTest extends TestCase
4 => '1.00 KB', 4 => '1.00 KB',
5 => '1.02KiB', 5 => '1.02KiB',
], ],
'invalud string number' => [ 'invalid string number' => [
0 => '1024 MB', 0 => '1024 MB',
1 => '1024 MB', 1 => '1024 MB',
2 => '1024 MB', 2 => '1024 MB',
@@ -123,47 +123,6 @@ final class CoreLibsConvertByteTest extends TestCase
]; ];
} }
/**
* Undocumented function
*
* @return array
*/
public function byteStringProvider(): array
{
return [
'negative number' => [
0 => '-117.42 MB',
1 => -123123794,
2 => -117420000,
],
'megabyte' => [
0 => '242.98 MB',
1 => 254782996,
2 => 242980000
],
'megabyte si' => [
0 => '254.78 MiB',
1 => 267156193,
2 => 254780000
],
'petabyte' => [
0 => '1 EiB',
1 => 1152921504606846976,
2 => 1000000000000000000,
],
'max int' => [
0 => '8 EB',
1 => -9223372036854775807 - 1,
2 => 8000000000000000000,
],
'exabyte, overflow' => [
0 => '867.36EB',
1 => 3873816255479021568,
2 => 363028535651074048,
]
];
}
/** /**
* Undocumented function * Undocumented function
* *
@@ -180,7 +139,7 @@ final class CoreLibsConvertByteTest extends TestCase
* @return void * @return void
*/ */
public function testHumanReadableByteFormat( public function testHumanReadableByteFormat(
$input, string|int|float $input,
string $expected, string $expected,
string $expected_si, string $expected_si,
string $expected_no_space, string $expected_no_space,
@@ -217,6 +176,73 @@ final class CoreLibsConvertByteTest extends TestCase
); );
} }
/**
* Undocumented function
*
* @return array
*/
public function byteStringProvider(): array
{
return [
'negative number' => [
0 => '-117.42 MB',
1 => -123123794,
2 => -117420000,
3 => "-123123793",
4 => "-117420000",
5 => null,
],
'megabyte' => [
0 => '242.98 MB',
1 => 254782996,
2 => 242980000,
3 => "254782996",
4 => "242980000",
5 => null,
],
'megabyte si' => [
0 => '254.78 MiB',
1 => 267156193,
2 => 254780000,
3 => "267156193",
4 => "254780000",
5 => null,
],
'petabyte' => [
0 => '1 EiB',
1 => 1152921504606846976,
2 => 1000000000000000000,
3 => "1152921504606846976",
4 => "1000000000000000000",
5 => null,
],
'max int' => [
0 => '8 EB',
1 => 0,
2 => 0,
3 => "9223372036854775808",
4 => "8000000000000000000",
5 => \LengthException::class,
],
'exabyte, overflow' => [
0 => '867.36EB',
1 => 0,
2 => 0,
3 => "999997996235794808832",
4 => "867360000000000000000",
5 => \LengthException::class,
],
'huge exabyte, overflow' => [
0 => '1000EB',
1 => 0,
2 => 0,
3 => "1152921504606846976000",
4 => "1000000000000000000000",
5 => \LengthException::class,
],
];
}
/** /**
* Undocumented function * Undocumented function
* *
@@ -227,10 +253,22 @@ final class CoreLibsConvertByteTest extends TestCase
* @param string|int|float $input * @param string|int|float $input
* @param string|int|float $expected * @param string|int|float $expected
* @param string|int|float $expected_si * @param string|int|float $expected_si
* @param string|int|float $expected_string
* @param string|int|float $expected_string_si
* @param ?string $exception
* @return void * @return void
*/ */
public function testStringByteFormat($input, $expected, $expected_si): void public function testStringByteFormat(
{ string|int|float $input,
string|int|float $expected,
string|int|float $expected_si,
string|int|float $expected_string,
string|int|float $expected_string_si,
?string $exception
): void {
if ($exception !== null) {
$this->expectException($exception);
}
$this->assertEquals( $this->assertEquals(
$expected, $expected,
\CoreLibs\Convert\Byte::stringByteFormat($input) \CoreLibs\Convert\Byte::stringByteFormat($input)
@@ -239,6 +277,17 @@ final class CoreLibsConvertByteTest extends TestCase
$expected_si, $expected_si,
\CoreLibs\Convert\Byte::stringByteFormat($input, \CoreLibs\Convert\Byte::BYTE_FORMAT_SI) \CoreLibs\Convert\Byte::stringByteFormat($input, \CoreLibs\Convert\Byte::BYTE_FORMAT_SI)
); );
$this->assertEquals(
$expected_string,
\CoreLibs\Convert\Byte::stringByteFormat($input, \CoreLibs\Convert\Byte::RETURN_AS_STRING)
);
$this->assertEquals(
$expected_string_si,
\CoreLibs\Convert\Byte::stringByteFormat(
$input,
\CoreLibs\Convert\Byte::BYTE_FORMAT_SI | \CoreLibs\Convert\Byte::RETURN_AS_STRING
)
);
} }
/** /**

View File

@@ -122,9 +122,9 @@ final class CoreLibsConvertMathTest extends TestCase
public function providerCbrt(): array public function providerCbrt(): array
{ {
return [ return [
'cube root of 2' => [2, 1.25992, 5], 'cube root of 2' => [2, 1.25992, 5, null],
'cube root of 3' => [3, 1.44225, 5], 'cube root of 3' => [3, 1.44225, 5, null],
'cube root of -1' => [-1, 'NAN', 0], 'cube root of -1' => [-1, 'NAN', 0, \InvalidArgumentException::class],
]; ];
} }
@@ -138,10 +138,14 @@ final class CoreLibsConvertMathTest extends TestCase
* @param float|int $number * @param float|int $number
* @param float $expected * @param float $expected
* @param int $round_to * @param int $round_to
* @param ?string $exception
* @return void * @return void
*/ */
public function testCbrt(float|int $number, float|string $expected, int $round_to): void public function testCbrt(float|int $number, float|string $expected, int $round_to, ?string $exception): void
{ {
if ($exception !== null) {
$this->expectException($exception);
}
$this->assertEquals( $this->assertEquals(
$expected, $expected,
round(\CoreLibs\Convert\Math::cbrt($number), $round_to) round(\CoreLibs\Convert\Math::cbrt($number), $round_to)

View File

@@ -0,0 +1,283 @@
<?php
// This code was created by Claude Sonnet 4
// FIX:
// '/test{/', // Unmatched brace -> this is valid
// '/test{1,}/', // Invalid quantifier -> this is valid
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Convert\Strings;
/**
* Test class for CoreLibs\Convert\Strings regex validation methods
*/
class CoreLibsConvertStringsRegexValidateTest extends TestCase
{
/**
* Test isValidRegex with valid regex patterns
*/
public function testIsValidRegexWithValidPatterns(): void
{
$validPatterns = [
'/^[a-zA-Z0-9]+$/',
'/test/',
'/\d+/',
'/^hello.*world$/',
'/[0-9]{3}-[0-9]{3}-[0-9]{4}/',
'#^https?://.*#i',
'~^[a-z]+~',
'|test|',
'/^$/m',
'/\w+/u',
];
foreach ($validPatterns as $pattern) {
$this->assertTrue(
Strings::isValidRegex($pattern),
"Pattern '{$pattern}' should be valid"
);
}
}
/**
* Test isValidRegex with invalid regex patterns
*/
public function testIsValidRegexWithInvalidPatterns(): void
{
$invalidPatterns = [
'/[/', // Unmatched bracket
'/test[/', // Unmatched bracket
'/(?P<name>/', // Unmatched parenthesis
'/(?P<>test)/', // Invalid named group
'/test\\/', // Invalid escape at end
'/(test/', // Unmatched parenthesis
'/test)/', // Unmatched parenthesis
// '/test{/', // Unmatched brace -> this is valid
// '/test{1,}/', // Invalid quantifier -> this is valid
'/[z-a]/', // Invalid character range
'invalid', // No delimiters
'', // Empty string
'/(?P<123>test)/', // Invalid named group name
];
foreach ($invalidPatterns as $pattern) {
$this->assertFalse(
Strings::isValidRegex($pattern),
"Pattern '{$pattern}' should be invalid"
);
}
}
/**
* Test getLastRegexErrorString returns correct error messages
*/
public function testGetLastRegexErrorStringReturnsCorrectMessages(): void
{
// Test with a valid regex first to ensure clean state
Strings::isValidRegex('/valid/');
$this->assertEquals('No error', Strings::getLastRegexErrorString());
// Test with invalid regex to trigger an error
Strings::isValidRegex('/[/');
$errorMessage = Strings::getLastRegexErrorString();
// The error message should be one of the defined messages
$this->assertContains($errorMessage, array_values(Strings::PREG_ERROR_MESSAGES));
$this->assertNotEquals('Unknown error', $errorMessage);
}
/**
* Test getLastRegexErrorString with unknown error
*/
public function testGetLastRegexErrorStringWithUnknownError(): void
{
// This is harder to test directly since we can't easily mock preg_last_error()
// but we can test the fallback behavior by reflection or assume it works
// At minimum, ensure it returns a string
$result = Strings::getLastRegexErrorString();
$this->assertIsString($result);
$this->assertNotEmpty($result);
}
/**
* Test validateRegex with valid patterns
*/
public function testValidateRegexWithValidPatterns(): void
{
$validPatterns = [
'/^test$/',
'/\d+/',
'/[a-z]+/i',
];
foreach ($validPatterns as $pattern) {
$result = Strings::validateRegex($pattern);
$this->assertIsArray($result);
$this->assertArrayHasKey('valid', $result);
$this->assertArrayHasKey('preg_error', $result);
$this->assertArrayHasKey('error', $result);
$this->assertTrue($result['valid'], "Pattern '{$pattern}' should be valid");
$this->assertEquals(PREG_NO_ERROR, $result['preg_error']);
$this->assertNull($result['error']);
}
}
/**
* Test validateRegex with invalid patterns
*/
public function testValidateRegexWithInvalidPatterns(): void
{
$invalidPatterns = [
'/[/', // Unmatched bracket
'/(?P<name>/', // Unmatched parenthesis
'/test\\/', // Invalid escape at end
'/(test/', // Unmatched parenthesis
];
foreach ($invalidPatterns as $pattern) {
$result = Strings::validateRegex($pattern);
$this->assertIsArray($result);
$this->assertArrayHasKey('valid', $result);
$this->assertArrayHasKey('preg_error', $result);
$this->assertArrayHasKey('error', $result);
$this->assertArrayHasKey('pcre_error', $result);
$this->assertFalse($result['valid'], "Pattern '{$pattern}' should be invalid");
$this->assertNotEquals(PREG_NO_ERROR, $result['preg_error']);
$this->assertIsString($result['error']);
$this->assertNotNull($result['error']);
$this->assertNotEmpty($result['error']);
// Verify error message is from our defined messages or 'Unknown error'
$this->assertTrue(
in_array($result['error'], array_values(Strings::PREG_ERROR_MESSAGES)) ||
$result['error'] === 'Unknown error'
);
}
}
/**
* Test validateRegex array structure
*/
public function testValidateRegexArrayStructure(): void
{
$result = Strings::validateRegex('/test/');
// Test array structure for valid regex
$this->assertIsArray($result);
$this->assertCount(4, $result);
$this->assertArrayHasKey('valid', $result);
$this->assertArrayHasKey('preg_error', $result);
$this->assertArrayHasKey('error', $result);
$result = Strings::validateRegex('/[/');
// Test array structure for invalid regex
$this->assertIsArray($result);
$this->assertCount(4, $result);
$this->assertArrayHasKey('valid', $result);
$this->assertArrayHasKey('preg_error', $result);
$this->assertArrayHasKey('error', $result);
$this->assertArrayHasKey('pcre_error', $result);
}
/**
* Test that methods handle edge cases properly
*/
public function testEdgeCases(): void
{
// Empty string
$this->assertFalse(Strings::isValidRegex(''));
$result = Strings::validateRegex('');
$this->assertFalse($result['valid']);
// Very long pattern
$longPattern = '/' . str_repeat('a', 1000) . '/';
$this->assertTrue(Strings::isValidRegex($longPattern));
// Unicode patterns
$this->assertTrue(Strings::isValidRegex('/\p{L}+/u'));
$this->assertTrue(Strings::isValidRegex('/[α-ω]+/u'));
}
/**
* Test PREG_ERROR_MESSAGES constant accessibility
*/
public function testPregErrorMessagesConstant(): void
{
$this->assertIsArray(Strings::PREG_ERROR_MESSAGES);
$this->assertNotEmpty(Strings::PREG_ERROR_MESSAGES);
// Check that all expected PREG constants are defined
$expectedKeys = [
PREG_NO_ERROR,
PREG_INTERNAL_ERROR,
PREG_BACKTRACK_LIMIT_ERROR,
PREG_RECURSION_LIMIT_ERROR,
PREG_BAD_UTF8_ERROR,
PREG_BAD_UTF8_OFFSET_ERROR,
PREG_JIT_STACKLIMIT_ERROR,
];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, Strings::PREG_ERROR_MESSAGES);
$this->assertIsString(Strings::PREG_ERROR_MESSAGES[$key]);
$this->assertNotEmpty(Strings::PREG_ERROR_MESSAGES[$key]);
}
}
/**
* Test error state isolation between method calls
*/
public function testErrorStateIsolation(): void
{
// Start with invalid regex
Strings::isValidRegex('/[/');
$firstError = Strings::getLastRegexErrorString();
$this->assertNotEquals('No error', $firstError);
// Use valid regex
Strings::isValidRegex('/valid/');
$secondError = Strings::getLastRegexErrorString();
$this->assertEquals('No error', $secondError);
// Verify validateRegex clears previous errors
$result = Strings::validateRegex('/valid/');
$this->assertTrue($result['valid']);
$this->assertEquals(PREG_NO_ERROR, $result['preg_error']);
}
/**
* Test various regex delimiters
*/
public function testDifferentDelimiters(): void
{
$patterns = [
'/test/', // forward slash
'#test#', // hash
'~test~', // tilde
'|test|', // pipe
'@test@', // at symbol
'!test!', // exclamation
'%test%', // percent
];
foreach ($patterns as $pattern) {
$this->assertTrue(
Strings::isValidRegex($pattern),
"Pattern with delimiter '{$pattern}' should be valid"
);
}
}
}
// __END__

View File

@@ -24,117 +24,83 @@ final class CoreLibsConvertStringsTest extends TestCase
{ {
// 0: input // 0: input
// 1: format // 1: format
// 2: split characters as string, null for default
// 3: expected // 3: expected
return [ return [
'all empty string' => [ 'all empty string' => [
'', '',
'', '',
null,
'' ''
], ],
'empty input string' => [ 'empty input string' => [
'', '',
'2-2', '2-2',
null,
'' ''
], ],
'empty format string string' => [ 'empty format string string' => [
'1234', '1234',
'', '',
null,
'1234' '1234'
], ],
'string format match' => [ 'string format match' => [
'1234', '1234',
'2-2', '2-2',
null,
'12-34' '12-34'
], ],
'string format trailing match' => [ 'string format trailing match' => [
'1234', '1234',
'2-2-', '2-2-',
null,
'12-34' '12-34'
], ],
'string format leading match' => [ 'string format leading match' => [
'1234', '1234',
'-2-2', '-2-2',
null,
'12-34' '12-34'
], ],
'string format double inside match' => [ 'string format double inside match' => [
'1234', '1234',
'2--2', '2--2',
null,
'12--34', '12--34',
], ],
'string format short first' => [ 'string format short first' => [
'1', '1',
'2-2', '2-2',
null,
'1' '1'
], ],
'string format match first' => [ 'string format match first' => [
'12', '12',
'2-2', '2-2',
null,
'12' '12'
], ],
'string format short second' => [ 'string format short second' => [
'123', '123',
'2-2', '2-2',
null,
'12-3' '12-3'
], ],
'string format too long' => [ 'string format too long' => [
'1234567', '1234567',
'2-2', '2-2',
null,
'12-34-567' '12-34-567'
], ],
'string format invalid format string' => [
'1234',
'2_2',
null,
'1234'
],
'different split character' => [ 'different split character' => [
'1234', '1234',
'2_2', '2_2',
'_',
'12_34' '12_34'
], ],
'mixed split characters' => [ 'mixed split characters' => [
'123456', '123456',
'2-2_2', '2-2_2',
'-_',
'12-34_56' '12-34_56'
], ],
'length mixed' => [ 'length mixed' => [
'ABCD12345568ABC13', 'ABCD12345568ABC13',
'2-4_5-2#4', '2-4_5-2#4',
'-_#',
'AB-CD12_34556-8A#BC13' 'AB-CD12_34556-8A#BC13'
], ],
'split with split chars in string' => [ 'split with split chars in string' => [
'12-34', '12-34',
'2-2', '2-2',
null,
'12--3-4' '12--3-4'
], ],
'mutltibyte string' => [
'あいうえ',
'2-2',
null,
'あいうえ'
],
'mutltibyte split string' => [
'1234',
'-',
null,
'1234'
],
]; ];
} }
@@ -143,29 +109,137 @@ final class CoreLibsConvertStringsTest extends TestCase
* *
* @covers ::splitFormatString * @covers ::splitFormatString
* @dataProvider splitFormatStringProvider * @dataProvider splitFormatStringProvider
* @testdox splitFormatString $input with format $format and splitters $split_characters will be $expected [$_dataName] * @testdox splitFormatString $input with format $format will be $expected [$_dataName]
* *
* @param string $input * @param string $input
* @param string $format * @param string $format
* @param string|null $split_characters
* @param string $expected * @param string $expected
* @return void * @return void
*/ */
public function testSplitFormatString( public function testSplitFormatString(
string $input, string $input,
string $format, string $format,
string $expected
): void {
$output = \CoreLibs\Convert\Strings::splitFormatString(
$input,
$format,
);
$this->assertEquals(
$expected,
$output
);
}
/** check exceptions */
public function splitFormatStringExceptionProvider(): array
{
return [
'string format with no splitter match' => [
'1234',
'22',
'12-34'
],
'invalid format string' => [
'1234',
'2あ2',
],
'mutltibyte string' => [
'あいうえ',
'2-2',
],
'mutltibyte split string' => [
'1234',
'-',
],
];
}
/**
* Undocumented function
*
* @covers ::splitFormatStringFixed
* @dataProvider splitFormatStringExceptionProvider
* @testdox splitFormatString Exception catch checks for $input with $format[$_dataName]
*
* @return void
*/
public function testSplitFormatStringExceptions(string $input, string $format): void
{
// catch exception
$this->expectException(\InvalidArgumentException::class);
\CoreLibs\Convert\Strings::splitFormatString($input, $format);
}
/**
* test for split Format string fixed length
*
* @return array
*/
public function splitFormatStringFixedProvider(): array
{
return [
'normal split, default split char' => [
'abcdefg',
4,
null,
'abcd-efg'
],
'noraml split, other single split char' => [
'abcdefg',
4,
"=",
'abcd=efg'
],
'noraml split, other multiple split char' => [
'abcdefg',
4,
"-=-",
'abcd-=-efg'
],
'non ascii characters' => [
'あいうえお',
2,
"-",
'あい-うえ-お'
],
'empty string' => [
'',
4,
"-",
''
]
];
}
/**
* Undocumented function
*
* @covers ::splitFormatStringFixed
* @dataProvider splitFormatStringFixedProvider
* @testdox splitFormatStringFixed $input with length $split_length and split chars $split_characters will be $expected [$_dataName]
*
* @param string $input
* @param int $split_length
* @param string|null $split_characters
* @param string $expected
* @return void
*/
public function testSplitFormatStringFixed(
string $input,
int $split_length,
?string $split_characters, ?string $split_characters,
string $expected string $expected
): void { ): void {
if ($split_characters === null) { if ($split_characters === null) {
$output = \CoreLibs\Convert\Strings::splitFormatString( $output = \CoreLibs\Convert\Strings::splitFormatStringFixed(
$input, $input,
$format $split_length
); );
} else { } else {
$output = \CoreLibs\Convert\Strings::splitFormatString( $output = \CoreLibs\Convert\Strings::splitFormatStringFixed(
$input, $input,
$format, $split_length,
$split_characters $split_characters
); );
} }
@@ -175,6 +249,36 @@ final class CoreLibsConvertStringsTest extends TestCase
); );
} }
public function splitFormatStringFixedExceptionProvider(): array
{
return [
'split length too short' => [
'abcdefg',
-1,
],
'split length longer than string' => [
'abcdefg',
20,
],
];
}
/**
* Undocumented function
*
* @covers ::splitFormatStringFixed
* @dataProvider splitFormatStringFixedExceptionProvider
* @testdox splitFormatStringFixed Exception catch checks for $input with $length [$_dataName]
*
* @return void
*/
public function testSplitFormatStringFixedExceptions(string $input, int $length): void
{
// catch exception
$this->expectException(\InvalidArgumentException::class);
\CoreLibs\Convert\Strings::splitFormatStringFixed($input, $length);
}
/** /**
* Undocumented function * Undocumented function
* *
@@ -378,6 +482,305 @@ final class CoreLibsConvertStringsTest extends TestCase
\CoreLibs\Convert\Strings::stripUTF8BomBytes($file) \CoreLibs\Convert\Strings::stripUTF8BomBytes($file)
); );
} }
/**
* Undocumented function
*
* @return array
*/
public function allCharsInSetProvider(): array
{
return [
'find' => [
'abc',
'abcdef',
true
],
'not found' => [
'abcz',
'abcdef',
false
]
];
}
/**
* Undocumented function
*
* @covers ::allCharsInSet
* @dataProvider allCharsInSetProvider
* @testdox allCharsInSet $input in $haystack with expected $expected [$_dataName]
*
* @param string $needle
* @param string $haystack
* @param bool $expected
* @return void
*/
public function testAllCharsInSet(string $needle, string $haystack, bool $expected): void
{
$this->assertEquals(
$expected,
\CoreLibs\Convert\Strings::allCharsInSet($needle, $haystack)
);
}
public function buildCharStringFromListsProvider(): array
{
return [
'test a' => [
'abc',
['a', 'b', 'c'],
],
'test b' => [
'abc123',
['a', 'b', 'c'],
['1', '2', '3'],
],
'test c: no params' => [
'',
],
'test c: empty 1' => [
'',
[]
],
'test nested' => [
'abc',
[['a'], ['b'], ['c']],
],
];
}
/**
* Undocumented function
*
* @covers ::buildCharStringFromLists
* @dataProvider buildCharStringFromListsProvider
* @testdox buildCharStringFromLists all $input convert to $expected [$_dataName]
*
* @param string $expected
* @param array ...$input
* @return void
*/
public function testBuildCharStringFromLists(string $expected, array ...$input): void
{
$this->assertEquals(
$expected,
\CoreLibs\Convert\Strings::buildCharStringFromLists(...$input)
);
}
/**
* Undocumented function
*
* @return array
*/
public function removeDuplicatesProvider(): array
{
return [
'test no change' => [
'ABCDEFG',
'ABCDEFG',
],
'test simple' => [
'aa',
'a'
],
'test keep lower and uppwer case' => [
'AaBbCc',
'AaBbCc'
],
'test unqiue' => [
'aabbcc',
'abc'
],
'test multibyte no change' => [
'あいうえお',
'あいうえお',
],
'test multibyte' => [
'ああいいううええおお',
'あいうえお',
],
'test multibyte special' => [
'あぁいぃうぅえぇおぉ',
'あぁいぃうぅえぇおぉ',
]
];
}
/**
* Undocumented function
*
* @covers ::removeDuplicates
* @dataProvider removeDuplicatesProvider
* @testdox removeDuplicates make $input unqiue to $expected [$_dataName]
*
* @param string $input
* @param string $expected
* @return void
*/
public function testRemoveDuplicates(string $input, string $expected): void
{
$this->assertEquals(
$expected,
\CoreLibs\Convert\Strings::removeDuplicates($input)
);
}
/**
* Undocumented function
*
* @return array
*/
public function isValidRegexSimpleProvider(): array
{
return [
'valid regex' => [
'/^[A-z]$/',
true,
[
'valid' => true,
'preg_error' => 0,
'error' => null,
'pcre_error' => null
],
],
'invalid regex A' => [
'/^[A-z]$',
false,
[
'valid' => false,
'preg_error' => 1,
'error' => 'Internal PCRE error',
'pcre_error' => 'Internal error'
],
],
'invalid regex B' => [
'/^[A-z$',
false,
[
'valid' => false,
'preg_error' => 1,
'error' => 'Internal PCRE error',
'pcre_error' => 'Internal error'
],
],
];
}
/**
* Undocumented function
*
* @covers ::isValidRegexSimple
* @dataProvider isValidRegexSimpleProvider
* @testdox isValidRegexSimple make $input unqiue to $expected [$_dataName]
*
* @param string $input
* @param bool $expected
* @return void
*/
public function testIsValidRegexSimple(string $input, bool $expected, array $expected_extended): void
{
$this->assertEquals(
$expected,
\CoreLibs\Convert\Strings::isValidRegex($input),
'Regex is not valid'
);
$this->assertEquals(
$expected_extended,
\CoreLibs\Convert\Strings::validateRegex($input),
'Validation of regex failed'
);
$this->assertEquals(
// for true null is set, so we get here No Error
$expected_extended['error'] ?? \CoreLibs\Convert\Strings::PREG_ERROR_MESSAGES[0],
\CoreLibs\Convert\Strings::getLastRegexErrorString(),
'Cannot match last preg error string'
);
}
/**
* Undocumented function
*
* @return array
*/
public function parseCharacterRangesProvider(): array
{
return [
'simple a-z' => [
['a-z'],
implode('', range('a', 'z')),
null,
],
'simple A-Z' => [
['A-Z'],
implode('', range('A', 'Z')),
null,
],
'simple 0-9' => [
['0-9'],
implode('', range('0', '9')),
null,
],
'mixed ranges' => [
['a-c', 'X-Z', '3-5'],
'abcXYZ345',
null,
],
'reverse ranges' => [
['z-a'],
'abcdefghijklmnopqrstuvwxyz',
null,
],
'overlapping ranges' => [
['a-f', 'd-j'],
'abcdefghij',
null,
],
'mixed valid and overlap ranges' => [
['a-f', 'z-a', '0-3'],
'abcdefghijklmnopqrstuvwxyz0123',
null,
],
'range without dashes' => [
['abcddfff'],
'abcdf',
null,
],
'invalid ranges' => [
['a-あ', 'A-あ', '0-あ'],
'',
\InvalidArgumentException::class,
],
];
}
/**
* Undocumented function
*
* @covers ::parseCharacterRanges
* @dataProvider parseCharacterRangesProvider
* @testdox parseCharacterRanges $input to $expected [$_dataName]
*
* @param array $input
* @param string $expected
* @param string|null $expected_exception
* @return void
*/
public function testParseCharacterRanges(
array $input,
string $expected,
?string $expected_exception
): void {
if ($expected_exception !== null) {
$this->expectException($expected_exception);
}
$this->assertEquals(
$expected,
implode('', \CoreLibs\Convert\Strings::parseCharacterRanges(implode('', $input)))
);
}
} }
// __END__ // __END__

View File

@@ -21,8 +21,10 @@ final class CoreLibsCreateHashTest extends TestCase
public function hashData(): array public function hashData(): array
{ {
return [ return [
'any string' => [ 'hash tests' => [
// this is the string
'text' => 'Some String Text', 'text' => 'Some String Text',
// hash list special
'crc32b_reverse' => 'c5c21d91', // crc32b (in revere) 'crc32b_reverse' => 'c5c21d91', // crc32b (in revere)
'sha1Short' => '4d2bc9ba0', // sha1Short 'sha1Short' => '4d2bc9ba0', // sha1Short
// via hash // via hash
@@ -31,6 +33,8 @@ final class CoreLibsCreateHashTest extends TestCase
'fnv132' => '9df444f9', // hash: fnv132 'fnv132' => '9df444f9', // hash: fnv132
'fnv1a32' => '2c5f91b9', // hash: fnv1a32 'fnv1a32' => '2c5f91b9', // hash: fnv1a32
'joaat' => '50dab846', // hash: joaat 'joaat' => '50dab846', // hash: joaat
'ripemd160' => 'aeae3f041b20136451519edd9361570909300342', // hash: ripemd160,
'sha256' => '9055080e022f224fa835929b80582b3c71c672206fa3a49a87412c25d9d42ceb', // hash: sha256
] ]
]; ];
} }
@@ -81,7 +85,7 @@ final class CoreLibsCreateHashTest extends TestCase
{ {
$list = []; $list = [];
foreach ($this->hashData() as $name => $values) { 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 // default value test
if ($_hash_type === null) { if ($_hash_type === null) {
$hash_type = \CoreLibs\Create\Hash::STANDARD_HASH_SHORT; $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 * Undocumented function
* *
@@ -136,9 +156,13 @@ final class CoreLibsCreateHashTest extends TestCase
/** /**
* Undocumented function * Undocumented function
* *
* phpcs:disable Generic.Files.LineLength
* @covers ::__sha1Short * @covers ::__sha1Short
* @covers ::__crc32b
* @covers ::sha1Short
* @dataProvider sha1ShortProvider * @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 $input
* @param string $expected * @param string $expected
@@ -149,16 +173,29 @@ final class CoreLibsCreateHashTest extends TestCase
// uses crc32b // uses crc32b
$this->assertEquals( $this->assertEquals(
$expected, $expected,
\CoreLibs\Create\Hash::__sha1Short($input) \CoreLibs\Create\Hash::__sha1Short($input),
'__sha1Short depreacted'
); );
$this->assertEquals( $this->assertEquals(
$expected, $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 // sha1 type
$this->assertEquals( $this->assertEquals(
$expected_sha1, $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 * Undocumented function
* *
* @covers ::__hash * @covers ::__hash
* @covers ::hashShort
* @covers ::hashShort
* @dataProvider hashProvider * @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 $input
* @param string|null $hash_type * @param string|null $hash_type
@@ -179,12 +218,24 @@ final class CoreLibsCreateHashTest extends TestCase
if ($hash_type === null) { if ($hash_type === null) {
$this->assertEquals( $this->assertEquals(
$expected, $expected,
\CoreLibs\Create\Hash::__hash($input) \CoreLibs\Create\Hash::__hash($input),
'__hash'
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashShort($input),
'hashShort'
); );
} else { } else {
$this->assertEquals( $this->assertEquals(
$expected, $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 * Undocumented function
* *
* @covers ::__hashLong * @covers ::__hashLong
* @covers ::hashLong
* @dataProvider hashLongProvider * @dataProvider hashLongProvider
* @testdox __hashLong $input will be $expected [$_dataName] * @testdox __hashLong/hashLong $input will be $expected [$_dataName]
* *
* @param string $input * @param string $input
* @param string $expected * @param string $expected
@@ -206,6 +258,168 @@ final class CoreLibsCreateHashTest extends TestCase
$expected, $expected,
\CoreLibs\Create\Hash::__hashLong($input) \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<mixed>
*/
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'
);
} }
} }

View File

@@ -13,32 +13,6 @@ use PHPUnit\Framework\TestCase;
*/ */
final class CoreLibsCreateRandomKeyTest extends TestCase final class CoreLibsCreateRandomKeyTest extends TestCase
{ {
/**
* Undocumented function
*
* @return array
*/
public function keyLenghtProvider(): array
{
return [
'valid key length' => [
0 => 6,
1 => true,
2 => 6,
],
'negative key length' => [
0 => -1,
1 => false,
2 => 4,
],
'tpp big key length' => [
0 => 300,
1 => false,
2 => 4,
],
];
}
/** /**
* Undocumented function * Undocumented function
* *
@@ -47,109 +21,67 @@ final class CoreLibsCreateRandomKeyTest extends TestCase
public function randomKeyGenProvider(): array public function randomKeyGenProvider(): array
{ {
return [ return [
'default key length' => [ // just key length
'default key length, default char set' => [
0 => null, 0 => null,
1 => 4 1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT
], ],
'set -1 key length default' => [ 'set -1 key length, default char set' => [
0 => -1, 0 => -1,
1 => 4, 1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
], ],
'set too large key length' => [ 'set 0 key length, default char set' => [
0 => -1,
1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
],
'set too large key length, default char set' => [
0 => 300, 0 => 300,
1 => 4, 1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
], ],
'set override key lenght' => [ 'set override key lenght, default char set' => [
0 => 6, 0 => 6,
1 => 6, 1 => 6,
], ],
// just character set
'default key length, different char set A' => [
0 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
2 => [
'A', 'B', 'C'
],
],
'different key length, different char set B' => [
0 => 16,
1 => 16,
2 => [
'A', 'B', 'C'
],
3 => [
'1', '2', '3'
]
],
]; ];
} }
// Alternative more efficient version using strpos
/** /**
* 1 * check if all characters are in set
* *
* @return array * @param string $input
* @param string $allowed_chars
* @return bool
*/ */
public function keepKeyLengthProvider(): array private function allCharsInSet(string $input, string $allowed_chars): bool
{ {
return [ $inputLength = strlen($input);
'set too large' => [
0 => 6,
1 => 300,
2 => 6,
],
'set too small' => [
0 => 8,
1 => -2,
2 => 8,
],
'change valid' => [
0 => 10,
1 => 6,
2 => 6,
]
];
}
/** for ($i = 0; $i < $inputLength; $i++) {
* run before each test and reset to default 4 if (strpos($allowed_chars, $input[$i]) === false) {
* return false;
* @before }
*
* @return void
*/
public function resetKeyLength(): void
{
\CoreLibs\Create\RandomKey::setRandomKeyLength(4);
}
/**
* check that first length is 4
*
* @covers ::getRandomKeyLength
* @testWith [4]
* @testdox getRandomKeyLength on init will be $expected [$_dataName]
*
* @param integer $expected
* @return void
*/
public function testGetRandomKeyLengthInit(int $expected): void
{
$this->assertEquals(
$expected,
\CoreLibs\Create\RandomKey::getRandomKeyLength()
);
}
/**
* Undocumented function
*
* @covers ::setRandomKeyLength
* @covers ::getRandomKeyLength
* @dataProvider keyLenghtProvider
* @testdox setRandomKeyLength $input will be $expected, compare to $compare [$_dataName]
*
* @param integer $input
* @param boolean $expected
* @param integer $compare
* @return void
*/
public function testSetRandomKeyLength(int $input, bool $expected, int $compare): void
{
// set
$this->assertEquals(
$expected,
\CoreLibs\Create\RandomKey::setRandomKeyLength($input)
);
// read test, if false, use compare check
if ($expected === false) {
$input = $compare;
} }
$this->assertEquals(
$input, return true;
\CoreLibs\Create\RandomKey::getRandomKeyLength()
);
} }
/** /**
@@ -163,43 +95,41 @@ final class CoreLibsCreateRandomKeyTest extends TestCase
* @param integer $expected * @param integer $expected
* @return void * @return void
*/ */
public function testRandomKeyGen(?int $input, int $expected): void public function testRandomKeyGen(?int $input, int $expected, array ...$key_range): void
{ {
$__key_data = \CoreLibs\Create\RandomKey::KEY_CHARACTER_RANGE_DEFAULT;
if (count($key_range)) {
$__key_data = join('', array_unique(array_merge(...$key_range)));
}
if ($input === null) { if ($input === null) {
$this->assertEquals( $this->assertEquals(
$expected, $expected,
strlen(\CoreLibs\Create\RandomKey::randomKeyGen()) strlen(\CoreLibs\Create\RandomKey::randomKeyGen())
); );
} else { } elseif ($input !== null && !count($key_range)) {
$random_key = \CoreLibs\Create\RandomKey::randomKeyGen($input);
$this->assertTrue(
$this->allCharsInSet($random_key, $__key_data),
'Characters not valid'
);
$this->assertEquals( $this->assertEquals(
$expected, $expected,
strlen(\CoreLibs\Create\RandomKey::randomKeyGen($input)) strlen($random_key),
'String length not matching'
);
} elseif (count($key_range)) {
$random_key = \CoreLibs\Create\RandomKey::randomKeyGen($input, ...$key_range);
$this->assertTrue(
$this->allCharsInSet($random_key, $__key_data),
'Characters not valid'
);
$this->assertEquals(
$expected,
strlen($random_key),
'String length not matching'
); );
} }
} }
/**
* Check that if set to n and then invalid, it keeps the previous one
* or if second change valid, second will be shown
*
* @covers ::setRandomKeyLength
* @dataProvider keepKeyLengthProvider
* @testdox keep setRandomKeyLength set with $input_valid and then $input_invalid will be $expected [$_dataName]
*
* @param integer $input_valid
* @param integer $input_invalid
* @param integer $expected
* @return void
*/
public function testKeepKeyLength(int $input_valid, int $input_invalid, int $expected): void
{
\CoreLibs\Create\RandomKey::setRandomKeyLength($input_valid);
\CoreLibs\Create\RandomKey::setRandomKeyLength($input_invalid);
$this->assertEquals(
$expected,
\CoreLibs\Create\RandomKey::getRandomKeyLength()
);
}
} }
// __END__ // __END__

View File

@@ -54,7 +54,9 @@ final class CoreLibsCreateSessionTest extends TestCase
'getSessionId' => '1234abcd4567' 'getSessionId' => '1234abcd4567'
], ],
'sessionNameGlobals', 'sessionNameGlobals',
false, [
'auto_write_close' => false,
],
], ],
'auto write close' => [ 'auto write close' => [
'sessionNameAutoWriteClose', 'sessionNameAutoWriteClose',
@@ -66,7 +68,9 @@ final class CoreLibsCreateSessionTest extends TestCase
'getSessionId' => '1234abcd4567' 'getSessionId' => '1234abcd4567'
], ],
'sessionNameAutoWriteClose', 'sessionNameAutoWriteClose',
true, [
'auto_write_close' => true,
],
], ],
]; ];
} }
@@ -81,13 +85,14 @@ final class CoreLibsCreateSessionTest extends TestCase
* @param string $input * @param string $input
* @param array<mixed> $mock_data * @param array<mixed> $mock_data
* @param string $expected * @param string $expected
* @param array<string,mixed> $options
* @return void * @return void
*/ */
public function testStartSession( public function testStartSession(
string $input, string $input,
array $mock_data, array $mock_data,
string $expected, string $expected,
?bool $auto_write_close, ?array $options,
): void { ): void {
/** @var \CoreLibs\Create\Session&MockObject $session_mock */ /** @var \CoreLibs\Create\Session&MockObject $session_mock */
$session_mock = $this->createPartialMock( $session_mock = $this->createPartialMock(
@@ -174,9 +179,14 @@ final class CoreLibsCreateSessionTest extends TestCase
4, 4,
'/^\[SESSION\] Failed to activate session/' '/^\[SESSION\] Failed to activate session/'
], ],
'expired session' => [
\RuntimeException::class,
5,
'/^\[SESSION\] Expired session found/'
],
'not a valid session id returned' => [ 'not a valid session id returned' => [
\UnexpectedValueException::class, \UnexpectedValueException::class,
5, 6,
'/^\[SESSION\] getSessionId did not return a session id/' '/^\[SESSION\] getSessionId did not return a session id/'
], */ ], */
]; ];
@@ -206,7 +216,8 @@ final class CoreLibsCreateSessionTest extends TestCase
$this->expectException($exception); $this->expectException($exception);
$this->expectExceptionCode($exception_code); $this->expectExceptionCode($exception_code);
$this->expectExceptionMessageMatches($expected_error); $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]);
} }
/** /**

View File

@@ -135,6 +135,7 @@ final class CoreLibsDBIOTest extends TestCase
} }
// check if they already exist, drop them // check if they already exist, drop them
if ($db->dbShowTableMetaData('table_with_primary_key') !== false) { 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_with_primary_key");
$db->dbExec("DROP TABLE table_without_primary_key"); $db->dbExec("DROP TABLE table_without_primary_key");
$db->dbExec("DROP TABLE test_meta"); $db->dbExec("DROP TABLE test_meta");
@@ -3692,7 +3693,7 @@ final class CoreLibsDBIOTest extends TestCase
* *
* @return array * @return array
*/ */
public function preparedProviderValue(): array public function providerDbGetPrepareCursorValue(): array
{ {
// 1: query (can be empty for do not set) // 1: query (can be empty for do not set)
// 2: stm name // 2: stm name
@@ -3736,7 +3737,7 @@ final class CoreLibsDBIOTest extends TestCase
* test return prepare cursor errors * test return prepare cursor errors
* *
* @covers ::dbGetPrepareCursorValue * @covers ::dbGetPrepareCursorValue
* @dataProvider preparedProviderValue * @dataProvider providerDbGetPrepareCursorValue
* @testdox prepared query $stm_name with $key expect error id $error_id [$_dataName] * @testdox prepared query $stm_name with $key expect error id $error_id [$_dataName]
* *
* @param string $query * @param string $query
@@ -3769,6 +3770,94 @@ final class CoreLibsDBIOTest extends TestCase
); );
} }
/**
* Undocumented function
*
* @return array
*/
public function providerDbPreparedCursorStatus(): array
{
return [
'empty statement pararm' => [
'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 // - schema set/get tests
// dbGetSchema, dbSetSchema // dbGetSchema, dbSetSchema
@@ -4656,7 +4745,7 @@ final class CoreLibsDBIOTest extends TestCase
$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']); $res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
// all hast to be string // all hast to be string
foreach ($res as $key => $value) { foreach ($res as $key => $value) {
$this->assertIsString($value, 'Aseert string for column: ' . $key); $this->assertIsString($value, 'Assert string for column: ' . $key);
} }
// convert base only // convert base only
$db->dbSetConvertFlag(Convert::on); $db->dbSetConvertFlag(Convert::on);
@@ -4669,10 +4758,10 @@ final class CoreLibsDBIOTest extends TestCase
} }
switch ($type_layout[$name]) { switch ($type_layout[$name]) {
case 'int': case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break; break;
default: default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break; break;
} }
} }
@@ -4686,13 +4775,13 @@ final class CoreLibsDBIOTest extends TestCase
} }
switch ($type_layout[$name]) { switch ($type_layout[$name]) {
case 'int': case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break; break;
case 'float': case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break; break;
default: default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break; break;
} }
} }
@@ -4706,17 +4795,17 @@ final class CoreLibsDBIOTest extends TestCase
} }
switch ($type_layout[$name]) { switch ($type_layout[$name]) {
case 'int': case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break; break;
case 'float': case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break; break;
case 'json': case 'json':
case 'jsonb': case 'jsonb':
$this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
break; break;
default: default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break; break;
} }
} }
@@ -4730,25 +4819,25 @@ final class CoreLibsDBIOTest extends TestCase
} }
switch ($type_layout[$name]) { switch ($type_layout[$name]) {
case 'int': case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break; break;
case 'float': case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break; break;
case 'json': case 'json':
case 'jsonb': case 'jsonb':
$this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
break; break;
case 'bytea': case 'bytea':
// for hex types it must not start with \x // for hex types it must not start with \x
$this->assertStringStartsNotWith( $this->assertStringStartsNotWith(
'\x', '\x',
$value, $value,
'Aseert bytes not starts with \x for column: ' . $key . '/' . $name 'Assert bytes not starts with \x for column: ' . $key . '/' . $name
); );
break; break;
default: default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break; break;
} }
} }
@@ -4920,8 +5009,8 @@ final class CoreLibsDBIOTest extends TestCase
) )
), ),
($params === null ? ($params === null ?
$db->dbGetQueryHash($query) : $db->dbBuildQueryHash($query) :
$db->dbGetQueryHash($query, $params) $db->dbBuildQueryHash($query, $params)
), ),
'Failed assertdbGetQueryHash ' 'Failed assertdbGetQueryHash '
); );
@@ -5147,6 +5236,9 @@ final class CoreLibsDBIOTest extends TestCase
$3 $3
-- comment 3 -- comment 3
, $4 , $4
-- ignore $5, $6
-- $7, $8
-- digest($9, 10)
) )
SQL, SQL,
'count' => 4, 'count' => 4,
@@ -5196,8 +5288,78 @@ final class CoreLibsDBIOTest extends TestCase
SQL, SQL,
'count' => 1, 'count' => 1,
'convert' => false, 'convert' => false,
],
'update with case' => [
'query' => <<<SQL
UPDATE table_with_primary_key SET
row_int = $1::INT,
row_varchar = CASE WHEN row_int = 1 THEN $2 ELSE 'bar'::VARCHAR END
WHERE
row_varchar = $3
SQL,
'count' => 3,
'convert' => false,
],
'select with case' => [
'query' => <<<SQL
SELECT row_int
FROM table_with_primary_key
WHERE
row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
SQL,
'count' => 2,
'convert' => false,
],
// special $$ string case
'text string, with $ placehoders that could be seen as $$ string' => [
'query' => <<<SQL
SELECT row_int
FROM table_with_primary_key
WHERE
row_bytea = digest($3::VARCHAR, $4) OR
row_varchar = encode(digest($3, $4), 'hex') OR
row_bytea = hmac($3, $5, $4) OR
row_varchar = encode(hmac($3, $5, $4), 'hex') OR
row_bytea = pgp_sym_encrypt($3, $6) OR
row_varchar = encode(pgp_sym_encrypt($1, $6), 'hex') OR
row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
SQL,
'count' => 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' => <<<SQL
SELECT row_int
FROM table_with_primary_key
WHERE
row_varchar = 'foo bar bar baz $5' OR
row_varchar = 'foo bar '' barbar $6' OR
row_varchar = E'foo bar \' barbar $7' OR
row_varchar = CASE WHEN row_int = 1 THEN $1 ELSE $2 END
SQL,
'count' => 2,
'convert' => false,
] ]
]; ];
$string = <<<SQL
'''
SQL;
} }
/** /**

View File

@@ -568,6 +568,9 @@ final class CoreLibsDebugSupportTest extends TestCase
'assert expected 12' 'assert expected 12'
); );
break; break;
default:
$this->assertTrue(true, 'Default fallback as true');
break;
} }
} }

View File

@@ -21,341 +21,6 @@ final class CoreLibsLanguageGetLocaleTest extends TestCase
. 'includes' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR
. 'locale' . 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<mixed>
*/
/* 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<mixed> $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 * all the test data
* *

View File

@@ -10,7 +10,7 @@ use CoreLibs\Logging\Logger\Level;
/** /**
* Test class for Logging * Test class for Logging
* @coversDefaultClass \CoreLibs\Logging\ErrorMessages * @coversDefaultClass \CoreLibs\Logging\ErrorMessages
* @testdox \CoreLibs\Logging\ErrorMEssages method tests * @testdox \CoreLibs\Logging\ErrorMessages method tests
*/ */
final class CoreLibsLoggingErrorMessagesTest extends TestCase final class CoreLibsLoggingErrorMessagesTest extends TestCase
{ {
@@ -105,11 +105,15 @@ final class CoreLibsLoggingErrorMessagesTest extends TestCase
'log_folder' => self::LOG_FOLDER, 'log_folder' => self::LOG_FOLDER,
'log_level' => Level::Error, 'log_level' => Level::Error,
]); ]);
$errorLogTmpfile = tmpfile();
$errorLogLocationBackup = ini_set('error_log', stream_get_meta_data($errorLogTmpfile)['uri']);
$em = new \CoreLibs\Logging\ErrorMessage($log); $em = new \CoreLibs\Logging\ErrorMessage($log);
$em->setMessage( $em->setMessage(
$level, $level,
$str $str
); );
// for exceptions if log level is set to catch them
$error_log_content = stream_get_contents($errorLogTmpfile);
$this->assertEquals( $this->assertEquals(
[ [
'level' => $expected, 'level' => $expected,
@@ -377,6 +381,8 @@ final class CoreLibsLoggingErrorMessagesTest extends TestCase
?bool $log_warning, ?bool $log_warning,
string $expected string $expected
): void { ): void {
$errorLogTmpfile = tmpfile();
$errorLogLocationBackup = ini_set('error_log', stream_get_meta_data($errorLogTmpfile)['uri']);
$log = new \CoreLibs\Logging\Logging([ $log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testErrorMessagesLogError', 'log_file_id' => 'testErrorMessagesLogError',
'log_folder' => self::LOG_FOLDER, 'log_folder' => self::LOG_FOLDER,
@@ -392,6 +398,9 @@ final class CoreLibsLoggingErrorMessagesTest extends TestCase
log_error: $log_error, log_error: $log_error,
log_warning: $log_warning log_warning: $log_warning
); );
ini_set('error_log', $errorLogLocationBackup);
// for exceptions if log level is set to catch them
$error_log_content = stream_get_contents($errorLogTmpfile);
$file_content = ''; $file_content = '';
if (is_file($log->getLogFolder() . $log->getLogFile())) { if (is_file($log->getLogFolder() . $log->getLogFile())) {
$file_content = file_get_contents( $file_content = file_get_contents(
@@ -447,6 +456,8 @@ final class CoreLibsLoggingErrorMessagesTest extends TestCase
'log_level' => Level::Debug, 'log_level' => Level::Debug,
'log_per_run' => true 'log_per_run' => true
]); ]);
$errorLogTmpfile = tmpfile();
$errorLogLocationBackup = ini_set('error_log', stream_get_meta_data($errorLogTmpfile)['uri']);
$em = new \CoreLibs\Logging\ErrorMessage($log); $em = new \CoreLibs\Logging\ErrorMessage($log);
$em->setErrorMsg( $em->setErrorMsg(
$id, $id,
@@ -456,6 +467,9 @@ final class CoreLibsLoggingErrorMessagesTest extends TestCase
log_error: $log_error, log_error: $log_error,
log_warning: $log_warning log_warning: $log_warning
); );
ini_set('error_log', $errorLogLocationBackup);
// for exceptions if log level is set to catch them
$error_log_content = stream_get_contents($errorLogTmpfile);
$file_content = ''; $file_content = '';
if (is_file($log->getLogFolder() . $log->getLogFile())) { if (is_file($log->getLogFolder() . $log->getLogFile())) {
$file_content = file_get_contents( $file_content = file_get_contents(

View File

@@ -18,7 +18,7 @@ use CoreLibs\Logging\Logger\Flag;
final class CoreLibsLoggingLoggingTest extends TestCase final class CoreLibsLoggingLoggingTest extends TestCase
{ {
private const LOG_FOLDER = __DIR__ . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR; private const LOG_FOLDER = __DIR__ . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR;
private const REGEX_BASE = "\[[\d\-\s\.:]+\]\s{1}" // date private const REGEX_BASE = "\[[\d\-\s\.:+T]+\]\s{1}" // date, just basic checks
. "\[[\w\.]+(:\d+)?\]\s{1}" // host:port . "\[[\w\.]+(:\d+)?\]\s{1}" // host:port
. "\[(phar:\/\/)?[\w\-\.\/]+:\d+\]\s{1}" // folder/file [note phar:// is for phpunit] . "\[(phar:\/\/)?[\w\-\.\/]+:\d+\]\s{1}" // folder/file [note phar:// is for phpunit]
. "\[\w+\]\s{1}" // run id . "\[\w+\]\s{1}" // run id
@@ -249,7 +249,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$this->assertFalse( $this->assertFalse(
$log->loggingLevelIsDebug() $log->loggingLevelIsDebug()
); );
// not set, should be debug] // not set, should be debug
$log = new \CoreLibs\Logging\Logging([ $log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetLoggingLevel', 'log_file_id' => 'testSetLoggingLevel',
'log_folder' => self::LOG_FOLDER, 'log_folder' => self::LOG_FOLDER,
@@ -297,6 +297,71 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$log->setLoggingLevel('NotGood'); $log->setLoggingLevel('NotGood');
} }
/**
* Undocumented function
*
* @covers ::setErrorLogWriteLevel
* @covers ::getErrorLogWriteLevel
* @testdox setErrorLogWriteLevel set/get checks
*
* @return void
*/
public function testSetErrorLogWriteLevel(): void
{
// valid that is not Debug
$log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetErrorLogWriteLevel',
'log_folder' => self::LOG_FOLDER,
'error_log_write_level' => Level::Error
]);
$this->assertEquals(
Level::Error,
$log->getErrorLogWriteLevel()
);
// not set on init
$log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetErrorLogWriteLevel',
'log_folder' => self::LOG_FOLDER,
]);
$this->assertEquals(
Level::Emergency,
$log->getErrorLogWriteLevel()
);
// invalid, should be Emergency, will throw excpetion too
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
'Option: "error_log_write_level" is not of instance \CoreLibs\Logging\Logger\Level'
);
$log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetLoggingLevel',
'log_folder' => self::LOG_FOLDER,
'error_log_write_level' => 'I'
]);
$this->assertEquals(
Level::Emergency,
$log->getErrorLogWriteLevel()
);
// set valid then change
$log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetErrorLogWriteLevel',
'log_folder' => self::LOG_FOLDER,
'error_log_write_level' => Level::Error
]);
$this->assertEquals(
Level::Error,
$log->getErrorLogWriteLevel()
);
$log->setErrorLogWriteLevel(Level::Notice);
$this->assertEquals(
Level::Notice,
$log->getErrorLogWriteLevel()
);
// illegal logging level
$this->expectException(\Psr\Log\InvalidArgumentException::class);
$this->expectExceptionMessageMatches("/^Level \"NotGood\" is not defined, use one of: /");
$log->setErrorLogWriteLevel('NotGood');
}
// setLogFileId // setLogFileId
// getLogFileId // getLogFileId
@@ -395,7 +460,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
} }
$per_run_id = $log->getLogUniqueId(); $per_run_id = $log->getLogUniqueId();
$this->assertMatchesRegularExpression( $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, $per_run_id,
'assert per log run id 1st' 'assert per log run id 1st'
); );
@@ -403,7 +468,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$log->setLogUniqueId(true); $log->setLogUniqueId(true);
$per_run_id_2nd = $log->getLogUniqueId(); $per_run_id_2nd = $log->getLogUniqueId();
$this->assertMatchesRegularExpression( $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, $per_run_id_2nd,
'assert per log run id 2nd' 'assert per log run id 2nd'
); );
@@ -824,13 +889,13 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$this->assertTrue($log_ok, 'assert ::log (debug) OK'); $this->assertTrue($log_ok, 'assert ::log (debug) OK');
$this->assertEquals( $this->assertEquals(
$log->getLogFile(), $log->getLogFile(),
$log->getLogFileId() . '_DEBUG.log' $log->getLogFileId() . '.DEBUG.log'
); );
$log_ok = $log->log(Level::Info, 'INFO', group_id: 'GROUP_ID', prefix: 'PREFIX:'); $log_ok = $log->log(Level::Info, 'INFO', group_id: 'GROUP_ID', prefix: 'PREFIX:');
$this->assertTrue($log_ok, 'assert ::log (info) OK'); $this->assertTrue($log_ok, 'assert ::log (info) OK');
$this->assertEquals( $this->assertEquals(
$log->getLogFile(), $log->getLogFile(),
$log->getLogFileId() . '_INFO.log' $log->getLogFileId() . '.INFO.log'
); );
} }

View File

@@ -0,0 +1,838 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Security\CreateKey;
use CoreLibs\Security\AsymmetricAnonymousEncryption;
/**
* Test class for Security\AsymmetricAnonymousEncryption and Security\CreateKey
* @coversDefaultClass \CoreLibs\Security\AsymmetricAnonymousEncryption
* @testdox \CoreLibs\Security\AsymmetricAnonymousEncryption method tests
*/
final class CoreLibsSecurityAsymmetricAnonymousEncryptionTest extends TestCase
{
// MARK: key set and compare
/**
* Undocumented function
*
* @covers ::getKeyPair
* @covers ::compareKeyPair
* @covers ::getPublicKey
* @covers ::comparePublicKey
* @testdox Check if init class set key pair matches to created key pair and public key
*
* @return void
*/
public function testKeyPairInitGetCompare(): 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),
'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__

View File

@@ -13,6 +13,11 @@ use PHPUnit\Framework\TestCase;
*/ */
final class CoreLibsSecurityPasswordTest extends TestCase final class CoreLibsSecurityPasswordTest extends TestCase
{ {
/**
* Undocumented function
*
* @return array
*/
public function passwordProvider(): array public function passwordProvider(): array
{ {
return [ 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 public function passwordRehashProvider(): array
{ {
return [ return [
@@ -63,6 +73,10 @@ final class CoreLibsSecurityPasswordTest extends TestCase
*/ */
public function testPasswordRehashCheck(string $input, bool $expected): void 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( $this->assertEquals(
$expected, $expected,
\CoreLibs\Security\Password::passwordRehashCheck($input) \CoreLibs\Security\Password::passwordRehashCheck($input)

View File

@@ -15,6 +15,77 @@ use CoreLibs\Security\SymmetricEncryption;
*/ */
final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase 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 * Undocumented function
* *
@@ -56,7 +127,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$decrypted, $decrypted,
'Class call', '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 // test indirect
$encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input);
$decrypted = SymmetricEncryption::getInstance($key)->decrypt($encrypted); $decrypted = SymmetricEncryption::getInstance($key)->decrypt($encrypted);
@@ -65,7 +153,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$decrypted, $decrypted,
'Class Instance call', '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 // test static
$encrypted = SymmetricEncryption::encryptKey($input, $key); $encrypted = SymmetricEncryption::encryptKey($input, $key);
$decrypted = SymmetricEncryption::decryptKey($encrypted, $key); $decrypted = SymmetricEncryption::decryptKey($encrypted, $key);
@@ -77,6 +182,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
); );
} }
// MARK: invalid key
/** /**
* Undocumented function * Undocumented function
* *
@@ -114,13 +221,51 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$crypt = new SymmetricEncryption($key); $crypt = new SymmetricEncryption($key);
$encrypted = $crypt->encrypt($input); $encrypted = $crypt->encrypt($input);
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
$crypt->setKey($key); $crypt->setKey($wrong_key);
$crypt->decrypt($encrypted); $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 // class instance
$encrypted = SymmetricEncryption::getInstance($key)->encrypt($input); $encrypted = SymmetricEncryption::getInstance($key)->encrypt($input);
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
SymmetricEncryption::getInstance($wrong_key)->decrypt($encrypted); 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 // class static
$encrypted = SymmetricEncryption::encryptKey($input, $key); $encrypted = SymmetricEncryption::encryptKey($input, $key);
@@ -128,6 +273,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
SymmetricEncryption::decryptKey($encrypted, $wrong_key); SymmetricEncryption::decryptKey($encrypted, $wrong_key);
} }
// MARK: wrong key
/** /**
* Undocumented function * Undocumented function
* *
@@ -144,6 +291,10 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b', 'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b',
'excpetion_message' => 'Key is not the correct size (must be ' '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(); $enc_key = CreateKey::generateRandomKey();
// class // class
$this->expectExceptionMessage($exception_message);
$crypt = new SymmetricEncryption($key); $crypt = new SymmetricEncryption($key);
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
$crypt->encrypt('test'); $crypt->encrypt('test');
@@ -172,6 +324,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
$crypt->setKey($key); $crypt->setKey($key);
$crypt->decrypt($encrypted); $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 // class instance
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
@@ -180,6 +349,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$encrypted = SymmetricEncryption::getInstance($enc_key)->encrypt('test'); $encrypted = SymmetricEncryption::getInstance($enc_key)->encrypt('test');
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
SymmetricEncryption::getInstance($key)->decrypt($encrypted); 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 // class static
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
@@ -190,6 +376,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
SymmetricEncryption::decryptKey($encrypted, $key); SymmetricEncryption::decryptKey($encrypted, $key);
} }
// MARK: wrong input
/** /**
* Undocumented function * Undocumented function
* *
@@ -232,6 +420,49 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$this->expectExceptionMessage($exception_message); $this->expectExceptionMessage($exception_message);
SymmetricEncryption::decryptKey($input, $key); 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__ // __END__

View File

@@ -59,8 +59,6 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
continue; continue;
} }
$this->url_basic = $url; $this->url_basic = $url;
// split out the last / part for url set test
curl_close($handle);
// print "Open: $url\n"; // print "Open: $url\n";
break; break;
} }
@@ -969,13 +967,23 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
"query" => ["foo-get" => "bar"] "query" => ["foo-get" => "bar"]
]); ]);
$this->assertEquals("200", $response["code"], "multi call: get response code not matching"); $this->assertEquals("200", $response["code"], "multi call: get response code not matching");
$request_expected = json_decode(
<<<JSON
{
"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
}
JSON,
true
);
$this->assertEquals( $this->assertEquals(
'{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' $request_expected,
. '"HTTP_FIRST_CALL":"get","HTTP_ACCEPT":"*\/*",' json_decode($response['content'], true),
. '"HTTP_HOST":"soba.egplusww.jp"},'
. '"REQUEST_TYPE":"GET",'
. '"PARAMS":{"foo-get":"bar"},"BODY":null}',
$response['content'],
'multi call: get content not matching' 'multi call: get content not matching'
); );
// post // post
@@ -984,13 +992,25 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
"body" => ["foo-post" => "baz"] "body" => ["foo-post" => "baz"]
]); ]);
$this->assertEquals("200", $response["code"], "multi call: post response code not matching"); $this->assertEquals("200", $response["code"], "multi call: post response code not matching");
$request_expected = json_decode(
<<<JSON
{
"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"}
}
JSON,
true
);
$this->assertEquals( $this->assertEquals(
'{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' $request_expected,
. '"HTTP_SECOND_CALL":"post","HTTP_ACCEPT":"*\/*",' json_decode($response['content'], true),
. '"HTTP_HOST":"soba.egplusww.jp"},'
. '"REQUEST_TYPE":"POST",'
. '"PARAMS":[],"BODY":{"foo-post":"baz"}}',
$response['content'],
'multi call: post content not matching' 'multi call: post content not matching'
); );
// delete // delete
@@ -998,13 +1018,24 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
"headers" => ["third-call" => "delete"], "headers" => ["third-call" => "delete"],
]); ]);
$this->assertEquals("200", $response["code"], "multi call: delete response code not matching"); $this->assertEquals("200", $response["code"], "multi call: delete response code not matching");
$request_expected = json_decode(
<<<JSON
{
"HEADERS":{
"HTTP_HOST":"soba.egplusww.jp",
"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",
"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*"
},
"REQUEST_TYPE":"DELETE",
"PARAMS":[],
"BODY":[]
}
JSON,
true
);
$this->assertEquals( $this->assertEquals(
'{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' $request_expected,
. '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*",' json_decode($response['content'], true),
. '"HTTP_HOST":"soba.egplusww.jp"},'
. '"REQUEST_TYPE":"DELETE",'
. '"PARAMS":[],"BODY":[]}',
$response['content'],
'multi call: delete content not matching' 'multi call: delete content not matching'
); );
} }