Compare commits

...

100 Commits

Author SHA1 Message Date
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
Clemens Schwaighofer
346cdaad72 Fix for params regex comment update 2024-12-11 11:23:19 +09:00
Clemens Schwaighofer
6887f17e15 Release: v9.23.2 2024-12-10 15:31:45 +09:00
Clemens Schwaighofer
5b1ca4241c phan min php update to 8.3, add missing phpunit test folder for language check 2024-12-10 15:29:54 +09:00
Clemens Schwaighofer
c8d6263c0f Fix DB IO placeholder count 2024-12-10 14:59:58 +09:00
Clemens Schwaighofer
bd1972d894 Composer keywords 2024-12-10 14:52:50 +09:00
Clemens Schwaighofer
fa29477c80 Release: v9.23.1 2024-12-05 14:08:56 +09:00
Clemens Schwaighofer
20ee958db9 Session class update with many methods and general clean up 2024-12-05 14:07:39 +09:00
Clemens Schwaighofer
157616582f Release: v9.23.0 2024-12-04 16:40:51 +09:00
Clemens Schwaighofer
0f7bf0ab44 Session class rewrite 2024-12-04 14:22:26 +09:00
Clemens Schwaighofer
10dc56c7cb Release: v9.22.0 2024-12-03 13:34:56 +09:00
Clemens Schwaighofer
d19842007c ACL Login cuid and cuuid update and add 2024-12-03 13:29:50 +09:00
Clemens Schwaighofer
c5bd16de74 Release: v9.21.1 2024-12-02 17:20:25 +09:00
Clemens Schwaighofer
e580e5430c Add ECUID (edit user cuid) to session and return in acl array 2024-12-02 17:18:34 +09:00
Clemens Schwaighofer
da9717265c Release: v9.21.0 2024-12-02 15:55:33 +09:00
Clemens Schwaighofer
6c6b33cacc add uuidv4 verify method 2024-12-02 15:54:35 +09:00
Clemens Schwaighofer
5509d1c92e Release: v9.20.1 2024-11-27 14:43:58 +09:00
Clemens Schwaighofer
16d789f061 Merge branch 'development' 2024-11-27 14:42:18 +09:00
Clemens Schwaighofer
3450b6263b Fixes in SQL PgSQL for identity column 2024-11-27 14:42:04 +09:00
Clemens Schwaighofer
bcc1e833c1 Release: v9.20.0 2024-11-20 22:54:37 +09:00
Clemens Schwaighofer
fea4c315f3 Merge branch 'development' 2024-11-20 22:52:27 +09:00
Clemens Schwaighofer
3ac57e05b0 DB SQL placeholder code updated 2024-11-20 22:52:15 +09:00
Clemens Schwaighofer
b0230e7050 Release: v9.19.1 2024-11-19 12:04:32 +09:00
Clemens Schwaighofer
70f78a5f9c Math update for Matrix multiplications 2024-11-19 12:03:12 +09:00
Clemens Schwaighofer
814ec50d3d phpstan 2.0 update 2024-11-18 18:34:31 +09:00
Clemens Schwaighofer
e60ce3f0dc Release: v9.19.0 2024-11-18 15:04:03 +09:00
Clemens Schwaighofer
6140bd8671 Color converter update with full sRGB/CieLab/OkLab conversion 2024-11-18 15:02:05 +09:00
Clemens Schwaighofer
e318a4fb9a Release: v9.18.1 2024-11-07 11:49:50 +09:00
Clemens Schwaighofer
9818410889 Local phan/phpstan install and config update, merge in UrlRequest changes 2024-11-07 11:39:33 +09:00
Clemens Schwaighofer
62e1e3b97c Release: v9.18.0 2024-11-06 18:59:35 +09:00
Clemens Schwaighofer
1bb4d5f426 UrlRequest curl class added 2024-11-06 18:56:17 +09:00
Clemens Schwaighofer
a65485e56a Release: v9.17.6.1 2024-11-06 13:37:05 +09:00
Clemens Schwaighofer
5e6ba85639 Merge branch 'development' 2024-10-17 14:04:45 +09:00
Clemens Schwaighofer
3cf891a7d2 Release: v9.17.6 2024-10-17 14:02:14 +09:00
Clemens Schwaighofer
6e19f30ff5 Bug fix in DB\IO 2024-10-17 14:01:13 +09:00
Clemens Schwaighofer
95079885c5 Release: v9.17.5 2024-10-17 13:56:59 +09:00
Clemens Schwaighofer
14dab54f2c Update DB\IO and do not print call steck on DB_INFO calls, array list
entry clean up
2024-10-17 13:55:38 +09:00
Clemens Schwaighofer
180fba596a Release: v9.17.4 2024-10-16 16:51:20 +09:00
Clemens Schwaighofer
69e2503a36 Admin\Backend change non filled dat part comment 2024-10-16 16:50:17 +09:00
Clemens Schwaighofer
c06ae55919 Release: v9.17.3 2024-10-16 16:31:05 +09:00
Clemens Schwaighofer
6098d1091a Update the Admin\Backend edit log call with query params and different data compressors 2024-10-16 16:29:29 +09:00
Clemens Schwaighofer
2a8038835f Release: v9.17.2 2024-10-16 12:37:41 +09:00
94 changed files with 18460 additions and 3278 deletions

View File

@@ -54,7 +54,8 @@ return [
// Note that the **only** effect of choosing `'5.6'` is to infer that functions removed in php 7.0 exist.
// (See `backward_compatibility_checks` for additional options)
// Automatically inferred from composer.json requirement for "php" of ">=8.2"
'target_php_version' => '8.1',
'target_php_version' => '8.2',
"minimum_target_php_version" => "8.2",
// If enabled, missing properties will be created when
// they are first seen. If false, we'll report an
@@ -370,6 +371,7 @@ return [
'file_list' => [
"./test/configs/config.php",
"./test/configs/config.other.php",
"./test/configs/config.path.php",
"./test/configs/config.master.php",
],
];

View File

@@ -3,6 +3,7 @@
"description": "CoreLibs in a composer package",
"type": "library",
"license": "MIT",
"keywords": ["corelib", "logging", "database", "templating", "tools"],
"autoload": {
"psr-4": {
"CoreLibs\\": "src/"
@@ -20,7 +21,11 @@
"psr/log": "^3.0@dev"
},
"require-dev": {
"egrajp/smarty-extended": "^4.3",
"phpstan/phpstan": "^2.0",
"phpstan/phpdoc-parser": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phan/phan": "^5.4",
"egrajp/smarty-extended": "^5.4",
"gullevek/dotenv": "dev-master",
"phpunit/phpunit": "^9"
},

View File

@@ -8,5 +8,6 @@ $_SERVER['HTTP_HOST'] = 'soba.tokyo.tequila.jp';
// for whatever reason it does not load that from the confing.master.php
// for includes/admin_header.php
define('BASE_NAME', '');
define('CONTENT_PATH', '');
// __END__

View File

@@ -22,6 +22,9 @@ parameters:
# - vendor
# ignore errores with
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
# 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

View File

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

View File

@@ -1 +1 @@
9.17.1
9.33.0

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

@@ -31,6 +31,9 @@ declare(strict_types=1);
namespace CoreLibs\Admin;
use CoreLibs\Create\Uids;
use CoreLibs\Convert\Json;
class Backend
{
// page name
@@ -42,7 +45,7 @@ class Backend
/** @var array<string> */
public array $action_list = [
'action', 'action_id', 'action_sub_id', 'action_yes', 'action_flag',
'action_menu', 'action_value', 'action_error', 'action_loaded'
'action_menu', 'action_value', 'action_type', 'action_error', 'action_loaded'
];
/** @var string */
public string $action;
@@ -61,20 +64,31 @@ class Backend
/** @var string */
public string $action_value;
/** @var string */
public string $action_type;
/** @var string */
public string $action_error;
// ACL array variable if we want to set acl data from outisde
/** @var array<mixed> */
public array $acl = [];
/** @var int */
public int $default_acl;
// queue key
/** @var string */
public string $queue_key;
/** @var array<string> list of allowed types for edit log write */
private const WRITE_TYPES = ['BINARY', 'BZIP2', 'LZIP', 'STRING', 'SERIAL', 'JSON'];
/** @var array<string> list of available write types for log */
private array $write_types_available = [];
// the current active edit access id
/** @var int|null */
public int|null $edit_access_id;
/** @var string */
public string $page_name;
// error/warning/info messages
/** @var array<mixed> */
public array $messages = [];
@@ -84,6 +98,7 @@ class Backend
public bool $warning = false;
/** @var bool */
public bool $info = false;
// internal lang & encoding vars
/** @var string */
public string $lang_dir = '';
@@ -95,6 +110,7 @@ class Backend
public string $domain;
/** @var string */
public string $encoding;
/** @var \CoreLibs\Logging\Logging logger */
public \CoreLibs\Logging\Logging $log;
/** @var \CoreLibs\DB\IO database */
@@ -103,6 +119,7 @@ class Backend
public \CoreLibs\Language\L10n $l;
/** @var \CoreLibs\Create\Session session class */
public \CoreLibs\Create\Session $session;
// smarty publics [end processing in smarty class]
/** @var array<mixed> */
public array $DATA = [];
@@ -175,6 +192,9 @@ class Backend
if (preg_match("/^(add|save|delete|remove|move|up|down|push_live)$/", $this->action ?? '')) {
$this->queue_key = \CoreLibs\Create\RandomKey::randomKeyGen(3);
}
// check what edit log data write types are allowed
$this->adbSetEditLogWriteTypeAvailable();
}
/**
@@ -185,7 +205,26 @@ class Backend
// NO OP
}
// PUBLIC METHODS |=================================================>
// MARK: PRIVATE METHODS
/**
* set the write types that are allowed
*
* @return void
*/
private function adbSetEditLogWriteTypeAvailable()
{
// check what edit log data write types are allowed
$this->write_types_available = self::WRITE_TYPES;
if (!function_exists('bzcompress')) {
$this->write_types_available = array_diff($this->write_types_available, ['BINARY', 'BZIP']);
}
if (!function_exists('gzcompress')) {
$this->write_types_available = array_diff($this->write_types_available, ['LZIP']);
}
}
// MARK: PUBLIC METHODS |=================================================>
/**
* set internal ACL from login ACL
@@ -220,30 +259,94 @@ class Backend
}
}
/**
* return all the action data, if not set, sets entry to null
*
* @return array{action:?string,action_id:null|string|int,action_sub_id:null|string|int,action_yes:null|string|int|bool,action_flag:?string,action_menu:?string,action_loaded:?string,action_value:?string,action_type:?string,action_error:?string}
*/
public function adbGetActionSet(): array
{
return [
'action' => $this->action ?? null,
'action_id' => $this->action_id ?? null,
'action_sub_id' => $this->action_sub_id ?? null,
'action_yes' => $this->action_yes ?? null,
'action_flag' => $this->action_flag ?? null,
'action_menu' => $this->action_menu ?? null,
'action_loaded' => $this->action_loaded ?? null,
'action_value' => $this->action_value ?? null,
'action_type' => $this->action_type ?? null,
'action_error' => $this->action_error ?? null,
];
}
/**
* writes all action vars plus other info into edit_log table
*
* @param string $event any kind of event description,
* @param string|array<mixed> $data any kind of data related to that event
* @param string $write_type write type can bei STRING or BINARY
* @param string|null $db_schema override target schema
* @param string $event [default=''] any kind of event description,
* @param string|array<mixed> $data [default=''] any kind of data related to that event
* @param string $write_type [default=JSON] write type can be
* JSON, STRING/SERIEAL, BINARY/BZIP or ZLIB
* @param string|null $db_schema [default=null] override target schema
* @return void
* @deprecated Use $login->writeLog($event, $data, action_set:$cms->adbGetActionSet(), write_type:$write_type)
*/
public function adbEditLog(
string $event = '',
string|array $data = '',
string $write_type = 'STRING',
string $write_type = 'JSON',
?string $db_schema = null
): void {
$data_binary = '';
$data_write = '';
if ($write_type == 'BINARY') {
$data_binary = $this->db->dbEscapeBytea((string)bzcompress(serialize($data)));
$data_write = 'see bzip compressed data_binary field';
// check if write type is valid, if not fallback to JSON
if (!in_array($write_type, $this->write_types_available)) {
$this->log->warning('Write type not in allowed array, fallback to JSON', context:[
"write_type" => $write_type,
"write_list" => $this->write_types_available,
]);
$write_type = 'JSON';
}
if ($write_type == 'STRING') {
$data_binary = '';
$data_write = $this->db->dbEscapeString(serialize($data));
switch ($write_type) {
case 'BINARY':
case 'BZIP':
$data_binary = $this->db->dbEscapeBytea((string)bzcompress(serialize($data)));
$data_write = Json::jsonConvertArrayTo([
'type' => 'BZIP',
'message' => 'see bzip compressed data_binary field'
]);
break;
case 'ZLIB':
$data_binary = $this->db->dbEscapeBytea((string)gzcompress(serialize($data)));
$data_write = Json::jsonConvertArrayTo([
'type' => 'ZLIB',
'message' => 'see zlib compressed data_binary field'
]);
break;
case 'STRING':
case 'SERIAL':
$data_binary = $this->db->dbEscapeBytea(Json::jsonConvertArrayTo([
'type' => 'SERIAL',
'message' => 'see serial string data field'
]));
$data_write = serialize($data);
break;
case 'JSON':
$data_binary = $this->db->dbEscapeBytea(Json::jsonConvertArrayTo([
'type' => 'JSON',
'message' => 'see json string data field'
]));
// must be converted to array
if (!is_array($data)) {
$data = ["data" => $data];
}
$data_write = Json::jsonConvertArrayTo($data);
break;
default:
$this->log->alert('Invalid type for data compression was set', context:[
"write_type" => $write_type
]);
break;
}
/** @var string $DB_SCHEMA check schema */
@@ -253,44 +356,69 @@ class Backend
} elseif (!empty($this->db->dbGetSchema())) {
$DB_SCHEMA = $this->db->dbGetSchema();
}
$q = "INSERT INTO " . $DB_SCHEMA . ".edit_log "
. "(euid, event_date, event, data, data_binary, page, "
. "ip, user_agent, referer, script_name, query_string, server_name, http_host, "
. "http_accept, http_accept_charset, http_accept_encoding, session_id, "
. "action, action_id, action_yes, action_flag, action_menu, action_loaded, action_value, action_error) "
. "VALUES "
. "(" . $this->db->dbEscapeString(isset($_SESSION['EUID']) && is_numeric($_SESSION['EUID']) ?
$_SESSION['EUID'] :
'NULL')
. ", "
. "NOW(), "
. "'" . $this->db->dbEscapeString((string)$event) . "', "
. "'" . $data_write . "', "
. "'" . $data_binary . "', "
. "'" . $this->db->dbEscapeString((string)$this->page_name) . "', "
. "'" . ($_SERVER["REMOTE_ADDR"] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_USER_AGENT'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_REFERER'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['SCRIPT_FILENAME'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['QUERY_STRING'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['SERVER_NAME'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_HOST'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_ACCEPT'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_ACCEPT_CHARSET'] ?? '') . "', "
. "'" . $this->db->dbEscapeString($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '') . "', "
. ($this->session->getSessionId() === false ?
"NULL" :
"'" . $this->session->getSessionId() . "'")
. ", "
. "'" . $this->db->dbEscapeString($this->action ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_id ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_yes ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_flag ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_menu ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_loaded ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_value ?? '') . "', "
. "'" . $this->db->dbEscapeString($this->action_error ?? '') . "')";
$this->db->dbExec($q, 'NULL');
$q = <<<SQL
INSERT INTO {DB_SCHEMA}.edit_log (
username, euid, eucuid, eucuuid, event_date, event, error, data, data_binary, page,
ip, user_agent, referer, script_name, query_string, server_name, http_host,
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_value, action_type, action_error
) VALUES (
$1, $2, $3, $4, NOW(), $5, $6, $7, $8, $9,
$10, $11, $12, $13, $14, $15, $16,
$17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27,
$28, $29, $30
)
SQL;
$this->db->dbExecParams(
str_replace(
['{DB_SCHEMA}'],
[$DB_SCHEMA],
$q
),
[
// row 1
'',
is_numeric($this->session->get('EUID')) ?
$this->session->get('EUID') : null,
is_string($this->session->get('ECUID')) ?
$this->session->get('ECUID') : null,
!empty($this->session->get('ECUUID')) && Uids::validateUuuidv4($this->session->get('ECUID')) ?
$this->session->get('ECUID') : null,
(string)$event,
'',
$data_write,
$data_binary,
(string)$this->page_name,
// row 2
$_SERVER["REMOTE_ADDR"] ?? '',
$_SERVER['HTTP_USER_AGENT'] ?? '',
$_SERVER['HTTP_REFERER'] ?? '',
$_SERVER['SCRIPT_FILENAME'] ?? '',
$_SERVER['QUERY_STRING'] ?? '',
$_SERVER['SERVER_NAME'] ?? '',
$_SERVER['HTTP_HOST'] ?? '',
// row 3
$_SERVER['HTTP_ACCEPT'] ?? '',
$_SERVER['HTTP_ACCEPT_CHARSET'] ?? '',
$_SERVER['HTTP_ACCEPT_ENCODING'] ?? '',
$this->session->getSessionId() !== '' ?
$this->session->getSessionId() : null,
// row 4
$this->action ?? '',
$this->action_id ?? '',
$this->action_sub_id ?? '',
$this->action_yes ?? '',
$this->action_flag ?? '',
$this->action_menu ?? '',
$this->action_loaded ?? '',
$this->action_value ?? '',
$this->action_type ?? '',
$this->action_error ?? '',
],
'NULL'
);
}
/**
@@ -327,10 +455,7 @@ class Backend
?string $set_content_path = null,
int $flag = 0,
): array {
if (
$set_content_path === null ||
!is_string($set_content_path)
) {
if ($set_content_path === null) {
/** @deprecated adbTopMenu missing set_content_path parameter */
trigger_error(
'Calling adbTopMenu without set_content_path parameter is deprecated',
@@ -343,7 +468,7 @@ class Backend
}
// get the session pages array
$PAGES = $_SESSION['PAGES'] ?? null;
$PAGES = $this->session->get('PAGES');
if (!isset($PAGES) || !is_array($PAGES)) {
$PAGES = [];
}
@@ -540,16 +665,30 @@ class Backend
} elseif (!empty($this->db->dbGetSchema())) {
$DB_SCHEMA = $this->db->dbGetSchema();
}
$q = "INSERT INTO " . $DB_SCHEMA . ".live_queue ("
. "queue_key, key_value, key_name, type, target, data, group_key, action, associate, file"
. ") VALUES ("
. "'" . $this->db->dbEscapeString($queue_key) . "', '" . $this->db->dbEscapeString($key_value) . "', "
. "'" . $this->db->dbEscapeString($key_name) . "', '" . $this->db->dbEscapeString($type) . "', "
. "'" . $this->db->dbEscapeString($target) . "', '" . $this->db->dbEscapeString($data) . "', "
. "'" . $this->queue_key . "', '" . $this->action . "', "
. "'" . $this->db->dbEscapeString((string)$associate) . "', "
. "'" . $this->db->dbEscapeString((string)$file) . "')";
$this->db->dbExec($q);
$q = <<<SQL
INSERT INTO {DB_SCHEMA}.live_queue (
queue_key, key_value, key_name, type,
target, data, group_key, action, associate, file
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8, $9, $10
)
SQL;
// $this->db->dbExec($q);
$this->db->dbExecParams(
str_replace(
['{DB_SCHEMA}'],
[$DB_SCHEMA],
$q
),
[
$queue_key, $key_value,
$key_name, $type,
$target, $data,
$this->queue_key, $this->action,
(string)$associate, (string)$file
]
);
}
/**

View File

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

View File

@@ -103,11 +103,7 @@ class Basic
'VIDEOS', 'DOCUMENTS', 'PDFS', 'BINARIES', 'ICONS', 'UPLOADS', 'CSV', 'JS',
'CSS', 'TABLE_ARRAYS', 'SMARTY', 'LANG', 'CACHE', 'TMP', 'LOG', 'TEMPLATES',
'TEMPLATES_C', 'DEFAULT_LANG', 'DEFAULT_ENCODING', 'DEFAULT_HASH',
'DEFAULT_ACL_LEVEL', 'LOGOUT_TARGET', 'PASSWORD_CHANGE', 'AJAX_REQUEST_TYPE',
'USE_PROTOTYPE', 'USE_SCRIPTACULOUS', 'USE_JQUERY', 'PAGE_WIDTH',
'MASTER_TEMPLATE_NAME', 'PUBLIC_SCHEMA', 'TEST_SCHEMA', 'DEV_SCHEMA',
'LIVE_SCHEMA', 'DB_CONFIG_NAME', 'DB_CONFIG', 'TARGET', 'DEBUG',
'SHOW_ALL_ERRORS'
'DB_CONFIG_NAME', 'DB_CONFIG', 'TARGET'
] as $constant
) {
if (!defined($constant)) {
@@ -387,7 +383,8 @@ class Basic
public function initRandomKeyLength(int $key_length): bool
{
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
* check that the source is actually matching
* 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
*/
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);
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
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__sha1Short()', E_USER_DEPRECATED);
return \CoreLibs\Create\Hash::__sha1Short($string, $use_sha);
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::sha1Short() or ::__crc32b()', E_USER_DEPRECATED);
if ($use_sha) {
return \CoreLibs\Create\Hash::sha1Short($string);
} else {
return \CoreLibs\Create\Hash::__crc32b($string);
}
}
/**
@@ -1044,8 +1045,8 @@ class Basic
*/
public function __hash(string $string, string $hash_type = 'adler32'): string
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::__hash()', E_USER_DEPRECATED);
return \CoreLibs\Create\Hash::__hash($string, $hash_type);
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Create\Hash::hash()', E_USER_DEPRECATED);
return \CoreLibs\Create\Hash::hash($string, $hash_type);
}
// *** HASH FUNCTIONS END
@@ -1139,118 +1140,6 @@ class Basic
// *** BETTER PASSWORD OPTIONS END ***
// *** COLORS ***
// [!!! DEPRECATED !!!]
// moved to \CoreLibs\Convert\Colors
/**
* converts a hex RGB color to the int numbers
* @param string $hexStr RGB hexstring
* @param bool $returnAsString flag to return as string
* @param string $seperator string seperator: default: ","
* @return string|array<mixed>|bool false on error or array with RGB or
* a string with the seperator
* @deprecated use \CoreLibs\Convert\Colors::hex2rgb() instead
*/
public static function hex2rgb(string $hexStr, bool $returnAsString = false, string $seperator = ',')
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::hex2rgb()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::hex2rgb($hexStr, $returnAsString, $seperator);
}
/**
* converts the rgb values from int data to the valid rgb html hex string
* optional can turn of leading #
* @param int $red red 0-255
* @param int $green green 0-255
* @param int $blue blue 0-255
* @param bool $hex_prefix default true, prefix with "#"
* @return string|bool rgb in hex values with leading # if set
* @deprecated use \CoreLibs\Convert\Colors::rgb2hex() instead
*/
public static function rgb2hex(int $red, int $green, int $blue, bool $hex_prefix = true)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::rgb2hex()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::rgb2hex($red, $green, $blue, $hex_prefix);
}
/**
* converts and int RGB to the HTML color string in hex format
* @param int $red red 0-255
* @param int $green green 0-255
* @param int $blue blue 0-255
* @return string|bool hex rgb string
* @deprecated use rgb2hex instead
*/
public static function rgb2html(int $red, int $green, int $blue)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::rgb2hex()', E_USER_DEPRECATED);
// check that each color is between 0 and 255
return \CoreLibs\Convert\Colors::rgb2hex($red, $green, $blue, true);
}
/**
* converts RGB to HSB/V values
* returns:
* array with hue (0-360), sat (0-100%), brightness/value (0-100%)
* @param int $red red 0-255
* @param int $green green 0-255
* @param int $blue blue 0-255
* @return array<mixed>|bool Hue, Sat, Brightness/Value
* @deprecated use \CoreLibs\Convert\Colors::rgb2hsb() instead
*/
public static function rgb2hsb(int $red, int $green, int $blue)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::rgb2hsb()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::rgb2hsb($red, $green, $blue);
}
/**
* converts HSB/V to RGB values RGB is full INT
* @param int $H hue 0-360
* @param float $S saturation 0-1 (float)
* @param float $V brightness/value 0-1 (float)
* @return array<mixed>|bool 0 red/1 green/2 blue array
* @deprecated use \CoreLibs\Convert\Colors::hsb2rgb() instead
*/
public static function hsb2rgb(int $H, float $S, float $V)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::hsb2rgb()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::hsb2rgb($H, (int)round($S * 100), (int)round($V * 100));
}
/**
* converts a RGB (0-255) to HSL
* return:
* array with hue (0-360), saturation (0-100%) and luminance (0-100%)
* @param int $r red 0-255
* @param int $g green 0-255
* @param int $b blue 0-255
* @return array<mixed>|bool hue/sat/luminance
* @deprecated use \CoreLibs\Convert\Colors::rgb2hsl() instead
*/
public static function rgb2hsl(int $r, int $g, int $b)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::rgb2hsl()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::rgb2hsb($r, $g, $b);
}
/**
* converts an HSL to RGB
* @param int $h hue: 0-360 (degrees)
* @param float $s saturation: 0-1
* @param float $l luminance: 0-1
* @return array<mixed>|bool red/blue/green 0-255 each
* @deprecated use \CoreLibs\Convert\Colors::hsl2rgb() instead
*/
public static function hsl2rgb(int $h, float $s, float $l)
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Convert\Colors::hsl2rgb()', E_USER_DEPRECATED);
return \CoreLibs\Convert\Colors::hsl2rgb($h, $s * 100, $l * 100);
}
// *** COLORS END ***
// *** EMAIL FUNCTIONS ***
// [!!! DEPRECATED !!!]
// Moved to \CoreLibs\Check\Email

View File

@@ -119,6 +119,13 @@ class Colors
/**
* check if html/css color string is valid
*
* TODO: update check for correct validate values
* - space instead of ","
* - / opcatiy checks
* - loose numeric values
* - lab/lch,oklab/oklch validation too
*
* @param string $color A color string of any format
* @param int $flags defaults to ALL, else use | to combined from
* HEX_RGB, HEX_RGBA, RGB, RGBA, HSL, HSLA
@@ -168,9 +175,9 @@ class Colors
if (preg_match("/$regex/", $color)) {
// if valid regex, we now need to check if the content is actually valid
// only for rgb/hsl type
/** @var int|false */
/** @var int<0, max>|false */
$rgb_flag = strpos($color, 'rgb');
/** @var int|false */
/** @var int<0, max>|false */
$hsl_flag = strpos($color, 'hsl');
// if both not match, return true
if (

View File

@@ -56,7 +56,11 @@ class Encoding
{
// return mb_substitute_character();
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 {
return self::$mb_error_char;
}
@@ -88,7 +92,13 @@ class Encoding
): array|false {
// convert to target encoding and convert back
$temp = mb_convert_encoding($string, $to_encoding, $from_encoding);
if ($temp === false) {
return false;
}
$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 == $compare) {
return false;

View File

@@ -10,6 +10,8 @@ namespace CoreLibs\Combined;
class ArrayHandler
{
public const string DATA_SEPARATOR = ':';
/**
* searches key = value in an array / array
* only returns the first one found
@@ -148,18 +150,22 @@ class ArrayHandler
* 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
*
* @param array<mixed> $array search in as array
* @param string|int $key key (key to search in)
* @param string|int|bool $value value (what to find)
* @param bool $strict [false], if set to true, will strict check key/value
* @return bool true on found, false on not found
* @param array<mixed> $array search in as array
* @param string|int $key key (key to search in)
* @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
* @return bool true on found, false on not found
*/
public static function arraySearchSimple(
array $array,
string|int $key,
string|int|bool $value,
string|int|bool|array $value,
bool $strict = false
): bool {
// convert to array
if (!is_array($value)) {
$value = [$value];
}
foreach ($array as $_key => $_value) {
// if value is an array, we search
if (is_array($_value)) {
@@ -167,9 +173,9 @@ class ArrayHandler
if (($result = self::arraySearchSimple($_value, $key, $value, $strict)) !== false) {
return $result;
}
} elseif ($strict === false && $_key == $key && $_value == $value) {
} elseif ($strict === false && $_key == $key && in_array($_value, $value)) {
return true;
} elseif ($strict === true && $_key === $key && $_value === $value) {
} elseif ($strict === true && $_key === $key && in_array($_value, $value, true)) {
return true;
}
}
@@ -236,6 +242,160 @@ class ArrayHandler
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> $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 $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 ($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> $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 $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 ($array == []) {
return [];
}
// init return result
$result = [];
// case sensitive convert if string
if ($case_insensitive && is_string($search)) {
$search = strtolower($search);
}
foreach ($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
*
@@ -509,6 +669,145 @@ class ArrayHandler
}
return $array;
}
/**
* Remove entries from a simple array, will not keep key order
*
* any array content is allowed
*
* https://stackoverflow.com/a/369608
*
* @param array<mixed> $array Array where elements are located
* @param array<mixed> $remove Elements to remove
* @return array<mixed> Array with $remove elements removed
*/
public static function arrayRemoveEntry(array $array, array $remove): array
{
return array_diff($array, $remove);
}
/**
* From the array with key -> mixed values,
* return only the entries where the key matches the key given in the key list parameter
*
* key list is a list[string]
* if key list is empty, return array as is
*
* @param array<string,mixed> $array
* @param array<string> $key_list
* @return array<string,mixed>
*/
public static function arrayReturnMatchingKeyOnly(
array $array,
array $key_list
): array {
// on empty return as is
if (empty($key_list)) {
return $array;
}
return array_filter(
$array,
fn($key) => in_array($key, $key_list),
ARRAY_FILTER_USE_KEY
);
}
/**
* Modifieds the key of an array with a prefix and/or suffix and
* returns it with the original value
* does not change order in array
*
* @param array<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> $array array to sort by values
* @param int $params sort flags
* @return array<mixed>
*/
public static function sortArray(
array $array,
bool $case_insensitive = false,
bool $reverse = false,
bool $maintain_keys = false,
int $params = 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($array, $reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case)) :
(usort($array, $reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case))
) :
(
$maintain_keys ?
($reverse ? arsort($array, $params) : asort($array, $params)) :
($reverse ? rsort($array, $params) : sort($array, $params))
);
return $array;
}
/**
* sort by key ascending or descending and return
*
* @param array<mixed> $array
* @param bool $case_insensitive [false]
* @param bool $reverse [false]
* @return array<mixed>
*/
public static function ksortArray(array $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(
$array,
$case_insensitive ?
($reverse ? $fk_sort_lower_case_reverse : $fk_sort_lower_case) :
($reverse ? $fk_sort_reverse : $fk_sort)
);
return $array;
}
}
// __END__

View File

@@ -639,16 +639,26 @@ class DateTime
*
* @param string $start_date valid start date (y/m/d)
* @param string $end_date valid end date (y/m/d)
* @param bool $return_named return array type, false (default), true for named
* @return array<mixed> 0/overall, 1/weekday, 2/weekend
* @param bool $return_named [default=false] return array type, false (default), true for named
* @param bool $include_end_date [default=true] include end date in calc
* @param bool $exclude_start_date [default=false] include end date in calc
* @return array{0:int,1:int,2:int,3:bool}|array{overall:int,weekday:int,weekend:int,reverse:bool}
* 0/overall, 1/weekday, 2/weekend, 3/reverse
*/
public static function calcDaysInterval(
string $start_date,
string $end_date,
bool $return_named = false
bool $return_named = false,
bool $include_end_date = true,
bool $exclude_start_date = false
): array {
// pos 0 all, pos 1 weekday, pos 2 weekend
$days = [];
$days = [
0 => 0,
1 => 0,
2 => 0,
3 => false,
];
// if anything invalid, return 0,0,0
try {
$start = new \DateTime($start_date);
@@ -659,19 +669,30 @@ class DateTime
'overall' => 0,
'weekday' => 0,
'weekend' => 0,
'reverse' => false
];
} else {
return [0, 0, 0];
return $days;
}
}
// so we include the last day too, we need to add +1 second in the time
$end->setTime(0, 0, 1);
// if end date before start date, only this will be filled
$days[0] = $end->diff($start)->days;
$days[1] = 0;
$days[2] = 0;
// if start is before end, switch dates and flag
$days[3] = false;
if ($start > $end) {
$new_start = $end;
$end = $start;
$start = $new_start;
$days[3] = true;
}
// get period for weekends/weekdays
$period = new \DatePeriod($start, new \DateInterval('P1D'), $end);
$options = 0;
if ($include_end_date) {
$options |= \DatePeriod::INCLUDE_END_DATE;
}
if ($exclude_start_date) {
$options |= \DatePeriod::EXCLUDE_START_DATE;
}
$period = new \DatePeriod($start, new \DateInterval('P1D'), $end, $options);
foreach ($period as $dt) {
$curr = $dt->format('D');
if ($curr == 'Sat' || $curr == 'Sun') {
@@ -679,18 +700,80 @@ class DateTime
} else {
$days[1]++;
}
$days[0]++;
}
if ($return_named === true) {
return [
'overall' => $days[0],
'weekday' => $days[1],
'weekend' => $days[2],
'reverse' => $days[3],
];
} else {
return $days;
}
}
/**
* wrapper for calcDaysInterval with numeric return only
*
* @param string $start_date valid start date (y/m/d)
* @param string $end_date valid end date (y/m/d)
* @param bool $include_end_date [default=true] include end date in calc
* @param bool $exclude_start_date [default=false] include end date in calc
* @return array{0:int,1:int,2:int,3:bool}
*/
public static function calcDaysIntervalNumIndex(
string $start_date,
string $end_date,
bool $include_end_date = true,
bool $exclude_start_date = false
): array {
$values = self::calcDaysInterval(
$start_date,
$end_date,
false,
$include_end_date,
$exclude_start_date
);
return [
$values[0] ?? 0,
$values[1] ?? 0,
$values[2] ?? 0,
$values[3] ?? false,
];
}
/**
* wrapper for calcDaysInterval with named return only
*
* @param string $start_date valid start date (y/m/d)
* @param string $end_date valid end date (y/m/d)
* @param bool $include_end_date [default=true] include end date in calc
* @param bool $exclude_start_date [default=false] include end date in calc
* @return array{overall:int,weekday:int,weekend:int,reverse:bool}
*/
public static function calcDaysIntervalNamedIndex(
string $start_date,
string $end_date,
bool $include_end_date = true,
bool $exclude_start_date = false
): array {
$values = self::calcDaysInterval(
$start_date,
$end_date,
true,
$include_end_date,
$exclude_start_date
);
return [
'overall' => $values['overall'] ?? 0,
'weekday' => $values['weekday'] ?? 0,
'weekend' => $values['weekend'] ?? 0,
'reverse' => $values['reverse'] ?? false,
];
}
/**
* check if a weekend day (sat/sun) is in the given date range
* Can have time too, but is not needed
@@ -705,6 +788,13 @@ class DateTime
): bool {
$dd_start = new \DateTime($start_date);
$dd_end = new \DateTime($end_date);
// flip if start is after end
if ($dd_start > $dd_end) {
$new_start = $dd_end;
$dd_end = $dd_start;
$dd_start = $new_start;
}
// if start > end, flip
if (
// starts with a weekend
$dd_start->format('N') >= 6 ||

View File

@@ -0,0 +1,359 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/12
* DESCRIPTION:
* CIE XYZ color space conversion
* This for various interims work
* none public
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color;
use CoreLibs\Convert\Math;
use CoreLibs\Convert\Color\Coordinates\RGB;
use CoreLibs\Convert\Color\Coordinates\Lab;
use CoreLibs\Convert\Color\Coordinates\XYZ;
class CieXyz
{
// MARK: public wrapper functions
/**
* Convert from RGB to OkLab
* via xyz D65
*
* @param RGB $rgb
* @return Lab
*/
public static function rgbViaXyzD65ToOkLab(RGB $rgb): Lab
{
return self::xyzD65ToOkLab(
self::linRgbToXyzD65($rgb)
);
}
/**
* Convet from OkLab to RGB
* via xyz D65
*
* @param Lab $lab
* @return RGB
*/
public static function okLabViaXyzD65ToRgb(Lab $lab): RGB
{
return self::xyzD65ToLinRgb(
self::okLabToXyzD65($lab)
)->fromLinear();
}
/**
* Convert RGB to CIE Lab
* via xyz D65 to xyz D50
*
* @param RGB $rgb
* @return Lab
*/
public static function rgbViaXyzD65ViaXyzD50ToLab(RGB $rgb): Lab
{
return self::xyzD50ToLab(
self::xyzD65ToXyzD50(
self::linRgbToXyzD65($rgb)
)
);
}
/**
* Convert CIE Lab to RGB
* via xyz D50 to xyz D65
*
* @param Lab $lab
* @return RGB
*/
public static function labViaXyzD50ViaXyzD65ToRgb(Lab $lab): RGB
{
return self::xyzD65ToLinRgb(
self::xyzD50ToXyxD65(
self::labToXyzD50($lab)
)
)->fromLinear();
}
/**
* Convert from oklab to cie lab
*
* @param Lab $lab
* @return Lab
*/
public static function okLabViaXyzD65ViaXyzD50ToLab(Lab $lab): Lab
{
return self::xyzD50ToLab(
self::xyzD65ToXyzD50(
self::okLabToXyzD65($lab)
)
);
}
/**
* Convert from cie lab to oklab
*
* @param Lab $lab
* @return Lab
*/
public static function labViaXyzD50ViaXyzD65ToOkLab(Lab $lab): Lab
{
return self::xyzD65ToOkLab(
self::xyzD50ToXyxD65(
self::labToXyzD50($lab)
)
);
}
// MARK: helper convert any array to array{float, float, float}
/**
* This is a hack for phpstan until we write a proper matrix to class
* conversion wrapper function
*
* @param array<array<float|int>|float|int> $_array
* @return array{0:float,1:float,2:float}
*/
private static function convertArray(array $_array): array
{
/** @var array{0:float,1:float,2:float} */
return [$_array[0], $_array[1], $_array[2]];
}
// MARK: xyzD65 <-> xyzD50
/**
* xyzD65 to xyzD50 whitepoint
*
* @param XYZ $xyz
* @return XYZ
*/
private static function xyzD65ToXyzD50(XYZ $xyz): XYZ
{
return new XYZ(self::convertArray(Math::multiplyMatrices(
a: [
[1.0479298208405488, 0.022946793341019088, -0.05019222954313557],
[0.029627815688159344, 0.990434484573249, -0.01707382502938514],
[-0.009243058152591178, 0.015055144896577895, 0.7518742899580008],
],
b: $xyz->returnAsArray(),
)), options: ["whitepoint" => 'D50']);
}
/**
* xyzD50 to xyzD65 whitepoint
*
* @param XYZ $xyz
* @return XYZ
*/
private static function xyzD50ToXyxD65(XYZ $xyz): XYZ
{
return new XYZ(self::convertArray(Math::multiplyMatrices(
a: [
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753],
],
b: $xyz->returnAsArray()
)), options: ["whitepoint" => 'D65']);
}
// MARK: xyzD50 <-> Lab
/**
* Convert xyzD50 to Lab (Cie)
*
* @param XYZ $xyz
* @return Lab
*/
private static function xyzD50ToLab(XYZ $xyz): Lab
{
$_xyz = $xyz->returnAsArray();
$d50 = [
0.3457 / 0.3585,
1.00000,
(1.0 - 0.3457 - 0.3585) / 0.3585,
];
$a = 216 / 24389;
$b = 24389 / 27;
$_xyz = array_map(
fn ($k, $v) => $v / $d50[$k],
array_keys($_xyz),
array_values($_xyz),
);
$f = array_map(
fn ($v) => (($v > $a) ?
pow($v, 1 / 3) :
(($b * $v + 16) / 116)
),
$_xyz,
);
return new Lab([
(116 * $f[1]) - 16,
500 * ($f[0] - $f[1]),
200 * ($f[1] - $f[2]),
], colorspace: 'CIELab');
}
/**
* Convert Lab (Cie) to xyz D50
*
* @param Lab $lab
* @return XYZ
*/
private static function labToXyzD50(Lab $lab): XYZ
{
$_lab = $lab->returnAsArray();
$a = 24389 / 27;
$b = 216 / 24389;
$f = [];
$f[1] = ($_lab[0] + 16) / 116;
$f[0] = $_lab[1] / 500 + $f[1];
$f[2] = $f[1] - $_lab[2] / 200;
$xyz = [
// x
pow($f[0], 3) > $b ?
pow($f[0], 3) :
(116 * $f[0] - 16) / $a,
// y
$_lab[0] > $a * $b ?
pow(($_lab[0] + 16) / 116, 3) :
$_lab[0] / $a,
// z
pow($f[2], 3) > $b ?
pow($f[2], 3) :
(116 * $f[2] - 16) / $a,
];
$d50 = [
0.3457 / 0.3585,
1.00000,
(1.0 - 0.3457 - 0.3585) / 0.3585,
];
return new XYZ(
self::convertArray(array_map(
fn ($k, $v) => $v * $d50[$k],
array_keys($xyz),
$xyz,
)),
options: ["whitepoint" => 'D50']
);
}
// MARK: xyzD65 <-> (linear)RGB
/**
* convert linear RGB to xyz D65
* if rgb is not flagged linear, it will be auto converted
*
* @param RGB $rgb
* @return XYZ
*/
private static function linRgbToXyzD65(RGB $rgb): XYZ
{
// if not linear, convert to linear
if (!(bool)$rgb->get('linear')) {
$rgb = (new RGB($rgb->returnAsArray()))->toLinear();
}
return new XYZ(self::convertArray(Math::multiplyMatrices(
[
[0.41239079926595934, 0.357584339383878, 0.1804807884018343],
[0.21263900587151027, 0.715168678767756, 0.07219231536073371],
[0.01933081871559182, 0.11919477979462598, 0.9505321522496607],
],
$rgb->returnAsArray()
)), options: ["whitepoint" => 'D65']);
}
/**
* Convert xyz D65 to linear RGB
*
* @param XYZ $xyz
* @return RGB
*/
private static function xyzD65ToLinRgb(XYZ $xyz): RGB
{
// xyz D65 to linrgb
return new RGB(self::convertArray(Math::multiplyMatrices(
a : [
[ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ],
[ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ],
[ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ],
],
b : $xyz->returnAsArray()
)), options: ["linear" => true]);
}
// MARK: xyzD65 <-> OkLab
/**
* xyz D65 to OkLab
*
* @param XYZ $xyz
* @return Lab
*/
private static function xyzD65ToOkLab(XYZ $xyz): Lab
{
return new Lab(self::convertArray(Math::multiplyMatrices(
[
[0.2104542553, 0.7936177850, -0.0040720468],
[1.9779984951, -2.4285922050, 0.4505937099],
[0.0259040371, 0.7827717662, -0.8086757660],
],
array_map(
callback: fn ($v) => pow((float)$v, 1 / 3),
array: Math::multiplyMatrices(
a: [
[0.8190224432164319, 0.3619062562801221, -0.12887378261216414],
[0.0329836671980271, 0.9292868468965546, 0.03614466816999844],
[0.048177199566046255, 0.26423952494422764, 0.6335478258136937],
],
b: $xyz->returnAsArray(),
),
)
)), colorspace: 'OkLab');
}
/**
* xyz D65 to OkLab
*
* @param Lab $lab
* @return XYZ
*/
private static function okLabToXyzD65(Lab $lab): XYZ
{
return new XYZ(self::convertArray(Math::multiplyMatrices(
a: [
[1.2268798733741557, -0.5578149965554813, 0.28139105017721583],
[-0.04057576262431372, 1.1122868293970594, -0.07171106666151701],
[-0.07637294974672142, -0.4214933239627914, 1.5869240244272418],
],
b: array_map(
callback: fn ($v) => is_numeric($v) ? $v ** 3 : 0,
array: Math::multiplyMatrices(
a: [
[0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339],
[1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402],
[1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399],
],
// Divide $lightness by 100 to convert from CSS OkLab
b: $lab->returnAsArray(),
),
),
)), options: ["whitepoint" => 'D65']);
}
}
// __END__

1103
src/Convert/Color/Color.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: HSB/HSV
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class HSB implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['sRGB'];
/** @var float hue */
private float $H = 0.0;
/** @var float saturation */
private float $S = 0.0;
/** @var float brightness / value */
private float $B = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = ''; /** @phpstan-ignore-line */
/**
* HSB (HSV) color coordinates
* Hue/Saturation/Brightness or Value
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = [])
{
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors);
}
/**
* set from array
* where 0: Hue, 1: Saturation, 2: Brightness
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self
{
return new HSB($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
$name = strtoupper($name);
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
case 'H':
if ($value == 360.0) {
$value = 0;
}
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
);
}
break;
case 'S':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for saturation is not in the range of 0 to 100',
2
);
}
break;
case 'B':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for brightness is not in the range of 0 to 100',
3
);
}
break;
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
$name = strtoupper($name);
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Hue, 1: Saturation, 2: Brightness
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->H, $this->S, $this->B];
}
/**
* set color as array
* where 0: Hue, 1: Saturation, 2: Brightness
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('H', $colors[0]);
$this->set('S', $colors[1]);
$this->set('B', $colors[2]);
return $this;
}
/**
* no hsb in css
*
* @param float|string|null $opacity
* @return string
* @throws \ErrorException
*/
public function toCssString(null|float|string $opacity = null): string
{
throw new \ErrorException('HSB is not available as CSS color string', 0);
}
}
// __END__

View File

@@ -0,0 +1,195 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: HSL
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class HSL implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['sRGB'];
/** @var float hue */
private float $H = 0.0;
/** @var float saturation */
private float $S = 0.0;
/** @var float lightness (luminance) */
private float $L = 0.0;
/** @var string color space: either sRGB */
private string $colorspace = '';
/**
* Color Coordinate HSL
* Hue/Saturation/Lightness
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = [])
{
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors);
}
/**
* set from array
* where 0: Hue, 1: Saturation, 2: Lightness
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self
{
return new HSL($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
case 'H':
if ($value == 360.0) {
$value = 0;
}
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
);
}
break;
case 'S':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for saturation is not in the range of 0 to 100',
2
);
}
break;
case 'L':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100',
3
);
}
break;
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Hue, 1: Saturation, 2: Lightness
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->H, $this->S, $this->L];
}
/**
* set color as array
* where 0: Hue, 1: Saturation, 2: Lightness
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('H', $colors[0]);
$this->set('S', $colors[1]);
$this->set('L', $colors[2]);
return $this;
}
/**
* convert to css string with optional opacityt
*
* @param float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string
{
$string = 'hsl('
. $this->H
. ' '
. $this->S
. ' '
. $this->L
. Utils::setOpacity($opacity)
. ')';
return $string;
}
}
// __END__

View File

@@ -0,0 +1,195 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: HWB
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class HWB implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['sRGB'];
/** @var float Hue */
private float $H = 0.0;
/** @var float Whiteness */
private float $W = 0.0;
/** @var float Blackness */
private float $B = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = '';
/**
* Color Coordinate: HWB
* Hue/Whiteness/Blackness
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = [])
{
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors);
}
/**
* set from array
* where 0: Hue, 1: Whiteness, 2: Blackness
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=sRGB]
* @param array<string,string> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self
{
return new HWB($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
case 'H':
if ($value == 360.0) {
$value = 0;
}
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0 to 360',
1
);
}
break;
case 'W':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for whiteness is not in the range of 0 to 100',
2
);
}
break;
case 'B':
// if ($value < 0 || $value > 100) {
if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for blackness is not in the range of 0 to 100',
3
);
}
break;
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Hue, 1: Whiteness, 2: Blackness
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->H, $this->W, $this->B];
}
/**
* set color as array
* where 0: Hue, 1: Whiteness, 2: Blackness
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('H', $colors[0]);
$this->set('W', $colors[1]);
$this->set('B', $colors[2]);
return $this;
}
/**
* convert to css string with optional opacityt
*
* @param float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string
{
$string = 'hwb('
. $this->H
. ' '
. $this->W
. ' '
. $this->B
. Utils::setOpacity($opacity)
. ')';
return $string;
}
}
// __END__

View File

@@ -0,0 +1,53 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: Ymd
* DESCRIPTION:
* DescriptionHere
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates\Interface;
interface CoordinatesInterface
{
/**
* create class via "Class::create()" call
* was used for multiple create interfaces
* no longer needed, use "new Class()" instead
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default='']
* @param array<string,string|bool|int> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = '', array $options = []): self;
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool;
/**
* Returns the color as array
* where 0: Lightness, 1: a, 2: b
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array;
/**
* Convert into css string with optional opacity
*
* @param null|float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string;
}
// __END__

View File

@@ -0,0 +1,227 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: Lch
* for oklch or cie
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class LCH implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['OkLab', 'CIELab'];
/** @var float Lightness/Luminance
* CIE: 0 to 100
* OKlch: 0.0 to 1.0
* BOTH: 0% to 100%
*/
private float $L = 0.0;
/** @var float Chroma
* CIE: 0 to 150, cannot be more than 230
* OkLch: 0 to 0.4, does not exceed 0.5
* BOTH: 0% to 100% (0 to 150, 0 to 0.4)
*/
private float $C = 0.0;
/** @var float Hue
* 0 to 360 deg
*/
private float $H = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = '';
/**
* Color Coordinate Lch
* for oklch
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default='']
* @param array<string,string> $options [default=[]]
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(string|array $colors, string $colorspace = '', array $options = [])
{
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors);
}
/**
* set from array
* where 0: Lightness, 1: Chroma, 2: Hue
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default='']
* @param array<string,string> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = '', array $options = []): self
{
return new LCH($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
case 'L':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab',
1
);
}
break;
case 'C':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 230)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 230.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for chroma is not in the range of '
. '0 to 150 and a maximum of 230 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of '
. '0.0 to 0.4 and a maximum of 0.5 for OkLab',
1
);
}
break;
case 'H':
// if ($value < 0 || $value > 360) {
if (Utils::compare(0.0, $value, 360.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for hue is not in the range of 0.0 to 360.0',
1
);
}
break;
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Lightness, 1: Chroma, 2: Hue
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->L, $this->C, $this->H];
}
/**
* set color as array
* where 0: Lightness, 1: Chroma, 2: Hue
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('L', $colors[0]);
$this->set('C', $colors[1]);
$this->set('H', $colors[2]);
return $this;
}
/**
* Convert into css string with optional opacity
*
* @param null|float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string
{
$string = '';
switch ($this->colorspace) {
case 'CIELab':
$string = 'lch';
break;
case 'OkLab':
$string = 'oklch';
break;
}
$string .= '('
. $this->L
. ' '
. $this->C
. ' '
. $this->H
. Utils::setOpacity($opacity)
. ');';
return $string;
}
}
// __END__

View File

@@ -0,0 +1,233 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: Lab
* for oklab or cie
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class Lab implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['OkLab', 'CIELab'];
/** @var float lightness/luminance
* CIE: 0f to 100f
* OKlab: 0.0 to 1.0
* BOTH: 0% to 100%
*/
private float $L = 0.0;
/** @var float a axis distance
* CIE: -125 to 125, cannot be more than +/- 160
* OKlab: -0.4 to 0.4, cannot exceed +/- 0.5
* BOTH: -100% to 100% (+/-125 or 0.4)
*/
private float $a = 0.0;
/** @var float b axis distance
* CIE: -125 to 125, cannot be more than +/- 160
* OKlab: -0.4 to 0.4, cannot exceed +/- 0.5
* BOTH: -100% to 100% (+/-125 or 0.4)
*/
private float $b = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = '';
/**
* Color Coordinate: Lab
* for oklab or cie
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default='']
* @param array<string,string> $options [default=[]]
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(string|array $colors, string $colorspace = '', array $options = [])
{
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)->parseOptions($options)->setFromArray($colors);
}
/**
* set from array
* where 0: Lightness, 1: a, 2: b
*
* @param array{0:float,1:float,2:float} $colors
* @param string $colorspace [default='']
* @param array<string,string> $options [default=[]]
* @return self
*/
public static function create(string|array $colors, string $colorspace = '', array $options = []): self
{
return new Lab($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
switch ($name) {
case 'L':
// if ($this->colorspace == 'CIELab' && ($value < 0 || $value > 100)) {
if ($this->colorspace == 'CIELab' && Utils::compare(0.0, $value, 100.0, Utils::ESPILON_BIG)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0 to 100 for CIE Lab',
1
);
// } elseif ($this->colorspace == 'OkLab' && ($value < 0 || $value > 1)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(0.0, $value, 1.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for lightness is not in the range of 0.0 to 1.0 for OkLab',
1
);
}
break;
case 'a':
// if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) {
if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for a is not in the range of -125 to 125 for CIE Lab',
2
);
// } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for a is not in the range of -0.5 to 0.5 for OkLab',
2
);
}
break;
case 'b':
// if ($this->colorspace == 'CIELab' && ($value < -125 || $value > 125)) {
if ($this->colorspace == 'CIELab' && Utils::compare(-125.0, $value, 125.0, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for b is not in the range of -125 to 125 for CIE Lab',
3
);
// } elseif ($this->colorspace == 'OkLab' && ($value < -0.55 || $value > 0.55)) {
} elseif ($this->colorspace == 'OkLab' && Utils::compare(-0.55, $value, 0.55, Utils::EPSILON_SMALL)) {
throw new \LengthException(
'Argument value ' . $value . ' for b is not in the range of -0.5 to 0.5 for OkLab',
3
);
}
break;
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Lightness, 1: a, 2: b
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->L, $this->a, $this->b];
}
/**
* set color as array
* where 0: Lightness, 1: a, 2: b
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('L', $colors[0]);
$this->set('a', $colors[1]);
$this->set('b', $colors[2]);
return $this;
}
/**
* Convert into css string with optional opacity
*
* @param null|float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string
{
$string = '';
switch ($this->colorspace) {
case 'CIELab':
$string = 'lab';
break;
case 'OkLab':
$string = 'oklab';
break;
}
$string .= '('
. $this->L
. ' '
. $this->a
. ' '
. $this->b
. Utils::setOpacity($opacity)
. ');';
return $string;
}
}
// __END__

View File

@@ -0,0 +1,329 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: RGB
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
use CoreLibs\Convert\Color\Utils;
class RGB implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['sRGB'];
/** @var float red 0 to 255 or 0.0f to 1.0f for linear RGB */
private float $R = 0.0;
/** @var float green 0 to 255 or 0.0f to 1.0f for linear RGB */
private float $G = 0.0;
/** @var float blue 0 to 255 or 0.0f to 1.0f for linear RGB */
private float $B = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = '';
/** @var bool set if this is linear */
private bool $linear = false;
/**
* Color Coordinate RGB
* @param array{0:float,1:float,2:float}|string $colors RGB color array or hex string
* @param string $colorspace [default=sRGB]
* @param array<string,bool> $options [default=[]] only "linear" allowed at the moment
*/
public function __construct(string|array $colors, string $colorspace = 'sRGB', array $options = [])
{
$this->setColorspace($colorspace)->parseOptions($options);
if (is_array($colors)) {
$this->setFromArray($colors);
} else {
$this->setFromHex($colors);
}
}
/**
* set from array or string
* where 0: Red, 1: Green, 2: Blue
* OR #ffffff or ffffff
*
* @param array{0:float,1:float,2:float}|string $colors RGB color array or hex string
* @param string $colorspace [default=sRGB]
* @param array<string,bool> $options [default=[]] only "linear" allowed at the moment
* @return self
*/
public static function create(string|array $colors, string $colorspace = 'sRGB', array $options = []): self
{
return new RGB($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,bool> $options
* @return self
*/
private function parseOptions(array $options): self
{
$this->flagLinear($options['linear'] ?? false);
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
// do not allow setting linear from outside
if ($name == 'linear') {
return;
}
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
// if not linear
if (!$this->linear && ((int)$value < 0 || (int)$value > 255)) {
throw new \LengthException('Argument value ' . $value . ' for color ' . $name
. ' is not in the range of 0 to 255', 1);
} elseif (
// $this->linear && ($value < 0.0 || $value > 1.0)
$this->linear && Utils::compare(0.0, $value, 1.0, 0.000001)
) {
throw new \LengthException('Argument value ' . $value . ' for color ' . $name
. ' is not in the range of 0 to 1 for linear rgb', 2);
}
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float|bool
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* Returns the color as array
* where 0: Red, 1: Green, 2: Blue
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->R, $this->G, $this->B];
}
/**
* set color as array
* where 0: Red, 1: Green, 2: Blue
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('R', $colors[0]);
$this->set('G', $colors[1]);
$this->set('B', $colors[2]);
return $this;
}
/**
* Return current set RGB as hex string. with or without # prefix
*
* @param bool $hex_prefix
* @return string
*/
public function returnAsHex(bool $hex_prefix = true): string
{
// prefix
$hex_color = '';
if ($hex_prefix === true) {
$hex_color = '#';
}
// convert if in linear
if ($this->linear) {
$this->fromLinear();
}
foreach ($this->returnAsArray() as $color) {
$hex_color .= str_pad(dechex((int)$color), 2, '0', STR_PAD_LEFT);
}
return $hex_color;
}
/**
* set colors RGB from hex string
*
* @param string $hex_string
* @return self
*/
private function setFromHex(string $hex_string): self
{
$hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string
if (empty($hex_string) || !is_string($hex_string)) {
throw new \InvalidArgumentException('hex_string argument cannot be empty', 3);
}
$rgbArray = [];
if (strlen($hex_string) == 6) {
// If a proper hex code, convert using bitwise operation.
// No overhead... faster
$colorVal = hexdec($hex_string);
$rgbArray = [
0xFF & ($colorVal >> 0x10),
0xFF & ($colorVal >> 0x8),
0xFF & $colorVal
];
} elseif (strlen($hex_string) == 3) {
// If shorthand notation, need some string manipulations
$rgbArray = [
hexdec(str_repeat(substr($hex_string, 0, 1), 2)),
hexdec(str_repeat(substr($hex_string, 1, 1), 2)),
hexdec(str_repeat(substr($hex_string, 2, 1), 2))
];
} else {
// Invalid hex color code
throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 4);
}
return $this->setFromArray($rgbArray);
}
/**
* set as linear
* can be used as chain call on create if input is linear RGB
* RGB::__construct**(...)->flagLinear();
* as it returns self
*
* @return self
*/
private function flagLinear(bool $linear): self
{
$this->linear = $linear;
return $this;
}
/**
* Both function source:
* https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
* but reverse f: fromLinear and f_inv for toLinear
* Code copied from here:
* https://stackoverflow.com/a/12894053
*
* converts RGB to linear
* We come from 0-255 so we need to divide by 255
*
* @return self
*/
public function toLinear(): self
{
// if linear, as is
if ($this->linear) {
return $this;
}
$this->flagLinear(true)->setFromArray(array_map(
callback: function (int|float $v) {
$v = (float)($v / 255);
$abs = abs($v);
$sign = ($v < 0) ? -1 : 1;
return (float)(
$abs <= 0.04045 ?
$v / 12.92 :
$sign * pow(($abs + 0.055) / 1.055, 2.4)
);
},
array: $this->returnAsArray(),
));
return $this;
}
/**
* convert back to normal sRGB from linear RGB
* we go to 0-255 rgb so we multiply by 255
*
* @return self
*/
public function fromLinear(): self
{
// if not linear, as is
if (!$this->linear) {
return $this;
}
$this->flagLinear(false)->setFromArray(array_map(
callback: function (int|float $v) {
$abs = abs($v);
$sign = ($v < 0) ? -1 : 1;
// during reverse in some situations the values can become negative in very small ways
// like -...E16 and ...E17
return ($ret = (float)(255 * (
$abs <= 0.0031308 ?
$v * 12.92 :
$sign * (1.055 * pow($abs, 1.0 / 2.4) - 0.055)
))) < 0 ? 0 : $ret;
},
array: $this->returnAsArray(),
));
return $this;
}
/**
* convert to css string with optional opacity
* Note: if this is a linear RGB, the data will converted during this operation and the converted back
*
* @param float|string|null $opacity
* @return string
*/
public function toCssString(null|float|string $opacity = null): string
{
// if we are in linear mode, convert to normal mode temporary
$was_linear = false;
if ($this->linear) {
$this->fromLinear();
$was_linear = true;
}
$string = 'rgb('
. (int)round($this->R, 0)
. ' '
. (int)round($this->G, 0)
. ' '
. (int)round($this->B, 0)
. Utils::setOpacity($opacity)
. ')';
if ($was_linear) {
$this->toLinear();
}
return $string;
}
}
// __END__

View File

@@ -0,0 +1,202 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Color Coordinate: XYZ (Cie) (colorspace CIEXYZ)
* Note, this is only for the D50 & D65 whitepoint conversion
* https://en.wikipedia.org/wiki/CIE_1931_color_space#Construction_of_the_CIE_XYZ_color_space_from_the_Wright%E2%80%93Guild_data
* https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D
* https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color\Coordinates;
// use CoreLibs\Convert\Color\Utils;
class XYZ implements Interface\CoordinatesInterface
{
/** @var array<string> allowed colorspaces */
private const COLORSPACES = ['CIEXYZ'];
/** @var array<string> allowed whitepoints
* D50: ICC profile PCS (horizon light) <-> CieLab
* D65: RGB color space (noon) <-> linear RGB
*/
private const ILLUMINANT = ['D50', 'D65'];
/** @var float X coordinate */
private float $X = 0.0;
/** @var float Y coordinate (Luminance) */
private float $Y = 0.0;
/** @var float Z coordinate (blue) */
private float $Z = 0.0;
/** @var string color space: either ok or cie */
private string $colorspace = '';
/** @var string illuminat white point: only D50 and D65 are allowed */
private string $whitepoint = '';
/**
* Color Coordinate Lch
* for oklch conversion
*
* @param string|array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=CIEXYZ]
* @param array<string,string> $options [default=[]] Only "whitepoint" option allowed
* @throws \InvalidArgumentException only array colors allowed
*/
public function __construct(
string|array $colors,
string $colorspace = 'CIEXYZ',
array $options = [],
) {
if (!is_array($colors)) {
throw new \InvalidArgumentException('Only array colors allowed', 0);
}
$this->setColorspace($colorspace)
->parseOptions($options)
->setFromArray($colors);
}
/**
* set from array
* where 0: X, 1: Y, 2: Z
*
* @param array{0:float,1:float,2:float} $colors
* @param string $colorspace [default=CIEXYZ]
* @param array<string,string> $options [default=[]] Only "whitepoint" option allowed
* @return self
*/
public static function create(
string|array $colors,
string $colorspace = 'CIEXYZ',
array $options = [],
): self {
return new XYZ($colors, $colorspace, $options);
}
/**
* parse options
*
* @param array<string,string> $options
* @return self
*/
private function parseOptions(array $options): self
{
$this->setWhitepoint($options['whitepoint'] ?? '');
return $this;
}
/**
* set color
*
* @param string $name
* @param float $value
* @return void
*/
private function set(string $name, float $value): void
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
// TODO: setup XYZ value limits
// X: 0 to 95.047, Y: 0 to 100, Z: 0 to 108.88
// if (Utils::compare(0.0, $value, 100.0, Utils::EPSILON_SMALL))) {
// throw new \LengthException('Argument value ' . $value . ' for color ' . $name
// . ' is not in the range of 0 to 100.0', 1);
// }
$this->$name = $value;
}
/**
* get color
*
* @param string $name
* @return float
*/
public function get(string $name): float|string|bool
{
if (!property_exists($this, $name)) {
throw new \ErrorException('Creation of dynamic property is not allowed', 0);
}
return $this->$name;
}
/**
* set the colorspace
*
* @param string $colorspace
* @return self
*/
private function setColorspace(string $colorspace): self
{
if (!in_array($colorspace, $this::COLORSPACES)) {
throw new \InvalidArgumentException('Not allowed colorspace', 0);
}
$this->colorspace = $colorspace;
return $this;
}
/**
* set the whitepoint flag
*
* @param string $whitepoint
* @return self
*/
private function setWhitepoint(string $whitepoint): self
{
if (empty($whitepoint)) {
$this->whitepoint = '';
return $this;
}
if (!in_array($whitepoint, $this::ILLUMINANT)) {
throw new \InvalidArgumentException('Not allowed whitepoint', 0);
}
$this->whitepoint = $whitepoint;
return $this;
}
/**
* Returns the color as array
* where 0: X, 1: Y, 2: Z
*
* @return array{0:float,1:float,2:float}
*/
public function returnAsArray(): array
{
return [$this->X, $this->Y, $this->Z];
}
/**
* set color as array
* where 0: X, 1: Y, 2: Z
*
* @param array{0:float,1:float,2:float} $colors
* @return self
*/
private function setFromArray(array $colors): self
{
$this->set('X', $colors[0]);
$this->set('Y', $colors[1]);
$this->set('Z', $colors[2]);
return $this;
}
/**
* no hsb in css
*
* @param float|string|null $opacity
* @return string
* @throws \ErrorException
*/
public function toCssString(null|float|string $opacity = null): string
{
throw new \ErrorException('XYZ is not available as CSS color string', 0);
}
}
// __END__

View File

@@ -0,0 +1,35 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/11
* DESCRIPTION:
* Convert color coordinate to CSS string
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color;
use CoreLibs\Convert\Color\Coordinates\RGB;
use CoreLibs\Convert\Color\Coordinates\HSL;
use CoreLibs\Convert\Color\Coordinates\HWB;
use CoreLibs\Convert\Color\Coordinates\Lab;
use CoreLibs\Convert\Color\Coordinates\LCH;
class Stringify
{
/**
* return the CSS string including optional opacity
*
* @param RGB|Lab|LCH|HSL|HWB $data
* @param null|float|string $opacity
* @return string
*/
public static function toCssString(RGB|Lab|LCH|HSL|HWB $data, null|float|string $opacity): string
{
return $data->toCssString($opacity);
}
}
// __END__

View File

@@ -0,0 +1,56 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/11/14
* DESCRIPTION:
* Utils for color
*/
declare(strict_types=1);
namespace CoreLibs\Convert\Color;
use CoreLibs\Convert\Math;
class Utils
{
/** @var float deviation allowed for valid data checks, small */
public const EPSILON_SMALL = 0.000000000001;
/** @var float deviation allowed for valid data checks, medium */
public const EPSILON_MEDIUM = 0.0000001;
/** @var float deviation allowed for valid data checks, big */
public const ESPILON_BIG = 0.0001;
public static function compare(float $lower, float $value, float $upper, float $epslion): bool
{
if (
Math::compareWithEpsilon($value, '<', $lower, $epslion) ||
Math::compareWithEpsilon($value, '>', $upper, $epslion)
) {
return true;
}
return false;
}
/**
* Build the opactiy sub string part and return it
*
* @param null|float|string|null $opacity
* @return string
*/
public static function setOpacity(null|float|string $opacity = null): string
{
// set opacity, either a string or float
if (is_string($opacity)) {
$opacity = ' / ' . $opacity;
} elseif ($opacity !== null) {
$opacity = ' / ' . $opacity;
} else {
$opacity = '';
}
return $opacity;
}
}
// __END__

View File

@@ -17,6 +17,9 @@ declare(strict_types=1);
namespace CoreLibs\Convert;
use CoreLibs\Convert\Color\Color;
use CoreLibs\Convert\Color\Coordinates;
class Colors
{
/**
@@ -30,6 +33,7 @@ class Colors
* @param bool $hex_prefix default true, prefix with "#"
* @return string rgb in hex values with leading # if set,
* @throws \LengthException If any argument is not in the range of 0~255
* @deprecated v9.20.0 use: new Coordinates\RGB([$red, $green, $blue]))->returnAsHex(true/false for #)
*/
public static function rgb2hex(
int $red,
@@ -37,20 +41,7 @@ class Colors
int $blue,
bool $hex_prefix = true
): string {
$hex_color = '';
if ($hex_prefix === true) {
$hex_color = '#';
}
foreach (['red', 'green', 'blue'] as $color) {
// if not valid, abort
if ($$color < 0 || $$color > 255) {
throw new \LengthException('Argument value ' . $$color . ' for color ' . $color
. ' is not in the range of 0 to 255', 1);
}
// pad left with 0
$hex_color .= str_pad(dechex($$color), 2, '0', STR_PAD_LEFT);
}
return $hex_color;
return (new Coordinates\RGB([$red, $green, $blue]))->returnAsHex($hex_prefix);
}
/**
@@ -63,32 +54,29 @@ class Colors
* or a string with the seperator
* @throws \InvalidArgumentException if hex string is empty
* @throws \UnexpectedValueException if the hex string value is not valid
* @deprecated v9.20.0 use: new Coordinates\RGB($hex_string) (build string/array from return data)
*/
public static function hex2rgb(
string $hex_string,
bool $return_as_string = false,
string $seperator = ','
): string|array {
$hex_string = preg_replace("/[^0-9A-Fa-f]/", '', $hex_string); // Gets a proper hex string
if (!is_string($hex_string)) {
throw new \InvalidArgumentException('hex_string argument cannot be empty', 1);
}
$rgbArray = [];
if (strlen($hex_string) == 6) {
// If a proper hex code, convert using bitwise operation.
// No overhead... faster
$colorVal = hexdec($hex_string);
$rgbArray['r'] = 0xFF & ($colorVal >> 0x10);
$rgbArray['g'] = 0xFF & ($colorVal >> 0x8);
$rgbArray['b'] = 0xFF & $colorVal;
} elseif (strlen($hex_string) == 3) {
// If shorthand notation, need some string manipulations
$rgbArray['r'] = hexdec(str_repeat(substr($hex_string, 0, 1), 2));
$rgbArray['g'] = hexdec(str_repeat(substr($hex_string, 1, 1), 2));
$rgbArray['b'] = hexdec(str_repeat(substr($hex_string, 2, 1), 2));
} else {
// Invalid hex color code
throw new \UnexpectedValueException('Invalid hex_string: ' . $hex_string, 2);
// rewrite to previous r/g/b key output
foreach ((new Coordinates\RGB($hex_string))->returnAsArray() as $p => $el) {
$k = '';
switch ($p) {
case 0:
$k = 'r';
break;
case 1:
$k = 'g';
break;
case 2:
$k = 'b';
break;
}
$rgbArray[$k] = (int)round($el);
}
// returns the rgb string or the associative array
return $return_as_string ? implode($seperator, $rgbArray) : $rgbArray;
@@ -105,42 +93,16 @@ class Colors
* @param int $blue blue 0-255
* @return array<int|float> Hue, Sat, Brightness/Value
* @throws \LengthException If any argument is not in the range of 0~255
* @deprecated v9.20.0 use: Color::rgbToHsb(...)->returnAsArray() will return float unrounded
*/
public static function rgb2hsb(int $red, int $green, int $blue): array
{
// check that rgb is from 0 to 255
foreach (['red', 'green', 'blue'] as $color) {
if ($$color < 0 || $$color > 255) {
throw new \LengthException('Argument value ' . $$color . ' for color ' . $color
. ' is not in the range of 0 to 255', 1);
}
$$color = $$color / 255;
}
$MAX = max($red, $green, $blue);
$MIN = min($red, $green, $blue);
$HUE = 0;
if ($MAX == $MIN) {
return [0, 0, round($MAX * 100)];
}
if ($red == $MAX) {
$HUE = ($green - $blue) / ($MAX - $MIN);
} elseif ($green == $MAX) {
$HUE = 2 + (($blue - $red) / ($MAX - $MIN));
} elseif ($blue == $MAX) {
$HUE = 4 + (($red - $green) / ($MAX - $MIN));
}
$HUE *= 60;
if ($HUE < 0) {
$HUE += 360;
}
return [
(int)round($HUE),
(int)round((($MAX - $MIN) / $MAX) * 100),
(int)round($MAX * 100)
];
return array_map(
fn ($v) => (int)round($v),
Color::rgbToHsb(
new Coordinates\RGB([$red, $green, $blue])
)->returnAsArray()
);
}
/**
@@ -153,80 +115,16 @@ class Colors
* @param float $V brightness/value 0-100 (int)
* @return array<int> 0 red/1 green/2 blue array as 0-255
* @throws \LengthException If any argument is not in the valid range
* @deprecated v9.20.0 use: Color::hsbToRgb(...)->returnAsArray() will return float unrounded
*/
public static function hsb2rgb(float $H, float $S, float $V): array
{
// check that H is 0 to 359, 360 = 0
// and S and V are 0 to 1
if ($H == 360) {
$H = 0;
}
if ($H < 0 || $H > 359) {
throw new \LengthException('Argument value ' . $H . ' for hue is not in the range of 0 to 359', 1);
}
if ($S < 0 || $S > 100) {
throw new \LengthException('Argument value ' . $S . ' for saturation is not in the range of 0 to 100', 2);
}
if ($V < 0 || $V > 100) {
throw new \LengthException('Argument value ' . $V . ' for brightness is not in the range of 0 to 100', 3);
}
// convert to internal 0-1 format
$S /= 100;
$V /= 100;
if ($S == 0) {
$V = (int)round($V * 255);
return [$V, $V, $V];
}
$Hi = floor($H / 60);
$f = ($H / 60) - $Hi;
$p = $V * (1 - $S);
$q = $V * (1 - ($S * $f));
$t = $V * (1 - ($S * (1 - $f)));
switch ($Hi) {
case 0:
$red = $V;
$green = $t;
$blue = $p;
break;
case 1:
$red = $q;
$green = $V;
$blue = $p;
break;
case 2:
$red = $p;
$green = $V;
$blue = $t;
break;
case 3:
$red = $p;
$green = $q;
$blue = $V;
break;
case 4:
$red = $t;
$green = $p;
$blue = $V;
break;
case 5:
$red = $V;
$green = $p;
$blue = $q;
break;
default:
$red = 0;
$green = 0;
$blue = 0;
}
return [
(int)round($red * 255),
(int)round($green * 255),
(int)round($blue * 255)
];
return array_map(
fn ($v) => (int)round($v),
Color::hsbToRgb(
new Coordinates\HSB([$H, $S, $V])
)->returnAsArray()
);
}
/**
@@ -239,50 +137,16 @@ class Colors
* @param int $blue blue 0-255
* @return array<float> hue/sat/luminance
* @throws \LengthException If any argument is not in the range of 0~255
* @deprecated v9.20.0 use: Color::rgbToHsl(...)->returnAsArray() will return float unrounded
*/
public static function rgb2hsl(int $red, int $green, int $blue): array
{
// check that rgb is from 0 to 255
foreach (['red', 'green', 'blue'] as $color) {
if ($$color < 0 || $$color > 255) {
throw new \LengthException('Argument value ' . $$color . ' for color ' . $color
. ' is not in the range of 0 to 255', 1);
}
$$color = $$color / 255;
}
$min = min($red, $green, $blue);
$max = max($red, $green, $blue);
$chroma = $max - $min;
$sat = 0;
$hue = 0;
// luminance
$lum = ($max + $min) / 2;
// achromatic
if ($chroma == 0) {
// H, S, L
return [0.0, 0.0, round($lum * 100, 1)];
} else {
$sat = $chroma / (1 - abs(2 * $lum - 1));
if ($max == $red) {
$hue = fmod((($green - $blue) / $chroma), 6);
if ($hue < 0) {
$hue = (6 - fmod(abs($hue), 6));
}
} elseif ($max == $green) {
$hue = ($blue - $red) / $chroma + 2;
} elseif ($max == $blue) {
$hue = ($red - $green) / $chroma + 4;
}
$hue = $hue * 60;
// $sat = 1 - abs(2 * $lum - 1);
return [
round($hue, 1),
round($sat * 100, 1),
round($lum * 100, 1)
];
}
return array_map(
fn ($v) => round($v, 1),
Color::rgbToHsl(
new Coordinates\RGB([$red, $green, $blue])
)->returnAsArray()
);
}
/**
@@ -294,57 +158,16 @@ class Colors
* @param float $lum luminance: 0-100
* @return array<int,float|int> red/blue/green 0-255 each
* @throws \LengthException If any argument is not in the valid range
* @deprecated v9.20.0 use: Color::hslToRgb(...)->returnAsArray() will return float unrounded
*/
public static function hsl2rgb(float $hue, float $sat, float $lum): array
{
if ($hue == 360) {
$hue = 0;
}
if ($hue < 0 || $hue > 359) {
throw new \LengthException('Argument value ' . $hue . ' for hue is not in the range of 0 to 359', 1);
}
if ($sat < 0 || $sat > 100) {
throw new \LengthException('Argument value ' . $sat . ' for saturation is not in the range of 0 to 100', 2);
}
if ($lum < 0 || $lum > 100) {
throw new \LengthException('Argument value ' . $lum . ' for luminance is not in the range of 0 to 100', 3);
}
// calc to internal convert value for hue
$hue = (1 / 360) * $hue;
// convert to internal 0-1 format
$sat /= 100;
$lum /= 100;
// if saturation is 0
if ($sat == 0) {
$lum = (int)round($lum * 255);
return [$lum, $lum, $lum];
} else {
$m2 = $lum < 0.5 ? $lum * ($sat + 1) : ($lum + $sat) - ($lum * $sat);
$m1 = $lum * 2 - $m2;
$hueue = function ($base) use ($m1, $m2) {
// base = hue, hue > 360 (1) - 360 (1), else < 0 + 360 (1)
$base = $base < 0 ? $base + 1 : ($base > 1 ? $base - 1 : $base);
// 6: 60, 2: 180, 3: 240
// 2/3 = 240
// 1/3 = 120 (all from 360)
if ($base * 6 < 1) {
return $m1 + ($m2 - $m1) * $base * 6;
}
if ($base * 2 < 1) {
return $m2;
}
if ($base * 3 < 2) {
return $m1 + ($m2 - $m1) * ((2 / 3) - $base) * 6;
}
return $m1;
};
return [
(int)round(255 * $hueue($hue + (1 / 3))),
(int)round(255 * $hueue($hue)),
(int)round(255 * $hueue($hue - (1 / 3)))
];
}
return array_map(
fn ($v) => round($v),
Color::hslToRgb(
new Coordinates\HSL([$hue, $sat, $lum])
)->returnAsArray()
);
}
}

View File

@@ -23,14 +23,14 @@ class Encoding
* @param bool $auto_check default true, if source encoding is set
* check that the source is actually matching
* 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(
string $string,
string $to_encoding,
string $source_encoding = '',
bool $auto_check = true
): string {
): string|false {
// set if not given
if (!$source_encoding) {
$source_encoding = mb_detect_encoding($string);

View File

@@ -10,9 +10,16 @@ namespace CoreLibs\Convert;
class Html
{
/** @var int */
public const SELECTED = 0;
/** @var int */
public const CHECKED = 1;
// TODO: check for not valid htmlentites encoding
// as of PHP 8.4: https://www.php.net/manual/en/function.htmlentities.php
/** @#var array<string> */
// public const VALID_HTMLENT_ENCODINGS = [];
/**
* full wrapper for html entities
*
@@ -22,14 +29,19 @@ class Html
* encodes in UTF-8
* does not double encode
*
* @param mixed $string string to html encode
* @param int $flags [default: ENT_QUOTES | ENT_HTML5]
* @param mixed $string string to html encode
* @param int $flags [default=ENT_QUOTES | ENT_HTML5]
* @param string $encoding [default=UTF-8]
* @return mixed if string, encoded, else as is (eg null)
*/
public static function htmlent(mixed $string, int $flags = ENT_QUOTES | ENT_HTML5): mixed
{
public static function htmlent(
mixed $string,
int $flags = ENT_QUOTES | ENT_HTML5,
string $encoding = 'UTF-8'
): mixed {
if (is_string($string)) {
return htmlentities($string, $flags, 'UTF-8', false);
// if not a valid encoding this will throw a warning and use UTF-8
return htmlentities($string, $flags, $encoding, false);
}
return $string;
}
@@ -37,7 +49,7 @@ class Html
/**
* strips out all line breaks or replaced with given string
* @param string $string string
* @param string $replace replace character, default ' '
* @param string $replace [default=' '] replace character
* @return string cleaned string without any line breaks
*/
public static function removeLB(string $string, string $replace = ' '): string

View File

@@ -119,6 +119,23 @@ class Json
}
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__

View File

@@ -56,6 +56,180 @@ class Math
return (float)$number;
}
}
/**
* calc cube root
*
* @param float $number Number to cubic root
* @return float Calculated value
*/
public static function cbrt(float|int $number): float
{
return pow((float)$number, 1.0 / 3);
}
/**
* use PHP_FLOAT_EPSILON to compare if two float numbers are matching
*
* @param float $x
* @param float $y
* @param float $epsilon [default=PHP_FLOAT_EPSILON]
* @return bool True equal
*/
public static function equalWithEpsilon(float $x, float $y, float $epsilon = PHP_FLOAT_EPSILON): bool
{
if (abs($x - $y) < $epsilon) {
return true;
}
return false;
}
/**
* Compare two value base on direction given
* The default delta is PHP_FLOAT_EPSILON
*
* @param float $value
* @param string $compare
* @param float $limit
* @param float $epsilon [default=PHP_FLOAT_EPSILON]
* @return bool True on smaller/large or equal
*/
public static function compareWithEpsilon(
float $value,
string $compare,
float $limit,
float $epsilon = PHP_FLOAT_EPSILON
): bool {
switch ($compare) {
case '<':
if ($value < ($limit - $epsilon)) {
return true;
}
break;
case '<=':
if ($value <= ($limit - $epsilon)) {
return true;
}
break;
case '==':
return self::equalWithEpsilon($value, $limit, $epsilon);
case '>':
if ($value > ($limit + $epsilon)) {
return true;
}
break;
case '>=':
if ($value >= ($limit + $epsilon)) {
return true;
}
break;
}
return false;
}
/**
* This function is directly inspired by the multiplyMatrices() function in color.js
* form Lea Verou and Chris Lilley.
* (see https://github.com/LeaVerou/color.js/blob/main/src/multiply-matrices.js)
* From:
* https://github.com/matthieumastadenis/couleur/blob/3842cf51c9517e77afaa0a36ec78643a0c258e0b/src/utils/utils.php#L507
*
* It returns an array which is the product of the two number matrices passed as parameters.
*
* NOTE:
* if the right side (B matrix) has a missing row, this row will be fillwed with 0 instead of
* throwing an error:
* A:
* [
* [1, 2, 3],
* [4, 5, 6],
* ]
* B:
* [
* [7, 8, 9],
* [10, 11, 12],
* ]
* The B will get a third row with [0, 0, 0] added to make the multiplication work as it will be
* rewritten as
* B-rewrite:
* [
* [7, 10, 0],
* [8, 11, 12],
* [0, 0, 0] <- automatically added
* ]
*
* The same is done for unbalanced entries, they are filled with 0
*
* @param array<float|int|array<int|float>> $a m x n matrice
* @param array<float|int|array<int|float>> $b n x p matrice
*
* @return array<float|int|array<int|float>> m x p product
*/
public static function multiplyMatrices(array $a, array $b): array
{
$m = count($a);
if (!is_array($a[0] ?? null)) {
// $a is vector, convert to [[a, b, c, ...]]
$a = [$a];
}
if (!is_array($b[0])) {
// $b is vector, convert to [[a], [b], [c], ...]]
$b = array_map(
callback: fn ($v) => [ $v ],
array: $b,
);
}
$p = count($b[0]);
// transpose $b:
// so that we can multiply row by row
$bCols = array_map(
callback: fn ($k) => array_map(
(fn ($i) => is_array($i) ? $i[$k] ?? 0 : 0),
$b,
),
array: array_keys($b[0]),
);
$product = array_map(
callback: fn ($row) => array_map(
callback: fn ($col) => is_array($row) ?
array_reduce(
array: $row,
callback: fn ($a, $v, $i = null) => $a + $v * (
// if last entry missing for full copy add a 0 to it
$col[$i ?? array_search($v, $row, true)] ?? 0 /** @phpstan-ignore-line */
),
initial: 0,
) :
array_reduce(
array: $col,
callback: fn ($a, $v) => $a + $v * $row,
initial: 0,
),
array: $bCols,
),
array: $a,
);
if ($m === 1) {
// Avoid [[a, b, c, ...]]:
return $product[0];
}
if ($p === 1) {
// Avoid [[a], [b], [c], ...]]:
return array_map(
callback: fn ($v) => $v[0] ?? 0,
array: $product,
);
}
return $product;
}
}
// __END__

View File

@@ -8,8 +8,20 @@ declare(strict_types=1);
namespace CoreLibs\Convert;
use CoreLibs\Combined\ArrayHandler;
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
* 0 if nothing / invalid split
@@ -52,29 +64,42 @@ class Strings
* Note a string LONGER then the maxium will be attached with the LAST
* split character. In above exmaple
* 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_characters list of charcters with which we split
* if not set uses dash ('-')
* @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(
string $value,
string $string,
string $split_format,
string $split_characters = '-'
): string {
if (
// abort if split format is empty
empty($split_format) ||
// if not in the valid ASCII character range for any of the strings
preg_match('/[^\x20-\x7e]/', $value) ||
// preg_match('/[^\x20-\x7e]/', $split_format) ||
preg_match('/[^\x20-\x7e]/', $split_characters) ||
// only numbers and split characters in split_format
!preg_match("/[0-9" . $split_characters . "]/", $split_format)
) {
return $value;
// skip if string or split format is empty is empty
if (empty($string) || empty($split_format)) {
return $string;
}
if (preg_match('/[^\x20-\x7e]/', $string)) {
throw new \InvalidArgumentException(
"The string to split can only be ascii characters: " . $string
);
}
// get the split characters that are not numerical and check they are ascii
$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_list = preg_split(
@@ -86,14 +111,14 @@ class Strings
);
// if this is false, or only one array, abort split
if (!is_array($split_list) || count($split_list) == 1) {
return $value;
return $string;
}
$out = '';
$pos = 0;
$last_split = '';
foreach ($split_list as $offset) {
if (is_numeric($offset)) {
$_part = substr($value, $pos, (int)$offset);
$_part = substr($string, $pos, (int)$offset);
if (empty($_part)) {
break;
}
@@ -104,8 +129,8 @@ class Strings
$last_split = $offset;
}
}
if (!empty($out) && $pos < strlen($value)) {
$out .= $last_split . substr($value, $pos);
if (!empty($out) && $pos < strlen($string)) {
$out .= $last_split . substr($string, $pos);
}
// if last is not alphanumeric remove, remove
if (!strcspn(substr($out, -1, 1), $split_characters)) {
@@ -115,10 +140,49 @@ class Strings
if (!empty($out)) {
return $out;
} 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
* eg: //foo///bar/foo.inc -> /foo/bar/foo.inc
@@ -146,6 +210,116 @@ class Strings
{
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)
)
));
}
/**
* 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__

View File

@@ -38,6 +38,7 @@ class Email
* @param string $encoding Encoding, if not set UTF-8
* @param bool $kv_folding If set to true and a valid encoding, do KV folding
* @return string Correctly encoded and build email string
* @throws \IntlException if email name cannot be converted to UTF-8
*/
public static function encodeEmailName(
string $email,
@@ -52,6 +53,10 @@ class Email
if ($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 =
mb_encode_mimeheader(
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,
* do KV folding
* @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(
string $subject,
@@ -102,6 +109,12 @@ class Email
$subject = mb_convert_encoding($subject, $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
$subject = mb_encode_mimeheader(
in_array($encoding, self::$encoding_kv_allowed) && $kv_folding ?

View File

@@ -10,9 +10,14 @@ namespace CoreLibs\Create;
class Hash
{
/** @var string default short hash -> deprecated use STANDARD_HASH_SHORT */
public const DEFAULT_HASH = 'adler32';
/** @var string default long hash (40 chars) */
public const STANDARD_HASH_LONG = 'ripemd160';
/** @var string default short hash (8 chars) */
public const STANDARD_HASH_SHORT = 'adler32';
/** @var string this is the standard hash to use hashStd and hash (64 chars) */
public const STANDARD_HASH = 'sha256';
/**
* checks php version and if >=5.2.7 it will flip the string
@@ -20,6 +25,7 @@ class Hash
* hash returns false
* preg_replace fails for older php version
* Use __hash with crc32b or hash('crc32b', ...) for correct output
* For future short hashes use hashShort() instead
*
* @param string $string string to crc
* @return string crc32b hash (old type)
@@ -43,19 +49,31 @@ class Hash
* replacement for __crc32b call
*
* @param string $string string to hash
* @param bool $use_sha use sha instead of crc32b (default false)
* @param bool $use_sha [default=false] use sha1 instead of crc32b
* @return string hash of the string
* @deprecated use __crc32b() for drop in replacement with default, or sha1Short() for use sha true
*/
public static function __sha1Short(string $string, bool $use_sha = false): string
{
if ($use_sha) {
// return only the first 9 characters
return substr(hash('sha1', $string), 0, 9);
return self::sha1Short($string);
} else {
return self::__crc32b($string);
}
}
/**
* returns a short sha1
*
* @param string $string string to hash
* @return string hash of the string
*/
public static function sha1Short(string $string): string
{
// return only the first 9 characters
return substr(hash('sha1', $string), 0, 9);
}
/**
* replacemend for __crc32b call (alternate)
* defaults to adler 32
@@ -63,34 +81,135 @@ class Hash
* all that create 8 char long hashes
*
* @param string $string string to hash
* @param string $hash_type hash type (default adler32)
* @param string $hash_type [default=STANDARD_HASH_SHORT] hash type (default adler32)
* @return string hash of the string
* @deprecated use hashShort() of short hashes with adler 32 or hash() for other hash types
*/
public static function __hash(
string $string,
string $hash_type = self::DEFAULT_HASH
string $hash_type = self::STANDARD_HASH_SHORT
): string {
return self::hash($string, $hash_type);
}
/**
* check if hash type is valid, returns false if not
*
* @param string $hash_type
* @return bool
*/
public static function isValidHashType(string $hash_type): bool
{
if (!in_array($hash_type, hash_algos())) {
return false;
}
return true;
}
/**
* check if hash hmac type is valid, returns false if not
*
* @param string $hash_hmac_type
* @return bool
*/
public static function isValidHashHmacType(string $hash_hmac_type): bool
{
if (!in_array($hash_hmac_type, hash_hmac_algos())) {
return false;
}
return true;
}
/**
* creates a hash over string if any valid hash given.
* if no hash type set use sha256
*
* @param string $string string to hash
* @param string $hash_type [default=STANDARD_HASH] hash type (default sha256)
* @return string hash of the string
*/
public static function hash(
string $string,
string $hash_type = self::STANDARD_HASH
): string {
// if not empty, check if in valid list
if (
empty($hash_type) ||
!in_array($hash_type, hash_algos())
) {
// fallback to default hash type if none set or invalid
$hash_type = self::DEFAULT_HASH;
// fallback to default hash type if empty or invalid
$hash_type = self::STANDARD_HASH;
}
return hash($hash_type, $string);
}
/**
* Wrapper function for standard long hashd
* creates a hash mac key
*
* @param string $string string to hash mac
* @param string $key key to use
* @param string $hash_type [default=STANDARD_HASH]
* @return string hash mac string
*/
public static function hashHmac(
string $string,
#[\SensitiveParameter]
string $key,
string $hash_type = self::STANDARD_HASH
): string {
if (
empty($hash_type) ||
!in_array($hash_type, hash_hmac_algos())
) {
// fallback to default hash type if e or invalid
$hash_type = self::STANDARD_HASH;
}
return hash_hmac($hash_type, $string, $key);
}
/**
* short hash with max length of 8, uses adler32
*
* @param string $string string to hash
* @return string hash of the string
*/
public static function hashShort(string $string): string
{
return hash(self::STANDARD_HASH_SHORT, $string);
}
/**
* Wrapper function for standard long hash
*
* @param string $string String to be hashed
* @return string Hashed string
* @deprecated use hashLong()
*/
public static function __hashLong(string $string): string
{
return self::hashLong($string);
}
/**
* Wrapper function for standard long hash, uses ripmd160
*
* @param string $string String to be hashed
* @return string Hashed string
*/
public static function __hashLong(string $string): string
public static function hashLong(string $string): string
{
return hash(self::STANDARD_HASH_LONG, $string);
}
/**
* create standard hash basd on STANDAR_HASH, currently sha256
*
* @param string $string string in
* @return string hash of the string
*/
public static function hashStd(string $string): string
{
return self::hash($string, self::STANDARD_HASH);
}
}
// __END__

View File

@@ -8,39 +8,97 @@ declare(strict_types=1);
namespace CoreLibs\Create;
use CoreLibs\Convert\Strings;
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
/** @var string */
private static string $key_range = '';
/** @var int */
private static int $one_key_length;
/** @var int */
private static int $key_length = 4; // default key length
/** @var int */
private static int $max_key_length = 256; // max allowed length
/** @var string all the characters that are int he current radnom key range */
private static string $key_character_range = '';
/** @var int character count in they key character range */
private static int $key_character_range_length = 0;
/** @var int default key lenghth */
/** @deprecated Will be removed */
private static int $key_length = 4;
/**
* 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(self::$key_character_range) <= 1) {
return '';
}
return $key_character_range;
}
/**
* 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
* @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
self::$key_range = join('', array_merge(
range('A', 'Z'),
range('a', 'z'),
range('0', '9')
));
self::$one_key_length = strlen(self::$key_range);
// if key range is not set
if (!count($key_range)) {
self::$key_character_range = self::KEY_CHARACTER_RANGE_DEFAULT;
} else {
self::$key_character_range = self::validateRandomKeyData(...$key_range);
// random key generation base string
}
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;
}
/**
@@ -53,7 +111,7 @@ class RandomKey
{
if (
$key_length > 0 &&
$key_length <= self::$max_key_length
$key_length <= self::KEY_LENGTH_MAX
) {
return true;
} else {
@@ -67,6 +125,7 @@ class RandomKey
*
* @param int $key_length key length
* @return bool true/false for set status
* @deprecated This function does no longer set the key length, the randomKeyGen parameter has to b used
*/
public static function setRandomKeyLength(int $key_length): bool
{
@@ -83,6 +142,7 @@ class RandomKey
* get the current set random 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
{
@@ -94,28 +154,37 @@ class RandomKey
* if override key length is set, it will check on valid key and use this
* this will not set the class key length variable
*
* @param int $key_length key length override, -1 for use default
* @return string random key
* @param int $key_length [default=-1] key length override,
* 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
{
// init random key strings if not set
if (
!isset(self::$one_key_length)
) {
self::initRandomKeyData();
}
$use_key_length = 0;
// only if valid int key with valid length
if (self::validateRandomKeyLenght($key_length) === true) {
$use_key_length = $key_length;
public static function randomKeyGen(
int $key_length = self::KEY_LENGTH_DEFAULT,
array ...$key_range
): string {
$key_character_range = '';
if (count($key_range)) {
$key_character_range = self::validateRandomKeyData(...$key_range);
$key_character_range_length = strlen($key_character_range);
} 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::validateRandomKeyLenght($key_length)) {
$key_length = self::KEY_LENGTH_DEFAULT;
}
// create random string
$random_string = '';
for ($i = 1; $i <= $use_key_length; $i++) {
$random_string .= self::$key_range[random_int(0, self::$one_key_length - 1)];
for ($i = 1; $i <= $key_length; $i++) {
$random_string .= $key_character_range[
random_int(0, $key_character_range_length - 1)
];
}
return $random_string;
}

View File

@@ -15,17 +15,111 @@ namespace CoreLibs\Create;
class Session
{
/** @var string current session name */
private string $session_name = '';
/** @var string current session id */
private string $session_id = '';
/** @var bool flag auto write close */
private bool $auto_write_close = false;
/** @var string regenerate option, default never */
private string $regenerate = 'never';
/** @var int regenerate interval either 1 to 100 for random or 0 to 3600 for interval */
private int $regenerate_interval = 0;
/** @var array<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
* then no auto init is run
*
* @param string $session_name if set and not empty, will start session
* @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options
*/
public function __construct(string $session_name = '')
public function __construct(
string $session_name,
array $options = []
) {
$this->setOptions($options);
$this->initSession($session_name);
}
// 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 (!empty($session_name)) {
$this->startSession($session_name);
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);
}
/**
@@ -36,38 +130,100 @@ class Session
*
* @return void
*/
protected function startSessionCall(): void
private function startSessionCall(): void
{
session_start();
}
/**
* check if we are in CLI, we set this, so we can mock this
* Not this is just a wrapper for the static System::checkCLI call
* get current set session id or false if none started
*
* @return bool True if we are in a CLI enviroment, or false for everything else
* @return string|false
*/
public function checkCliStatus(): bool
public function getSessionIdCall(): string|false
{
return \CoreLibs\Get\System::checkCLI();
return session_id();
}
/**
* Set session name call. If not valid session name, will return false
* automatically closes a session if the auto write close flag is set
*
* @param string $session_name A valid string for session name
* @return bool True if session name is valid,
* False if not
* @return bool
*/
public function setSessionName(string $session_name): bool
private function closeSessionCall(): bool
{
if (!$this->checkValidSessionName($session_name)) {
return false;
if ($this->auto_write_close) {
return $this->writeClose();
}
session_name($session_name);
return true;
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
*
@@ -94,15 +250,34 @@ class Session
}
/**
* start session with given session name if set
* validate _SESSION key, must be valid variable
*
* @param int|float|string $key
* @return true
*/
private function checkValidSessionEntryKey(int|float|string $key): true
{
if (!is_string($key) || is_numeric($key)) {
throw new \UnexpectedValueException(
'[SESSION] Given key for _SESSION is not a valid value for a varaible: ' . $key,
1
);
}
return true;
}
// MARK: init session (on class start)
/**
* stinitart session with given session name if set
* aborts on command line or if sessions are not enabled
* also aborts if session cannot be started
* On sucess returns the session id
*
* @param string|null $session_name
* @return string|bool
* @param string $session_name
* @return void
*/
public function startSession(?string $session_name = null): string|bool
private function initSession(string $session_name): void
{
// we can't start sessions on command line
if ($this->checkCliStatus()) {
@@ -115,39 +290,95 @@ class Session
// session_status
// initial the session if there is no session running already
if (!$this->checkActiveSession()) {
// if session name is emtpy, check if there is a global set
// this is a deprecated fallback
$session_name = $session_name ?? $GLOBALS['SET_SESSION_NAME'] ?? '';
// DEPRECTED: constant SET_SESSION_NAME is no longer used
// if set, set special session name
if (!empty($session_name)) {
// invalid session name, abort
if (!$this->checkValidSessionName($session_name)) {
throw new \UnexpectedValueException('[SESSION] Invalid session name: ' . $session_name, 3);
}
$this->setSessionName($session_name);
// invalid session name, abort
if (!$this->checkValidSessionName($session_name)) {
throw new \UnexpectedValueException('[SESSION] Invalid session name: ' . $this->session_name, 3);
}
// set session name
$this->session_name = $session_name;
session_name($this->session_name);
// start session
$this->startSessionCall();
// if we faild to start the session
if (!$this->checkActiveSession()) {
throw new \RuntimeException('[SESSION] Failed to activate session', 5);
}
if (
!empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) &&
!empty($_SESSION['SESSION_REGENERATE_EXPIRES']) && $_SESSION['SESSION_REGENERATE_EXPIRES'] < time()
) {
$this->sessionDestroy();
throw new \RuntimeException('[SESSION] Expired session found', 6);
}
} elseif ($session_name != $this->getSessionName()) {
throw new \UnexpectedValueException(
'[SESSION] Another session exists with a different name: ' . $this->getSessionName(),
4
);
}
// if we still have no active session
// check session id
if (false === ($session_id = $this->getSessionIdCall())) {
throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 7);
}
// set session id
$this->session_id = $session_id;
// run session id re-create from time to time
$this->sessionRegenerateSessionId();
// if flagged auto close, write close session
if ($this->auto_write_close) {
$this->writeClose();
}
}
// MARK: public set/get status
/**
* start session, will only run after initSession
*
* @return bool True if started, False if alrady running
*/
public function restartSession(): bool
{
if (!$this->checkActiveSession()) {
throw new \RuntimeException('[SESSION] Failed to activate session', 4);
if (empty($this->session_name)) {
throw new \RuntimeException('[SESSION] Cannot restart session without a session name', 1);
}
$this->startSessionCall();
return true;
}
if (false === ($session_id = $this->getSessionId())) {
throw new \UnexpectedValueException('[SESSION] getSessionId did not return a session id', 5);
}
return $session_id;
return false;
}
/**
* get current set session id or false if none started
* current set session id
*
* @return string|bool
* @return string
*/
public function getSessionId(): string|bool
public function getSessionId(): string
{
return session_id();
return $this->session_id;
}
/**
* set the auto write close flag
*
* @param bool $flag
* @return Session
*/
public function setAutoWriteClose(bool $flag): Session
{
$this->auto_write_close = $flag;
return $this;
}
/**
* return the auto write close flag
*
* @return bool
*/
public function checkAutoWriteClose(): bool
{
return $this->auto_write_close;
}
/**
@@ -175,6 +406,34 @@ class Session
}
}
/**
* check if we are in CLI, we set this, so we can mock this
* Not this is just a wrapper for the static System::checkCLI call
*
* @return bool True if we are in a CLI enviroment, or false for everything else
*/
public function checkCliStatus(): bool
{
return \CoreLibs\Get\System::checkCLI();
}
/**
* get session status
* PHP_SESSION_DISABLED if sessions are disabled.
* PHP_SESSION_NONE if sessions are enabled, but none exists.
* PHP_SESSION_ACTIVE if sessions are enabled, and one exists.
*
* https://www.php.net/manual/en/function.session-status.php
*
* @return int See possible return int values above
*/
public function getSessionStatus(): int
{
return session_status();
}
// MARK: write close session
/**
* unlock the session file, so concurrent AJAX requests can be done
* NOTE: after this has been called, no changes in _SESSION will be stored
@@ -188,17 +447,24 @@ class Session
return session_write_close();
}
// MARK: session close and clean up
/**
* Proper destroy a session
* - unset the _SESSION array
* - unset cookie if cookie on and we have not strict mode
* - unset session_name and session_id internal vars
* - destroy session
*
* @return bool
* @return bool True on successful session destroy
*/
public function sessionDestroy(): bool
{
$_SESSION = [];
// abort to false if not unsetable
if (!session_unset()) {
return false;
}
$this->clear();
if (
ini_get('session.use_cookies') &&
!ini_get('session.use_strict_mode')
@@ -218,68 +484,93 @@ class Session
$params['httponly']
);
}
// unset internal vars
$this->session_name = '';
$this->session_id = '';
return session_destroy();
}
/**
* get session status
* PHP_SESSION_DISABLED if sessions are disabled.
* PHP_SESSION_NONE if sessions are enabled, but none exists.
* PHP_SESSION_ACTIVE if sessions are enabled, and one exists.
*
* https://www.php.net/manual/en/function.session-status.php
*
* @return int See possible return int values above
*/
public function getSessionStatus(): int
{
return session_status();
}
// _SESSION set/unset methods
// MARK: _SESSION set/unset methods
/**
* unset all _SESSION entries
*
* @return void
*/
public function unsetAllS(): void
public function clear(): void
{
foreach (array_keys($_SESSION ?? []) as $name) {
unset($_SESSION[$name]);
$this->restartSession();
if (!session_unset()) {
throw new \RuntimeException('[SESSION] Cannot unset session vars', 1);
}
if (!empty($_SESSION)) {
$_SESSION = [];
}
$this->closeSessionCall();
}
/**
* set _SESSION entry 'name' with any value
*
* @param string|int $name array name in _SESSION
* @param mixed $value value to set (can be anything)
* @param string $name array name in _SESSION
* @param mixed $value value to set (can be anything)
* @return Session
*/
public function set(string $name, mixed $value): Session
{
$this->checkValidSessionEntryKey($name);
$this->restartSession();
$_SESSION[$name] = $value;
$this->closeSessionCall();
return $this;
}
/**
* set many session entries in one set
*
* @param array<string,mixed> $set key is the key in the _SESSION, value is any data to set
* @return void
*/
public function setS(string|int $name, mixed $value): void
public function setMany(array $set): void
{
$_SESSION[$name] = $value;
$this->restartSession();
// skip any that are not valid
foreach ($set as $key => $value) {
$this->checkValidSessionEntryKey($key);
$_SESSION[$key] = $value;
}
$this->closeSessionCall();
}
/**
* get _SESSION 'name' entry or empty string if not set
*
* @param string|int $name value key to get from _SESSION
* @return mixed value stored in _SESSION
* @param string $name value key to get from _SESSION
* @return mixed value stored in _SESSION, if not found set to null
*/
public function getS(string|int $name): mixed
public function get(string $name): mixed
{
return $_SESSION[$name] ?? '';
return $_SESSION[$name] ?? null;
}
/**
* get multiple session entries
*
* @param array<string> $set
* @return array<string,mixed>
*/
public function getMany(array $set): array
{
return array_intersect_key($_SESSION, array_flip($set));
}
/**
* Check if a name is set in the _SESSION array
*
* @param string|int $name Name to check for
* @return bool True for set, False fornot set
* @param string $name Name to check for
* @return bool True for set, False fornot set
*/
public function issetS(string|int $name): bool
public function isset(string $name): bool
{
return isset($_SESSION[$name]);
}
@@ -287,67 +578,36 @@ class Session
/**
* unset one _SESSION entry 'name' if exists
*
* @param string|int $name _SESSION key name to remove
* @param string $name _SESSION key name to remove
* @return Session
*/
public function unset(string $name): Session
{
if (!isset($_SESSION[$name])) {
return $this;
}
$this->restartSession();
unset($_SESSION[$name]);
$this->closeSessionCall();
return $this;
}
/**
* reset many session entry
*
* @param array<string> $set list of session keys to reset
* @return void
*/
public function unsetS(string|int $name): void
public function unsetMany(array $set): void
{
if (isset($_SESSION[$name])) {
unset($_SESSION[$name]);
}
}
// set/get below
// ->var = value;
/**
* Undocumented function
*
* @param string|int $name
* @param mixed $value
* @return void
*/
public function __set(string|int $name, mixed $value): void
{
$_SESSION[$name] = $value;
}
/**
* Undocumented function
*
* @param string|int $name
* @return mixed If name is not found, it will return null
*/
public function __get(string|int $name): mixed
{
if (isset($_SESSION[$name])) {
return $_SESSION[$name];
}
return null;
}
/**
* Undocumented function
*
* @param string|int $name
* @return bool
*/
public function __isset(string|int $name): bool
{
return isset($_SESSION[$name]);
}
/**
* Undocumented function
*
* @param string|int $name
* @return void
*/
public function __unset(string|int $name): void
{
if (isset($_SESSION[$name])) {
unset($_SESSION[$name]);
$this->restartSession();
foreach ($set as $key) {
if (!isset($_SESSION[$key])) {
continue;
}
unset($_SESSION[$key]);
}
$this->closeSessionCall();
}
}

View File

@@ -38,7 +38,7 @@ class Uids
$uniqid_length++;
}
/** @var int<1,max> make sure that internal this is correct */
$random_bytes_length = ($uniqid_length - ($uniqid_length % 2)) / 2;
$random_bytes_length = (int)(($uniqid_length - ($uniqid_length % 2)) / 2);
$uniqid = bin2hex(random_bytes($random_bytes_length));
// if not forced shorten return next lower length
if (!$force_length) {
@@ -56,26 +56,6 @@ class Uids
*/
public static function uuidv4(): string
{
/* return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
// 16 bits for "time_mid"
mt_rand(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand(0, 0x3fff) | 0x8000,
// 48 bits for "node"
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff)
); */
$data = random_bytes(16);
assert(strlen($data) == 16);
@@ -93,6 +73,20 @@ class Uids
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* regex validate uuid v4
*
* @param string $uuidv4
* @return 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)) {
return false;
}
return true;
}
/**
* creates a uniq id based on lengths
*

View File

@@ -39,9 +39,9 @@ class ArrayIO extends \CoreLibs\DB\IO
{
// main calss variables
/** @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 */
private string $table_name; // the table_name
private string $table_name = ''; // the table_name
/** @var string */
private string $pk_name = ''; // the primary key from this table
/** @var int|string|null */
@@ -127,9 +127,9 @@ class ArrayIO extends \CoreLibs\DB\IO
public function getTableArray(bool $reset = false): array
{
if (!$reset) {
return $this->table_array ?? [];
return $this->table_array;
}
$table_array = $this->table_array ?? [];
$table_array = $this->table_array;
reset($table_array);
return $table_array;
}
@@ -194,7 +194,7 @@ class ArrayIO extends \CoreLibs\DB\IO
*/
public function getTableName(): string
{
return $this->table_name ?? '';
return $this->table_name;
}
/**
@@ -374,7 +374,7 @@ class ArrayIO extends \CoreLibs\DB\IO
public function dbDelete(array $table_array = [], bool $acl_limit = false): array
{
// is array and has values, override set and set new
if (is_array($table_array) && count($table_array)) {
if (count($table_array)) {
$this->table_array = $table_array;
}
if (!$this->dbCheckPkSet()) {
@@ -440,7 +440,7 @@ class ArrayIO extends \CoreLibs\DB\IO
public function dbRead(bool $edit = false, array $table_array = []): array
{
// if array give, overrules internal array
if (is_array($table_array) && count($table_array)) {
if (count($table_array)) {
$this->table_array = $table_array;
}
if (!$this->dbCheckPkSet()) {

View File

@@ -284,7 +284,8 @@ class IO
public const ERROR_HASH_TYPE = 'adler32';
/** @var string regex to get returning with matches at position 1 */
public const REGEX_RETURNING = '/\s+returning\s+(.+\s*(?:.+\s*)+);?$/i';
/** @var array<string> allowed convert target for placeholder: pg or pdo (currently not available) */
/** @var array<string> allowed convert target for placeholder:
* pg or pdo (currently not available) */
public const DB_CONVERT_PLACEHOLDER_TARGET = ['pg'];
// REGEX_SELECT
// REGEX_UPDATE
@@ -302,6 +303,8 @@ class IO
private string $query = '';
/** @var array<mixed> current params for query */
private array $params = [];
/** @var string current hash build from query and params */
private string $query_hash = '';
// if we do have a convert call, store the convert data in here, else it will be empty
/** @var array{}|array{original:array{query:string,params:array<mixed>},type:''|'named'|'numbered'|'question_mark',found:int,matches:array<string>,params_lookup:array<mixed>,query:string,params:array<mixed>} */
private array $placeholder_converted = [];
@@ -499,7 +502,7 @@ class IO
die('<!-- Cannot load db functions class for: ' . $this->db_type . ' -->');
}
// write to internal one, once OK
$this->db_functions = $db_functions;
$this->db_functions = $db_functions; /** @phan-suppress-current-line PhanPossiblyNullTypeMismatchProperty */
// connect to DB
if (!$this->__connectToDB()) {
@@ -823,6 +826,10 @@ class IO
);
break;
default:
// no context on DB_INFO
if ($id == 'DB_INFO') {
$context = [];
}
// used named arguments so we can easy change the order of debug
$this->log->debug(
group_id: $debug_id,
@@ -910,7 +917,7 @@ class IO
if ($cursor !== false) {
[$db_prefix, $db_error_string] = $this->db_functions->__dbPrintError($cursor);
}
if ($cursor === false && method_exists($this->db_functions, '__dbPrintError')) {
if ($cursor === false && method_exists($this->db_functions, '__dbPrintError')) { /** @phpstan-ignore-line */
[$db_prefix, $db_error_string] = $this->db_functions->__dbPrintError();
}
// prefix the master if not the same
@@ -1307,33 +1314,14 @@ class IO
}
/**
* count $ leading parameters only
* count placeholder entries in the query
*
* @param string $query Query to check
* @return int Number of parameters found
*/
private function __dbCountQueryParams(string $query): int
{
$match = [];
// regex for params: only stand alone $number allowed
// exclude all '' enclosed strings, ignore all numbers [note must start with digit]
// can have space/tab/new line
// must have <> = , ( [not equal, equal, comma, opening round bracket]
// can have space/tab/new line
// $ number with 1-9 for first and 0-9 for further digits
// /s for matching new line in . list
// [disabled, we don't used ^ or $] /m for multi line match
// Matches in 1:, must be array_filtered to remove empty, count with array_unique
$query_split = '[(=,?-]|->|->>|#>|#>>|@>|<@|\?\|\?\&|\|\||#-';
preg_match_all(
'/'
. '(?:\'.*?\')?\s*(?:\?\?|<>|' . $query_split . ')\s*'
. '(?:\d+|(?:\'.*?\')|(\$[1-9]{1}(?:[0-9]{1,})?))'
. '/s',
$query,
$match
);
return count(array_unique(array_filter($match[1])));
return count($this->db_functions->__dbGetQueryParams($query));
}
/**
@@ -1396,6 +1384,8 @@ class IO
$this->query = $query;
// current params
$this->params = $params;
// empty on new
$this->query_hash = '';
// no query set
if (empty($this->query)) {
$this->__dbError(11);
@@ -1427,10 +1417,7 @@ class IO
$this->pk_name_table[$table] ?
$this->pk_name_table[$table] : 'NULL';
}
if (
!preg_match(self::REGEX_RETURNING, $this->query) &&
$this->pk_name && $this->pk_name != 'NULL'
) {
if (!preg_match(self::REGEX_RETURNING, $this->query) && $this->pk_name != 'NULL') {
// check if this query has a ; at the end and remove it
$__query = preg_replace("/(;\s*)$/", '', $this->query);
// must be query, if preg replace failed, use query as before
@@ -1440,7 +1427,7 @@ class IO
} elseif (
preg_match(self::REGEX_RETURNING, $this->query, $matches)
) {
if ($this->pk_name && $this->pk_name != 'NULL') {
if ($this->pk_name != 'NULL') {
// add the primary key if it is not in the returning set
if (!preg_match("/$this->pk_name/", $matches[1])) {
$this->query .= " , " . $this->pk_name;
@@ -1458,7 +1445,7 @@ class IO
$this->returning_id = true;
}
// import protection, hash needed
$query_hash = $this->dbGetQueryHash($this->query, $this->params);
$query_hash = $this->dbBuildQueryHash($this->query, $this->params);
// QUERY PARAMS: run query params check and rewrite
if ($this->dbGetConvertPlaceholder() === true) {
try {
@@ -1492,7 +1479,8 @@ class IO
return false;
}
}
// set query hash
$this->query_hash = $query_hash;
// $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id);
// for DEBUG, only on first time ;)
$this->__dbDebug(
@@ -1733,7 +1721,7 @@ class IO
{
if (
!empty($this->dbh) &&
$this->dbh instanceof \PgSql\Connection
$this->dbh instanceof \PgSql\Connection /** @phpstan-ignore-line future could be other */
) {
// reset any client encodings set
$this->dbResetEncoding();
@@ -1814,14 +1802,13 @@ class IO
$html_tags = ['{b}', '{/b}', '{br}'];
$replace_html = ['<b>', '</b>', '<br>'];
$replace_text = ['', '', ' **** '];
$string = '';
$string .= '{b}-DB-info->{/b} Connected to db {b}\'' . $this->db_name . '\'{/b} ';
$string .= 'with schema {b}\'' . $this->db_schema . '\'{/b} ';
$string .= 'as user {b}\'' . $this->db_user . '\'{/b} ';
$string .= 'at host {b}\'' . $this->db_host . '\'{/b} ';
$string .= 'on port {b}\'' . $this->db_port . '\'{/b} ';
$string .= 'with ssl mode {b}\'' . $this->db_ssl . '\'{/b}{br}';
$string .= '{b}-DB-info->{/b} DB IO Class debug output: {b}'
$string = '{b}-DB-info->{/b} Connected to db {b}\'' . $this->db_name . '\'{/b} '
. 'with schema {b}\'' . $this->db_schema . '\'{/b} '
. 'as user {b}\'' . $this->db_user . '\'{/b} '
. 'at host {b}\'' . $this->db_host . '\'{/b} '
. 'on port {b}\'' . $this->db_port . '\'{/b} '
. 'with ssl mode {b}\'' . $this->db_ssl . '\'{/b}{br}'
. '{b}-DB-info->{/b} DB IO Class debug output: {b}'
. ($this->dbGetDebug() ? 'Yes' : 'No') . '{/b}';
if ($log === true) {
// if debug, remove / change b
@@ -1829,7 +1816,7 @@ class IO
$html_tags,
$replace_text,
$string
), 'dbInfo');
), 'DB_INFO');
} else {
$string = $string . '{br}';
}
@@ -1977,7 +1964,7 @@ class IO
{
// set start array
if ($query) {
$array = $this->cursor_ext[$this->dbGetQueryHash($query)] ?? [];
$array = $this->cursor_ext[$this->dbBuildQueryHash($query)] ?? [];
} else {
$array = $this->cursor_ext;
}
@@ -1985,7 +1972,7 @@ class IO
if (is_array($array)) {
$this->nbsp = '';
$string .= $this->__printArray($array);
$this->__dbDebugMessage('db', $string, 'dbDumpData');
$this->__dbDebugMessage('db', $string, 'DB_INFO');
}
return $string;
}
@@ -2379,7 +2366,7 @@ class IO
return false;
}
// create hash from query ...
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
// pre declare array
if (!isset($this->cursor_ext[$query_hash])) {
$this->cursor_ext[$query_hash] = [
@@ -2557,7 +2544,10 @@ class IO
} // only go if NO cursor exists
// if cursor exists ...
if ($this->cursor_ext[$query_hash]['cursor']) {
if (
$this->cursor_ext[$query_hash]['cursor'] instanceof \PgSql\Result ||
$this->cursor_ext[$query_hash]['cursor'] == 1
) {
if ($first_call === true) {
$this->cursor_ext[$query_hash]['log'][] = 'First call';
// count the rows returned (if select)
@@ -2955,13 +2945,15 @@ class IO
* data to create a unique call one, optional
* @return bool False if query not found, true if success
*/
public function dbCacheReset(string $query, array $params = []): bool
public function dbCacheReset(string $query, array $params = [], bool $show_warning = true): bool
{
$this->__dbErrorReset();
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
// clears cache for this query
if (empty($this->cursor_ext[$query_hash]['query'])) {
$this->__dbError(18, context: [
if (
$show_warning &&
empty($this->cursor_ext[$query_hash]['query'])
) {
$this->__dbWarning(18, context: [
'query' => $query,
'params' => $params,
'hash' => $query_hash,
@@ -3000,7 +2992,7 @@ class IO
if ($query === null) {
return $this->cursor_ext;
}
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
if (
!empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash])
@@ -3030,7 +3022,7 @@ class IO
$this->__dbError(11);
return false;
}
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
if (
!empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash])
@@ -3056,7 +3048,7 @@ class IO
$this->__dbError(11);
return false;
}
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
if (
!empty($this->cursor_ext) &&
isset($this->cursor_ext[$query_hash])
@@ -3082,7 +3074,7 @@ class IO
*/
public function dbResetQueryCalled(string $query, array $params = []): void
{
$this->query_called[$this->dbGetQueryHash($query, $params)] = 0;
$this->query_called[$this->dbBuildQueryHash($query, $params)] = 0;
}
/**
@@ -3095,7 +3087,7 @@ class IO
*/
public function dbGetQueryCalled(string $query, array $params = []): int
{
$query_hash = $this->dbGetQueryHash($query, $params);
$query_hash = $this->dbBuildQueryHash($query, $params);
if (!empty($this->query_called[$query_hash])) {
return $this->query_called[$query_hash];
} else {
@@ -3156,8 +3148,10 @@ class IO
'pk_name' => '',
'count' => 0,
'query' => '',
'query_raw' => $query,
'result' => null,
'returning_id' => false
'returning_id' => false,
'placeholder_converted' => [],
];
// if this is an insert query, check if we can add a return
if ($this->dbCheckQueryForInsert($query, true)) {
@@ -3197,6 +3191,39 @@ class IO
$this->prepare_cursor[$stm_name]['pk_name'] = $pk_name;
}
}
// QUERY PARAMS: run query params check and rewrite
if ($this->dbGetConvertPlaceholder() === true) {
try {
$this->placeholder_converted = ConvertPlaceholder::convertPlaceholderInQuery(
$query,
null,
$this->dbGetConvertPlaceholderTarget()
);
// write the new queries over the old
if (!empty($this->placeholder_converted['query'])) {
$query = $this->placeholder_converted['query'];
}
$this->prepare_cursor[$stm_name]['placeholder_converted'] = $this->placeholder_converted;
} catch (\OutOfRangeException $e) {
$this->__dbError($e->getCode(), context:[
'statement_name' => $stm_name,
'query' => $query,
'location' => 'dbPrepare',
'error' => 'OutOfRangeException',
'exception' => $e
]);
return false;
} catch (\RuntimeException $e) {
$this->__dbError($e->getCode(), context:[
'statement_name' => $stm_name,
'query' => $query,
'location' => 'dbPrepare',
'error' => 'RuntimeException',
'exception' => $e
]);
return false;
}
}
// check prepared curser parameter count
$this->prepare_cursor[$stm_name]['count'] = $this->__dbCountQueryParams($query);
$this->prepare_cursor[$stm_name]['query'] = $query;
@@ -3218,11 +3245,12 @@ class IO
}
} else {
// if we try to use the same statement name for a differnt query, error abort
if ($this->prepare_cursor[$stm_name]['query'] != $query) {
if ($this->prepare_cursor[$stm_name]['query_raw'] != $query) {
// thrown error
$this->__dbError(26, false, context: [
'statement_name' => $stm_name,
'prepared_query' => $this->prepare_cursor[$stm_name]['query'],
'prepared_query_raw' => $this->prepare_cursor[$stm_name]['query_raw'],
'query' => $query,
'pk_name' => $pk_name,
]);
@@ -3732,7 +3760,7 @@ class IO
}
/**
* convert db values (set)
* convert db values (set) to php matching types
*
* @param Convert $convert
* @return void
@@ -3743,7 +3771,7 @@ class IO
}
/**
* unsert convert db values flag
* unsert convert db values flag for converting db to php matching types
*
* @param Convert $convert
* @return void
@@ -3754,7 +3782,7 @@ class IO
}
/**
* Reset to origincal config file set
* Reset to original config file set for converting db to php matching type
*
* @return void
*/
@@ -3766,7 +3794,7 @@ class IO
}
/**
* check if a conert flag is set
* check if a convert flag is set for converting db to php matching type
*
* @param Convert $convert
* @return bool
@@ -3780,7 +3808,7 @@ class IO
}
/**
* Set if we want to auto convert PDO/\Pg placeholders
* Set if we want to auto convert to PDO/\Pg placeholders
*
* @param bool $flag
* @return void
@@ -4028,7 +4056,7 @@ class IO
}
/**
* Returns hash for query
* Creates hash for query and parameters
* Hash is used in all internal storage systems for return data
*
* @param string $query The query to create the hash from
@@ -4036,9 +4064,9 @@ class IO
* data to create a unique call one, optional
* @return string Hash, as set by hash long
*/
public function dbGetQueryHash(string $query, array $params = []): string
public function dbBuildQueryHash(string $query, array $params = []): string
{
return Hash::__hashLong(
return Hash::hashLong(
$query . (
$params !== [] ?
'#' . json_encode($params) : ''
@@ -4086,6 +4114,26 @@ class IO
$this->params = [];
}
/**
* get the current set query hash
*
* @return string Current Query hash
*/
public function dbGetQueryHash(): string
{
return $this->query_hash;
}
/**
* reset query hash
*
* @return void
*/
public function dbResetQueryHash(): void
{
$this->query_hash = '';
}
/**
* Returns the placeholder convert set or empty
*
@@ -4265,6 +4313,17 @@ class IO
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,
* will return false if field is not found in list
@@ -4291,7 +4350,7 @@ class IO
* @param string $stm_name The name of the stored statement
* @param string $key Key field name in prepared cursor array
* Allowed are: pk_name, count, query, returning_id
* @return null|string|int|bool Entry from each of the valid keys
* @return null|string|int|bool|array<string,mixed> Entry from each of the valid keys
* Will return false on error
* Not ethat returnin_id also can return false
* but will not set an error entry
@@ -4299,7 +4358,7 @@ class IO
public function dbGetPrepareCursorValue(
string $stm_name,
string $key
): null|string|int|bool {
): null|string|int|bool|array {
// if no statement name
if (empty($stm_name)) {
$this->__dbError(
@@ -4310,7 +4369,7 @@ class IO
return false;
}
// if not a valid key
if (!in_array($key, ['pk_name', 'count', 'query', 'returning_id'])) {
if (!in_array($key, ['pk_name', 'count', 'query', 'returning_id', 'placeholder_converted'])) {
$this->__dbError(
102,
false,
@@ -4345,6 +4404,37 @@ class IO
return $this->prepare_cursor[$stm_name][$key];
}
/**
* Checks if a prepared query eixsts
*
* @param string $stm_name Statement to check
* @param string $query [default=''] If set then query must also match
* @return false|int<0,2> False on missing stm_name
* 0: ok, 1: stm_name matchin, 2: stm_name and query matching
*/
public function dbPreparedCursorStatus(string $stm_name, string $query = ''): false|int
{
if (empty($stm_name)) {
$this->__dbError(
101,
false,
'No statement name given'
);
return false;
}
// does not exist
$return_value = 0;
if (!empty($this->prepare_cursor[$stm_name]['query_raw'])) {
// statement name eixts
$return_value = 1;
if ($this->prepare_cursor[$stm_name]['query_raw'] == $query) {
// query also matches
$return_value = 2;
}
}
return $return_value;
}
// ***************************
// ERROR AND WARNING DATA
// ***************************

View File

@@ -285,6 +285,22 @@ interface SqlFunctions
*/
public function __dbConnectionBusySocketWait(int $timeout_seconds = 3): bool;
/**
* Undocumented function
*
* @param string $parameter
* @param bool $strip
* @return string
*/
public function __dbVersionInfo(string $parameter, bool $strip = true): string;
/**
* Undocumented function
*
* @return array<mixed>
*/
public function __dbVersionInfoParameterList(): array;
/**
* Undocumented function
*
@@ -292,6 +308,13 @@ interface SqlFunctions
*/
public function __dbVersion(): string;
/**
* Undocumented function
*
* @return int
*/
public function __dbVersionNumeric(): int;
/**
* Undocumented function
*
@@ -306,6 +329,14 @@ interface SqlFunctions
?int &$end = null
): ?array;
/**
* Undocumented function
*
* @param string $parameter
* @return string|bool
*/
public function __dbParameter(string $parameter): string|bool;
/**
* Undocumented function
*
@@ -343,6 +374,14 @@ interface SqlFunctions
* @return string
*/
public function __dbGetEncoding(): string;
/**
* Undocumented function
*
* @param string $query
* @return array<string>
*/
public function __dbGetQueryParams(string $query): array;
}
// __END__

View File

@@ -51,6 +51,8 @@ declare(strict_types=1);
namespace CoreLibs\DB\SQL;
use CoreLibs\DB\Support\ConvertPlaceholder;
// below no ignore is needed if we want to use PgSql interface checks with PHP 8.0
// as main system. Currently all @var sets are written as object
/** @#phan-file-suppress PhanUndeclaredTypeProperty,PhanUndeclaredTypeParameter,PhanUndeclaredTypeReturnType */
@@ -102,7 +104,7 @@ class PgSQL implements Interface\SqlFunctions
* SELECT foo FROM bar WHERE foobar = $1
*
* @param string $query Query string with placeholders $1, ..
* @param array<mixed> $params Matching parameters for each placerhold
* @param array<mixed> $params Matching parameters for each placeholder
* @return \PgSql\Result|false Query result
*/
public function __dbQueryParams(string $query, array $params): \PgSql\Result|false
@@ -140,7 +142,7 @@ class PgSQL implements Interface\SqlFunctions
* sends an async query to the server with params
*
* @param string $query Query string with placeholders $1, ..
* @param array<mixed> $params Matching parameters for each placerhold
* @param array<mixed> $params Matching parameters for each placeholder
* @return bool true/false Query sent successful status
*/
public function __dbSendQueryParams(string $query, array $params): bool
@@ -405,17 +407,13 @@ class PgSQL implements Interface\SqlFunctions
}
// no PK name given at all
if (empty($pk_name)) {
// if name is plurar, make it singular
// if (preg_match("/.*s$/i", $table))
// $table = substr($table, 0, -1);
// set pk_name to "id"
$pk_name = $table . "_id";
}
$seq = ($schema ? $schema . '.' : '') . $table . "_" . $pk_name . "_seq";
$q = "SELECT CURRVAL('$seq') AS insert_id";
$q = "SELECT CURRVAL(pg_get_serial_sequence($1, $2)) AS insert_id";
// I have to do manually or I overwrite the original insert internal vars ...
if ($q = $this->__dbQuery($q)) {
if (is_array($res = $this->__dbFetchArray($q))) {
if ($cursor = $this->__dbQueryParams($q, [$table, $pk_name])) {
if (is_array($res = $this->__dbFetchArray($cursor))) {
list($id) = $res;
} else {
return false;
@@ -449,26 +447,36 @@ class PgSQL implements Interface\SqlFunctions
$table_prefix = $schema . '.';
}
}
$params = [$table_prefix . $table];
$replace = ['', ''];
// read from table the PK name
// faster primary key get
$q = "SELECT pg_attribute.attname AS column_name, "
. "format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS type "
. "FROM pg_index, pg_class, pg_attribute ";
$q = <<<SQL
SELECT
pg_attribute.attname AS column_name,
format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS type
FROM pg_index, pg_class, pg_attribute{PG_NAMESPACE}
WHERE
-- regclass translates the OID to the name
pg_class.oid = $1::regclass AND
indrelid = pg_class.oid AND
pg_attribute.attrelid = pg_class.oid AND
pg_attribute.attnum = any(pg_index.indkey) AND
indisprimary
{NSPNAME}
SQL;
if ($schema) {
$q .= ", pg_namespace ";
$params[] = $schema;
$replace = [
", pg_namespace",
"AND pg_class.relnamespace = pg_namespace.oid AND nspname = $2"
];
}
$q .= "WHERE "
// regclass translates the OID to the name
. "pg_class.oid = '" . $table_prefix . $table . "'::regclass AND "
. "indrelid = pg_class.oid AND ";
if ($schema) {
$q .= "nspname = '" . $schema . "' AND "
. "pg_class.relnamespace = pg_namespace.oid AND ";
}
$q .= "pg_attribute.attrelid = pg_class.oid AND "
. "pg_attribute.attnum = any(pg_index.indkey) "
. "AND indisprimary";
$cursor = $this->__dbQuery($q);
$cursor = $this->__dbQueryParams(str_replace(
['{PG_NAMESPACE}', '{NSPNAME}'],
$replace,
$q
), $params);
if ($cursor !== false) {
$__db_fetch_array = $this->__dbFetchArray($cursor);
if (!is_array($__db_fetch_array)) {
@@ -893,11 +901,13 @@ class PgSQL implements Interface\SqlFunctions
public function __dbSetSchema(string $db_schema): int
{
// check if schema actually exists
$query = "SELECT EXISTS("
. "SELECT 1 FROM information_schema.schemata "
. "WHERE schema_name = " . $this->__dbEscapeLiteral($db_schema)
. ")";
$cursor = $this->__dbQuery($query);
$query = <<<SQL
SELECT EXISTS (
SELECT 1 FROM information_schema.schemata
WHERE schema_name = $1
)
SQL;
$cursor = $this->__dbQueryParams($query, [$db_schema]);
// abort if execution fails
if ($cursor === false) {
return 1;
@@ -966,6 +976,34 @@ class PgSQL implements Interface\SqlFunctions
{
return $this->__dbShow('client_encoding');
}
/**
* Get the all the $ params, as a unique list
*
* @param string $query
* @return array<string>
*/
public function __dbGetQueryParams(string $query): array
{
$matches = [];
// regex for params: only stand alone $number allowed
// exclude all '' enclosed strings, ignore all numbers [note must start with digit]
// can have space/tab/new line
// must have <> = , ( [not equal, equal, comma, opening round bracket]
// can have space/tab/new line
// $ number with 1-9 for first and 0-9 for further digits
// Collects also PDO ? and :named, but they are ignored
// /s for matching new line in . list
// [disabled, we don't used ^ or $] /m for multi line match
// Matches in 1:, must be array_filtered to remove empty, count with array_unique
// Regex located in the ConvertPlaceholder class
preg_match_all(
ConvertPlaceholder::REGEX_LOOKUP_NUMBERED,
$query,
$matches
);
return array_unique(array_filter($matches[ConvertPlaceholder::MATCHING_POS]));
}
}
// __END__

View File

@@ -14,6 +14,88 @@ namespace CoreLibs\DB\Support;
class ConvertPlaceholder
{
/** @var string text block in SQL, single quited
* Note that does not include $$..$$ strings or anything with token name or nested ones
*/
private const PATTERN_TEXT_BLOCK_SINGLE_QUOTE = '(?:\'(?:[^\'\\\\]|\\\\.)*\')';
/** @var string text block in SQL, dollar quoted
* NOTE: if this is added everything shifts by one lookup number
*/
private const PATTERN_TEXT_BLOCK_DOLLAR = '(?:\$(\w*)\$.*?\$\1\$)';
/** @var string comment regex
* anything that starts with -- and ends with a line break but any character that is not line break inbetween
* this is the FIRST thing in the line and will skip any further lookups */
private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)';
// below are the params lookups
/** @var string named parameters, must start with single : */
private const PATTERN_NAMED = '((?<!:):(?:\w+))';
/** @var string question mark parameters, will catch any */
private const PATTERN_QUESTION_MARK = '(\?{1})';
/** @var string numbered parameters, can only start 1 to 9, second and further digits can be 0-9
* This ignores the $$ ... $$ escape syntax. If we find something like this will fail
* It is recommended to use proper string escape quiting for writing data to the DB
*/
private const PATTERN_NUMBERED = '(\$[1-9]{1}(?:[0-9]{1,})?)';
// below here are full regex that will be used
/** @var string replace regex for named (:...) entries */
public const REGEX_REPLACE_NAMED = '/'
. self::PATTERN_COMMENT . '|'
. self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_NAMED
. '/s';
/** @var string replace regex for question mark (?) entries */
public const REGEX_REPLACE_QUESTION_MARK = '/'
. self::PATTERN_COMMENT . '|'
. self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_QUESTION_MARK
. '/s';
/** @var string replace regex for numbered ($n) entries */
public const REGEX_REPLACE_NUMBERED = '/'
. self::PATTERN_COMMENT . '|'
. self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
. self::PATTERN_NUMBERED
. '/s';
/** @var string the main lookup query for all placeholders */
public const REGEX_LOOKUP_PLACEHOLDERS = '/'
. self::PATTERN_COMMENT . '|'
. self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
// match for replace part
. '(?:'
// :name named part (PDO) [1]
. self::PATTERN_NAMED . '|'
// ? question mark part (PDO) [2]
. self::PATTERN_QUESTION_MARK . '|'
// $n numbered part (\PG php) [3]
. self::PATTERN_NUMBERED
// end match
. ')'
// single line -> add line break to matches in "."
. '/s';
/** @var string lookup for only numbered placeholders */
public const REGEX_LOOKUP_NUMBERED = '/'
. self::PATTERN_COMMENT . '|'
. self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|'
. self::PATTERN_TEXT_BLOCK_DOLLAR . '|'
// match for replace part
. '(?:'
// $n numbered part (\PG php) [1]
. self::PATTERN_NUMBERED
// end match
. ')'
. '/s';
/** @var int position for regex in full placeholder lookup: named */
public const LOOOKUP_NAMED_POS = 2;
/** @var int position for regex in full placeholder lookup: question mark */
public const LOOOKUP_QUESTION_MARK_POS = 3;
/** @var int position for regex in full placeholder lookup: numbered */
public const LOOOKUP_NUMBERED_POS = 4;
/** @var int matches position for replacement and single lookup */
public const MATCHING_POS = 2;
/**
* Convert PDO type query with placeholders to \PG style and vica versa
* For PDO to: ? and :named
@@ -27,60 +109,41 @@ class ConvertPlaceholder
* found has -1 if an error occoured in the preg_match_all call
*
* @param string $query Query with placeholders to convert
* @param array<mixed> $params The parameters that are used for the query, and will be updated
* @param ?array<mixed> $params The parameters that are used for the query, and will be updated
* @param string $convert_to Either pdo or pg, will be converted to lower case for check
* @return 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>}
* @throws \OutOfRangeException 200
* @return array{original:array{query:string,params:array<mixed>,empty_params:bool},type:''|'named'|'numbered'|'question_mark',found:int,matches:array<string>,params_lookup:array<mixed>,query:string,params:array<mixed>}
* @throws \OutOfRangeException 200 If mixed placeholder types
* @throws \InvalidArgumentException 300 or 301 if wrong convert to with found placeholders
*/
public static function convertPlaceholderInQuery(
string $query,
array $params,
?array $params,
string $convert_to = 'pg'
): array {
$convert_to = strtolower($convert_to);
$matches = [];
$query_split = '[(=,?-]|->|->>|#>|#>>|@>|<@|\?\|\?\&|\|\||#-';
$pattern = '/'
// prefix string part, must match towards
// seperator for ( = , ? - [and json/jsonb in pg doc section 9.15]
. '(?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*'
// match for replace part
. '(?:'
// digit -> ignore
. '\d+|'
// other string -> ignore
. '(?:\'.*?\')|'
// :name named part (PDO)
. '(:\w+)|'
// ? question mark part (PDO)
. '(?:(?:\?\?)?\s*(\?{1}))|'
// $n numbered part (\PG php)
. '(\$[1-9]{1}(?:[0-9]{1,})?)'
// end match
. ')'
// single line -> add line break to matches in "."
. '/s';
// matches:
// 1: :named
// 2: ? question mark
// 3: $n numbered
$found = preg_match_all($pattern, $query, $matches, PREG_UNMATCHED_AS_NULL);
$found = preg_match_all(self::REGEX_LOOKUP_PLACEHOLDERS, $query, $matches, PREG_UNMATCHED_AS_NULL);
// if false or null set to -1
// || $found === null
if ($found === false) {
$found = -1;
}
/** @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 ? */
$qmark_matches = array_filter($matches[2]);
$qmark_matches = array_filter($matches[self::LOOOKUP_QUESTION_MARK_POS]);
/** @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_named = count($named_matches);
$count_named = count(array_unique($named_matches));
$count_qmark = count($qmark_matches);
$count_numbered = count($numbered_matches);
// throw if mixed
$count_numbered = count(array_unique($numbered_matches));
// throw exception if mixed found
if (
($count_named && $count_qmark) ||
($count_named && $count_numbered) ||
@@ -88,140 +151,194 @@ class ConvertPlaceholder
) {
throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200);
}
// rebuild
$matches_return = [];
$type = '';
// // throw if invalid conversion
// if (($count_named || $count_qmark) && $convert_to != 'pg') {
// throw new \InvalidArgumentException('Cannot convert from named or question mark placeholders to PDO', 300);
// }
// if ($count_numbered && $convert_to != 'pdo') {
// throw new \InvalidArgumentException('Cannot convert from numbered placeholders to Pg', 301);
// }
// return array
$return_placeholders = [
// original
'original' => [
'query' => $query,
'params' => $params ?? [],
'empty_params' => $params === null ? true : false,
],
// type found, empty if nothing was done
'type' => '',
// int: found, not found; -1: problem (set from false)
'found' => (int)$found,
'matches' => [],
// old to new lookup check
'params_lookup' => [],
// this must match the count in params in new
'needed' => 0,
// new
'query' => '',
'params' => [],
];
// replace basic regex and name settings
if ($count_named) {
$return_placeholders['type'] = 'named';
$return_placeholders['matches'] = $named_matches;
$return_placeholders['needed'] = $count_named;
} elseif ($count_qmark) {
$return_placeholders['type'] = 'question_mark';
$return_placeholders['matches'] = $qmark_matches;
$return_placeholders['needed'] = $count_qmark;
// for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove
} elseif ($count_numbered) {
$return_placeholders['type'] = 'numbered';
$return_placeholders['matches'] = $numbered_matches;
$return_placeholders['needed'] = $count_numbered;
}
// run convert only if matching type and direction
if (
(($count_named || $count_qmark) && $convert_to == 'pg') ||
($count_numbered && $convert_to == 'pdo')
) {
$param_list = self::updateParamList($return_placeholders);
$return_placeholders['params_lookup'] = $param_list['params_lookup'];
$return_placeholders['query'] = $param_list['query'];
$return_placeholders['params'] = $param_list['params'];
}
// return data
return $return_placeholders;
}
/**
* Updates the params list from one style to the other to match the query output
* if original.empty_params is set to true, no params replacement is done
* if param replacement has been done in a dbPrepare then this has to be run
* with the return palceholders array with params in original filled and empty_params turned off
*
* phpcs:disable Generic.Files.LineLength
* @param array{original:array{query:string,params:array<mixed>,empty_params:bool},type:''|'named'|'numbered'|'question_mark',found:int,matches?:array<string>,params_lookup?:array<mixed>,query?:string,params?:array<mixed>} $converted_placeholders
* phpcs:enable Generic.Files.LineLength
* @return array{params_lookup:array<mixed>,query:string,params:array<mixed>}
*/
public static function updateParamList(array $converted_placeholders): array
{
// skip if nothing set
if (!$converted_placeholders['found']) {
return [
'params_lookup' => [],
'query' => '',
'params' => []
];
}
$query_new = '';
$params_new = [];
$params_lookup = [];
if ($count_named && $convert_to == 'pg') {
$type = 'named';
$matches_return = $named_matches;
// only check for :named
$pattern_replace = '/'
. '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)'
. '(\d+|(?:\'.*?\')|(:\w+))'
. '/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part :named
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = '$' . $pos;
$params_new[] = $params[$matches[3]] ??
// set to null if params is empty
$params = $converted_placeholders['original']['params'];
$empty_params = $converted_placeholders['original']['empty_params'];
switch ($converted_placeholders['type']) {
case 'named':
// 1: replace part :named
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_NAMED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
if (!isset($matches[self::MATCHING_POS])) {
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params list',
210
'Cannot lookup ' . self::MATCHING_POS . ' in matches list',
209
);
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
211
)
);
},
$query
);
} elseif ($count_qmark && $convert_to == 'pg') {
$type = 'question_mark';
$matches_return = $qmark_matches;
// order and data stays the same
$params_new = $params;
// only check for ?
$pattern_replace = '/'
. '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)'
. '(\d+|(?:\'.*?\')|(?:(?:\?\?)?\s*(\?{1})))'
. '/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part ?
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_lookup) {
// only count pos up for actual replacements we will do
if (!empty($matches[3])) {
$pos++;
$params_lookup[] = '$' . $pos;
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
'$' . $pos
);
},
$query
);
// for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove
} elseif ($count_numbered && $convert_to == 'pdo') {
// convert numbered to named
$type = 'numbered';
$matches_return = $numbered_matches;
// only check for $n
$pattern_replace = '/'
. '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)'
. '(\d+|(?:\'.*?\')|(\$[1-9]{1}(?:[0-9]{1,})?))'
. '/s';
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part $numbered
$pos = 0;
$query_new = preg_replace_callback(
$pattern_replace,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = ':' . $pos . '_named';
$params_new[] = $params[($pos - 1)] ??
}
$match = $matches[self::MATCHING_POS];
// only count up if $match[1] is not yet in lookup table
if (empty($params_lookup[$match])) {
$pos++;
$params_lookup[$match] = '$' . $pos;
// skip params setup if param list is empty
if (!$empty_params) {
$params_new[] = $params[$match] ??
throw new \RuntimeException(
'Cannot lookup ' . $match . ' in params list',
210
);
}
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $params_lookup[$match] ??
throw new \RuntimeException(
'Cannot lookup ' . ($pos - 1) . ' in params list',
220
'Cannot lookup ' . $match . ' in params lookup list',
211
);
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
},
$converted_placeholders['original']['query']
);
break;
case 'question_mark':
if (!$empty_params) {
// order and data stays the same
$params_new = $params ?? [];
}
// 1: replace part ?
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_QUESTION_MARK,
function ($matches) use (&$pos, &$params_lookup) {
if (!isset($matches[self::MATCHING_POS])) {
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
221
)
);
},
$query
);
'Cannot lookup ' . self::MATCHING_POS . ' in matches list',
229
);
}
$match = $matches[self::MATCHING_POS];
// only count pos up for actual replacements we will do
if (!empty($match)) {
$pos++;
$params_lookup[] = '$' . $pos;
}
// add the connectors back (1), and the data sets only if no replacement will be done
return '$' . $pos;
},
$converted_placeholders['original']['query']
);
break;
case 'numbered':
// 1: replace part $numbered
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_NUMBERED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
if (!isset($matches[self::MATCHING_POS])) {
throw new \RuntimeException(
'Cannot lookup ' . self::MATCHING_POS . ' in matches list',
239
);
}
$match = $matches[self::MATCHING_POS];
// only count up if $match[1] is not yet in lookup table
if (empty($params_lookup[$match])) {
$pos++;
$params_lookup[$match] = ':' . $pos . '_named';
// skip params setup if param list is empty
if (!$empty_params) {
$params_new[] = $params[($pos - 1)] ??
throw new \RuntimeException(
'Cannot lookup ' . ($pos - 1) . ' in params list',
230
);
}
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $params_lookup[$match] ??
throw new \RuntimeException(
'Cannot lookup ' . $match . ' in params lookup list',
231
);
},
$converted_placeholders['original']['query']
);
break;
}
// return, old query is always set
return [
// original
'original' => [
'query' => $query,
'params' => $params,
],
// type found, empty if nothing was done
'type' => $type,
// int: found, not found; -1: problem (set from false)
'found' => (int)$found,
'matches' => $matches_return,
// old to new lookup check
'params_lookup' => $params_lookup,
// new
'query' => $query_new ?? '',
'params' => $params_new,
];

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

@@ -190,7 +190,6 @@ class GetTextReader
private function loadTables(): void
{
if (
is_array($this->cache_translations) &&
is_array($this->table_originals) &&
is_array($this->table_translations)
) {
@@ -318,10 +317,7 @@ class GetTextReader
if ($this->enable_cache) {
// Caching enabled, get translated string from cache
if (
is_array($this->cache_translations) &&
array_key_exists($string, $this->cache_translations)
) {
if (array_key_exists($string, $this->cache_translations)) {
return $this->cache_translations[$string];
} else {
return $string;
@@ -481,7 +477,7 @@ class GetTextReader
$key = $single . chr(0) . $plural;
if ($this->enable_cache) {
if (is_array($this->cache_translations) && !array_key_exists($key, $this->cache_translations)) {
if (!array_key_exists($key, $this->cache_translations)) {
return ($number != 1) ? $plural : $single;
} else {
$result = $this->cache_translations[$key];

View File

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

View File

@@ -30,7 +30,12 @@ class Logging
{
/** @var int minimum size for a max file size, so we don't set 1 byte, 10kb */
public const MIN_LOG_MAX_FILESIZE = 10 * 1024;
/** @var string log file extension, not changeable */
private const LOG_FILE_NAME_EXT = "log";
/** @var string log file block separator, not changeable */
private const LOG_FILE_BLOCK_SEPARATOR = '.';
// MARK: OPTION array
// 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}} */
private const OPTIONS = [
@@ -46,6 +51,7 @@ class Logging
'type' => 'string', 'mandatory' => false,
'default' => '', 'deprecated' => true, 'use' => 'log_file_id'
],
// log level
'log_level' => [
'type' => 'instance',
'type_info' => '\CoreLibs\Logging\Logger\Level',
@@ -53,6 +59,14 @@ class Logging
'default' => Level::Debug,
'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
'log_per_run' => [
'type' => 'bool', 'mandatory' => false,
@@ -88,8 +102,10 @@ class Logging
/** @var array<mixed> */
private array $options = [];
/** @var Level set level */
/** @var Level set logging 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
/** @var string */
@@ -104,8 +120,6 @@ class Logging
private string $log_folder = '';
/** @var string a alphanumeric name that has to be set as global definition */
private string $log_file_id = '';
/** @var string log file name extension */
private string $log_file_name_ext = 'log';
/** @var string log file name with folder, for actual writing */
private string $log_file_name = '';
/** @var int set in bytes */
@@ -143,12 +157,13 @@ class Logging
];
/**
* Init logger
* MARK: Init logger
*
* options array layout
* - log_folder:
* - log_file_id / file_id (will be deprecated):
* - log_level:
* - error_log_write_level: at what level we write to error_log
*
* - log_per_run:
* - log_per_date: (was print_file_date)
@@ -170,6 +185,8 @@ class Logging
// set log level
$this->initLogLevel();
// set error log write level
$this->initErrorLogWriteLevel();
// set log folder from options
$this->initLogFolder();
// set per run UID for logging
@@ -188,8 +205,10 @@ class Logging
// PRIVATE METHODS
// *********************************************************************
// MARK: options check
/**
* Undocumented function
* validate options
*
* @param array<mixed> $options
* @return bool
@@ -261,6 +280,8 @@ class Logging
return true;
}
// MARK: init log elvels
/**
* init log level, just a wrapper to auto set from options
*
@@ -278,6 +299,24 @@ class Logging
$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
* If folder is not writeable the script will throw an E_USER_ERROR
@@ -319,6 +358,8 @@ class Logging
return $status;
}
// MARK: set host name
/**
* Set the hostname and port
* If port is not defaul 80 it will be added to the host name
@@ -335,6 +376,8 @@ class Logging
}
}
// MARK: set log file id (file)
/**
* set log file prefix id
*
@@ -393,6 +436,8 @@ class Logging
return $status;
}
// MARK init log flags and levels
/**
* set flags from options and option flags connection internal settings
*
@@ -421,6 +466,19 @@ class Logging
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
*
@@ -431,7 +489,7 @@ class Logging
private function buildLogFileName(Level $level, string $group_id = ''): string
{
// init base file path
$fn = $this->log_print_file . '.' . $this->log_file_name_ext;
$fn = $this->log_print_file . '.' . self::LOG_FILE_NAME_EXT;
// log ID prefix settings, if not valid, replace with empty
if (!empty($this->log_file_id)) {
$rpl_string = $this->log_file_id;
@@ -440,14 +498,15 @@ class Logging
}
$fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix)
$rpl_string = !$this->getLogFlag(Flag::per_level) ? '' :
'_' . $level->getName();
$rpl_string = $this->getLogFlag(Flag::per_level) ?
self::LOG_FILE_BLOCK_SEPARATOR . $level->getName() :
'';
$fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename
// write per level
$rpl_string = !$this->getLogFlag(Flag::per_group) ? '' :
$rpl_string = $this->getLogFlag(Flag::per_group) ?
// normalize level, replace all non alphanumeric characters with -
'_' . (
self::LOG_FILE_BLOCK_SEPARATOR . (
// if return is only - then set error string
preg_match(
"/^-+$/",
@@ -455,25 +514,29 @@ class Logging
) ?
'INVALID-LEVEL-STRING' :
$level_string
);
) :
'';
$fn = str_replace('{GROUP}', $rpl_string, $fn); // create output filename
// set per class, but don't use get_class as we will only get self
$rpl_string = !$this->getLogFlag(Flag::per_class) ? '' : '_'
// set sub class settings
. str_replace('\\', '-', Support::getCallerTopLevelClass());
$rpl_string = $this->getLogFlag(Flag::per_class) ?
// set sub class settings
self::LOG_FILE_BLOCK_SEPARATOR . str_replace('\\', '-', Support::getCallerTopLevelClass()) :
'';
$fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename
// if request to write to one file
$rpl_string = !$this->getLogFlag(Flag::per_page) ?
'' :
'_' . System::getPageName(System::NO_EXTENSION);
$rpl_string = $this->getLogFlag(Flag::per_page) ?
self::LOG_FILE_BLOCK_SEPARATOR . System::getPageName(System::NO_EXTENSION) :
'';
$fn = str_replace('{PAGENAME}', $rpl_string, $fn); // create output filename
// if run id, we auto add ymd, so we ignore the log file date
if ($this->getLogFlag(Flag::per_run)) {
$rpl_string = '_' . $this->getLogUniqueId(); // add 8 char unique string
// add 8 char unique string and date block with time
$rpl_string = self::LOG_FILE_BLOCK_SEPARATOR . $this->getLogUniqueId();
} elseif ($this->getLogFlag(Flag::per_date)) {
$rpl_string = '_' . $this->getLogDate(); // add date to file
// add date to file
$rpl_string = self::LOG_FILE_BLOCK_SEPARATOR . $this->getLogDate();
} else {
$rpl_string = '';
}
@@ -483,6 +546,8 @@ class Logging
return $fn;
}
// MARK: master write log to file
/**
* writes error msg data to file for current level
*
@@ -500,6 +565,10 @@ class Logging
if (!$this->checkLogLevel($level)) {
return false;
}
// if we match level then write to error_log
if ($this->checkErrorLogWriteLevel($level)) {
error_log((string)$message);
}
// build logging file name
// fn is log folder + file name
@@ -524,6 +593,8 @@ class Logging
return true;
}
// MARK: master prepare log
/**
* Prepare the log message with all needed info blocks:
* [timestamp] [host name] [file path + file::row number] [running uid] {class::/->method}
@@ -603,6 +674,7 @@ class Logging
// PUBLIC STATIC METHJODS
// *********************************************************************
// MARK: set log level
/**
* set the log level
*
@@ -663,7 +735,7 @@ class Logging
// **** GET/SETTER
// log level set and get
// MARK: log level
/**
* set new log level
@@ -698,7 +770,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
@@ -726,7 +821,7 @@ class Logging
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)
@@ -739,7 +834,10 @@ class Logging
{
if (empty($this->log_file_unique_id) || $override == true) {
$this->log_file_unique_id =
date('Y-m-d_His') . '_U_'
date('Y-m-d_His')
. self::LOG_FILE_BLOCK_SEPARATOR
. 'U_'
// this doesn't have to be unique for everything, just for this logging purpose
. substr(hash(
'sha1',
random_bytes(63)
@@ -758,7 +856,7 @@ class Logging
return $this->log_file_unique_id;
}
// general log date
// MARK: general log date
/**
* set the log file date to Y-m-d
@@ -781,7 +879,7 @@ class Logging
return $this->log_file_date;
}
// general flag set
// MARK: general flag set
/**
* set one of the basic flags
@@ -836,7 +934,7 @@ class Logging
return $this->log_flags;
}
// log folder/file
// MARK: log folder/file
/**
* set new log folder, check that folder is writeable
@@ -880,7 +978,7 @@ class Logging
return $this->log_file_name;
}
// max log file size
// MARK: max log file size
/**
* set mag log file size
@@ -911,7 +1009,7 @@ class Logging
}
// *********************************************************************
// OPTIONS CALLS
// MARK: OPTIONS CALLS
// *********************************************************************
/**
@@ -929,6 +1027,8 @@ class Logging
// MAIN CALLS
// *********************************************************************
// MARK: main log call
/**
* Commong log interface
*
@@ -966,7 +1066,7 @@ class Logging
}
/**
* DEBUG: 100
* MARK: DEBUG: 100
*
* write debug data to error_msg array
*
@@ -998,7 +1098,7 @@ class Logging
}
/**
* INFO: 200
* MARK: INFO: 200
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1017,7 +1117,7 @@ class Logging
}
/**
* NOTICE: 250
* MARK: NOTICE: 250
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1036,7 +1136,7 @@ class Logging
}
/**
* WARNING: 300
* MARK: WARNING: 300
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1055,7 +1155,7 @@ class Logging
}
/**
* ERROR: 400
* MARK: ERROR: 400
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1074,7 +1174,7 @@ class Logging
}
/**
* CTRITICAL: 500
* MARK: CTRITICAL: 500
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1093,7 +1193,7 @@ class Logging
}
/**
* ALERT: 550
* MARK: ALERT: 550
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1112,7 +1212,7 @@ class Logging
}
/**
* EMERGENCY: 600
* MARK: EMERGENCY: 600
*
* @param string|Stringable $message
* @param mixed[] $context
@@ -1131,7 +1231,7 @@ class Logging
}
// *********************************************************************
// DEPRECATED SUPPORT CALLS
// MARK: DEPRECATED SUPPORT CALLS
// *********************************************************************
// legacy, but there are too many implemented
@@ -1189,7 +1289,7 @@ class Logging
}
// *********************************************************************
// DEPRECATED METHODS
// MARK: DEPRECATED METHODS
// *********************************************************************
/**
@@ -1355,7 +1455,7 @@ class Logging
}
// *********************************************************************
// DEBUG METHODS
// MARK: DEBUG METHODS
// *********************************************************************
/**
@@ -1388,6 +1488,7 @@ class Logging
}
// back to options level
$this->initLogLevel();
$this->initErrorLogWriteLevel();
print "OPT set level: " . $this->getLoggingLevel()->getName() . "<br>";
}
}

View File

@@ -474,7 +474,7 @@ class Generate
$page_name_camel_case
);
try {
/** @var TableArrays\Interface\TableArraysInterface|false $class */
/** @var TableArrays\Interface\TableArraysInterface $class */
$class = new $class_string($this);
} catch (\Throwable $t) {
$this->log->critical('CLASS LOADING: Failed loading: ' . $class_string . ' => ' . $t->getMessage());
@@ -1371,7 +1371,7 @@ class Generate
) {
$this->msg .= sprintf(
$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']
);
}
@@ -1757,14 +1757,9 @@ class Generate
$this->dba->setTableArrayEntry($this->dba->getTableArray()[$key]['preset'], $key, 'value');
}
}
if (is_array($this->reference_array)) {
if (!is_array($this->reference_array)) {
$this->reference_array = [];
}
reset($this->reference_array);
foreach ($this->reference_array as $key => $value) {
unset($this->reference_array[$key]['selected']);
}
reset($this->reference_array);
foreach ($this->reference_array as $key => $value) {
unset($this->reference_array[$key]['selected']);
}
$this->warning = 1;
$this->msg = $this->l->__('Cleared for new Dataset!');
@@ -1787,20 +1782,15 @@ class Generate
$this->dba->unsetTableArrayEntry($key, 'input_value');
}
if (is_array($this->reference_array)) {
// load each reference_table
if (!is_array($this->reference_array)) {
$this->reference_array = [];
}
reset($this->reference_array);
foreach ($this->reference_array as $key => $value) {
unset($this->reference_array[$key]['selected']);
$q = 'SELECT ' . $this->reference_array[$key]['other_table_pk']
. ' FROM ' . $this->reference_array[$key]['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
while (is_array($res = $this->dba->dbReturn($q))) {
$this->reference_array[$key]['selected'][] = $res[$this->reference_array[$key]['other_table_pk']];
}
// load each reference_table
reset($this->reference_array);
foreach ($this->reference_array as $key => $value) {
unset($this->reference_array[$key]['selected']);
$q = 'SELECT ' . $this->reference_array[$key]['other_table_pk']
. ' FROM ' . $this->reference_array[$key]['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
while (is_array($res = $this->dba->dbReturn($q))) {
$this->reference_array[$key]['selected'][] = $res[$this->reference_array[$key]['other_table_pk']];
}
}
$this->warning = 1;
@@ -1979,24 +1969,19 @@ class Generate
// write the object
$this->dba->dbWrite($addslashes, [], true);
// write reference array (s) if necessary
if (is_array($this->reference_array)) {
if (!is_array($this->reference_array)) {
$this->reference_array = [];
reset($this->reference_array);
foreach ($this->reference_array as $reference_array) {
$q = 'DELETE FROM ' . $reference_array['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
$this->dba->dbExec($q);
$q = 'INSERT INTO ' . $reference_array['table_name']
. ' (' . $reference_array['other_table_pk'] . ', ' . $this->int_pk_name . ') VALUES ';
for ($i = 0, $i_max = count($reference_array['selected']); $i < $i_max; $i++) {
$t_q = '(' . $reference_array['selected'][$i] . ', '
. $this->dba->getTableArray()[$this->int_pk_name]['value'] . ')';
$this->dba->dbExec($q . $t_q);
}
reset($this->reference_array);
foreach ($this->reference_array as $reference_array) {
$q = 'DELETE FROM ' . $reference_array['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
$this->dba->dbExec($q);
$q = 'INSERT INTO ' . $reference_array['table_name']
. ' (' . $reference_array['other_table_pk'] . ', ' . $this->int_pk_name . ') VALUES ';
for ($i = 0, $i_max = count($reference_array['selected']); $i < $i_max; $i++) {
$t_q = '(' . $reference_array['selected'][$i] . ', '
. $this->dba->getTableArray()[$this->int_pk_name]['value'] . ')';
$this->dba->dbExec($q . $t_q);
}
} // foreach reference arrays
} // if reference arrays
} // foreach reference arrays
// write element list
if (!empty($this->element_list)) {
$type = [];
@@ -2230,16 +2215,11 @@ class Generate
public function formDeleteTableArray()
{
// remove any reference arrays
if (is_array($this->reference_array)) {
if (!is_array($this->reference_array)) {
$this->reference_array = [];
}
reset($this->reference_array);
foreach ($this->reference_array as $reference_array) {
$q = 'DELETE FROM ' . $reference_array['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
$this->dba->dbExec($q);
}
reset($this->reference_array);
foreach ($this->reference_array as $reference_array) {
$q = 'DELETE FROM ' . $reference_array['table_name']
. ' WHERE ' . $this->int_pk_name . ' = ' . $this->dba->getTableArray()[$this->int_pk_name]['value'];
$this->dba->dbExec($q);
}
// remove any element list references
if (!empty($this->element_list)) {
@@ -2622,7 +2602,7 @@ class Generate
}
}
// add lost error ones
$this->log->error('P: ' . $data['prefix'] . ', '
$this->log->error('Prefix: ' . $data['prefix'] . ', '
. Support::prAr($_POST['ERROR'][$data['prefix']] ?? []));
if ($this->error && !empty($_POST['ERROR'][$data['prefix']])) {
$prfx = $data['prefix']; // short

View File

@@ -50,7 +50,8 @@ class EditUsers implements Interface\TableArraysInterface
'HIDDEN_value' => $_POST['HIDDEN_password'] ?? '',
'CONFIRM_value' => $_POST['CONFIRM_password'] ?? '',
'output_name' => 'Password',
'mandatory' => 1,
// make it not mandatory to create dummy accounts that can only login via login url id
'mandatory' => 0,
'type' => 'password', // later has to be password for encryption in database
'update' => [ // connected field updates, and update data
'password_change_date' => [ // db row to update
@@ -135,30 +136,6 @@ class EditUsers implements Interface\TableArraysInterface
'min_edit_acl' => '100',
'min_show_acl' => '100',
],
'debug' => [
'value' => $_POST['debug'] ?? '',
'output_name' => 'Debug',
'type' => 'binary',
'int' => 1,
'element_list' => [
'1' => 'Yes',
'0' => 'No'
],
'min_edit_acl' => '100',
'min_show_acl' => '100',
],
'db_debug' => [
'value' => $_POST['db_debug'] ?? '',
'output_name' => 'DB Debug',
'type' => 'binary',
'int' => 1,
'element_list' => [
'1' => 'Yes',
'0' => 'No'
],
'min_edit_acl' => '100',
'min_show_acl' => '100',
],
'email' => [
'value' => $_POST['email'] ?? '',
'output_name' => 'E-Mail',
@@ -206,6 +183,7 @@ class EditUsers implements Interface\TableArraysInterface
'type' => 'text',
'error_check' => 'unique|custom',
'error_regex' => "/^[A-Za-z0-9]+$/",
'error_example' => "ABCdef123",
'emptynull' => 1,'min_edit_acl' => '100',
'min_show_acl' => '100',
],

View File

@@ -2,7 +2,7 @@
/*
* sets a form token in the _SESSION variable
* session must be started for this to work
* session must be started and running for this to work
*/
declare(strict_types=1);

View File

@@ -256,8 +256,8 @@ class Image
}
// check resize parameters
if ($inc_width > $thumb_width || $inc_height > $thumb_height) {
$thumb_width_r = 0;
$thumb_height_r = 0;
$thumb_width_r = 1;
$thumb_height_r = 1;
// we need to keep the aspect ration on longest side
if (
($inc_height > $inc_width &&
@@ -288,6 +288,12 @@ class Image
!file_exists($thumbnail_write_path . $thumbnail)
) {
// image, copy source image, offset in image, source x/y, new size, source image size
if ($thumb_width_r < 1) {
$thumb_width_r = 1;
}
if ($thumb_height_r < 1) {
$thumb_height_r = 1;
}
$thumb = imagecreatetruecolor($thumb_width_r, $thumb_height_r);
if ($thumb === false) {
throw new \RuntimeException(
@@ -380,9 +386,7 @@ class Image
}
}
// add output path
if ($thumbnail !== false) {
$thumbnail = $thumbnail_web_path . $thumbnail;
}
$thumbnail = $thumbnail_web_path . $thumbnail;
} elseif ($create_dummy === true) {
// create dummy image in the thumbnail size
// if one side is missing, use the other side to create a square
@@ -399,10 +403,10 @@ class Image
!file_exists($thumbnail_write_path . $thumbnail)
) {
// if both are unset, set to 250
if ($thumb_height == 0) {
if ($thumb_height < 1) {
$thumb_height = 250;
}
if ($thumb_width == 0) {
if ($thumb_width < 1) {
$thumb_width = 250;
}
$thumb = imagecreatetruecolor($thumb_width, $thumb_height);

View File

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

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);
}
/**
* creates a sodium cyptobox keypair as hex string
*
* @return string hex string for the keypair
*/
public static function createKeyPair(): string
{
return self::bin2hex(sodium_crypto_box_keypair());
}
/**
* extracts the public key and returns it as hex string from the hex keypari
*
* @param string $hex_keypair hex encoded keypair
* @return string hex encoded public key
*/
public static function getPublicKey(
#[\SensitiveParameter]
string $hex_keypair
): string {
return self::bin2hex(sodium_crypto_box_publickey(self::hex2bin($hex_keypair)));
}
/**
* convert binary key to hex string
*
* @param string $hex_key Convert binary key string to hex
* @return string
*/
public static function bin2hex(string $hex_key): string
{
public static function bin2hex(
#[\SensitiveParameter]
string $hex_key
): string {
return sodium_bin2hex($hex_key);
}
@@ -52,8 +77,10 @@ class CreateKey
* @param string $string_key Convery hex key string to binary
* @return string
*/
public static function hex2bin(string $string_key): string
{
public static function hex2bin(
#[\SensitiveParameter]
string $string_key
): string {
return sodium_hex2bin($string_key);
}
}

View File

@@ -16,8 +16,10 @@ class Password
* @param string $password password
* @return string hashed password
*/
public static function passwordSet(string $password): string
{
public static function passwordSet(
#[\SensitiveParameter]
string $password
): string {
// always use the PHP default for the password
// password options ca be set in the password init,
// but should be kept as default
@@ -31,8 +33,11 @@ class Password
* @param string $hash password hash
* @return bool true or false
*/
public static function passwordVerify(string $password, string $hash): bool
{
public static function passwordVerify(
#[\SensitiveParameter]
string $password,
string $hash
): bool {
if (password_verify($password, $hash)) {
return true;
} else {

View File

@@ -24,19 +24,19 @@ class SymmetricEncryption
/** @var SymmetricEncryption self instance */
private static SymmetricEncryption $instance;
/** @var string bin hex key */
private string $key = '';
/** @var ?string bin hex key */
private ?string $key = null;
/**
* init class
* if key not passed, key must be set with createKey
*
* @param string|null|null $key
* @param string|null $key encryption key
*/
public function __construct(
string|null $key = null
?string $key = null
) {
if ($key != null) {
if ($key !== null) {
$this->setKey($key);
}
}
@@ -45,16 +45,49 @@ class SymmetricEncryption
* Returns the singleton self object.
* For function wrapper use
*
* @param string|null $key encryption key
* @return SymmetricEncryption object
*/
public static function getInstance(string|null $key = null): self
public static function getInstance(?string $key = null): self
{
if (empty(self::$instance)) {
// new if no instsance or key is different
if (
empty(self::$instance) ||
self::$instance->key != $key
) {
self::$instance = new self($key);
}
return self::$instance;
}
/**
* clean up
*
* @return void
*/
public function __deconstruct()
{
if (empty($this->key)) {
return;
}
try {
// would set it to null, but we we do not want to make key null
sodium_memzero($this->key);
return;
} catch (SodiumException) {
// empty catch
}
if (is_null($this->key)) {
return;
}
$zero = str_repeat("\0", mb_strlen($this->key, '8bit'));
$this->key = $this->key ^ (
$zero ^ $this->key
);
unset($zero);
unset($this->key); /** @phan-suppress-current-line PhanTypeObjectUnsetDeclaredProperty */
}
/* ************************************************************************
* MARK: PRIVATE
* *************************************************************************/
@@ -62,11 +95,19 @@ class SymmetricEncryption
/**
* create key and check validity
*
* @param string $key The key from which the binary key will be created
* @return string Binary key string
* @param ?string $key The key from which the binary key will be created
* @return string Binary key string
* @throws \UnexpectedValueException empty key
* @throws \UnexpectedValueException invalid hex key
* @throws \RangeException invalid length
*/
private function createKey(string $key): string
{
private function createKey(
#[\SensitiveParameter]
?string $key
): string {
if (empty($key)) {
throw new \UnexpectedValueException('Key cannot be empty');
}
try {
$key = CreateKey::hex2bin($key);
} catch (SodiumException $e) {
@@ -87,36 +128,42 @@ class SymmetricEncryption
* @param string $encrypted Text to decrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string Plain text
* @throws \RangeException
* @throws \UnexpectedValueException
* @throws \UnexpectedValueException
* @throws \UnexpectedValueException key cannot be empty
* @throws \UnexpectedValueException decipher message failed
* @throws \UnexpectedValueException invalid key
*/
private function decryptData(string $encrypted, ?string $key): string
{
if (empty($key)) {
throw new \UnexpectedValueException('Key not set');
private function decryptData(
#[\SensitiveParameter]
string $encrypted,
#[\SensitiveParameter]
?string $key
): string {
if (empty($encrypted)) {
throw new \UnexpectedValueException('Encrypted string cannot be empty');
}
$key = $this->createKey($key);
$decoded = base64_decode($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = false;
$plaintext = false;
try {
$plain = sodium_crypto_secretbox_open(
$plaintext = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
} catch (SodiumException $e) {
sodium_memzero($ciphertext);
sodium_memzero($key);
throw new \UnexpectedValueException('Decipher message failed: ' . $e->getMessage());
}
if (!is_string($plain)) {
throw new \UnexpectedValueException('Invalid Key');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
if (!is_string($plaintext)) {
throw new \UnexpectedValueException('Invalid Key');
}
return $plaintext;
}
/**
@@ -124,15 +171,15 @@ class SymmetricEncryption
*
* @param string $message Message to encrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string
* @throws \Exception
* @throws \RangeException
* @return string Ciphered text
* @throws \UnexpectedValueException create message failed
*/
private function encryptData(string $message, ?string $key): string
{
if (empty($this->key) || $key === null) {
throw new \UnexpectedValueException('Key not set');
}
private function encryptData(
#[\SensitiveParameter]
string $message,
#[\SensitiveParameter]
?string $key
): string {
$key = $this->createKey($key);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
try {
@@ -145,6 +192,8 @@ class SymmetricEncryption
)
);
} catch (SodiumException $e) {
sodium_memzero($message);
sodium_memzero($key);
throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage());
}
sodium_memzero($message);
@@ -156,19 +205,49 @@ class SymmetricEncryption
* MARK: PUBLIC
* *************************************************************************/
/**
* set a new key for encryption
*
* @param string $key
* @return void
* @return SymmetricEncryption
* @throws \UnexpectedValueException key cannot be empty
*/
public function setKey(string $key)
{
public function setKey(
#[\SensitiveParameter]
string $key
): SymmetricEncryption {
if (empty($key)) {
throw new \UnexpectedValueException('Key cannot be empty');
}
// check that this is a valid key
$this->createKey($key);
// set key
$this->key = $key;
sodium_memzero($key);
return $this;
}
/**
* Checks if set key is equal to parameter key
*
* @param string $key
* @return bool
*/
public function compareKey(
#[\SensitiveParameter]
string $key
): bool {
return $key === $this->key;
}
/**
* returns the current set key, null if not set
*
* @return ?string
*/
public function getKey(): ?string
{
return $this->key;
}
/**
@@ -178,13 +257,13 @@ class SymmetricEncryption
* @param string $encrypted Message encrypted with safeEncrypt()
* @param string $key Encryption key (as hex string)
* @return string
* @throws \Exception
* @throws \RangeException
* @throws \UnexpectedValueException
* @throws \UnexpectedValueException
*/
public static function decryptKey(string $encrypted, string $key): string
{
public static function decryptKey(
#[\SensitiveParameter]
string $encrypted,
#[\SensitiveParameter]
string $key
): string {
return self::getInstance()->decryptData($encrypted, $key);
}
@@ -193,12 +272,11 @@ class SymmetricEncryption
*
* @param string $encrypted Message encrypted with safeEncrypt()
* @return string
* @throws \RangeException
* @throws \UnexpectedValueException
* @throws \UnexpectedValueException
*/
public function decrypt(string $encrypted): string
{
public function decrypt(
#[\SensitiveParameter]
string $encrypted
): string {
return $this->decryptData($encrypted, $this->key);
}
@@ -209,11 +287,13 @@ class SymmetricEncryption
* @param string $message Message to encrypt
* @param string $key Encryption key (as hex string)
* @return string
* @throws \Exception
* @throws \RangeException
*/
public static function encryptKey(string $message, string $key): string
{
public static function encryptKey(
#[\SensitiveParameter]
string $message,
#[\SensitiveParameter]
string $key
): string {
return self::getInstance()->encryptData($message, $key);
}
@@ -222,11 +302,11 @@ class SymmetricEncryption
*
* @param string $message Message to encrypt
* @return string
* @throws \Exception
* @throws \RangeException
*/
public function encrypt(string $message): string
{
public function encrypt(
#[\SensitiveParameter]
string $message
): string {
return $this->encryptData($message, $this->key);
}
}

View File

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

1041
src/UrlRequests/Curl.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/10/29
* DESCRIPTION:
* Curl Client Trait for get/post/put/delete requests through the php curl inteface
*
* For anything more complex use guzzlehttp/http
* https://docs.guzzlephp.org/en/stable/index.html
*/
// phpcs:disable Generic.Files.LineLength
declare(strict_types=1);
namespace CoreLibs\UrlRequests;
trait CurlTrait
{
/**
* combined set call for any type of request with options type parameters
* The following options can be set:
* header: as array string:string
* query as string or array string:string
* body as string or array of any type
*
* @param string $type What type of request we send, will throw exception if not a valid one
* @param string $url The url to send
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Request options
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
*/
abstract public function request(string $type, string $url, array $options = []): array;
/**
* Makes an request to the target url via curl: GET
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function get(string $url, array $options = []): array
{
return $this->request(
"get",
$url,
$options,
);
}
/**
* Makes an request to the target url via curl: POST
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function post(string $url, array $options): array
{
return $this->request(
"post",
$url,
$options,
);
}
/**
* Makes an request to the target url via curl: PUT
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function put(string $url, array $options): array
{
return $this->request(
"put",
$url,
$options,
);
}
/**
* Makes an request to the target url via curl: PATCH
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function patch(string $url, array $options): array
{
return $this->request(
"patch",
$url,
$options,
);
}
/**
* Makes an request to the target url via curl: DELETE
* Returns result as string (json)
* Note that DELETE body is optional
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function delete(string $url, array $options = []): array
{
return $this->request(
"delete",
$url,
$options,
);
}
}
// __END__

View File

@@ -0,0 +1,83 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/9/20
* DESCRIPTION:
* URL Requests client interface
*/
namespace CoreLibs\UrlRequests\Interface;
interface RequestsInterface
{
/**
* get the config array with all settings
*
* @return array<string,mixed> all current config settings
*/
public function getConfig(): array;
/**
* Return the full url as it was sent
*
* @return string url sent
*/
public function getUrlSent(): string;
/**
* get the parsed url
*
* @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}
*/
public function getUrlParsedSent(): array;
/**
* Return the full headers as they where sent
*
* @return array<string,string>
*/
public function getHeadersSent(): array;
/**
* set, add or overwrite header
* On default this will overwrite header, and not set
*
* @param array<string,string|array<string>> $header
* @param bool $add [default=false] if set will add header to existing value
* @return void
*/
public function setHeaders(array $header, bool $add = false): void;
/**
* remove header entry
* if key is only set then match only key, if both are set both sides must match
*
* @param array<string,string> $remove_headers
* @return void
*/
public function removeHeaders(array $remove_headers): void;
/**
* Update the base url set, if empty will unset the base url
*
* @param string $base_uri
* @return void
*/
public function setBaseUri(string $base_uri): void;
/**
* combined set call for any type of request with options type parameters
*
* phpcs:disable Generic.Files.LineLength
* @param string $type
* @param string $url
* @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength
*/
public function request(string $type, string $url, array $options = []): array;
}
// __END__

View File

@@ -100,27 +100,6 @@ define('DEFAULT_ACL_LEVEL', 80);
/************* LOGOUT ********************/
// logout target
define('LOGOUT_TARGET', '');
// password change allowed
define('PASSWORD_CHANGE', false);
define('PASSWORD_FORGOT', false);
// min/max password length
define('PASSWORD_MIN_LENGTH', 9);
define('PASSWORD_MAX_LENGTH', 255);
// defines allowed special characters
define('PASSWORD_SPECIAL_RANGE', '@$!%*?&');
// password must have upper case, lower case, number, special
// comment out for not mandatory
define('PASSWORD_LOWER', '(?=.*[a-z])');
define('PASSWORD_UPPER', '(?=.*[A-Z])');
define('PASSWORD_NUMBER', '(?=.*\d)');
define('PASSWORD_SPECIAL', "(?=.*[" . PASSWORD_SPECIAL_RANGE . "])");
// define full regex
define('PASSWORD_REGEX', "/^"
. (defined('PASSWORD_LOWER') ? PASSWORD_LOWER : '')
. (defined('PASSWORD_UPPER') ? PASSWORD_UPPER : '')
. (defined('PASSWORD_NUMBER') ? PASSWORD_NUMBER : '')
. (defined('PASSWORD_SPECIAL') ? PASSWORD_SPECIAL : '')
. "[A-Za-z\d" . PASSWORD_SPECIAL_RANGE . "]{" . PASSWORD_MIN_LENGTH . "," . PASSWORD_MAX_LENGTH . "}$/");
/************* AJAX / ACCESS *************/
// ajax request type
@@ -161,13 +140,6 @@ define('DEFAULT_LOCALE', 'en_US.UTF-8');
// default web page encoding setting
define('DEFAULT_ENCODING', 'UTF-8');
/************* LOGGING *******************/
// below two can be defined here, but they should be
// defined in either the header file or the file itself
// as $LOG_FILE_ID which takes presence over LOG_FILE_ID
// see Basic class constructor
define('LOG_FILE_ID', BASE_NAME);
/************* QUEUE TABLE *************/
// if we have a dev/live system
// set_live is a per page/per item
@@ -211,8 +183,9 @@ list($HOST_NAME) = array_pad(explode(':', $_SERVER['HTTP_HOST'], 2), 2, null);
define('HOST_NAME', $HOST_NAME);
// BAIL ON MISSING MASTER SITE CONFIG
if (!isset($SITE_CONFIG[HOST_NAME]['location'])) {
echo 'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator';
exit;
throw new \InvalidArgumentException(
'Missing SITE_CONFIG entry for: "' . HOST_NAME . '". Contact Administrator'
);
}
// BAIL ON MISSING DB CONFIG:
// we have either no db selction for this host but have db config entries
@@ -228,8 +201,9 @@ if (
empty($DB_CONFIG[$SITE_CONFIG[HOST_NAME]['db_host']]))
)
) {
echo 'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator';
exit;
throw new \InvalidArgumentException(
'No matching DB config found for: "' . HOST_NAME . '". Contact Administrator'
);
}
// set SSL on
$is_secure = false;
@@ -291,22 +265,4 @@ if (file_exists(BASE . CONFIGS . 'config.other.php')) {
require BASE . CONFIGS . 'config.other.php';
}
/************* DEBUG *******************/
// turn off debug if debug flag is OFF
if (defined('DEBUG') && DEBUG == false) {
$ECHO_ALL = false;
$DEBUG_ALL = false;
$PRINT_ALL = false;
$DB_DEBUG = false;
$ENABLE_ERROR_HANDLING = false;
$DEBUG_ALL_OVERRIDE = false;
} else {
$ECHO_ALL = false;
$DEBUG_ALL = true;
$PRINT_ALL = true;
$DB_DEBUG = true;
$ENABLE_ERROR_HANDLING = false;
$DEBUG_ALL_OVERRIDE = false;
}
// __END__

View File

@@ -0,0 +1,28 @@
<?php // phpcs:ignore PSR1.Files.SideEffects
/********************************************************************
* AUTHOR: Clemens Schwaighofer
* CREATED: 2018/10/11
* SHORT DESCRIPTION:
* configuration file for core path settings
* CSV target paths, and other download access URLS or paths needed
* HISTORY:
*********************************************************************/
declare(strict_types=1);
// find trigger name "admin/" or "frontend/" in the getcwd() folder
$folder = '';
foreach (['admin', 'frontend'] as $_folder) {
if (strstr(getcwd() ?: '', DIRECTORY_SEPARATOR . $_folder)) {
$folder = $_folder;
break;
}
}
// if content path is empty, fallback is default
if (empty($folder)) {
$folder = 'default';
}
define('CONTENT_PATH', $folder . DIRECTORY_SEPARATOR);
// __END__

View File

@@ -53,19 +53,6 @@ for (
\gullevek\dotEnv\DotEnv::readEnvFile(
$__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH
);
// find trigger name "admin/" or "frontend/" in the getcwd() folder
$folder = '';
foreach (['admin', 'frontend'] as $_folder) {
if (strstr(getcwd() ?: '', DIRECTORY_SEPARATOR . $_folder)) {
$folder = $_folder;
break;
}
}
// if content path is empty, fallback is default
if (empty($folder)) {
$folder = 'default';
}
define('CONTENT_PATH', $folder . DIRECTORY_SEPARATOR);
// load master config file that loads all other config files
require $__DIR__PATH . $CONFIG_PATH_PREFIX . CONFIG_PATH . 'config.master.php';
break;

View File

@@ -4,4 +4,7 @@ require "../vendor/autoload.php";
print "Bytes: " . CoreLibs\Convert\Byte::humanReadableByteFormat(123414) . "<br>";
$curl = new CoreLibs\UrlRequests\Curl();
print "Config: " . print_r($curl->getConfig(), true) . "<br>";
// __END__

View File

@@ -0,0 +1,65 @@
<?php // phpcs:ignore PSR1.Files.SideEffects
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: Ymd
* DESCRIPTION:
* DescriptionHere
*/
declare(strict_types=1);
/**
* build return json
*
* @param array<string,mixed> $http_headers
* @param ?string $body
* @return string
*/
function buildContent(array $http_headers, ?string $body): string
{
if (is_string($body) && !empty($body)) {
$_body = json_decode($body, true);
if (!is_array($_body)) {
$body = [$body];
} else {
$body = $_body;
}
} elseif (is_string($body)) {
$body = [];
}
return json_encode([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => $body,
]);
}
$http_headers = array_filter($_SERVER, function ($value, $key) {
if (str_starts_with($key, 'HTTP_')) {
return true;
}
}, ARRAY_FILTER_USE_BOTH);
header("Content-Type: application/json; charset=UTF-8");
// if the header has Authorization and RunAuthTest then exit with 401
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}');
exit(1);
}
// if server request type is get set file_get to null -> no body
if ($_SERVER['REQUEST_METHOD'] == "GET") {
$file_get = null;
} elseif (($file_get = file_get_contents('php://input')) === false) {
header("HTTP/1.1 404 Not Found");
print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}');
exit(1);
}
print buildContent($http_headers, $file_get);
// __END__

View File

@@ -12,6 +12,8 @@ Not yet covered tests:
- loginGetLocale
- loginGetHeaderColor
- loginGetPages
- loginGetPageLookupList
- loginPageAccessAllowed
- loginGetEuid
*/
@@ -22,8 +24,12 @@ Not yet covered tests:
*/
final class CoreLibsACLLoginTest extends TestCase
{
private static $db;
private static $log;
private static \CoreLibs\DB\IO $db;
private static \CoreLibs\Logging\Logging $log;
private static string $edit_access_cuid;
private static string $edit_user_cuid;
private static string $edit_user_cuuid;
/**
* start DB conneciton, setup DB, etc
@@ -108,21 +114,46 @@ final class CoreLibsACLLoginTest extends TestCase
self::$db->dbSetMaxQueryCall(-1);
// insert additional content for testing (locked user, etc)
$queries = [
"INSERT INTO edit_access_data "
. "(edit_access_id, name, value, enabled) VALUES "
. "((SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'), "
. "'test', 'value', 1)"
<<<SQL
INSERT INTO edit_access_data (
edit_access_id, name, value, enabled
) VALUES (
(SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'),
'test', 'value', 1
)
SQL
];
foreach ($queries as $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
// must set
// TARGET
define('TARGET', 'test');
// LOGIN DB SCHEMA
// define('LOGIN_DB_SCHEMA', '');
// SHOULD SET
// DEFAULT_ACL_LEVEL (d80)
@@ -235,22 +266,25 @@ final class CoreLibsACLLoginTest extends TestCase
'ajax_post_action' => 'login',
],
],
'load, session euid set only, php error' => [
'load, session eucuuid set only, php error' => [
[
'page_name' => 'edit_users.php',
],
[],
[],
[
'EUID' => 1,
'LOGIN_EUID' => 1,
'LOGIN_EUCUID' => 'abc',
'LOGIN_EUCUUID' => '1233456-1234-1234-1234-123456789012',
],
2,
[],
],
'load, session euid set, all set' => [
'load, session eucuuid set, all set' => [
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -259,20 +293,23 @@ final class CoreLibsACLLoginTest extends TestCase
[],
[],
[
'EUID' => 1,
'USER_NAME' => '',
'GROUP_NAME' => '',
'ADMIN' => 1,
'GROUP_ACL_LEVEL' => -1,
'PAGES_ACL_LEVEL' => [],
'USER_ACL_LEVEL' => -1,
'USER_ADDITIONAL_ACL' => [],
'GROUP_ADDITIONAL_ACL' => [],
'UNIT_UID' => [
'AdminAccess' => 1,
'LOGIN_EUID' => 1,
'LOGIN_EUCUID' => 'abc',
'LOGIN_EUCUUID' => 'SET_EUCUUID_IN_TEST',
'LOGIN_USER_NAME' => '',
'LOGIN_GROUP_NAME' => '',
'LOGIN_ADMIN' => 1,
'LOGIN_GROUP_ACL_LEVEL' => -1,
'LOGIN_PAGES_ACL_LEVEL' => [],
'LOGIN_USER_ACL_LEVEL' => -1,
'LOGIN_USER_ADDITIONAL_ACL' => [],
'LOGIN_GROUP_ADDITIONAL_ACL' => [],
'LOGIN_UNIT_UID' => [
'AdminAccess' => '123456789012',
],
'UNIT' => [
1 => [
'LOGIN_UNIT' => [
'123456789012' => [
'id' => 1,
'acl_level' => 80,
'name' => 'Admin Access',
'uid' => 'AdminAccess',
@@ -284,8 +321,8 @@ final class CoreLibsACLLoginTest extends TestCase
'additional_acl' => []
],
],
// 'UNIT_DEFAULT' => '',
// 'DEFAULT_ACL_LIST' => [],
// 'LOGIN_UNIT_DEFAULT' => '',
// 'LOGIN_DEFAULT_ACL_LIST' => [],
],
0,
[
@@ -293,6 +330,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -412,6 +450,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_deleted' => true
@@ -437,6 +476,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_enabled' => true
@@ -462,6 +502,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_locked' => true
@@ -487,6 +528,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_get_locked' => true,
@@ -511,6 +553,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_locked_period_until' => 'on'
@@ -536,6 +579,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -555,6 +599,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -565,6 +610,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_locked_period_after' => 'on'
@@ -590,6 +636,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_locked_period_until' => 'on',
@@ -616,6 +663,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_login_user_id_locked' => true
@@ -641,6 +689,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -659,6 +708,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -669,6 +719,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -688,6 +739,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -698,6 +750,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -717,6 +770,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -727,6 +781,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -746,6 +801,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -777,6 +833,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -800,6 +857,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -810,6 +868,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -833,6 +892,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -843,6 +903,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_login_user_id_revalidate_after' => 'on',
@@ -869,6 +930,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -889,6 +951,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -899,6 +962,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_login_user_id_valid_from' => 'on',
@@ -925,6 +989,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -945,6 +1010,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -955,6 +1021,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_login_user_id_valid_until' => 'on',
@@ -981,6 +1048,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'base_access' => 'list',
'page_access' => 'list',
'test_login_user_id_valid_from' => 'on',
@@ -1008,6 +1076,7 @@ final class CoreLibsACLLoginTest extends TestCase
[
'page_name' => 'edit_users.php',
'edit_access_id' => 1,
'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'edit_access_uid' => 'AdminAccess',
'edit_access_data' => 'test',
'base_access' => 'list',
@@ -1038,6 +1107,7 @@ final class CoreLibsACLLoginTest extends TestCase
'admin_flag' => true,
'check_access' => true,
'check_access_id' => 1,
'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
'check_access_data' => 'value',
'base_access' => true,
'page_access' => true,
@@ -1085,9 +1155,9 @@ final class CoreLibsACLLoginTest extends TestCase
/** @var \CoreLibs\Create\Session&MockObject */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
['startSession', 'checkActiveSession', 'sessionDestroy']
['getSessionId', 'checkActiveSession', 'sessionDestroy']
);
$session_mock->method('startSession')->willReturn('ACLLOGINTEST12');
$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST12');
$session_mock->method('checkActiveSession')->willReturn(true);
$session_mock->method('sessionDestroy')->will(
$this->returnCallback(function () {
@@ -1107,11 +1177,15 @@ final class CoreLibsACLLoginTest extends TestCase
$_POST[$post_var] = $post_value;
}
// set ingoing session cuuid if requested
if (isset($session['LOGIN_EUCUUID']) && $session['LOGIN_EUCUUID'] == 'SET_EUCUUID_IN_TEST') {
$session['LOGIN_EUCUUID'] = self::$edit_user_cuuid;
}
// set _SESSION data
foreach ($session as $session_var => $session_value) {
$_SESSION[$session_var] = $session_value;
}
/** @var \CoreLibs\ACL\Login&MockObject */
$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
->setConstructorArgs([
@@ -1130,7 +1204,7 @@ final class CoreLibsACLLoginTest extends TestCase
. 'locale' . DIRECTORY_SEPARATOR,
]
])
->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin'])
->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity'])
->getMock();
$login_mock->expects($this->any())
->method('loginTerminate')
@@ -1148,6 +1222,10 @@ final class CoreLibsACLLoginTest extends TestCase
->method('loginPrintLogin')
->willReturnCallback(function () {
});
$login_mock->expects($this->any())
->method('loginEnhanceHttpSecurity')
->willReturnCallback(function () {
});
// if mock_settings: enabled OFF
// run DB update and set off
@@ -1365,6 +1443,19 @@ final class CoreLibsACLLoginTest extends TestCase
// run test
try {
// preset, we cannot set that in the provider
if (
isset($expected['check_access_cuid']) &&
$expected['check_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST'
) {
$expected['check_access_cuid'] = self::$edit_access_cuid;
}
if (
isset($mock_settings['edit_access_cuid']) &&
$mock_settings['edit_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST'
) {
$mock_settings['edit_access_cuid'] = self::$edit_access_cuid;
}
// if ajax call
// check if parameter, or globals (old type)
// else normal call
@@ -1423,6 +1514,31 @@ final class CoreLibsACLLoginTest extends TestCase
$login_mock->loginCheckAccessPage($mock_settings['page_access']),
'Assert page access'
);
// - loginCheckEditAccessCuid
$this->assertEquals(
$expected['check_access'],
$login_mock->loginCheckEditAccessCuid($mock_settings['edit_access_cuid']),
'Assert check access'
);
// - loginCheckEditAccessValidCuid
$this->assertEquals(
$expected['check_access_cuid'],
$login_mock->loginCheckEditAccessValidCuid($mock_settings['edit_access_cuid']),
'Assert check access cuid valid'
);
// - loginGetEditAccessCuidFromUid
$this->assertEquals(
$expected['check_access_cuid'],
$login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_uid']),
'Assert check access uid to cuid valid'
);
// - loginGetEditAccessCuidFromId
$this->assertEquals(
$expected['check_access_cuid'],
$login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_id']),
'Assert check access id to cuid valid'
);
// Deprecated
// - loginCheckEditAccess
$this->assertEquals(
$expected['check_access'],
@@ -1445,7 +1561,7 @@ final class CoreLibsACLLoginTest extends TestCase
$this->assertEquals(
$expected['check_access_data'],
$login_mock->loginGetEditAccessData(
$mock_settings['edit_access_id'],
$mock_settings['edit_access_uid'],
$mock_settings['edit_access_data']
),
'Assert check access id data value valid'
@@ -1476,11 +1592,12 @@ final class CoreLibsACLLoginTest extends TestCase
// - loginCheckPermissions
// - loginGetPermissionOkay
} catch (\Exception $e) {
// print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/"
// . ($expected['login_error'] ?? 0) . "\n";
// print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n";
// print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n";
// print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n";
/* print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/"
. ($expected['login_error'] ?? 0) . "\n";
print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n";
print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n";
print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n";
print "POST exit: " . ($_POST['login_exit'] ?? '{0}') . "\n"; */
// if this is 100, then we do further error checks
if (
$e->getCode() == 100 ||
@@ -1788,9 +1905,9 @@ final class CoreLibsACLLoginTest extends TestCase
/** @var \CoreLibs\Create\Session&MockObject */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
['startSession', 'checkActiveSession', 'sessionDestroy']
['getSessionId', 'checkActiveSession', 'sessionDestroy']
);
$session_mock->method('startSession')->willReturn('ACLLOGINTEST34');
$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
$session_mock->method('checkActiveSession')->willReturn(true);
$session_mock->method('sessionDestroy')->will(
$this->returnCallback(function () {
@@ -1902,9 +2019,9 @@ final class CoreLibsACLLoginTest extends TestCase
/** @var \CoreLibs\Create\Session&MockObject */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
['startSession', 'checkActiveSession', 'sessionDestroy']
['getSessionId', 'checkActiveSession', 'sessionDestroy']
);
$session_mock->method('startSession')->willReturn('ACLLOGINTEST34');
$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
$session_mock->method('checkActiveSession')->willReturn(true);
$session_mock->method('sessionDestroy')->will(
$this->returnCallback(function () {
@@ -1990,9 +2107,9 @@ final class CoreLibsACLLoginTest extends TestCase
/** @var \CoreLibs\Create\Session&MockObject */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
['startSession', 'checkActiveSession', 'sessionDestroy']
['getSessionId', 'checkActiveSession', 'sessionDestroy']
);
$session_mock->method('startSession')->willReturn('ACLLOGINTEST34');
$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
$session_mock->method('checkActiveSession')->willReturn(true);
$session_mock->method('sessionDestroy')->will(
$this->returnCallback(function () {
@@ -2086,9 +2203,9 @@ final class CoreLibsACLLoginTest extends TestCase
/** @var \CoreLibs\Create\Session&MockObject */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
['startSession', 'checkActiveSession', 'sessionDestroy']
['getSessionId', 'checkActiveSession', 'sessionDestroy']
);
$session_mock->method('startSession')->willReturn('ACLLOGINTEST34');
$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
$session_mock->method('checkActiveSession')->willReturn(true);
$session_mock->method('sessionDestroy')->will(
$this->returnCallback(function () {

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

@@ -926,48 +926,114 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
public function daysIntervalProvider(): array
{
return [
'valid interval /, not named array' => [
'2020/1/1',
'2020/1/30',
false,
[29, 22, 8],
// normal and format tests
'valid interval / not named array' => [
'input_a' => '2020/1/1',
'input_b' => '2020/1/30',
'return_named' => false, // return_named
'include_end_date' => true, // include_end_date
'exclude_start_date' => false, // exclude_start_date
'expected' => [30, 22, 8, false],
],
'valid interval /, named array' => [
'2020/1/1',
'2020/1/30',
true,
['overall' => 29, 'weekday' => 22, 'weekend' => 8],
'valid interval / named array' => [
'input_a' => '2020/1/1',
'input_b' => '2020/1/30',
'return_named' => true,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => ['overall' => 30, 'weekday' => 22, 'weekend' => 8, 'reverse' => false],
],
'valid interval -' => [
'2020-1-1',
'2020-1-30',
false,
[29, 22, 8],
],
'valid interval switched' => [
'2020/1/30',
'2020/1/1',
false,
[28, 0, 0],
'valid interval with "-"' => [
'input_a' => '2020-1-1',
'input_b' => '2020-1-30',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [30, 22, 8, false],
],
'valid interval with time' => [
'2020/1/1 12:12:12',
'2020/1/30 13:13:13',
false,
[28, 21, 8],
'input_a' => '2020/1/1 12:12:12',
'input_b' => '2020/1/30 13:13:13',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [30, 22, 8, false],
],
// invalid
'invalid dates' => [
'abc',
'xyz',
false,
[0, 0, 0]
'input_a' => 'abc',
'input_b' => 'xyz',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [0, 0, 0, false]
],
// this test will take a long imte
// this test will take a long time
'out of bound dates' => [
'1900-1-1',
'9999-12-31',
false,
[2958463,2113189,845274],
'input_a' => '1900-1-1',
'input_b' => '9999-12-31',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [2958463, 2113189, 845274, false],
],
// tests for include/exclude
'exclude end date' => [
'input_b' => '2020/1/1',
'input_a' => '2020/1/30',
'return_named' => false,
'include_end_date' => false,
'exclude_start_date' => false,
'expected' => [29, 21, 8, false],
],
'exclude start date' => [
'input_b' => '2020/1/1',
'input_a' => '2020/1/30',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => true,
'expected' => [29, 21, 8, false],
],
'exclude start and end date' => [
'input_b' => '2020/1/1',
'input_a' => '2020/1/30',
'return_named' => false,
'include_end_date' => false,
'exclude_start_date' => true,
'expected' => [28, 20, 8, false],
],
// reverse
'reverse: valid interval' => [
'input_a' => '2020/1/30',
'input_b' => '2020/1/1',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => false,
'expected' => [30, 22, 8, true],
],
'reverse: exclude end date' => [
'input_a' => '2020/1/30',
'input_b' => '2020/1/1',
'return_named' => false,
'include_end_date' => false,
'exclude_start_date' => false,
'expected' => [29, 21, 8, true],
],
'reverse: exclude start date' => [
'input_a' => '2020/1/30',
'input_b' => '2020/1/1',
'return_named' => false,
'include_end_date' => true,
'exclude_start_date' => true,
'expected' => [29, 21, 8, true],
],
'reverse: exclude start and end date' => [
'input_a' => '2020/1/30',
'input_b' => '2020/1/1',
'return_named' => false,
'include_end_date' => false,
'exclude_start_date' => true,
'expected' => [28, 20, 8, true],
],
];
}
@@ -982,20 +1048,52 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
*
* @param string $input_a
* @param string $input_b
* @param bool $flag
* @param array $expected
* @param bool $return_named
* @param array $expected
* @return void
*/
public function testCalcDaysInterval(
string $input_a,
string $input_b,
bool $flag,
bool $return_named,
bool $include_end_date,
bool $exclude_start_date,
$expected
): void {
$this->assertEquals(
$expected,
\CoreLibs\Combined\DateTime::calcDaysInterval($input_a, $input_b, $flag)
\CoreLibs\Combined\DateTime::calcDaysInterval(
$input_a,
$input_b,
return_named:$return_named,
include_end_date:$include_end_date,
exclude_start_date:$exclude_start_date
),
'call calcDaysInterval'
);
if ($return_named) {
$this->assertEquals(
$expected,
\CoreLibs\Combined\DateTime::calcDaysIntervalNamedIndex(
$input_a,
$input_b,
include_end_date:$include_end_date,
exclude_start_date:$exclude_start_date
),
'call calcDaysIntervalNamedIndex'
);
} else {
$this->assertEquals(
$expected,
\CoreLibs\Combined\DateTime::calcDaysIntervalNumIndex(
$input_a,
$input_b,
include_end_date:$include_end_date,
exclude_start_date:$exclude_start_date
),
'call calcDaysIntervalNamedIndex'
);
}
}
/**
@@ -1187,7 +1285,38 @@ final class CoreLibsCombinedDateTimeTest extends TestCase
'2023-07-03',
'2023-07-27',
true
]
],
// reverse
'reverse: no weekend' => [
'2023-07-04',
'2023-07-03',
false
],
'reverse: start weekend sat' => [
'2023-07-04',
'2023-07-01',
true
],
'reverse: start weekend sun' => [
'2023-07-04',
'2023-07-02',
true
],
'reverse: end weekend sat' => [
'2023-07-08',
'2023-07-03',
true
],
'reverse: end weekend sun' => [
'2023-07-09',
'2023-07-03',
true
],
'reverse: long period > 6 days' => [
'2023-07-27',
'2023-07-03',
true
],
];
}

View File

@@ -40,7 +40,7 @@ final class CoreLibsConvertByteTest extends TestCase
4 => '1.00 KB',
5 => '1.02KiB',
],
'invalud string number' => [
'invalid string number' => [
0 => '1024 MB',
1 => '1024 MB',
2 => '1024 MB',

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
/**
* Test class for Convert\Colors
* @coversDefaultClass \CoreLibs\Convert\Colors
* @testdox \CoreLibs\Convert\Colors method tests
* @testdox \CoreLibs\Convert\Colors legacy method tests
*/
final class CoreLibsConvertColorsTest extends TestCase
{
@@ -21,7 +21,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function rgb2hexColorProvider(): array
public function providerRgb2hexColor(): array
{
return [
'color' => [
@@ -88,7 +88,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function hex2rgbColorProvider(): array
public function providerHex2rgbColor(): array
{
return [
'color' => [
@@ -215,7 +215,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function rgb2hsbColorProvider(): array
public function providerRgb2hsbColor(): array
{
$list = [];
foreach ($this->rgb2hslAndhsbList() as $name => $values) {
@@ -234,7 +234,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function hsb2rgbColorProvider(): array
public function providerHsb2rgbColor(): array
{
$list = [];
foreach ($this->rgb2hslAndhsbList() as $name => $values) {
@@ -253,7 +253,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function rgb2hslColorProvider(): array
public function providerRgb2hslColor(): array
{
$list = [];
foreach ($this->rgb2hslAndhsbList() as $name => $values) {
@@ -272,7 +272,7 @@ final class CoreLibsConvertColorsTest extends TestCase
*
* @return array
*/
public function hsl2rgbColorProvider(): array
public function providerHsl2rgbColor(): array
{
$list = [];
foreach ($this->rgb2hslAndhsbList() as $name => $values) {
@@ -291,7 +291,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* TODO: add cross convert check
*
* @covers ::rgb2hex
* @dataProvider rgb2hexColorProvider
* @dataProvider providerRgb2hexColor
* @testdox rgb2hex $input_r,$input_g,$input_b will be $expected [$_dataName]
*
* @param int $input_r
@@ -342,7 +342,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* Undocumented function
*
* @covers ::hex2rgb
* @dataProvider hex2rgbColorProvider
* @dataProvider providerHex2rgbColor
* @testdox hex2rgb $input will be $expected, $expected_str str[,], $expected_str_sep str[$separator] [$_dataName]
*
* @param string $input
@@ -385,7 +385,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* Undocumented function
*
* @covers ::rgb2hsb
* @dataProvider rgb2hsbColorProvider
* @dataProvider providerRgb2hsbColor
* @testdox rgb2hsb $input_r,$input_g,$input_b will be $expected [$_dataName]
*
* @param integer $input_r
@@ -409,7 +409,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* Undocumented function
*
* @covers ::hsb2rgb
* @dataProvider hsb2rgbColorProvider
* @dataProvider providerHsb2rgbColor
* @testdox hsb2rgb $input_h,$input_s,$input_b will be $expected [$_dataName]
*
* @param float $input_h
@@ -434,7 +434,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* Undocumented function
*
* @covers ::rgb2hsl
* @dataProvider rgb2hslColorProvider
* @dataProvider providerRgb2hslColor
* @testdox rgb2hsl $input_r,$input_g,$input_b will be $expected [$_dataName]
*
* @param integer $input_r
@@ -458,7 +458,7 @@ final class CoreLibsConvertColorsTest extends TestCase
* Undocumented function
*
* @covers ::hsl2rgb
* @dataProvider hsl2rgbColorProvider
* @dataProvider providerHsl2rgbColor
* @testdox hsl2rgb $input_h,$input_s,$input_l will be $expected [$_dataName]
*
* @param integer|float $input_h

View File

@@ -18,7 +18,7 @@ final class CoreLibsConvertMathTest extends TestCase
*
* @return array<mixed>
*/
public function fceilProvider(): array
public function providerFceil(): array
{
return [
'5.5 must be 6' => [5.5, 6],
@@ -31,7 +31,7 @@ final class CoreLibsConvertMathTest extends TestCase
* Undocumented function
*
* @covers ::fceil
* @dataProvider fceilProvider
* @dataProvider providerFceil
* @testdox fceil: Input $input must be $expected
*
* @param float $input
@@ -51,7 +51,7 @@ final class CoreLibsConvertMathTest extends TestCase
*
* @return array<mixed>
*/
public function floorProvider(): array
public function providerFloor(): array
{
return [
'5123456 with -3 must be 5123000' => [5123456, -3, 5123000],
@@ -63,7 +63,7 @@ final class CoreLibsConvertMathTest extends TestCase
* Undocumented function
*
* @covers ::floorp
* @dataProvider floorProvider
* @dataProvider providerFloor
* @testdox floor: Input $input with cutoff $cutoff must be $expected
*
* @param int $input
@@ -84,7 +84,7 @@ final class CoreLibsConvertMathTest extends TestCase
*
* @return array<mixed>
*/
public function initNumericProvider(): array
public function providerInitNumeric(): array
{
return [
'5 must be 5' => [5, 5, 'int'],
@@ -98,7 +98,7 @@ final class CoreLibsConvertMathTest extends TestCase
* Undocumented function
*
* @covers ::initNumeric
* @dataProvider initNumericProvider
* @dataProvider providerInitNumeric
* @testdox initNumeric: Input $info $input must match $expected [$_dataName]
*
* @param int|float|string $input
@@ -113,6 +113,388 @@ final class CoreLibsConvertMathTest extends TestCase
\CoreLibs\Convert\Math::initNumeric($input)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerCbrt(): array
{
return [
'cube root of 2' => [2, 1.25992, 5],
'cube root of 3' => [3, 1.44225, 5],
'cube root of -1' => [-1, 'NAN', 0],
];
}
/**
* Undocumented function
*
* @covers ::cbrt
* @dataProvider providerCbrt
* @testdox initNumeric: Input $input must match $expected [$_dataName]
*
* @param float|int $number
* @param float $expected
* @param int $round_to
* @return void
*/
public function testCbrt(float|int $number, float|string $expected, int $round_to): void
{
$this->assertEquals(
$expected,
round(\CoreLibs\Convert\Math::cbrt($number), $round_to)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerMultiplyMatrices(): array
{
return [
'[3] x [3] => [3x1]' => [
[1, 2, 3],
[1, 2, 3],
[14]
],
'[3] x [3x1]' => [
[1, 2, 3],
[[1], [2], [3]],
[14]
],
'[3] x [3x1]' => [
[1, 2, 3],
[[1], [2], [3]],
[14]
],
'[1x3L] x [3x1]' => [
[[1, 2, 3]],
[[1], [2], [3]],
[14]
],
'[1x3] x [3x1]' => [
[[1], [2], [3]],
[[1], [2], [3]],
[1, 2, 3]
],
'[2x3] x [3] => [3x1]' => [
[
[1, 2, 3],
[1, 2, 3]
],
[1, 2, 3],
[
14,
14
]
],
'[2x3] x [3x1]' => [
[
[1, 2, 3],
[1, 2, 3]
],
[[1], [2], [3]],
[
14,
14
]
],
'[2x3] x [2x3] => [3x3]' => [
[
[1, 2, 3],
[1, 2, 3],
],
[
[1, 2, 3],
[1, 2, 3],
],
[
[3, 6, 9],
[3, 6, 9]
]
],
'[2x3] x [3x3]' => [
[
[1, 2, 3],
[1, 2, 3],
],
[
[1, 2, 3],
[1, 2, 3],
[0, 0, 0],
],
[
[3, 6, 9],
[3, 6, 9]
]
],
'[2x3] x [3x2]' => [
'a' => [
[1, 2, 3],
[1, 2, 3],
],
'b' => [
[1, 1],
[2, 2],
[3, 3],
],
'prod' => [
[14, 14],
[14, 14],
]
],
'[3x3] x [3] => [1x3]' => [
[
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
],
[1, 2, 3],
[
14,
14,
14
]
],
'[3x3] x [2x3] => [3x3]' => [
[
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
],
[
[1, 2, 3],
[1, 2, 3],
],
[
[3, 6, 9],
[3, 6, 9],
[3, 6, 9],
]
],
'[3x3] x [3x3]' => [
[
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
],
[
[1, 2, 3],
[1, 2, 3],
// [0, 0, 0],
],
[
[3, 6, 9],
[3, 6, 9],
[3, 6, 9],
]
],
'[3] x [3x3]' => [
[1, 2, 3],
[
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
],
[
[6, 12, 18],
]
],
'[2x3] x [3x3]' => [
[
[1, 2, 3],
[1, 2, 3],
],
[
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
],
[
[6, 12, 18],
[6, 12, 18],
]
],
'inblanaced [2x2,3] x [3x2]' => [
'a' => [
[1, 2, 3],
[4, 5]
],
'b' => [
[6, 7],
[8, 9],
[10, 11]
],
'result' => [
[52, 58],
[64, 73],
]
],
'inblanaced [2x3] x [3x1,2]' => [
'a' => [
[1, 2, 3],
[4, 5, 7]
],
'b' => [
[7, 8],
[9, 10],
[11]
],
'result' => [
[58, 28],
[150, 82],
]
],
];
}
/**
* Undocumented function
*
* @covers ::multiplyMatrices
* @dataProvider providerMultiplyMatrices
* @testdox initNumeric: Input $input_a x $input_b must match $expected [$_dataName]
*
* @param array $input_a
* @param array $input_b
* @param array $expected
* @return void
*/
public function testMultiplyMatrices(array $input_a, array $input_b, array $expected): void
{
$this->assertEquals(
$expected,
\CoreLibs\Convert\Math::multiplyMatrices($input_a, $input_b)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerEqualWithEpsilon(): array
{
return [
'equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000000222,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => true,
],
'almost equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000000232,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => true,
],
'not equal' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000004222,
'epsilon' => PHP_FLOAT_EPSILON,
'equal' => false,
],
'equal, different epsilon' => [
'a' => 0.000000000000000222,
'b' => 0.000000000000004222,
'epsilon' => 0.0001,
'equal' => true,
],
'not equal, different epsilon' => [
'a' => 0.0001,
'b' => 0.0002,
'epsilon' => 0.0001,
'equal' => false,
]
];
}
/**
* Undocumented function
*
* @covers ::equalWithEpsilon
* @dataProvider providerEqualWithEpsilon
* @testdox equalWithEpsilon with $a and $b and Epsilon: $epsilon must be equal: $equal [$_dataName]
*
* @return void
*/
public function testEqualWithEpsilon(float $a, float $b, float $epsilon, bool $equal): void
{
$this->assertEquals(
$equal,
\CoreLibs\Convert\Math::equalWithEpsilon($a, $b, $epsilon)
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerCompareWithEpsilon(): array
{
return [
'smaller, true' => [
'value' => 0.0001,
'compare' => '<',
'limit' => 0.0002,
'epsilon' => 0.00001,
'match' => true,
],
'smaller, false' => [
'value' => 0.0001,
'compare' => '<',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => false,
],
'bigger, true' => [
'value' => 0.0002,
'compare' => '>',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => true,
],
'bigger, false' => [
'value' => 0.0001,
'compare' => '>',
'limit' => 0.0001,
'epsilon' => 0.00001,
'match' => false,
],
];
}
/**
* Undocumented function
*
* @covers ::compareWithEpsilon
* @dataProvider providerCompareWithEpsilon
* @testdox compareWithEpsilon $value $compare $limit with $epsilon must match: $match [$_dataName]
*
* @param float $value
* @param string $compare
* @param float $limit
* @param float $epslion
* @param bool $match
* @return void
*/
public function testCompareWithEpsilon(
float $value,
string $compare,
float $limit,
float $epsilon,
bool $match
): void {
$this->assertEquals(
$match,
\CoreLibs\Convert\Math::compareWithEpsilon($value, $compare, $limit, $epsilon)
);
}
}
// __END__

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
// 1: format
// 2: split characters as string, null for default
// 3: expected
return [
'all empty string' => [
'',
'',
null,
''
],
'empty input string' => [
'',
'2-2',
null,
''
],
'empty format string string' => [
'1234',
'',
null,
'1234'
],
'string format match' => [
'1234',
'2-2',
null,
'12-34'
],
'string format trailing match' => [
'1234',
'2-2-',
null,
'12-34'
],
'string format leading match' => [
'1234',
'-2-2',
null,
'12-34'
],
'string format double inside match' => [
'1234',
'2--2',
null,
'12--34',
],
'string format short first' => [
'1',
'2-2',
null,
'1'
],
'string format match first' => [
'12',
'2-2',
null,
'12'
],
'string format short second' => [
'123',
'2-2',
null,
'12-3'
],
'string format too long' => [
'1234567',
'2-2',
null,
'12-34-567'
],
'string format invalid format string' => [
'1234',
'2_2',
null,
'1234'
],
'different split character' => [
'1234',
'2_2',
'_',
'12_34'
],
'mixed split characters' => [
'123456',
'2-2_2',
'-_',
'12-34_56'
],
'length mixed' => [
'ABCD12345568ABC13',
'2-4_5-2#4',
'-_#',
'AB-CD12_34556-8A#BC13'
],
'split with split chars in string' => [
'12-34',
'2-2',
null,
'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
* @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 $format
* @param string|null $split_characters
* @param string $expected
* @return void
*/
public function testSplitFormatString(
string $input,
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 $expected
): void {
if ($split_characters === null) {
$output = \CoreLibs\Convert\Strings::splitFormatString(
$output = \CoreLibs\Convert\Strings::splitFormatStringFixed(
$input,
$format
$split_length
);
} else {
$output = \CoreLibs\Convert\Strings::splitFormatString(
$output = \CoreLibs\Convert\Strings::splitFormatStringFixed(
$input,
$format,
$split_length,
$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
*
@@ -378,6 +482,222 @@ final class CoreLibsConvertStringsTest extends TestCase
\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'
);
}
}
// __END__

View File

@@ -21,8 +21,10 @@ final class CoreLibsCreateHashTest extends TestCase
public function hashData(): array
{
return [
'any string' => [
'hash tests' => [
// this is the string
'text' => 'Some String Text',
// hash list special
'crc32b_reverse' => 'c5c21d91', // crc32b (in revere)
'sha1Short' => '4d2bc9ba0', // sha1Short
// via hash
@@ -31,6 +33,8 @@ final class CoreLibsCreateHashTest extends TestCase
'fnv132' => '9df444f9', // hash: fnv132
'fnv1a32' => '2c5f91b9', // hash: fnv1a32
'joaat' => '50dab846', // hash: joaat
'ripemd160' => 'aeae3f041b20136451519edd9361570909300342', // hash: ripemd160,
'sha256' => '9055080e022f224fa835929b80582b3c71c672206fa3a49a87412c25d9d42ceb', // hash: sha256
]
];
}
@@ -81,7 +85,7 @@ final class CoreLibsCreateHashTest extends TestCase
{
$list = [];
foreach ($this->hashData() as $name => $values) {
foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat'] as $_hash_type) {
foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat', 'ripemd160', 'sha256'] as $_hash_type) {
// default value test
if ($_hash_type === null) {
$hash_type = \CoreLibs\Create\Hash::STANDARD_HASH_SHORT;
@@ -114,6 +118,22 @@ final class CoreLibsCreateHashTest extends TestCase
];
}
/**
* Undocumented function
*
* @return array
*/
public function hashStandardProvider(): array
{
$hash_source = 'Some String Text';
return [
'Long Hash check: ' . \CoreLibs\Create\Hash::STANDARD_HASH => [
$hash_source,
hash(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source)
],
];
}
/**
* Undocumented function
*
@@ -136,9 +156,13 @@ final class CoreLibsCreateHashTest extends TestCase
/**
* Undocumented function
*
* phpcs:disable Generic.Files.LineLength
* @covers ::__sha1Short
* @covers ::__crc32b
* @covers ::sha1Short
* @dataProvider sha1ShortProvider
* @testdox __sha1Short $input will be $expected (crc32b) and $expected_sha1 (sha1 short) [$_dataName]
* @testdox __sha1Short/__crc32b/sha1short $input will be $expected (crc32b) and $expected_sha1 (sha1 short) [$_dataName]
* phpcs:enable Generic.Files.LineLength
*
* @param string $input
* @param string $expected
@@ -149,16 +173,29 @@ final class CoreLibsCreateHashTest extends TestCase
// uses crc32b
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::__sha1Short($input)
\CoreLibs\Create\Hash::__sha1Short($input),
'__sha1Short depreacted'
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::__sha1Short($input, false)
\CoreLibs\Create\Hash::__sha1Short($input, false),
'__sha1Short (false) depreacted'
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::__crc32b($input),
'__crc32b'
);
// sha1 type
$this->assertEquals(
$expected_sha1,
\CoreLibs\Create\Hash::__sha1Short($input, true)
\CoreLibs\Create\Hash::__sha1Short($input, true),
'__sha1Short (true) depreacted'
);
$this->assertEquals(
$expected_sha1,
\CoreLibs\Create\Hash::sha1Short($input),
'sha1Short'
);
}
@@ -166,8 +203,10 @@ final class CoreLibsCreateHashTest extends TestCase
* Undocumented function
*
* @covers ::__hash
* @covers ::hashShort
* @covers ::hashShort
* @dataProvider hashProvider
* @testdox __hash $input with $hash_type will be $expected [$_dataName]
* @testdox __hash/hashShort/hash $input with $hash_type will be $expected [$_dataName]
*
* @param string $input
* @param string|null $hash_type
@@ -179,12 +218,24 @@ final class CoreLibsCreateHashTest extends TestCase
if ($hash_type === null) {
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::__hash($input)
\CoreLibs\Create\Hash::__hash($input),
'__hash'
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashShort($input),
'hashShort'
);
} else {
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::__hash($input, $hash_type)
\CoreLibs\Create\Hash::__hash($input, $hash_type),
'__hash with hash type'
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hash($input, $hash_type),
'hash with hash type'
);
}
}
@@ -193,8 +244,9 @@ final class CoreLibsCreateHashTest extends TestCase
* Undocumented function
*
* @covers ::__hashLong
* @covers ::hashLong
* @dataProvider hashLongProvider
* @testdox __hashLong $input will be $expected [$_dataName]
* @testdox __hashLong/hashLong $input will be $expected [$_dataName]
*
* @param string $input
* @param string $expected
@@ -206,6 +258,168 @@ final class CoreLibsCreateHashTest extends TestCase
$expected,
\CoreLibs\Create\Hash::__hashLong($input)
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashLong($input)
);
}
/**
* Undocumented function
*
* @covers ::hash
* @covers ::hashStd
* @dataProvider hashStandardProvider
* @testdox hash/hashStd $input will be $expected [$_dataName]
*
* @param string $input
* @param string $expected
* @return void
*/
public function testHashStandard(string $input, string $expected): void
{
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashStd($input)
);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hash($input)
);
}
/**
* Undocumented function
*
* @covers ::hash
* @testdox hash with invalid type
*
* @return void
*/
public function testInvalidHashType(): void
{
$hash_source = 'Some String Text';
$expected = hash(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hash($hash_source, 'DOES_NOT_EXIST')
);
}
/**
* Note: this only tests default sha256
*
* @covers ::hashHmac
* @testdox hash hmac test
*
* @return void
*/
public function testHashMac(): void
{
$hash_key = 'FIX KEY';
$hash_source = 'Some String Text';
$expected = '16479b3ef6fa44e1cdd8b2dcfaadf314d1a7763635e8738f1e7996d714d9b6bf';
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key)
);
}
/**
* Undocumented function
*
* @covers ::hashHmac
* @testdox hash hmac with invalid type
*
* @return void
*/
public function testInvalidHashMacType(): void
{
$hash_key = 'FIX KEY';
$hash_source = 'Some String Text';
$expected = hash_hmac(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source, $hash_key);
$this->assertEquals(
$expected,
\CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key, 'DOES_NOT_EXIST')
);
}
/**
* Undocumented function
*
* @return array<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
{
/**
* 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
*
@@ -47,109 +21,67 @@ final class CoreLibsCreateRandomKeyTest extends TestCase
public function randomKeyGenProvider(): array
{
return [
'default key length' => [
// just key length
'default key length, default char set' => [
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,
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,
1 => 4,
1 => \CoreLibs\Create\RandomKey::KEY_LENGTH_DEFAULT,
],
'set override key lenght' => [
'set override key lenght, default char set' => [
0 => 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 [
'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,
]
];
}
$inputLength = strlen($input);
/**
* run before each test and reset to default 4
*
* @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;
for ($i = 0; $i < $inputLength; $i++) {
if (strpos($allowed_chars, $input[$i]) === false) {
return false;
}
}
$this->assertEquals(
$input,
\CoreLibs\Create\RandomKey::getRandomKeyLength()
);
return true;
}
/**
@@ -163,43 +95,41 @@ final class CoreLibsCreateRandomKeyTest extends TestCase
* @param integer $expected
* @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) {
$this->assertEquals(
$expected,
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(
$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__

View File

@@ -22,7 +22,6 @@ final class CoreLibsCreateSessionTest extends TestCase
public function sessionProvider(): array
{
// 0: session name as parameter or for GLOBAL value
// 1: type p: parameter, g: global, d: php.ini default
// 2: mock data as array
// checkCliStatus: true/false,
// getSessionStatus: PHP_SESSION_DISABLED for abort,
@@ -31,13 +30,10 @@ final class CoreLibsCreateSessionTest extends TestCase
// checkActiveSession: true/false, [1st call, 2nd call]
// getSessionId: string or false
// 3: exepcted name (session)]
// 4: Exception thrown on error
// 5: exception code, null for none
// 6: expected error string
// 4: auto write close flag
return [
'session parameter' => [
'sessionNameParameter',
'p',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
@@ -47,12 +43,9 @@ final class CoreLibsCreateSessionTest extends TestCase
],
'sessionNameParameter',
null,
null,
'',
],
'session globals' => [
'sessionNameGlobals',
'g',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
@@ -61,13 +54,12 @@ final class CoreLibsCreateSessionTest extends TestCase
'getSessionId' => '1234abcd4567'
],
'sessionNameGlobals',
null,
null,
'',
[
'auto_write_close' => false,
],
],
'session name default' => [
'',
'd',
'auto write close' => [
'sessionNameAutoWriteClose',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
@@ -75,109 +67,10 @@ final class CoreLibsCreateSessionTest extends TestCase
'checkActiveSession' => [false, true],
'getSessionId' => '1234abcd4567'
],
'',
null,
null,
'',
],
// error checks
// 1: we are in cli
'on cli error' => [
'',
'd',
'sessionNameAutoWriteClose',
[
'checkCliStatus' => true,
'getSessionStatus' => PHP_SESSION_NONE,
'setSessionName' => true,
'checkActiveSession' => [false, true],
'getSessionId' => '1234abcd4567'
'auto_write_close' => true,
],
'',
'RuntimeException',
1,
'[SESSION] No sessions in php cli'
],
// 2: session disabled
'session disabled error' => [
'',
'd',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_DISABLED,
'setSessionName' => true,
'checkActiveSession' => [false, true],
'getSessionId' => '1234abcd4567'
],
'',
'RuntimeException',
2,
'[SESSION] Sessions are disabled'
],
// 3: invalid session name: string
'invalid name chars error' => [
'1invalid$session#;',
'p',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
'setSessionName' => false,
'checkActiveSession' => [false, true],
'getSessionId' => '1234abcd4567'
],
'',
'UnexpectedValueException',
3,
'[SESSION] Invalid session name: 1invalid$session#;'
],
// 3: invalid session name: only numbers
'invalid name numbers only error' => [
'123',
'p',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
'setSessionName' => false,
'checkActiveSession' => [false, true],
'getSessionId' => '1234abcd4567'
],
'',
'UnexpectedValueException',
3,
'[SESSION] Invalid session name: 123'
],
// 3: invalid session name: invalid name short
// 3: invalid session name: too long (128)
// 4: failed to start session (2nd false on check active session)
'invalid name numbers only error' => [
'',
'd',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
'setSessionName' => true,
'checkActiveSession' => [false, false],
'getSessionId' => '1234abcd4567'
],
'',
'RuntimeException',
4,
'[SESSION] Failed to activate session'
],
// 5: get session id return false
'invalid name numbers only error' => [
'',
'd',
[
'checkCliStatus' => false,
'getSessionStatus' => PHP_SESSION_NONE,
'setSessionName' => true,
'checkActiveSession' => [false, true],
'getSessionId' => false
],
'',
'UnexpectedValueException',
5,
'[SESSION] getSessionId did not return a session id'
],
];
}
@@ -190,32 +83,24 @@ final class CoreLibsCreateSessionTest extends TestCase
* @testdox startSession $input name for $type will be $expected (error: $expected_error) [$_dataName]
*
* @param string $input
* @param string $type
* @param array<mixed> $mock_data
* @param string $expected
* @param string|null $exception
* @param string $expected_error
* @param array<string,mixed> $options
* @return void
*/
public function testStartSession(
string $input,
string $type,
array $mock_data,
string $expected,
?string $exception,
?int $exception_code,
string $expected_error
?array $options,
): void {
// override expected
if ($type == 'd') {
$expected = ini_get('session.name');
}
/** @var \CoreLibs\Create\Session&MockObject $session_mock */
$session_mock = $this->createPartialMock(
\CoreLibs\Create\Session::class,
[
'checkCliStatus', 'getSessionStatus', 'checkActiveSession',
'setSessionName', 'startSessionCall', 'getSessionId',
'checkCliStatus',
'getSessionStatus', 'checkActiveSession',
'getSessionId',
'getSessionName'
]
);
@@ -234,12 +119,8 @@ final class CoreLibsCreateSessionTest extends TestCase
$mock_data['checkActiveSession'][0],
$mock_data['checkActiveSession'][1],
);
// dummy set for session name
$session_mock->method('setSessionName')->with($input)->willReturn($mock_data['setSessionName']);
// set session name & return bsed on request data
$session_mock->method('getSessionName')->willReturn($expected);
// will not return anything
$session_mock->method('startSessionCall');
// in test case only return string
// false: will return false
$session_mock->method('getSessionId')->willReturn($mock_data['getSessionId']);
@@ -247,25 +128,7 @@ final class CoreLibsCreateSessionTest extends TestCase
// regex for session id
$ression_id_regex = "/^\w+$/";
if ($exception !== null) {
$this->expectException($exception);
$this->expectExceptionCode($exception_code);
}
unset($GLOBALS['SET_SESSION_NAME']);
$session_id = '';
switch ($type) {
case 'p':
$session_id = $session_mock->startSession($input);
break;
case 'g':
$GLOBALS['SET_SESSION_NAME'] = $input;
$session_id = $session_mock->startSession();
break;
case 'd':
$session_id = $session_mock->startSession();
break;
}
$session_id = $session_mock->getSessionId();
// asert checks
if (!empty($session_id)) {
$this->assertMatchesRegularExpression(
@@ -284,6 +147,79 @@ final class CoreLibsCreateSessionTest extends TestCase
}
}
/**
* Undocumented function
*
* @return array
*/
public function providerSessionException(): array
{
return [
'not cli' => [
'TEST_EXCEPTION',
\RuntimeException::class,
1,
'/^\[SESSION\] No sessions in php cli$/',
],
/* 'session disabled ' => [
'TEST_EXCEPTION',
\RuntimeException::class,
2,
'/^\[SESSION\] Sessions are disabled/'
],
'invalid session name' => [
'--#as^-292p-',
\UnexpectedValueException::class,
3,
'/^\[SESSION\] Invalid session name: /'
],
'failed to activate session' => [
'TEST_EXCEPTION',
\RuntimeException::class,
4,
'/^\[SESSION\] Failed to activate session/'
],
'expired session' => [
\RuntimeException::class,
5,
'/^\[SESSION\] Expired session found/'
],
'not a valid session id returned' => [
\UnexpectedValueException::class,
6,
'/^\[SESSION\] getSessionId did not return a session id/'
], */
];
}
/**
* exception checks
*
* @covers ::initSession
* @dataProvider providerSessionException
* @testdox create session $session_name with exception $exception ($exception_code) [$_dataName]
*
* @param string $session_name
* @param string $exception
* @param int $exception_code
* @param string $expected_error
* @return void
*/
public function testSessionException(
string $session_name,
string $exception,
int $exception_code,
string $expected_error,
): void {
//
// throws only on new Object creation
$this->expectException($exception);
$this->expectExceptionCode($exception_code);
$this->expectExceptionMessageMatches($expected_error);
// 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]);
}
/**
* provider for session name check
*
@@ -347,109 +283,147 @@ final class CoreLibsCreateSessionTest extends TestCase
*
* @return array
*/
public function sessionDataProvider(): array
public function providerSessionData(): array
{
return [
'test' => [
'foo',
'bar',
'bar',
null,
],
'int key test' => [
123,
'bar',
'bar',
\UnexpectedValueException::class
],
// more complex value tests
'array values' => [
'array',
[1, 2, 3],
[1, 2, 3],
null,
]
];
}
// NOTE: with auto start session, we cannot test this in the command line
/**
* method call test
*
* @covers ::setS
* @covers ::getS
* @covers ::issetS
* @covers ::unsetS
* @dataProvider sessionDataProvider
* @testdox setS/getS/issetS/unsetS $name with $input is $expected [$_dataName]
* @covers ::set
* @covers ::get
* @covers ::isset
* @covers ::unset
* @dataProvider providerSessionData
* @testdox set/get/isset/unset $name with $input is $expected ($exception) [$_dataName]
*
* @param string|int $name
* @param mixed $input
* @param mixed $expected
* @param ?mixed $exception
* @return void
*/
public function testMethodSetGet($name, $input, $expected): void
public function testMethodSetGet($name, $input, $expected, $exception): void
{
$session = new \CoreLibs\Create\Session();
$session->setS($name, $input);
if (\CoreLibs\Get\System::checkCLI()) {
$this->markTestSkipped('Cannot run testMethodSetGet in CLI');
}
$session = new \CoreLibs\Create\Session('TEST_METHOD');
if ($expected !== null) {
$this->expectException($exception);
}
$session->set($name, $input);
$this->assertEquals(
$expected,
$session->getS($name),
$session->get($name),
'method set assert'
);
// isset true
$this->assertTrue(
$session->issetS($name),
$session->isset($name),
'method isset assert ok'
);
$session->unsetS($name);
$session->unset($name);
$this->assertEquals(
'',
$session->getS($name),
$session->get($name),
'method unset assert'
);
// iset false
// isset false
$this->assertFalse(
$session->issetS($name),
$session->isset($name),
'method isset assert false'
);
}
/**
* magic call test
* Undocumented function
*
* @covers ::__set
* @covers ::__get
* @covers ::__isset
* @covers ::__unset
* @dataProvider sessionDataProvider
* @testdox __set/__get/__iseet/__unset $name with $input is $expected [$_dataName]
* @return array
*/
public function providerSessionDataMany(): array
{
return [
'valid set' => [
[
'foo 1' => 'bar 1',
'foo 2' => 'bar 1',
],
[
'foo 1' => 'bar 1',
'foo 2' => 'bar 1',
],
null,
],
'invalid entry' => [
[
'foo 1' => 'bar 1',
123 => 'bar 1',
],
[
'foo 1' => 'bar 1',
],
\UnexpectedValueException::class
]
];
}
/**
* Undocumented function
*
* @param string|int $name
* @param mixed $input
* @param mixed $expected
* @covers ::setMany
* @covers ::getMany
* @dataProvider providerSessionDataMany
* @testdox setMany/getMany/unsetMany $set is $expected ($exception) [$_dataName]
*
* @param array<string|int,mixed> $set
* @param array<string,mixed> $expected
* @param ?mixed $exception
* @return void
*/
public function testMagicSetGet($name, $input, $expected): void
public function testMany($set, $expected, $exception): void
{
$session = new \CoreLibs\Create\Session();
$session->$name = $input;
if (\CoreLibs\Get\System::checkCLI()) {
$this->markTestSkipped('Cannot run testMethodSetGet in CLI');
}
$session = new \CoreLibs\Create\Session('TEST_METHOD');
if ($expected !== null) {
$this->expectException($exception);
}
$session->setMany($set);
$this->assertEquals(
$expected,
$session->$name,
'magic set assert'
$session->getMany(array_keys($set)),
'set many failed'
);
// isset true
$this->assertTrue(
isset($session->$name),
'magic isset assert ok'
);
unset($session->$name);
$session->unsetMany(array_keys($set));
$this->assertEquals(
'',
$session->$name,
'magic unset assert'
);
// isset true
$this->assertFalse(
isset($session->$name),
'magic isset assert false'
[],
$session->getMany(array_keys($set)),
'unset many failed'
);
}
@@ -463,27 +437,30 @@ final class CoreLibsCreateSessionTest extends TestCase
*/
public function testUnsetAll(): void
{
if (\CoreLibs\Get\System::checkCLI()) {
$this->markTestSkipped('Cannot run testUnsetAll in CLI');
}
$test_values = [
'foo' => 'abc',
'bar' => '123'
];
$session = new \CoreLibs\Create\Session();
$session = new \CoreLibs\Create\Session('TEST_UNSET');
foreach ($test_values as $name => $value) {
$session->setS($name, $value);
$session->set($name, $value);
// confirm set
$this->assertEquals(
$value,
$session->getS($name),
$session->get($name),
'set assert: ' . $name
);
}
// unset all
$session->unsetAllS();
$session->clear();
// check unset
foreach (array_keys($test_values) as $name) {
$this->assertEquals(
'',
$session->getS($name),
$session->get($name),
'unsert assert: ' . $name
);
}

View File

@@ -121,6 +121,7 @@ final class CoreLibsCreateUidsTest extends TestCase
* must match 7e78fe0d-59b8-4637-af7f-e88d221a7d1e
*
* @covers ::uuidv4
* @covers ::validateUuidv4
* @testdox uuidv4 check that return is matching regex [$_dataName]
*
* @return void
@@ -129,13 +130,18 @@ final class CoreLibsCreateUidsTest extends TestCase
{
$uuid = \CoreLibs\Create\Uids::uuidv4();
$this->assertMatchesRegularExpression(
'/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/',
$uuid
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/',
$uuid,
'Failed regex check'
);
$this->assertTrue(
\CoreLibs\Create\Uids::validateUuuidv4($uuid),
'Failed validate regex method'
);
$this->assertFalse(
\CoreLibs\Create\Uids::validateUuuidv4('not-a-uuidv4'),
'Failed wrong uuid validated as true'
);
// $this->assertStringMatchesFormat(
// '%4s%4s-%4s-%4s-%4s-%4s%4s%4s',
// $uuid
// );
}
/**

View File

@@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase;
* Test class for DB\Extended\ArrayIO
* This will only test the PgSQL parts
* @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO
* @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO
* @testdox \CoreLibs\Extended\ArrayIO method tests for extended DB interface
*/
final class CoreLibsDBExtendedArrayIOTest extends TestCase

View File

@@ -17,7 +17,7 @@ Table with Primary Key: table_with_primary_key
Table without Primary Key: table_without_primary_key
Table with primary key has additional row:
row_primary_key SERIAL PRIMARY KEY,
row_primary_key INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
Each table has the following rows
row_int INT,
row_numeric NUMERIC,
@@ -37,8 +37,9 @@ namespace tests;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use CoreLibs\Logging\Logger\Level;
use CoreLibs\Logging;
use CoreLibs\DB\Options\Convert;
use CoreLibs\DB\Support\ConvertPlaceholder;
/**
* Test class for DB\IO + DB\SQL\PgSQL
@@ -117,7 +118,7 @@ final class CoreLibsDBIOTest extends TestCase
);
}
// define basic connection set valid and one invalid
self::$log = new \CoreLibs\Logging\Logging([
self::$log = new Logging\Logging([
// 'log_folder' => __DIR__ . DIRECTORY_SEPARATOR . 'log',
'log_folder' => DIRECTORY_SEPARATOR . 'tmp',
'log_file_id' => 'CoreLibs-DB-IO-Test',
@@ -134,6 +135,7 @@ final class CoreLibsDBIOTest extends TestCase
}
// check if they already exist, drop them
if ($db->dbShowTableMetaData('table_with_primary_key') !== false) {
$db->dbExec("CREATE EXTENSION IF NOT EXISTS pgcrypto");
$db->dbExec("DROP TABLE table_with_primary_key");
$db->dbExec("DROP TABLE table_without_primary_key");
$db->dbExec("DROP TABLE test_meta");
@@ -161,13 +163,9 @@ final class CoreLibsDBIOTest extends TestCase
// primary key name is table + '_id'
<<<SQL
CREATE TABLE table_with_primary_key (
table_with_primary_key_id SERIAL PRIMARY KEY,
table_with_primary_key_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
$base_table
SQL
/* "CREATE TABLE table_with_primary_key ("
// primary key name is table + '_id'
. "table_with_primary_key_id SERIAL PRIMARY KEY, "
. $base_table */
);
$db->dbExec(
<<<SQL
@@ -570,11 +568,11 @@ final class CoreLibsDBIOTest extends TestCase
);
$db->dbClose();
// second conenction with log set NOT debug
$log = new \CoreLibs\Logging\Logging([
$log = new Logging\Logging([
// 'log_folder' => __DIR__ . DIRECTORY_SEPARATOR . 'log',
'log_folder' => DIRECTORY_SEPARATOR . 'tmp',
'log_file_id' => 'CoreLibs-DB-IO-Test',
'log_level' => \CoreLibs\Logging\Logger\Level::Notice,
'log_level' => Logging\Logger\Level::Notice,
]);
$db = new \CoreLibs\DB\IO(
self::$db_config[$connection],
@@ -3293,6 +3291,7 @@ final class CoreLibsDBIOTest extends TestCase
'query' => 'INSERT INTO table_with_primary_key (row_int, uid) '
. 'VALUES ($1, $2) RETURNING table_with_primary_key_id',
'returning_id' => true,
'placeholder_converted' => [],
],
],
// update
@@ -3327,6 +3326,7 @@ final class CoreLibsDBIOTest extends TestCase
'query' => 'UPDATE table_with_primary_key SET row_int = $1, '
. 'row_varchar = $2 WHERE uid = $3',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// select
@@ -3356,6 +3356,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 1,
'query' => 'SELECT row_int, uid FROM table_with_primary_key WHERE uid = $1',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// any query but with no parameters
@@ -3388,6 +3389,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 0,
'query' => 'SELECT row_int, uid FROM table_with_primary_key',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// no statement name (25)
@@ -3411,6 +3413,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 0,
'query' => '',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// no query (prepare 11)
@@ -3435,6 +3438,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 0,
'query' => '',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// no db connection (prepare/execute 16)
@@ -3464,6 +3468,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 0,
'query' => 'SELECT row_int, uid FROM table_with_primary_key',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// prepare with different statement name
@@ -3489,6 +3494,7 @@ final class CoreLibsDBIOTest extends TestCase
'count' => 0,
'query' => 'SELECT row_int, uid FROM table_with_primary_key',
'returning_id' => false,
'placeholder_converted' => [],
],
],
// insert wrong data count compared to needed (execute 23)
@@ -3514,10 +3520,12 @@ final class CoreLibsDBIOTest extends TestCase
'query' => 'INSERT INTO table_with_primary_key (row_int, uid) VALUES '
. '($1, $2) RETURNING table_with_primary_key_id',
'returning_id' => true,
'placeholder_converted' => [],
],
],
// execute does not return a result (22)
// TODO execute does not return a result
// TODO prepared statement with placeholder params auto convert
];
}
@@ -3662,7 +3670,7 @@ final class CoreLibsDBIOTest extends TestCase
}
// check dbGetPrepareCursorValue
foreach (['pk_name', 'count', 'query', 'returning_id'] as $key) {
foreach (['pk_name', 'count', 'query', 'returning_id', 'placeholder_converted'] as $key) {
$this->assertEquals(
$prepare_cursor[$key],
$db->dbGetPrepareCursorValue($stm_name, $key),
@@ -3685,7 +3693,7 @@ final class CoreLibsDBIOTest extends TestCase
*
* @return array
*/
public function preparedProviderValue(): array
public function providerDbGetPrepareCursorValue(): array
{
// 1: query (can be empty for do not set)
// 2: stm name
@@ -3729,7 +3737,7 @@ final class CoreLibsDBIOTest extends TestCase
* test return prepare cursor errors
*
* @covers ::dbGetPrepareCursorValue
* @dataProvider preparedProviderValue
* @dataProvider providerDbGetPrepareCursorValue
* @testdox prepared query $stm_name with $key expect error id $error_id [$_dataName]
*
* @param string $query
@@ -3762,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
// dbGetSchema, dbSetSchema
@@ -4649,7 +4745,7 @@ final class CoreLibsDBIOTest extends TestCase
$res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']);
// all hast to be string
foreach ($res as $key => $value) {
$this->assertIsString($value, 'Aseert string for column: ' . $key);
$this->assertIsString($value, 'Assert string for column: ' . $key);
}
// convert base only
$db->dbSetConvertFlag(Convert::on);
@@ -4662,10 +4758,10 @@ final class CoreLibsDBIOTest extends TestCase
}
switch ($type_layout[$name]) {
case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name);
$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break;
default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name);
$this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break;
}
}
@@ -4679,13 +4775,13 @@ final class CoreLibsDBIOTest extends TestCase
}
switch ($type_layout[$name]) {
case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name);
$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break;
case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name);
$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break;
default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name);
$this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break;
}
}
@@ -4699,17 +4795,17 @@ final class CoreLibsDBIOTest extends TestCase
}
switch ($type_layout[$name]) {
case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name);
$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break;
case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name);
$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break;
case 'json':
case 'jsonb':
$this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name);
$this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
break;
default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name);
$this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break;
}
}
@@ -4723,25 +4819,25 @@ final class CoreLibsDBIOTest extends TestCase
}
switch ($type_layout[$name]) {
case 'int':
$this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name);
$this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name);
break;
case 'float':
$this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name);
$this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name);
break;
case 'json':
case 'jsonb':
$this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name);
$this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name);
break;
case 'bytea':
// for hex types it must not start with \x
$this->assertStringStartsNotWith(
'\x',
$value,
'Aseert bytes not starts with \x for column: ' . $key . '/' . $name
'Assert bytes not starts with \x for column: ' . $key . '/' . $name
);
break;
default:
$this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name);
$this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name);
break;
}
}
@@ -4913,8 +5009,8 @@ final class CoreLibsDBIOTest extends TestCase
)
),
($params === null ?
$db->dbGetQueryHash($query) :
$db->dbGetQueryHash($query, $params)
$db->dbBuildQueryHash($query) :
$db->dbBuildQueryHash($query, $params)
),
'Failed assertdbGetQueryHash '
);
@@ -5031,8 +5127,285 @@ final class CoreLibsDBIOTest extends TestCase
$db->dbClose();
}
// query placeholder convert
// MARK: QUERY PLACEHOLDERS
// test query placeholder detection for all possible sets
// ::dbPrepare
/**
* placeholder sql
*
* @return array
*/
public function providerDbCountQueryParams(): array
{
return [
'one place holder' => [
'query' => 'SELECT row_varchar FROM table_with_primary_key WHERE row_varchar = $1',
'count' => 1,
'convert' => false,
],
'one place holder, json call' => [
'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_jsonb->>'data' = $1",
'count' => 1,
'convert' => false,
],
'one place holder, <> compare' => [
'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar <> $1",
'count' => 1,
'convert' => false,
],
'one place holder, named' => [
'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar <> :row_varchar",
'count' => 1,
'convert' => true,
],
'no replacement' => [
'query' => "SELECT row_varchar FROM table_with_primary_key WHERE row_varchar = '$1'",
'count' => 0,
'convert' => false,
],
'insert' => [
'query' => "INSERT INTO table_with_primary_key (row_varchar, row_jsonb, row_int) VALUES ($1, $2, $3)",
'count' => 3,
'convert' => false,
],
'update' => [
'query' => "UPDATE table_with_primary_key SET row_varchar = $1, row_jsonb = $2, row_int = $3 WHERE row_numeric = $4",
'count' => 4,
'convert' => false,
],
'multiple, multline' => [
'query' => <<<SQL
SELECT
row_varchar
FROM
table_with_primary_key
WHERE
row_varchar = $1 AND row_int = $2
AND row_numeric = ANY($3)
SQL,
'count' => 3,
'convert' => false,
],
'two digit numbers' => [
'query' => <<<SQL
INSERT INTO table_with_primary_key (
row_int, row_numeric, row_varchar, row_varchar_literal, row_json,
row_jsonb, row_bytea, row_timestamp, row_date, row_interval
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10
)
SQL,
'count' => 10,
'convert' => false,
],
'things in brackets' => [
'query' => <<<SQL
SELECT row_varchar
FROM table_with_primary_key
WHERE
row_varchar = $1 AND
(row_int = ANY($2) OR row_int = $3)
AND row_varchar_literal = $4
SQL,
'count' => 4,
'convert' => false,
],
'number compare' => [
'query' => <<<SQL
SELECT row_varchar
FROM table_with_primary_key
WHERE
row_int >= $1 OR row_int <= $2 OR
row_int > $3 OR row_int < $4
OR row_int = $5 OR row_int <> $6
SQL,
'count' => 6,
'convert' => false,
],
'comments in insert' => [
'query' => <<<SQL
INSERT INTO table_with_primary_key (
row_int, row_numeric, row_varchar, row_varchar_literal
) VALUES (
-- comment 1 かな
$1, $2,
-- comment 2 -
$3
-- comment 3
, $4
-- ignore $5, $6
-- $7, $8
-- digest($9, 10)
)
SQL,
'count' => 4,
'convert' => false
],
'comment in update' => [
'query' => <<<SQL
UPDATE table_with_primary_key SET
row_int =
-- COMMENT 1
$1,
row_numeric =
$2 -- COMMENT 2
,
row_varchar -- COMMENT 3
= $3
WHERE
row_varchar = $4
SQL,
'count' => 4,
'convert' => false,
],
// Note some are not set
'a complete set of possible' => [
'query' => <<<SQL
UPDATE table_with_primary_key SET
-- ROW
row_varchar = $1
WHERE
row_varchar = ANY($2) AND row_varchar <> $3
AND row_varchar > $4 AND row_varchar < $5
AND row_varchar >= $6 AND row_varchar <=$7
AND row_jsonb->'a' = $8 AND row_jsonb->>$9 = 'a'
AND row_jsonb<@$10 AND row_jsonb@>$11
AND row_varchar ^@ $12
SQL,
'count' => 12,
'convert' => false,
],
// all the same
'all the same numbered' => [
'query' => <<<SQL
UPDATE table_with_primary_key SET
row_int = $1::INT, row_numeric = $1::NUMERIC, row_varchar = $1
WHERE
row_varchar = $1
SQL,
'count' => 1,
'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;
}
/**
* Placeholder check and convert tests
*
* @covers ::dbPrepare
* @covers ::__dbCountQueryParams
* @onvers ::convertPlaceholderInQuery
* @dataProvider providerDbCountQueryParams
* @testdox Query replacement count test [$_dataName]
*
* @param string $query
* @param int $count
* @return void
*/
public function testDbCountQueryParams(string $query, int $count, bool $convert): void
{
$db = new \CoreLibs\DB\IO(
self::$db_config['valid'],
self::$log
);
$id = sha1($query);
$db->dbSetConvertPlaceholder($convert);
$db->dbPrepare($id, $query);
// print "\n**\n";
// print "PCount: " . $db->dbGetPrepareCursorValue($id, 'count') . "\n";
// print "\n**\n";
$this->assertEquals(
$count,
$db->dbGetPrepareCursorValue($id, 'count'),
'DB count params'
);
$placeholder = ConvertPlaceholder::convertPlaceholderInQuery($query, null, 'pg');
// print "RES: " . print_r($placeholder, true) . "\n";
$this->assertEquals(
$count,
$placeholder['needed'],
'convert params'
);
}
/**
* query placeholder convert
*
* @return array
*/
public function queryPlaceholderReplaceProvider(): array
{
// WHERE row_varchar = $1
@@ -5076,7 +5449,9 @@ final class CoreLibsDBIOTest extends TestCase
WHERE row_varchar = $1
SQL,
'expected_params' => ['string a'],
]
],
// TODO: test with multiple entries
// TODO: test with same entry ($1, $1, :var, :var)
];
}
@@ -5178,6 +5553,8 @@ final class CoreLibsDBIOTest extends TestCase
// - data debug
// dbDumpData
// MARK: ASYNC
// ASYNC at the end because it has 1s timeout
// - asynchronous executions
// dbExecAsync, dbCheckAsync

View File

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

View File

@@ -21,341 +21,6 @@ final class CoreLibsLanguageGetLocaleTest extends TestCase
. 'includes' . DIRECTORY_SEPARATOR
. 'locale' . DIRECTORY_SEPARATOR;
/**
* set all constant variables that must be set before call
*
* @return void
*/
public static function setUpBeforeClass(): void
{
// default web page encoding setting
/* if (!defined('DEFAULT_ENCODING')) {
define('DEFAULT_ENCODING', 'UTF-8');
}
if (!defined('DEFAULT_LOCALE')) {
// default lang + encoding
define('DEFAULT_LOCALE', 'en_US.UTF-8');
}
// site
if (!defined('SITE_ENCODING')) {
define('SITE_ENCODING', DEFAULT_ENCODING);
}
if (!defined('SITE_LOCALE')) {
define('SITE_LOCALE', DEFAULT_LOCALE);
} */
// just set
/* if (!defined('BASE')) {
define('BASE', str_replace('/configs', '', __DIR__) . DIRECTORY_SEPARATOR);
}
if (!defined('INCLUDES')) {
define('INCLUDES', 'includes' . DIRECTORY_SEPARATOR);
}
if (!defined('LANG')) {
define('LANG', 'lang' . DIRECTORY_SEPARATOR);
}
if (!defined('LOCALE')) {
define('LOCALE', 'locale' . DIRECTORY_SEPARATOR);
}
if (!defined('CONTENT_PATH')) {
define('CONTENT_PATH', 'frontend' . DIRECTORY_SEPARATOR);
} */
// array session
$_SESSION = [];
global $_SESSION;
}
/**
* all the test data
*
* @return array<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
*

View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -10,7 +10,7 @@ use CoreLibs\Logging\Logger\Level;
/**
* Test class for Logging
* @coversDefaultClass \CoreLibs\Logging\ErrorMessages
* @testdox \CoreLibs\Logging\ErrorMEssages method tests
* @testdox \CoreLibs\Logging\ErrorMessages method tests
*/
final class CoreLibsLoggingErrorMessagesTest extends TestCase
{

View File

@@ -249,7 +249,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$this->assertFalse(
$log->loggingLevelIsDebug()
);
// not set, should be debug]
// not set, should be debug
$log = new \CoreLibs\Logging\Logging([
'log_file_id' => 'testSetLoggingLevel',
'log_folder' => self::LOG_FOLDER,
@@ -297,6 +297,71 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$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
// getLogFileId
@@ -395,7 +460,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
}
$per_run_id = $log->getLogUniqueId();
$this->assertMatchesRegularExpression(
"/^\d{4}-\d{2}-\d{2}_\d{6}_U_[a-z0-9]{8}$/",
"/^\d{4}-\d{2}-\d{2}_\d{6}\.U_[a-z0-9]{8}$/",
$per_run_id,
'assert per log run id 1st'
);
@@ -403,7 +468,7 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$log->setLogUniqueId(true);
$per_run_id_2nd = $log->getLogUniqueId();
$this->assertMatchesRegularExpression(
"/^\d{4}-\d{2}-\d{2}_\d{6}_U_[a-z0-9]{8}$/",
"/^\d{4}-\d{2}-\d{2}_\d{6}\.U_[a-z0-9]{8}$/",
$per_run_id_2nd,
'assert per log run id 2nd'
);
@@ -824,13 +889,13 @@ final class CoreLibsLoggingLoggingTest extends TestCase
$this->assertTrue($log_ok, 'assert ::log (debug) OK');
$this->assertEquals(
$log->getLogFile(),
$log->getLogFileId() . '_DEBUG.log'
$log->getLogFileId() . '.DEBUG.log'
);
$log_ok = $log->log(Level::Info, 'INFO', group_id: 'GROUP_ID', prefix: 'PREFIX:');
$this->assertTrue($log_ok, 'assert ::log (info) OK');
$this->assertEquals(
$log->getLogFile(),
$log->getLogFileId() . '_INFO.log'
$log->getLogFileId() . '.INFO.log'
);
}

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
{
/**
* Undocumented function
*
* @return array
*/
public function passwordProvider(): array
{
return [
@@ -21,6 +26,11 @@ final class CoreLibsSecurityPasswordTest extends TestCase
];
}
/**
* Note: we need different hash types for PHP versions
*
* @return array
*/
public function passwordRehashProvider(): array
{
return [
@@ -63,6 +73,10 @@ final class CoreLibsSecurityPasswordTest extends TestCase
*/
public function testPasswordRehashCheck(string $input, bool $expected): void
{
// in PHP 8.4 the length is $12
if (PHP_VERSION_ID > 80400) {
$input = str_replace('$2y$10$', '$2y$12$', $input);
}
$this->assertEquals(
$expected,
\CoreLibs\Security\Password::passwordRehashCheck($input)

View File

@@ -15,6 +15,77 @@ use CoreLibs\Security\SymmetricEncryption;
*/
final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
{
// MARK: key set compare
/**
* Undocumented function
*
* @covers ::compareKey
* @covers ::getKey
* @testdox Check if init class set key matches to created key
*
* @return void
*/
public function testKeyInitGetCompare(): void
{
$key = CreateKey::generateRandomKey();
$crypt = new SymmetricEncryption($key);
$this->assertTrue(
$crypt->compareKey($key),
'set key not equal to original key'
);
$this->assertEquals(
$key,
$crypt->getKey(),
'set key returned not equal to original key'
);
}
/**
* Undocumented function
*
* @covers ::setKey
* @covers ::compareKey
* @covers ::getKey
* @testdox Check if set key after class init matches to created key
*
* @return void
*/
public function testKeySetGetCompare(): void
{
$key = CreateKey::generateRandomKey();
$crypt = new SymmetricEncryption();
$crypt->setKey($key);
$this->assertTrue(
$crypt->compareKey($key),
'set key not equal to original key'
);
$this->assertEquals(
$key,
$crypt->getKey(),
'set key returned not equal to original key'
);
}
// MARK: empty encrypted string
/**
* Undocumented function
*
* @covers ::decryptKey
* @covers ::decrypt
* @testdox Test empty encrypted string to decrypt
*
* @return void
*/
public function testEmptyDecryptionString(): void
{
$this->expectExceptionMessage('Encrypted string cannot be empty');
SymmetricEncryption::decryptKey('', CreateKey::generateRandomKey());
}
// MARK: encrypt/decrypt compare
/**
* Undocumented function
*
@@ -56,7 +127,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$decrypted,
'Class call',
);
}
/**
* test encrypt/decrypt produce correct output
*
* @covers ::generateRandomKey
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerEncryptDecryptSuccess
* @testdox encrypt/decrypt indirect $input must be $expected [$_dataName]
*
* @param string $input
* @param string $expected
* @return void
*/
public function testEncryptDecryptSuccessIndirect(string $input, string $expected): void
{
$key = CreateKey::generateRandomKey();
// test indirect
$encrypted = SymmetricEncryption::getInstance($key)->encrypt($input);
$decrypted = SymmetricEncryption::getInstance($key)->decrypt($encrypted);
@@ -65,7 +153,24 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$decrypted,
'Class Instance call',
);
}
/**
* test encrypt/decrypt produce correct output
*
* @covers ::generateRandomKey
* @covers ::encryptKey
* @covers ::decryptKey
* @dataProvider providerEncryptDecryptSuccess
* @testdox encrypt/decrypt static $input must be $expected [$_dataName]
*
* @param string $input
* @param string $expected
* @return void
*/
public function testEncryptDecryptSuccessStatic(string $input, string $expected): void
{
$key = CreateKey::generateRandomKey();
// test static
$encrypted = SymmetricEncryption::encryptKey($input, $key);
$decrypted = SymmetricEncryption::decryptKey($encrypted, $key);
@@ -77,6 +182,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
);
}
// MARK: invalid key
/**
* Undocumented function
*
@@ -114,13 +221,51 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$crypt = new SymmetricEncryption($key);
$encrypted = $crypt->encrypt($input);
$this->expectExceptionMessage($exception_message);
$crypt->setKey($key);
$crypt->setKey($wrong_key);
$crypt->decrypt($encrypted);
}
/**
* Test decryption with wrong key
*
* @covers ::generateRandomKey
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerEncryptFailed
* @testdox decrypt indirect with wrong key $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testEncryptFailedIndirect(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
$wrong_key = CreateKey::generateRandomKey();
// class instance
$encrypted = SymmetricEncryption::getInstance($key)->encrypt($input);
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::getInstance($wrong_key)->decrypt($encrypted);
}
/**
* Test decryption with wrong key
*
* @covers ::generateRandomKey
* @covers ::encryptKey
* @covers ::decryptKey
* @dataProvider providerEncryptFailed
* @testdox decrypt static with wrong key $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testEncryptFailedStatic(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
$wrong_key = CreateKey::generateRandomKey();
// class static
$encrypted = SymmetricEncryption::encryptKey($input, $key);
@@ -128,6 +273,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
SymmetricEncryption::decryptKey($encrypted, $wrong_key);
}
// MARK: wrong key
/**
* Undocumented function
*
@@ -144,6 +291,10 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b',
'excpetion_message' => 'Key is not the correct size (must be '
],
'empty key' => [
'key' => '',
'excpetion_message' => 'Key cannot be empty'
]
];
}
@@ -164,6 +315,7 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$enc_key = CreateKey::generateRandomKey();
// class
$this->expectExceptionMessage($exception_message);
$crypt = new SymmetricEncryption($key);
$this->expectExceptionMessage($exception_message);
$crypt->encrypt('test');
@@ -172,6 +324,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$this->expectExceptionMessage($exception_message);
$crypt->setKey($key);
$crypt->decrypt($encrypted);
}
/**
* test invalid key provided to decrypt or encrypt
*
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerWrongKey
* @testdox wrong key indirect $key throws $exception_message [$_dataName]
*
* @param string $key
* @param string $exception_message
* @return void
*/
public function testWrongKeyIndirect(string $key, string $exception_message): void
{
$enc_key = CreateKey::generateRandomKey();
// class instance
$this->expectExceptionMessage($exception_message);
@@ -180,6 +349,23 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$encrypted = SymmetricEncryption::getInstance($enc_key)->encrypt('test');
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::getInstance($key)->decrypt($encrypted);
}
/**
* test invalid key provided to decrypt or encrypt
*
* @covers ::encryptKey
* @covers ::decryptKey
* @dataProvider providerWrongKey
* @testdox wrong key static $key throws $exception_message [$_dataName]
*
* @param string $key
* @param string $exception_message
* @return void
*/
public function testWrongKeyStatic(string $key, string $exception_message): void
{
$enc_key = CreateKey::generateRandomKey();
// class static
$this->expectExceptionMessage($exception_message);
@@ -190,6 +376,8 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
SymmetricEncryption::decryptKey($encrypted, $key);
}
// MARK: wrong input
/**
* Undocumented function
*
@@ -232,6 +420,49 @@ final class CoreLibsSecuritySymmetricEncryptionTest extends TestCase
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decryptKey($input, $key);
}
/**
* Undocumented function
*
* @covers ::decryptKey
* @dataProvider providerWrongCiphertext
* @testdox too short ciphertext indirect $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testWrongCiphertextIndirect(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
// class instance
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::getInstance($key)->decrypt($input);
// class static
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decryptKey($input, $key);
}
/**
* Undocumented function
*
* @covers ::decryptKey
* @dataProvider providerWrongCiphertext
* @testdox too short ciphertext static $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testWrongCiphertextStatic(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
// class static
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decryptKey($input, $key);
}
}
// __END__

File diff suppressed because it is too large Load Diff