Compare commits

...

222 Commits

Author SHA1 Message Date
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
Clemens Schwaighofer
984dec37e2 Bug fix for Admin\Backend ->action var access 2024-10-16 12:36:38 +09:00
Clemens Schwaighofer
d91fbd5a46 Release: v9.17.1 2024-10-16 12:28:40 +09:00
Clemens Schwaighofer
668954c1c4 Admin\Backend move _POST action read to sub function and trigger not auto loading it 2024-10-16 12:26:33 +09:00
Clemens Schwaighofer
adbae19fca Release: v9.17.0 2024-09-24 15:18:43 +09:00
Clemens Schwaighofer
39de680b66 Extend Error Message collector print warning log 2024-09-24 15:15:23 +09:00
Clemens Schwaighofer
2e81a4da82 Release: v9.16.0 2024-09-20 13:43:20 +09:00
Clemens Schwaighofer
61782be11c Various updates as follows
* commit 5cec54d508fd4a34de509b3ac28d6277b6abc090 (HEAD -> master, all/master, all/development, all/NewFeatures)
| log size 644
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-09-20 13:33:19 +0900
|
|     Add "Success" to message logging levels, fixes for PHP 8.4, other preg_match fixes
|
|     The Logger/MessageLevel gets "success" as level 110 to something a bit
|     heigher than "ok" which is the general "OK" for anything ending without
|     an error. The "success" is currently only used in file uploads with the
|     java script ajax file uploader
|
|     Fix any "type $var = null" with correctly "?type $var = null" for PHP 8.4 (phphan)
|
|     Fix preg match no return catches for DB IO compare version and for language
|     look up.
|
|  4dev/tests/Logging/CoreLibsLoggingErrorMessagesTest.php |  5 +++++
|  www/admin/class_test.html.php                           |  1 +
|  www/admin/class_test.php                                |  2 +-
|  www/admin/layout/javascript/edit.jq.js                  |  2 +-
|  www/lib/CoreLibs/Admin/Backend.php                      |  6 +++---
|  www/lib/CoreLibs/Basic.php                              |  2 +-
|  www/lib/CoreLibs/Convert/Extends/SetVarTypeMain.php     |  6 +++---
|  www/lib/CoreLibs/Convert/SetVarTypeNull.php             |  6 +++---
|  www/lib/CoreLibs/DB/IO.php                              | 24 ++++++++++++++++++------
|  www/lib/CoreLibs/Language/GetLocale.php                 |  4 ++--
|  www/lib/CoreLibs/Logging/Logger/MessageLevel.php        |  2 ++
|  www/lib/CoreLibs/Output/Form/Elements.php               | 12 ++++++------
|  www/lib/CoreLibs/Template/HtmlBuilder/Element.php       |  4 ++--
|  13 files changed, 48 insertions(+), 28 deletions(-)
|
* commit 8e60c992f109993b40d9e5ec9354ffb7d7db9f79
| log size 123
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-09-03 12:06:01 +0900
|
|     Fixes phan/phpstan
|
|  phpstan.neon                                    | 6 +++---
|  www/lib/CoreLibs/Get/System.php                 | 2 +-
|  www/lib/CoreLibs/Template/HtmlBuilder/Block.php | 4 ----
|  3 files changed, 4 insertions(+), 8 deletions(-)
|
* commit 1b5437b675f6ceda4318f19522d47a08f28f95a8
| log size 147
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-09-03 11:58:36 +0900
|
|     Add testing for new added bom utf8 replace
|
|  4dev/tests/Convert/CoreLibsConvertStringsTest.php | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
|  4dev/tests/Convert/data/UTF8.csv                  |  1 +
|  4dev/tests/Convert/data/UTF8BOM.csv               |  1 +
|  4dev/tests/Get/CoreLibsGetSystemTest.php          | 23 +++++++++++++++++++++++
|  www/admin/class_test.system.php                   |  4 +++-
|  5 files changed, 76 insertions(+), 1 deletion(-)
|
* commit ef80cba5610b8acae8fc11d3f0a441c1f5a03735
| log size 659
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-09-03 09:49:01 +0900
|
|     Add new functions: get IPs, strip UTF8 BOM from text, text updates
|
|     Add the following new static methods
|
|     Convert\Strings::stripUTF8BomBytes: removes the UTF8 BOM bytes from the beginning of a line
|     Used for CSV files created in Excel for the first header entry (line 0/row 0)
|
|     Get\Systen::getIpAddresses: gets all IP addresses for the the current access user
|     and returns an array
|
|     Moved the frontend folder detection from the first load config to the config.path.php
|
|     Cleaned up the translations JS scripts
|
|  4dev/bin/create_mo.sh                           |  9 +++++----
|  4dev/bin/mo_to_js.sh                            | 63 +++++++++++++++++++++++++++++++++++----------------------------
|  www/configs/config.path.php                     | 16 +++++++++++++++-
|  www/configs/config.php                          | 15 +--------------
|  www/lib/CoreLibs/Convert/Strings.php            | 12 ++++++++++++
|  www/lib/CoreLibs/DB/IO.php                      |  2 +-
|  www/lib/CoreLibs/Get/System.php                 | 23 +++++++++++++++++++++++
|  www/lib/CoreLibs/Template/HtmlBuilder/Block.php | 37 ++++++++++++++++++++++++++-----------
|  8 files changed, 118 insertions(+), 59 deletions(-)
|
* commit 2d71e760e8fb7b4b0a5552a2f62bedffad928f4f
| log size 120
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-08-21 11:45:17 +0900
|
|     Composer update
|
|  www/composer.lock                          | 12 ++++++------
|  www/lib/CoreLibs/Template/SmartyExtend.php |  3 ++-
|  www/vendor/composer/installed.json         | 14 +++++++-------
|  www/vendor/composer/installed.php          |  6 +++---
|  www/vendor/gullevek/dotenv/Readme.md       | 23 +++++++++++++++++++++++
|  www/vendor/gullevek/dotenv/src/DotEnv.php  | 13 ++++++++++---
|  6 files changed, 51 insertions(+), 20 deletions(-)
|
* commit a8d07634ffaf961a37557437aac1f202ae18fa6e
| log size 182
| Author: Clemens Schwaighofer <clemens.schwaighofer@egplusww.com>
| Date:   2024-08-07 13:41:09 +0900
|
|     Add soba.egplusww.jp as local development host, jshint esversion update to 11
|
|  www/admin/layout/javascript/edit.jq.js |  2 +-
|  www/configs/config.host.php            | 28 +++++++++++-----------------
|  2 files changed, 12 insertions(+), 18 deletions(-)
|
2024-09-20 13:40:20 +09:00
Clemens Schwaighofer
e7fe4655d1 Release: v9.15.0 2024-09-03 12:15:49 +09:00
Clemens Schwaighofer
561be4bce6 Update with get address method and utf8 bom replacer 2024-09-03 12:12:11 +09:00
Clemens Schwaighofer
51fef30364 Release: v9.14.0 2024-08-05 13:34:05 +09:00
Clemens Schwaighofer
89a4b4cf3e HTML::htmlent and HTML::checked method updates 2024-08-05 13:28:11 +09:00
Clemens Schwaighofer
3eb1229590 Release: v9.13.2 2024-07-29 16:34:07 +09:00
Clemens Schwaighofer
f174e9ec34 DB-IO fix for JSON/JSONB operators when checking for placeholders 2024-07-29 16:32:45 +09:00
Clemens Schwaighofer
928369cff7 Release: v9.13.1 2024-07-29 16:14:37 +09:00
Clemens Schwaighofer
29f4800e1a DB IO Placeholder regex fix for JSON/JSONB placeholders 2024-07-29 16:13:08 +09:00
Clemens Schwaighofer
e706a67427 Shellcheck updates for publish.sh script 2024-07-19 18:39:30 +09:00
Clemens Schwaighofer
672de694ee Readme file update 2024-06-18 10:00:11 +09:00
Clemens Schwaighofer
c69eac3258 Move temp folder for phpstan to local, update github actions
Note: Github actions are not used right now
2024-05-22 18:53:16 +09:00
Clemens Schwaighofer
eca62e53c9 Disabled workflow
For this to work we have to move the smarty-extended package to an official
composer package
2024-05-22 14:21:00 +09:00
Clemens Schwaighofer
62cd3badfe GitHub action update 2024-05-22 14:18:21 +09:00
Clemens Schwaighofer
4bd2568d94 composer.json file update 2024-05-22 14:15:59 +09:00
Clemens Schwaighofer
b3c7947c67 Github actions wrong folder name 2024-05-22 14:13:46 +09:00
Clemens Schwaighofer
6710f44646 GitHub action for PHPstan CI 2024-05-22 14:11:05 +09:00
Clemens Schwaighofer
f89be6bd19 Update composer publish script
check for GITEA_PUBLISH and GITLAB_PUBLISH to trigger actual publish run
2024-05-22 11:11:18 +09:00
Clemens Schwaighofer
9696336abc Release: v9.13.0 2024-05-22 11:10:30 +09:00
Clemens Schwaighofer
fec1c5ad57 Symmetric Encryption class update to standard class and static call type 2024-05-22 11:03:44 +09:00
Clemens Schwaighofer
a6e5e86c9e Remove tools folder from git 2024-05-22 11:02:42 +09:00
Clemens Schwaighofer
1357b98883 phpcs, phpstan, psalm update 2024-05-15 17:05:52 +09:00
Clemens Schwaighofer
fa18a6f422 Merge branch 'master' into development 2024-04-17 10:22:37 +09:00
Clemens Schwaighofer
ada130329f Publish package script fixes 2024-04-17 10:22:25 +09:00
Clemens Schwaighofer
4c489adf9b Release: v9.12.2 2024-04-17 10:21:56 +09:00
Clemens Schwaighofer
c8a2ad4d48 test scripts update 2024-04-17 10:16:56 +09:00
Clemens Schwaighofer
d54a6cbdf5 Remove mb encode mimeheader special code and replace it with default function call 2024-04-17 10:13:32 +09:00
Clemens Schwaighofer
e8f4c82f59 Publish script update with versio file not existing check 2024-04-16 18:27:43 +09:00
Clemens Schwaighofer
24eb2c2bee Release: v9.12.1 2024-03-21 12:47:14 +09:00
Clemens Schwaighofer
94edb7f556 Add unique page name check for edit pages, bug fixes in Form Template Generate and DB Array IO classes 2024-03-21 12:45:39 +09:00
Clemens Schwaighofer
3f5b3f02ad Update Readme to reflect that current version is v9 2024-03-08 14:41:07 +09:00
Clemens Schwaighofer
a270922b4b Published: v9.12.0 2024-03-07 15:04:18 +09:00
Clemens Schwaighofer
8c607ae610 tools update, bug fix Strings to return path if null, Fix form list loader, fix paths 2024-03-07 15:01:19 +09:00
Clemens Schwaighofer
d3d4cf512f add php code sniffer xml 2023-12-05 17:38:01 +09:00
Clemens Schwaighofer
efae352c35 Published: v9.11.1 2023-11-29 10:51:19 +09:00
Clemens Schwaighofer
a1614bace2 Bug Fix in DB IO with parameter detection with <> 2023-11-29 10:50:25 +09:00
Clemens Schwaighofer
62ce863f2c Release: v9.11.0 2023-11-02 14:08:23 +09:00
Clemens Schwaighofer
b6ef3b29f6 ArrayHandler add next/prev key search 2023-11-02 14:06:31 +09:00
Clemens Schwaighofer
1f178628a2 Release: v9.10.1 2023-10-31 10:24:18 +09:00
Clemens Schwaighofer
67b725cb65 DB\IO fixes for logging 2023-10-31 10:21:52 +09:00
Clemens Schwaighofer
648d2c3eb5 Release: v9.10.0 2023-10-23 17:14:47 +09:00
Clemens Schwaighofer
428c10b547 new Support::getCallStack, new DateTime::intervalStringFormat, exception updates Convert\Byte, Output\Image 2023-10-23 17:13:23 +09:00
Clemens Schwaighofer
dd705be420 Release: v9.9.1 2023-10-16 16:15:09 +09:00
Clemens Schwaighofer
a1ba1257ac DB\IO dbWarning context updates 2023-10-16 16:14:10 +09:00
Clemens Schwaighofer
9569145054 Release: v9.9.0 2023-10-16 15:07:36 +09:00
Clemens Schwaighofer
13602bdd73 DB\IO update with better error reporting and auto update for placeholders in queries 2023-10-16 15:02:30 +09:00
Clemens Schwaighofer
1ef91305d4 Release: v9.8.3 2023-10-12 17:13:32 +09:00
Clemens Schwaighofer
4ab382990e Output\Image Exceptions update 2023-10-12 17:12:15 +09:00
Clemens Schwaighofer
c61a68b131 Release: v9.8.2 2023-10-06 16:53:33 +09:00
Clemens Schwaighofer
c7254c45b7 Update DB\IO with fixed regex for query params detection 2023-10-06 16:52:42 +09:00
Clemens Schwaighofer
4edacc0d13 phpunit tools symlink fix, phpunit boostrap update 2023-10-04 15:01:23 +09:00
Clemens Schwaighofer
128d6533fc Remove composer install phan/phpstan/etc and switch to phive install 2023-10-04 14:49:49 +09:00
Clemens Schwaighofer
c740fb1af1 Release: v9.8.1 2023-10-02 17:34:37 +09:00
Clemens Schwaighofer
64e60d97e7 Logging\ErrorMsg fix for jump target list return array 2023-10-02 17:33:49 +09:00
Clemens Schwaighofer
f414224c54 Release: v9.8.0 2023-10-02 14:06:32 +09:00
Clemens Schwaighofer
71c9fd401d Logging\ErrorMsg add jump target list 2023-10-02 14:04:59 +09:00
Clemens Schwaighofer
0a885f215c Release: v9.7.9 2023-10-02 12:31:21 +09:00
Clemens Schwaighofer
dad6b797e0 ErrorMsg auto log if log level is debug, Form\Generate split out DBArrayIO class 2023-10-02 12:29:50 +09:00
Clemens Schwaighofer
2d7c3c2bba Release: v9.7.8 2023-09-27 11:44:09 +09:00
Clemens Schwaighofer
fb8216ae86 ErrorMessage add notice level 2023-09-27 11:43:12 +09:00
Clemens Schwaighofer
df5070ffbb Release: v9.7.7 2023-09-27 11:26:07 +09:00
Clemens Schwaighofer
872409ef54 All null primary keys in dbWriteDataExt in DB\IO 2023-09-27 11:25:18 +09:00
Clemens Schwaighofer
c4d5cad9e8 Release: v9.7.6 2023-09-27 09:44:52 +09:00
Clemens Schwaighofer
90550746ab Bug fix in DB\ArrayIO for pk_id access 2023-09-27 09:43:32 +09:00
Clemens Schwaighofer
724031b944 Release: v9.7.5 2023-09-26 18:47:58 +09:00
Clemens Schwaighofer
027c35f9f0 Bug fix for Edit Order not loading 2023-09-26 18:47:02 +09:00
Clemens Schwaighofer
6ad844b519 Release: v9.7.4 2023-09-15 18:33:54 +09:00
Clemens Schwaighofer
e10987ce8b ErrorMsg update with target_style 2023-09-15 18:32:55 +09:00
Clemens Schwaighofer
b3617954eb Release: v9.7.3 2023-09-15 18:24:33 +09:00
Clemens Schwaighofer
67fd7b172a ErrorMessage: flag to also log 'error' level messages to log 2023-09-11 13:37:58 +09:00
Clemens Schwaighofer
fe729453ac Released: v9.7.2 2023-09-11 13:37:25 +09:00
Clemens Schwaighofer
b939edac3f Logging\ErrorMessage rename setError to setMessage 2023-09-08 21:01:05 +09:00
Clemens Schwaighofer
00528cb7d7 Release: v9.7.1 2023-09-08 18:50:35 +09:00
Clemens Schwaighofer
3b8583de61 Rename ErrorMessage method from setErrorMsgLevel to setError for backend 2023-09-08 18:49:58 +09:00
Clemens Schwaighofer
f6821b7c21 Release: v9.7.0 2023-09-08 18:39:11 +09:00
Clemens Schwaighofer
7b49394c5a Logging/ErrorMessage class 2023-09-08 18:38:19 +09:00
Clemens Schwaighofer
f624776397 Release: v9.6.2 2023-09-08 18:31:03 +09:00
Clemens Schwaighofer
5e31559c3e Release: v9.6.1: wrong 2023-09-05 15:14:04 +09:00
Clemens Schwaighofer
52a7f1197b Fixs in SetType class for string/int/numeric 2023-09-05 14:30:03 +09:00
Clemens Schwaighofer
e1466432a8 Release: v9.6.0 2023-09-01 18:34:22 +09:00
Clemens Schwaighofer
c57e798591 Add proper Exceptions to class instead of false return on critical problems 2023-09-01 18:32:57 +09:00
Clemens Schwaighofer
8126f08b8c Update publish script and zip file location
All zip files are now in published/package-download/
Move the download file name to the .env file in GITEA_UPLOAD_FILENAME
2023-09-01 09:57:05 +09:00
Clemens Schwaighofer
6f657f2890 Release: v9.5.1 2023-08-28 09:54:02 +09:00
Clemens Schwaighofer
92214ae136 DB\IO bug fixes for unset returns 2023-08-28 09:52:51 +09:00
Clemens Schwaighofer
71f9afd6c7 Release: v9.5.0 2023-08-22 13:36:08 +09:00
Clemens Schwaighofer
11de4f9915 Logging update for class/method trace 2023-08-22 13:34:43 +09:00
Clemens Schwaighofer
dfb46d4f4a Release: v9.4.0 2023-08-22 13:33:08 +09:00
Clemens Schwaighofer
53c7dda9a0 JSON Array to convert fix, bool/false return fix 2023-08-02 16:34:57 +09:00
Clemens Schwaighofer
daf2706e7e Release: v9.3.6 2023-07-26 12:13:59 +09:00
Clemens Schwaighofer
6d3a7b7b28 ACL\Login bug fix for acl:base must always be int 2023-07-26 11:52:53 +09:00
Clemens Schwaighofer
745340a7f5 Release: v9.3.5 2023-07-24 09:16:09 +09:00
Clemens Schwaighofer
419c578c46 ACL\Login get edit access id return value fix 2023-07-24 09:14:16 +09:00
Clemens Schwaighofer
6beff9c6ac Release: v9.3.4 2023-07-21 19:07:40 +09:00
Clemens Schwaighofer
79dbd053fa Remove per class loggingn from ACL\Login 2023-07-21 19:06:30 +09:00
Clemens Schwaighofer
74004e5221 Release: v9.3.3 2023-07-21 17:53:18 +09:00
Clemens Schwaighofer
0392187299 DB\IO init with null 2023-07-21 17:52:16 +09:00
Clemens Schwaighofer
edcc65df3e Release: v9.3.2 2023-07-21 17:49:09 +09:00
Clemens Schwaighofer
5dde52a309 DB\IO error/warning log prefix remove, Admin\Backend acl default value check 2023-07-14 15:10:03 +09:00
Clemens Schwaighofer
5f223fb50d Release: v9.3.1 2023-07-04 11:47:24 +09:00
Clemens Schwaighofer
2eaf80b1bd new Combined\DateTime method dateRangeHasWeekend 2023-07-04 11:46:34 +09:00
Clemens Schwaighofer
b5d601aec0 Release: v9.3.0 2023-06-28 15:34:31 +09:00
Clemens Schwaighofer
edcc77a6ab HtmlBuilder classes 2023-06-28 15:33:12 +09:00
Clemens Schwaighofer
0b1df7a901 Release: v9.2.0 2023-06-16 13:28:44 +09:00
Clemens Schwaighofer
e0f8bad2d9 DB\IO Call trace in context array for any debug calls 2023-06-16 13:24:34 +09:00
Clemens Schwaighofer
f5cd71cfbc Release: v9.1.0 2023-06-13 11:57:30 +09:00
Clemens Schwaighofer
1203164d7e DB\IO: add convert types to php type (TTD-606)
Convert DB types to PHP types
Current settings:
- off
- on: int/bool on;y
- json: json strings to array
- numeric: and real/float to php float (possible precision loss)
- bytea: convert PostgreSQL bytea strings to php data strings
2023-06-13 11:54:53 +09:00
Clemens Schwaighofer
b82e08ba05 Release: v9.0.8 2023-06-05 18:32:58 +09:00
Clemens Schwaighofer
6dfb68a6da Logging: internal fixes 2023-06-05 09:34:29 +09:00
Clemens Schwaighofer
5b944cd12d Release: v9.0.7 2023-06-05 09:32:12 +09:00
Clemens Schwaighofer
65cac4c6e2 Logging bug fixes for per_date/per_run flags 2023-06-02 17:45:21 +09:00
Clemens Schwaighofer
1c1ace58db Release: v9.0.6 2023-06-01 13:10:28 +09:00
Clemens Schwaighofer
16e12b5b8f Bug fixes 2023-06-01 13:08:24 +09:00
Clemens Schwaighofer
73063d28b2 Release: v9.0.5 2023-06-01 12:05:03 +09:00
Clemens Schwaighofer
e80d8006a2 print_r methods use mixed paramter 2023-06-01 12:03:38 +09:00
Clemens Schwaighofer
d648e4339a Must give go flag for publish 2023-06-01 11:09:37 +09:00
Clemens Schwaighofer
4b084f8785 Release: v9.0.4 2023-06-01 11:06:36 +09:00
Clemens Schwaighofer
d0d088b354 DB\IO bug fixes 2023-06-01 11:05:25 +09:00
Clemens Schwaighofer
e0d42af1d2 Release: v9.0.3 2023-06-01 09:17:33 +09:00
Clemens Schwaighofer
c1d26f122e phpunit tests updates 2023-06-01 09:16:06 +09:00
Clemens Schwaighofer
2c2826ac24 Bug fixes and clean ups 2023-06-01 08:47:40 +09:00
Clemens Schwaighofer
72f0810898 Release: v9.0.2 2023-05-31 18:49:58 +09:00
Clemens Schwaighofer
b69539b340 Merge branch 'development' 2023-05-31 18:48:58 +09:00
Clemens Schwaighofer
0b133133dd Release: v9.0.1 2023-05-31 18:48:55 +09:00
Clemens Schwaighofer
8fbe855fd4 Logger\Logger Excpetion calls update 2023-05-31 18:48:00 +09:00
Clemens Schwaighofer
d7410dfe78 Merge branch 'development' 2023-05-31 18:46:02 +09:00
Clemens Schwaighofer
5d36ac2f3e CoreLibs update v9.0.1 2023-05-31 18:45:47 +09:00
Clemens Schwaighofer
2e4ace1a39 Release: v9.0.0 2023-05-31 16:52:59 +09:00
Clemens Schwaighofer
aa5c3b9dcc composer add psr/log 2023-05-31 16:50:21 +09:00
Clemens Schwaighofer
24f7a903ef Composer json update for psr\log required 2023-05-31 16:42:55 +09:00
Clemens Schwaighofer
72993925f0 ACL\Login settings fix 2023-05-31 16:31:44 +09:00
Clemens Schwaighofer
29d5ef92d4 Updates for v9.0 release 2023-05-31 16:27:50 +09:00
Clemens Schwaighofer
f66f8f282e Release: v8.5.0 2023-05-24 16:02:05 +09:00
Clemens Schwaighofer
c010673705 CoreLibs add Security\SymmetricEncryption 2023-05-24 16:00:49 +09:00
Clemens Schwaighofer
b16ff4c613 Release: v8.4.0 2023-05-18 15:21:50 +09:00
Clemens Schwaighofer
e9c791c164 Add better error reporting to DB\IO for query with params
On error with query with params the query was sent to the server and
if ther query itself is ok but there is a problem with the parameters
a wrong error message ($1 not found) will be returned

Add pg_last_error reporting to catch this too.

Update both error reporting to return not string and prefix combined
but prefix + error string in array

In error return check that both strings are not equal, so we do not
return the same error string twice.

Also default set dbh variable in the PgSQL class to false so it will
skip last error report if there is no dbh set yet.

Bug fix for db query with params debug output. if there are more than 9
entries the $1 of eg $10 is replaced with $1 entry again. Changed to
'#' instead '$' to avoid this.

Other:
ACL\Login: replace EOM with HTML
config.master: replace list() with []
Add single DB tester where we can test single db calls without adding
more to the general test run
2023-05-18 15:20:36 +09:00
Clemens Schwaighofer
c7ec1300b7 Published: v8.3.1 2023-04-26 15:43:11 +09:00
Clemens Schwaighofer
064710324e Bug fix in arraySearchKey path reset 2023-04-26 15:41:56 +09:00
Clemens Schwaighofer
e0356dcadf Release: v8.3.0 2023-04-26 14:56:11 +09:00
Clemens Schwaighofer
62a5992e3a Array combined: new arraySearchKey method
Also update publish script and move URLS to .env file
2023-04-26 14:54:13 +09:00
Clemens Schwaighofer
6bb957fcb3 Publish v8.2.2 2023-04-11 11:04:41 +09:00
Clemens Schwaighofer
0c1f060759 Merge branch 'development' 2023-04-11 11:03:24 +09:00
Clemens Schwaighofer
aad46ec80a DB\IO: add missing debug query, clean up not needed code
in dbReturn with params on not matching param the system exited on fail
without printing the query making it hard to find where the error is.
Added debug output in case the params count is not matching.
Same move in the dbExecute call

removed param count check from dbReturnRow/dbReturnArray as this check
is done in the dbExecParams call anyway
2023-04-11 11:03:04 +09:00
Clemens Schwaighofer
f5e9f0610d Publish: v8.2.1 2023-04-10 17:24:47 +09:00
Clemens Schwaighofer
14a5250cd7 DB\IO: Bug fix for missing query params replacement in debug messages 2023-04-10 17:23:27 +09:00
Clemens Schwaighofer
6e6edef57d Release: v8.2.0 2023-04-10 14:38:50 +09:00
Clemens Schwaighofer
d3810db965 Add ACL\Login additional acl fields to export acl array 2023-04-10 14:37:44 +09:00
Clemens Schwaighofer
187a012284 Published v8.1.4 2023-04-10 09:05:35 +09:00
156 changed files with 27041 additions and 5995 deletions

33
.github/workflows-disabled/ci.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: CI
run-name: ${{ github.actor}} runs CI
on: [push]
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# - uses: php-actions/composer@v6
# env:
# COMPOSER_ROOT_VERSION: dev-master
- name: "Restore result cache"
uses: actions/cache/restore@v4
with:
path: ./tmp
key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}"
restore-keys: |
result-cache-v1-${{ matrix.php-version }}-
- name: PHPStan Static Analysis
uses: php-actions/phpstan@v3
with:
path: src/
- name: "Save result cache"
uses: actions/cache/save@v4
if: always()
with:
path: ./tmp
key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}"
# - name: PHPunit Tests
# run: |
# vendor/bin/phpunit

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
vendor
vendor/
composer.lock
tools/

View File

@@ -0,0 +1,123 @@
<?php
/**
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*
* @see src/Phan/Config.php
* See Config for all configurable options.
*
* A Note About Paths
* ==================
*
* Files referenced from this file should be defined as
*
* ```
* Config::projectPath('relative_path/to/file')
* ```
*
* where the relative path is relative to the root of the
* project which is defined as either the working directory
* of the phan executable or a path passed in via the CLI
* '-d' flag.
*/
use Phan\Config;
return [
// "target_php_version" => "8.2",
// turn color on (-C)
"color_issue_messages_if_supported" => true,
// If true, missing properties will be created when
// they are first seen. If false, we'll report an
// error message.
"allow_missing_properties" => false,
// Allow null to be cast as any type and for any
// type to be cast to null.
"null_casts_as_any_type" => false,
// Backwards Compatibility Checking
'backward_compatibility_checks' => false,
// Run a quick version of checks that takes less
// time
"quick_mode" => false,
// Only emit critical issues to start with
// (0 is low severity, 5 is normal severity, 10 is critical)
"minimum_severity" => 0,
// enable for dead code check
// this will spill out errors for all methods never called
// use after all is OK to try to find unused code blocks
// ignore recommended: PhanUnreferencedPublicMethod
// "dead_code_detection" => true,
// default false for include path check
"enable_include_path_checks" => true,
"include_paths" => [
'.',
// '../test/configs/'
],
'ignore_undeclared_variables_in_global_scope' => true,
"file_list" => [
"./test/configs/config.php",
// "./test/configs/config.db.php",
// "./test/configs/config.host.php",
// "./test/configs/config.path.php",
"./test/configs/config.other.php",
"./test/configs/config.master.php",
],
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
// Change this to include the folders you wish to analyze
// (and the folders of their dependencies)
'src',
// To speed up analysis, we recommend going back later and
// limiting this to only the vendor/ subdirectories your
// project depends on.
// `phan --init` will generate a list of folders for you
'vendor/egrajp/smarty-extended',
],
// A list of directories holding code that we want
// to parse, but not analyze
"exclude_analysis_directory_list" => [
'vendor/egrajp/smarty-extended',
],
'exclude_file_list' => [
],
// what not to show as problem
'suppress_issue_types' => [
// 'PhanUndeclaredMethod',
'PhanEmptyFile',
// ignore unreferences public methods, etc here (for dead code check)
'PhanUnreferencedPublicMethod',
'PhanUnreferencedClass',
'PhanWriteOnlyPublicProperty',
'PhanUnreferencedConstant',
'PhanWriteOnlyPublicProperty',
'PhanReadOnlyPublicProperty',
// start ignore annotations
'PhanUnextractableAnnotationElementName',
'PhanUnextractableAnnotationSuffix',
],
// Override to hardcode existence and types of (non-builtin) globals in the global scope.
// Class names should be prefixed with `\`.
//
// (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
'globals_type_map' => [],
];

View File

@@ -1,12 +1,25 @@
<?php
/**
* This configuration file was automatically generated by 'phan --init --init-level=3'
*
* TODOs (added by 'phan --init'):
*
* - Go through this file and verify that there are no missing/unnecessary files/directories.
* (E.g. this only includes direct composer dependencies
* - You may have to manually add indirect composer dependencies to 'directory_list')
* - Look at 'plugins' and add or remove plugins if appropriate
* (see https://github.com/phan/phan/tree/v5/.phan/plugins#plugins)
* - Add global suppressions for pre-existing issues to suppress_issue_types
* (https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base)
* - Consider setting up a baseline if there are a large number of pre-existing issues (see `phan --extended-help`)
*
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*
* @see src/Phan/Config.php
* See Config for all configurable options.
* @see https://github.com/phan/phan/wiki/Phan-Config-Settings for all configurable options
* @see https://github.com/phan/phan/tree/v5/src/Phan/Config.php
*
* A Note About Paths
* ==================
@@ -23,98 +36,342 @@
* '-d' flag.
*/
use Phan\Config;
use Phan\Issue;
return [
// "target_php_version" => "8.2",
// turn color on (-C)
"color_issue_messages_if_supported" => true,
// If true, missing properties will be created when
// The PHP version that the codebase will be checked for compatibility against.
// For best results, the PHP binary used to run Phan should have the same PHP version.
// (Phan relies on Reflection for some types, param counts,
// and checks for undefined classes/methods/functions)
//
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`,
// `'8.0'`, `'8.1'`, `null`.
// If this is set to `null`,
// then Phan assumes the PHP version which is closest to the minor version
// of the php executable used to execute Phan.
//
// 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.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
// error message.
"allow_missing_properties" => false,
// error message if there is an attempt to write
// to a class property that wasn't explicitly
// defined.
'allow_missing_properties' => false,
// Allow null to be cast as any type and for any
// type to be cast to null.
"null_casts_as_any_type" => false,
// If enabled, null can be cast to any type and any
// type can be cast to null. Setting this to true
// will cut down on false positives.
'null_casts_as_any_type' => false,
// Backwards Compatibility Checking
'backward_compatibility_checks' => false,
// If enabled, allow null to be cast as any array-like type.
//
// This is an incremental step in migrating away from `null_casts_as_any_type`.
// If `null_casts_as_any_type` is true, this has no effect.
'null_casts_as_array' => true,
// Run a quick version of checks that takes less
// time
"quick_mode" => false,
// If enabled, allow any array-like type to be cast to null.
// This is an incremental step in migrating away from `null_casts_as_any_type`.
// If `null_casts_as_any_type` is true, this has no effect.
'array_casts_as_null' => true,
// Only emit critical issues to start with
// (0 is low severity, 5 is normal severity, 10 is critical)
"minimum_severity" => 0,
// If enabled, scalars (int, float, bool, string, null)
// are treated as if they can cast to each other.
// This does not affect checks of array keys. See `scalar_array_key_cast`.
'scalar_implicit_cast' => false,
// enable for dead code check
// this will spill out errors for all methods never called
// use after all is OK to try to find unused code blocks
// ignore recommended: PhanUnreferencedPublicMethod
// "dead_code_detection" => true,
// If enabled, any scalar array keys (int, string)
// are treated as if they can cast to each other.
// E.g. `array<int,stdClass>` can cast to `array<string,stdClass>` and vice versa.
// Normally, a scalar type such as int could only cast to/from int and mixed.
'scalar_array_key_cast' => true,
// default false for include path check
"enable_include_path_checks" => true,
"include_paths" => [
'.',
// '../test/configs/'
],
// If this has entries, scalars (int, float, bool, string, null)
// are allowed to perform the casts listed.
//
// E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]`
// allows casting null to a string, but not vice versa.
// (subset of `scalar_implicit_cast`)
'scalar_implicit_partial' => [],
// If enabled, Phan will warn if **any** type in a method invocation's object
// is definitely not an object,
// or if **any** type in an invoked expression is not a callable.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_method_checking' => false,
// If enabled, Phan will warn if **any** type of the object expression for a property access
// does not contain that property.
'strict_object_checking' => false,
// If enabled, Phan will warn if **any** type in the argument's union type
// cannot be cast to a type in the parameter's expected union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_param_checking' => false,
// If enabled, Phan will warn if **any** type in a property assignment's union type
// cannot be cast to a type in the property's declared union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_property_checking' => false,
// If enabled, Phan will warn if **any** type in a returned value's union type
// cannot be cast to the declared return type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_return_checking' => false,
// If true, seemingly undeclared variables in the global
// scope will be ignored.
//
// This is useful for projects with complicated cross-file
// globals that you have no hope of fixing.
'ignore_undeclared_variables_in_global_scope' => true,
"file_list" => [
"./test/configs/config.php",
// "./test/configs/config.db.php",
// "./test/configs/config.host.php",
// "./test/configs/config.path.php",
"./test/configs/config.other.php",
"./test/configs/config.master.php",
],
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
// Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for,
// but aren't available in the codebase, or from Reflection.
// (may lead to false positives if an extension isn't loaded)
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
// Change this to include the folders you wish to analyze
// (and the folders of their dependencies)
'src',
// To speed up analysis, we recommend going back later and
// limiting this to only the vendor/ subdirectories your
// project depends on.
// `phan --init` will generate a list of folders for you
'vendor/egrajp/smarty-extended',
],
// If this is true(default), then Phan will not warn.
//
// Even when this is false, Phan will still infer return values and check parameters of internal functions
// if Phan has the signatures.
'ignore_undeclared_functions_with_known_signatures' => true,
// Backwards Compatibility Checking. This is slow
// and expensive, but you should consider running
// it before upgrading your version of PHP to a
// new version that has backward compatibility
// breaks.
//
// If you are migrating from PHP 5 to PHP 7,
// you should also look into using
// [php7cc (no longer maintained)](https://github.com/sstalle/php7cc)
// and [php7mar](https://github.com/Alexia/php7mar),
// which have different backwards compatibility checks.
//
// If you are still using versions of php older than 5.6,
// `PHP53CompatibilityPlugin` may be worth looking into if you are not running
// syntax checks for php 5.3 through another method such as
// `InvokePHPNativeSyntaxCheckPlugin` (see .phan/plugins/README.md).
'backward_compatibility_checks' => false,
// A list of directories holding code that we want
// to parse, but not analyze
"exclude_analysis_directory_list" => [
'vendor/egrajp/smarty-extended',
],
'exclude_file_list' => [
],
// If true, check to make sure the return type declared
// in the doc-block (if any) matches the return type
// declared in the method signature.
'check_docblock_signature_return_type_match' => false,
// what not to show as problem
'suppress_issue_types' => [
// 'PhanUndeclaredMethod',
'PhanEmptyFile',
// ignore unreferences public methods, etc here (for dead code check)
'PhanUnreferencedPublicMethod',
'PhanUnreferencedClass',
'PhanWriteOnlyPublicProperty',
'PhanUnreferencedConstant',
'PhanWriteOnlyPublicProperty',
'PhanReadOnlyPublicProperty'
],
// This setting maps case-insensitive strings to union types.
//
// This is useful if a project uses phpdoc that differs from the phpdoc2 standard.
//
// If the corresponding value is the empty string,
// then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
//
// If the corresponding value is not empty,
// then Phan will act as though it saw the corresponding UnionTypes(s)
// when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc.
//
// This matches the **entire string**, not parts of the string.
// (E.g. `@return the|null` will still look for a class with the name `the`,
// but `@return the` will be ignored with the below setting)
//
// (These are not aliases, this setting is ignored outside of doc comments).
// (Phan does not check if classes with these names exist)
//
// Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']`
'phpdoc_type_mapping' => [],
// Set to true in order to attempt to detect dead
// (unreferenced) code. Keep in mind that the
// results will only be a guess given that classes,
// properties, constants and methods can be referenced
// as variables (like `$class->$property` or
// `$class->$method()`) in ways that we're unable
// to make sense of.
//
// To more aggressively detect dead code,
// you may want to set `dead_code_detection_prefer_false_negative` to `false`.
'dead_code_detection' => false,
// Set to true in order to attempt to detect unused variables.
// `dead_code_detection` will also enable unused variable detection.
//
// This has a few known false positives, e.g. for loops or branches.
'unused_variable_detection' => false,
// Set to true in order to attempt to detect redundant and impossible conditions.
//
// This has some false positives involving loops,
// variables set in branches of loops, and global variables.
'redundant_condition_detection' => false,
// If enabled, Phan will act as though it's certain of real return types of a subset of internal functions,
// even if those return types aren't available in reflection
// (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version).
//
// Note that with php 7 and earlier, php would return null or false for many internal functions
// if the argument types or counts were incorrect.
// As a result, enabling this setting with target_php_version 8.0 may result in false positives
// for `--redundant-condition-detection` when codebases also support php 7.x.
'assume_real_types_for_internal_functions' => false,
// If true, this runs a quick version of checks that takes less
// time at the cost of not running as thorough
// of an analysis. You should consider setting this
// to true only when you wish you had more **undiagnosed** issues
// to fix in your code base.
//
// In quick-mode the scanner doesn't rescan a function
// or a method's code block every time a call is seen.
// This means that the problem here won't be detected:
//
// ```php
// <?php
// function test($arg):int {
// return $arg;
// }
// test("abc");
// ```
//
// This would normally generate:
//
// ```
// test.php:3 PhanTypeMismatchReturn Returning type string but test() is declared to return int
// ```
//
// The initial scan of the function's code block has no
// type information for `$arg`. It isn't until we see
// the call and rescan `test()`'s code block that we can
// detect that it is actually returning the passed in
// `string` instead of an `int` as declared.
'quick_mode' => false,
// Override to hardcode existence and types of (non-builtin) globals in the global scope.
// Class names should be prefixed with `\`.
//
// (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
'globals_type_map' => [],
// The minimum severity level to report on. This can be
// set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or
// `Issue::SEVERITY_CRITICAL`. Setting it to only
// critical issues is a good place to start on a big
// sloppy mature code base.
'minimum_severity' => Issue::SEVERITY_LOW,
// Add any issue types (such as `'PhanUndeclaredMethod'`)
// to this list to inhibit them from being reported.
'suppress_issue_types' => [
// start ignore annotations
'PhanUnextractableAnnotationElementName',
'PhanUnextractableAnnotationSuffix',
// wrongly thinks Enum is class
'PhanCommentObjectInClassConstantType',
],
// A regular expression to match files to be excluded
// from parsing and analysis and will not be read at all.
//
// This is useful for excluding groups of test or example
// directories/files, unanalyzable files, or files that
// can't be removed for whatever reason.
// (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`)
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
// A list of files that will be excluded from parsing and analysis
// and will not be read at all.
//
// This is useful for excluding hopelessly unanalyzable
// files that can't be removed for whatever reason.
'exclude_file_list' => [],
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as "vendor/") in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to the `directory_list` as well as
// to `exclude_analysis_directory_list`.
'exclude_analysis_directory_list' => [
'vendor/',
],
// Enable this to enable checks of require/include statements referring to valid paths.
// The settings `include_paths` and `warn_about_relative_include_statement` affect the checks.
'enable_include_path_checks' => true,
"include_paths" => [
'.',
],
// The number of processes to fork off during the analysis
// phase.
'processes' => 1,
// List of case-insensitive file extensions supported by Phan.
// (e.g. `['php', 'html', 'htm']`)
'analyzed_file_extensions' => [
'php',
],
// You can put paths to stubs of internal extensions in this config option.
// If the corresponding extension is **not** loaded, then Phan will use the stubs instead.
// Phan will continue using its detailed type annotations,
// but load the constants, classes, functions, and classes (and their Reflection types)
// from these stub files (doubling as valid php files).
// Use a different extension from php to avoid accidentally loading these.
// The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now)
//
// (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`)
'autoload_internal_extension_signatures' => [],
// A list of plugin files to execute.
//
// Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`)
//
// Documentation about available bundled plugins can be found
// [here](https://github.com/phan/phan/tree/v5/.phan/plugins).
//
// Alternately, you can pass in the full path to a PHP file with the plugin's implementation
// (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`)
'plugins' => [
'AlwaysReturnPlugin',
'PregRegexCheckerPlugin',
'UnreachableCodePlugin',
],
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in `exclude_analysis_directory_list`, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'src',
'vendor/egrajp/smarty-extended/src',
'vendor/psr/log/src',
'vendor/gullevek/dotenv',
],
// A list of individual files to include in analysis
// with a path relative to the root directory of the
// project.
'file_list' => [
"./test/configs/config.php",
"./test/configs/config.other.php",
"./test/configs/config.path.php",
"./test/configs/config.master.php",
],
];

9
.phive/phars.xml Normal file
View File

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

2
.shellcheckrc Normal file
View File

@@ -0,0 +1,2 @@
shell=bash
external-sources=true

View File

@@ -9,17 +9,60 @@ For local install only
- Template\SmartyExtended
- Admin\EditBase
## Setup from central composer
## Publish to gitea or gitlab server
Setup from gitea internal servers
Currently there are only gitea and gitlab supported, github does not have support for composer packages
```sh
composer config repositories.git.egplusww.jp.Composer composer https://git.egplusww.jp/api/packages/Composer/composer
`publish\publish.sh go` will run the publish script
All the configuration is done in the `publish\.env.deploy` file
```ini
# downlaod file name is "Repository name" "-" "version" where
# version is "vN.N.N"
GITEA_PUBLISH=1
GITEA_UPLOAD_FILENAME="Upload-File-Name";
GITEA_USER=gitea-user
GITEA_TOKEN=gitea-tokek
GITEA_URL_DL=https://[gitea.hostname]/[to/package/folder]/archive
GITEA_URL_PUSH=https://[gitea.hostname]/api/packages/[organization]/composer
GITLAB_PUBLISH=1
GITLAB_URL=gitlab URl to repository
GITLAB_DEPLOY_TOKEN=gitlab-token
```
Alternative setup composer local zip file repot:
`composer config repositories.composer.egplusww.jp composer http://composer.egplusww.jp`
At the moment there is only one gitea or gitlab target setable
## Setup from central composer
Setup from gitea servers
[hostname] is the hostname for your gitea server (or wherever this is published)
[OrgName] is the organization name where the composer packages are hosted
```sh
composer config repositories.[hostname].Composer composer https://[hostname]/api/packages/[OrgName]/composer
```
## Install package
`composer require egrajp/corelibs-composer-all:^8.0`
`composer require egrajp/corelibs-composer-all:^9.0`
## Tests
All tests must be run from the base folder
### phan
`phan --progress-bar -C --analyze-twic`
### phpstan
`phpstan`
### phpunit
PHP unit is installed via "phiev"
`tools/phpunit test/phpunit`

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/"
@@ -16,14 +17,17 @@
],
"minimum-stability": "dev",
"require": {
"php": ">=8.1"
"php": ">=8.2",
"psr/log": "^3.0@dev"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phan/phan": "v5.x-dev",
"phpunit/phpunit": "^9",
"phpstan/phpstan": "^2.0",
"phpstan/phpdoc-parser": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phan/phan": "^5.4",
"egrajp/smarty-extended": "^4.3",
"vimeo/psalm": "^5.0@dev"
"gullevek/dotenv": "dev-master",
"phpunit/phpunit": "^9"
},
"repositories": {
"git.egplusww.jp.Composer": {

18
phpcs.xml Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<ruleset name="MyStandard">
<description>PSR12 override rules (strict, standard). Switch spaces indent to tab.</description>
<arg name="tab-width" value="4"/>
<rule ref="PSR1"/>
<rule ref="PSR12">
<!-- turn off white space check for tab -->
<exclude name="Generic.WhiteSpace.DisallowTabIndent"/>
</rule>
<!-- no space indent, must be tab, 4 is tab iwdth -->
<rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
<property name="tabIndent" value="true"/>
</properties>
</rule>
</ruleset>

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

@@ -2,7 +2,7 @@
includes:
- phpstan-conditional.php
parameters:
tmpDir: /tmp/phpstan-corelibs-composer
tmpDir: %currentWorkingDirectory%/tmp/phpstan-corelibs-composer
level: 8 # max is now 9
checkMissingCallableSignature: true
treatPhpDocTypesAsCertain: false

View File

@@ -2,5 +2,11 @@
cacheResultFile="/tmp/phpunit-corelibs-composer.result.cache"
colors="true"
verbose="true"
bootstrap="test/phpunit/bootstrap.php"
>
<testsuites>
<testsuite name="deploy">
<directory>test/phpunit</directory>
</testsuite>
</testsuites>
</phpunit>

1
publish/.gitignore vendored
View File

@@ -1,2 +1 @@
*.zip
.env*

View File

@@ -1 +1 @@
8.1.3
9.26.5

1
publish/package-download/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.zip

View File

@@ -1,8 +1,13 @@
#!/usr/bin/env bash
BASE_FOLDER=$(dirname $(readlink -f $0))"/";
BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/";
PACKAGE_DOWNLOAD="${BASE_FOLDER}package-download/";
if [ ! -d "${PACKAGE_DOWNLOAD}" ]; then
mkdir "${PACKAGE_DOWNLOAD}";
fi;
VERSION=$(git tag --list | sort -V | tail -n1 | sed -e "s/^v//");
file_last_published="${BASE_FOLDER}last.published";
go_flag="$1";
if [ -z "${VERSION}" ]; then
echo "Version must be set in the form x.y.z without any leading characters";
@@ -10,52 +15,82 @@ if [ -z "${VERSION}" ]; then
fi;
# compare version, if different or newer, deploy
if [ -f "${file_last_published}" ]; then
LAST_PUBLISHED_VERSION=$(cat ${file_last_published});
if $(dpkg --compare-versions "${VERSION}" le "${LAST_PUBLISHED_VERSION}"); then
LAST_PUBLISHED_VERSION=$(cat "${file_last_published}");
if dpkg --compare-versions "${VERSION}" le "${LAST_PUBLISHED_VERSION}"; then
echo "git tag version ${VERSION} is not newer than previous published version ${LAST_PUBLISHED_VERSION}";
exit;
fi;
fi;
# read in the .env.deploy file and we must have
# for gitea
# GITEA_PUBLISH: must be set with a value to trigger publish run
# GITEA_UPLOAD_FILENAME
# GITEA_USER
# GITEA_DEPLOY_TOKEN
# GITEA_URL_DL
# GITEA_URL_PUSH
# for gitlab
# GITLAB_PUBLISH: must be set with a value to trigger publish run
# GITLAB_USER
# GITLAB_TOKEN
# GITEA_DEPLOY_TOKEN
# GITLAB_URL
if [ ! -f "${BASE_FOLDER}.env.deploy" ]; then
echo "Deploy enviroment file .env.deploy is missing";
exit;
fi;
set -o allexport;
cd ${BASE_FOLDER};
cd "${BASE_FOLDER}" || exit;
# shellcheck source=.env.deploy
source .env.deploy;
cd -;
cd - || exit;
set +o allexport;
if [ "${go_flag}" != "go" ]; then
echo "No go flag given";
echo "Would publish ${VERSION}";
echo "[END]";
exit;
fi;
echo "[START]";
# gitea
if [ ! -z "${GITEA_USER}" ] && [ ! -z "${GITEA_TOKEN}" ]; then
curl -LJO \
--output-dir "${BASE_FOLDER}" \
https://git.egplusww.jp/Composer/CoreLibs-Composer-All/archive/v${VERSION}.zip;
curl --user ${GITEA_USER}:${GITEA_TOKEN} \
--upload-file "${BASE_FOLDER}/CoreLibs-Composer-All-v${VERSION}.zip" \
https://git.egplusww.jp/api/packages/Composer/composer?version=${VERSION};
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing either GITEA_USER or GITEA_TOKEN environment variable";
# skip iof
if [ -n "${GITEA_PUBLISH}" ]; then
if [ -n "${GITEA_UPLOAD_FILENAME}" ] &&
[ -n "${GITEA_URL_DL}" ] && [ -n "${GITEA_URL_PUSH}" ] &&
[ -n "${GITEA_USER}" ] && [ -n "${GITEA_TOKEN}" ]; then
if [ ! -f "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" ]; then
curl -LJO \
--output-dir "${PACKAGE_DOWNLOAD}" \
"${GITEA_URL_DL}"/v"${VERSION}".zip;
fi;
if [ ! -f "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" ]; then
echo "Version file does not exist for ${VERSION}";
else
curl --user "${GITEA_USER}":"${GITEA_TOKEN}" \
--upload-file "${PACKAGE_DOWNLOAD}${GITEA_UPLOAD_FILENAME}-v${VERSION}.zip" \
"${GITEA_URL_PUSH}"?version="${VERSION}";
echo "${VERSION}" > "${file_last_published}";
fi;
else
echo "Missing either GITEA_UPLOAD_FILENAME, GITEA_URL_DL, GITEA_URL_PUSH, GITEA_USER or GITEA_TOKEN environment variable";
fi;
fi;
# gitlab
if [ ! -z "${GITLAB_DEPLOY_TOKEN}" ]; then
curl --data tag=v${VERSION} \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"https://gitlab-na.factory.tools/api/v4/projects/950/packages/composer";
curl --data branch=master \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"https://gitlab-na.factory.tools/api/v4/projects/950/packages/composer";
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing GITLAB_DEPLOY_TOKEN environment variable";
if [ -n "${GITLAB_PUBLISH}" ]; then
if [ -n "${GITLAB_URL}" ] && [ -n "${GITLAB_DEPLOY_TOKEN}" ]; then
curl --data tag=v"${VERSION}" \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"${GITLAB_URL}";
curl --data branch=master \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"${GITLAB_URL}";
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing GITLAB_URL or GITLAB_DEPLOY_TOKEN environment variable";
fi;
fi;
echo "";
echo "[DONE]";

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -31,109 +31,128 @@ declare(strict_types=1);
namespace CoreLibs\Admin;
use CoreLibs\Create\Uids;
use CoreLibs\Convert\Json;
class Backend
{
// page name
/** @var array<mixed> */
public $menu = [];
public array $menu = [];
/** @var int|string */
public $menu_show_flag = 0; // top menu flag (mostly string)
public int|string $menu_show_flag = 0; // top menu flag (mostly string)
// action ids
/** @var array<string> */
public $action_list = [
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 $action;
public string $action;
/** @var string|int */
public $action_id;
public string|int $action_id;
/** @var string|int */
public $action_sub_id;
public string|int $action_sub_id;
/** @var string|int|bool */
public $action_yes;
public string|int|bool $action_yes;
/** @var string */
public $action_flag;
public string $action_flag;
/** @var string */
public $action_menu;
public string $action_menu;
/** @var string */
public $action_loaded;
public string $action_loaded;
/** @var string */
public $action_value;
public string $action_value;
/** @var string */
public $action_error;
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 $acl = [];
public array $acl = [];
/** @var int */
public $default_acl;
public int $default_acl;
// queue key
/** @var string */
public $queue_key;
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 */
public $edit_access_id;
/** @var int|null */
public int|null $edit_access_id;
/** @var string */
public $page_name;
public string $page_name;
// error/warning/info messages
/** @var array<mixed> */
public $messages = [];
public array $messages = [];
/** @var bool */
public $error = false;
public bool $error = false;
/** @var bool */
public $warning = false;
public bool $warning = false;
/** @var bool */
public $info = false;
public bool $info = false;
// internal lang & encoding vars
/** @var string */
public $lang_dir = '';
public string $lang_dir = '';
/** @var string */
public $lang;
public string $lang;
/** @var string */
public $lang_short;
public string $lang_short;
/** @var string */
public $domain;
public string $domain;
/** @var string */
public $encoding;
/** @var \CoreLibs\Debug\Logging logger */
public $log;
public string $encoding;
/** @var \CoreLibs\Logging\Logging logger */
public \CoreLibs\Logging\Logging $log;
/** @var \CoreLibs\DB\IO database */
public $db;
public \CoreLibs\DB\IO $db;
/** @var \CoreLibs\Language\L10n language */
public $l;
public \CoreLibs\Language\L10n $l;
/** @var \CoreLibs\Create\Session session class */
public $session;
public \CoreLibs\Create\Session $session;
// smarty publics [end processing in smarty class]
/** @var array<mixed> */
public $DATA;
public array $DATA = [];
/** @var array<mixed> */
public $HEADER;
public array $HEADER = [];
/** @var array<mixed> */
public $DEBUG_DATA;
public array $DEBUG_DATA = [];
/** @var array<mixed> */
public $CONTENT_DATA;
public array $CONTENT_DATA = [];
// CONSTRUCTOR / DECONSTRUCTOR |====================================>
/**
* main class constructor
*
* @param \CoreLibs\DB\IO $db Database connection class
* @param \CoreLibs\Debug\Logging $log Logging class
* @param \CoreLibs\Create\Session $session Session interface class
* @param \CoreLibs\Language\L10n $l10n l10n language class
* @param int|null $set_default_acl_level Default ACL level
* @param \CoreLibs\DB\IO $db Database connection class
* @param \CoreLibs\Logging\Logging $log Logging class
* @param \CoreLibs\Create\Session $session Session interface class
* @param \CoreLibs\Language\L10n $l10n l10n language class
* @param int|null $set_default_acl_level [default=null] Default ACL level
* @param bool $init_action_vars [default=true] If the action vars should be set
*/
public function __construct(
\CoreLibs\DB\IO $db,
\CoreLibs\Debug\Logging $log,
\CoreLibs\Logging\Logging $log,
\CoreLibs\Create\Session $session,
\CoreLibs\Language\L10n $l10n,
?int $set_default_acl_level = null
?int $set_default_acl_level = null,
bool $init_action_vars = true
) {
// attach db class
$this->db = $db;
// set to log not per class
$log->setLogPer('class', false);
$log->unsetLogFlag(\CoreLibs\Logging\Logger\Flag::per_class);
// attach logger
$this->log = $log;
// attach session class
@@ -151,9 +170,9 @@ class Backend
// set the page name
$this->page_name = \CoreLibs\Get\System::getPageName();
// set the action ids
foreach ($this->action_list as $_action) {
$this->$_action = $_POST[$_action] ?? '';
// NOTE: if any of the "action" vars are used somewhere, it is recommended to NOT set them here
if ($init_action_vars) {
$this->adbSetActionVars();
}
if ($set_default_acl_level === null) {
@@ -164,11 +183,18 @@ class Backend
);
}
$this->default_acl = $set_default_acl_level ?? DEFAULT_ACL_LEVEL;
// if negative or larger than 100, reset to 0
if ($this->default_acl < 0 || $this->default_acl > 100) {
$this->default_acl = 0;
}
// queue key
if (preg_match("/^(add|save|delete|remove|move|up|down|push_live)$/", $this->action)) {
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();
}
/**
@@ -179,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
@@ -191,30 +236,117 @@ class Backend
$this->acl = $acl;
}
/**
* Return current set ACL
*
* @return array<mixed>
*/
public function adbGetAcl(): array
{
return $this->acl;
}
/**
* Set _POST action vars if needed
*
* @return void
*/
public function adbSetActionVars()
{
// set the action ids
foreach ($this->action_list as $_action) {
$this->$_action = $_POST[$_action] ?? '';
}
}
/**
* 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 */
@@ -224,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'
);
}
/**
@@ -298,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',
@@ -314,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 = [];
}
@@ -500,9 +654,9 @@ class Backend
string $data,
string $key_name,
string $key_value,
string $associate = null,
string $file = null,
string $db_schema = null,
?string $associate = null,
?string $file = null,
?string $db_schema = null,
): void {
/** @var string $DB_SCHEMA check schema */
$DB_SCHEMA = 'public';
@@ -511,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
]
);
}
/**
@@ -552,7 +720,7 @@ class Backend
string $suffix = '',
int $min_steps = 1,
bool $name_pos_back = false
) {
): string {
// get the build layout
$html_time = \CoreLibs\Output\Form\Elements::printDateTime(
$year,

View File

@@ -14,56 +14,58 @@ declare(strict_types=1);
namespace CoreLibs\Admin;
use Exception;
use SmartyException;
class EditBase
{
/** @var array<mixed> */
private $HEADER = [];
private array $HEADER = [];
/** @var array<mixed> */
private $DATA = [];
private array $DATA = [];
/** @var array<mixed> */
private $DEBUG_DATA = [];
private array $DEBUG_DATA = [];
/** @var string the template name */
private $EDIT_TEMPLATE = '';
private string $EDIT_TEMPLATE = '';
/** @var \CoreLibs\Template\SmartyExtend smarty system */
private $smarty;
private \CoreLibs\Template\SmartyExtend $smarty;
/** @var \CoreLibs\Output\Form\Generate form generate system */
private $form;
/** @var \CoreLibs\Debug\Logging */
public $log;
private \CoreLibs\Output\Form\Generate $form;
/** @var \CoreLibs\Logging\Logging */
public \CoreLibs\Logging\Logging $log;
/** @var \CoreLibs\Language\L10n */
public \CoreLibs\Language\L10n $l;
/** @var \CoreLibs\ACL\Login */
public $login;
public \CoreLibs\ACL\Login $login;
/**
* construct form generator
*
* @param array<mixed> $db_config db config array, mandatory
* @param \CoreLibs\Debug\Logging $log Logging class, null auto set
* phpcs:ignore
* @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string,db_debug_replace_placeholder?:bool} $db_config db config array, mandatory
* @param \CoreLibs\Logging\Logging $log Logging class, null auto set
* @param \CoreLibs\Language\L10n $l10n l10n language class, null auto set
* @param \CoreLibs\ACL\Login $login login class for ACL settings
* @param array<string,mixed> $options Various settings options
*/
public function __construct(
array $db_config,
\CoreLibs\Debug\Logging $log,
\CoreLibs\Logging\Logging $log,
\CoreLibs\Language\L10n $l10n,
\CoreLibs\ACL\Login $login,
array $options
) {
$this->log = $log;
$this->login = $login;
$this->l = $l10n;
// smarty template engine (extended Translation version)
$this->smarty = new \CoreLibs\Template\SmartyExtend(
$l10n,
$log,
$options['cache_id'] ?? '',
$options['compile_id'] ?? '',
);
// turn off set log per class
$log->setLogPer('class', false);
$log->unsetLogFlag(\CoreLibs\Logging\Logger\Flag::per_class);
// create form class
$this->form = new \CoreLibs\Output\Form\Generate(
@@ -76,7 +78,7 @@ class EditBase
echo "I am sorry, but this page cannot be viewed by a mobile phone";
exit;
}
// $this->form->log->debug('POST', $this->form->log->prAr($_POST));
// $this->log->debug('POST', $this->log->prAr($_POST));
}
/**
@@ -150,7 +152,7 @@ class EditBase
$q = "UPDATE " . $table_name
. " SET order_number = " . $row_data_order[$i]
. " WHERE " . $table_name . "_id = " . $row_data_id[$i];
$q = $this->form->dbExec($q);
$q = $this->form->dba->dbExec($q);
}
} // for all article ids ...
} // if write
@@ -169,7 +171,7 @@ class EditBase
$options_name = [];
$options_selected = [];
// DB read data for menu
while (is_array($res = $this->form->dbReturn($q))) {
while (is_array($res = $this->form->dba->dbReturn($q))) {
$row_data[] = [
"id" => $res[$table_name . "_id"],
"name" => $res["name"],
@@ -178,7 +180,7 @@ class EditBase
} // while read data ...
// html title
$this->HEADER['HTML_TITLE'] = $this->form->l->__('Edit Order');
$this->HEADER['HTML_TITLE'] = $this->l->__('Edit Order');
$messages = [];
$error = $_POST['error'] ?? 0;
@@ -411,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');
@@ -427,9 +427,9 @@ class EditBase
$elements[] = $this->form->formCreateElement('template');
break;
case 'edit_pages':
if (!isset($this->form->table_array['edit_page_id']['value'])) {
if (!isset($this->form->dba->getTableArray()['edit_page_id']['value'])) {
$q = "DELETE FROM temp_files";
$this->form->dbExec($q);
$this->form->dba->dbExec($q);
// gets all files in the current dir and dirs given ending with .php
$folders = ['../admin/', '../frontend/'];
$files = ['*.php'];
@@ -457,16 +457,16 @@ class EditBase
if ($t_q) {
$t_q .= ', ';
}
$t_q .= "('" . $this->form->dbEscapeString($pathinfo['dirname']) . "', '"
. $this->form->dbEscapeString($pathinfo['basename']) . "')";
$t_q .= "('" . $this->form->dba->dbEscapeString($pathinfo['dirname']) . "', '"
. $this->form->dba->dbEscapeString($pathinfo['basename']) . "')";
}
$this->form->dbExec($q . $t_q, 'NULL');
$this->form->dba->dbExec($q . $t_q, 'NULL');
$elements[] = $this->form->formCreateElement('filename');
} else {
// show file menu
// just show name of file ...
$this->DATA['filename_exist'] = 1;
$this->DATA['filename'] = $this->form->table_array['filename']['value'];
$this->DATA['filename'] = $this->form->dba->getTableArray()['filename']['value'];
} // File Name View IF
$elements[] = $this->form->formCreateElement('hostname');
$elements[] = $this->form->formCreateElement('name');
@@ -536,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,
@@ -631,7 +630,7 @@ class EditBase
'editAdmin_' . $this->smarty->lang
);
$this->form->log->debug('DEBUGEND', '==================================== [Form END]');
$this->log->debug('DEBUGEND', '==================================== [Form END]');
}
}

View File

@@ -58,39 +58,39 @@ class Basic
{
// page and host name
/** @var string */
public $page_name;
public string $page_name;
/** @var string */
public $host_name;
public string $host_name;
/** @var int */
public $host_port;
public int $host_port;
// logging interface, Debug\Logging class
/** @var \CoreLibs\Debug\Logging */
public $log;
/** @var \CoreLibs\Logging\Logging */
public \CoreLibs\Logging\Logging $log;
/** @var \CoreLibs\Create\Session */
public $session;
public \CoreLibs\Create\Session $session;
// email valid checks
/** @var array<mixed> */
public $email_regex_check = [];
public array $email_regex_check = [];
/** @var string */
public $email_regex; // regex var for email check
public string $email_regex; // regex var for email check
// data path for files
/** @var array<mixed> */
public $data_path = [];
public array $data_path = [];
// ajax flag
/** @var bool */
protected $ajax_page_flag = false;
protected bool $ajax_page_flag = false;
/**
* main Basic constructor to init and check base settings
* @param \CoreLibs\Debug\Logging|null $log Logging class
* @param \CoreLibs\Logging\Logging|null $log Logging class
* @param string|null $session_name Set session name
* @deprecated DO NOT USE Class\Basic anymore. Use dedicated logger and sub classes
*/
public function __construct(
\CoreLibs\Debug\Logging $log = null,
?\CoreLibs\Logging\Logging $log = null,
?string $session_name = null
) {
trigger_error('Class \CoreLibs\Basic is deprected', E_USER_DEPRECATED);
@@ -120,7 +120,10 @@ class Basic
}
// logging interface moved here (->debug is now ->log->debug)
$this->log = $log ?? new \CoreLibs\Debug\Logging();
$this->log = $log ?? new \CoreLibs\Logging\Logging([
'log_folder' => BASE . LOG,
'log_file_id' => 'ClassBasic-DEPRECATED',
]);
// set ajax page flag based on the AJAX_PAGE varaibles
// convert to true/false so if AJAX_PAGE is 0 or false it is
@@ -176,8 +179,9 @@ class Basic
*/
public function basicSetLogId(string $string): string
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use $basic->log->basicSetLogId() or use \CoreLibs\Debug\Logging() class', E_USER_DEPRECATED);
return $this->log->setLogId($string);
trigger_error('Method ' . __METHOD__ . ' is deprecated, use log->setLogId() or use \CoreLibs\Logging\Logging() class', E_USER_DEPRECATED);
$this->log->setLogFileId($string);
return $this->log->getLogFileId();
}
// ****** DEBUG/ERROR FUNCTIONS ******
@@ -252,7 +256,7 @@ class Basic
}
// ****** DEBUG LOGGING FUNCTIONS ******
// Moved to \CoreLibs\Debug\Logging
// Moved to \CoreLibs\Logging\Logging
/**
* passes list of level names, to turn on debug
@@ -265,68 +269,9 @@ class Basic
*/
public function debugFor(string $type, string $flag): void
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use $basic->log->debugFor() or use \CoreLibs\Debug\Logging() class', E_USER_DEPRECATED);
/** @phan-suppress-next-line PhanTypeMismatchArgumentReal, PhanParamTooFew @phpstan-ignore-next-line */
$this->log->setLogLevel(...[func_get_args()]);
trigger_error('Method ' . __METHOD__ . ' functionaility is fully deprecated', E_USER_DEPRECATED);
}
/**
* checks if we have a need to work on certain debug output
* Needs debug/echo/print ad target for which of the debug flag groups we check
* also needs level string to check in the per level output flag check.
* In case we have invalid target it will return false
* @param string $target target group to check debug/echo/print
* @param string $level level to check in detailed level flag
* @return bool true on access allowed or false on no access
*/
/* private function doDebugTrigger(string $target, string $level): bool
{
$access = false;
// check if we do debug, echo or print
switch ($target) {
case 'debug':
if ((
(isset($this->debug_output[$level]) && $this->debug_output[$level]) ||
$this->debug_output_all
) &&
(!isset($this->debug_output_not[$level]) ||
(isset($this->debug_output_not[$level]) && !$this->debug_output_not[$level])
)
) {
$access = true;
}
break;
case 'echo':
if ((
(isset($this->echo_output[$level]) && $this->echo_output[$level]) ||
$this->echo_output_all
) &&
(!isset($this->echo_output_not[$level]) ||
(isset($this->echo_output_not[$level]) && !$this->echo_output_not[$level])
)
) {
$access = true;
}
break;
case 'print':
if ((
(isset($this->print_output[$level]) && $this->print_output[$level]) ||
$this->print_output_all
) &&
(!isset($this->print_output_not[$level]) ||
(isset($this->print_output_not[$level]) && !$this->print_output_not[$level])
)
) {
$access = true;
}
break;
default:
// fall through with access false
break;
}
return $access;
} */
/**
* write debug data to error_msg array
* @param string $level id for error message, groups messages together
@@ -335,11 +280,12 @@ class Basic
* all html tags will be stripped and <br> changed to \n
* this is only used for debug output
* @return void has no return
* @deprecated Use $basic->log->debug() instead
* @deprecated Use Logger\Logger->debug() instead
*/
public function debug(string $level, string $string, bool $strip = false): void
{
$this->log->debug($level, $string, $strip);
trigger_error('Method ' . __METHOD__ . ' has moved to Logger\Logger->debug()', E_USER_DEPRECATED);
$this->log->debug($level, $string);
}
/**
@@ -351,8 +297,7 @@ class Basic
*/
public function mergeErrors(array $error_msg = []): void
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use $basic->log->mergeErrors() or use \CoreLibs\Debug\Logging() class', E_USER_DEPRECATED);
$this->log->mergeErrors($error_msg);
trigger_error('Method ' . __METHOD__ . ' is fully deprecated', E_USER_DEPRECATED);
}
/**
@@ -363,7 +308,8 @@ class Basic
*/
public function printErrorMsg(string $string = ''): string
{
return $this->log->printErrorMsg($string);
trigger_error('Method ' . __METHOD__ . ' is fully deprecated', E_USER_DEPRECATED);
return '';
}
/**
@@ -376,8 +322,7 @@ class Basic
*/
public function resetErrorMsg(string $level = ''): void
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use $basic->log->resetErrorMsg() or use \CoreLibs\Debug\Logging() class', E_USER_DEPRECATED);
$this->log->resetErrorMsg($level);
trigger_error('Method ' . __METHOD__ . ' is fully deprecated', E_USER_DEPRECATED);
}
// ****** DEBUG SUPPORT FUNCTIONS ******
@@ -388,11 +333,11 @@ class Basic
* prints a html formatted (pre) array
* @param array<mixed> $array any array
* @return string formatted array for output with <pre> tag added
* @deprecated Use $this->log->prAr() instead
* @deprecated Use \CoreLibs\Debug\Support::prAr() instead
*/
public function printAr(array $array): string
{
return $this->log->prAr($array);
return \CoreLibs\Debug\Support::prAr($array);
}
/**
@@ -1164,7 +1109,7 @@ class Basic
public function passwordSet(string $password): string
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordSet()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordSet($password);
return \CoreLibs\Security\Password::passwordSet($password);
}
/**
@@ -1177,7 +1122,7 @@ class Basic
public function passwordVerify(string $password, string $hash): bool
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordVerify()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordVerify($password, $hash);
return \CoreLibs\Security\Password::passwordVerify($password, $hash);
}
/**
@@ -1189,123 +1134,11 @@ class Basic
public function passwordRehashCheck(string $hash): bool
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordRehashCheck()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordRehashCheck($hash);
return \CoreLibs\Security\Password::passwordRehashCheck($hash);
}
// *** 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

@@ -14,8 +14,6 @@ declare(strict_types=1);
namespace CoreLibs\Check;
use Exception;
class Colors
{
/** @var int 1 for HEX rgb */
@@ -41,6 +39,7 @@ class Colors
* @param int|false $rgb_flag flag to check for rgb
* @param int|false $hsl_flag flag to check for hsl type
* @return bool True if no error, False if error
* @throws \UnexpectedValueException 1: cannot extract color from string
*/
private static function rgbHslContentCheck(
string $color,
@@ -52,7 +51,7 @@ class Colors
if (
!is_array($color_list = preg_split("/,\s*/", $matches[1] ?? ''))
) {
throw new \Exception("Could not extract color list from rgg/hsl", 3);
throw new \UnexpectedValueException("Could not extract color list from rgg/hsl", 1);
}
// based on rgb/hsl settings check that entries are valid
// rgb: either 0-255 OR 0-100%
@@ -120,11 +119,19 @@ 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
* @return bool True if valid, False if not
* @throws Exception 1: no valid flag set
* @throws \UnexpectedValueException 1: no valid flag set
* @throws \InvalidArgumentException 2: no regex block set
*/
public static function validateColor(string $color, int $flags = self::ALL): bool
{
@@ -152,10 +159,10 @@ class Colors
}
// wrong flag set
if ($flags > self::ALL) {
throw new \Exception("Invalid flags parameter: $flags", 1);
throw new \UnexpectedValueException("Invalid flags parameter: $flags", 1);
}
if (!count($regex_blocks)) {
throw new \Exception("No regex blocks set: $flags", 2);
throw new \InvalidArgumentException("No regex blocks set: $flags", 2);
}
// build regex
@@ -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

@@ -8,7 +8,7 @@ class Email
{
// this is for error check parts in where the email regex failed
/** @var array<int,string> */
private static $email_regex_check = [
private static array $email_regex_check = [
0 => "^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}@"
. "[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]{1,})*\.([a-zA-Z]{2,}){1}$", // MASTER
1 => "@(.*)@(.*)", // double @
@@ -21,7 +21,7 @@ class Email
];
// for above position, description string below
/** @var array<int,string> */
private static $email_regex_check_message = [
private static array $email_regex_check_message = [
0 => 'Invalid email address',
1 => 'Double @ mark in email address',
2 => 'Invalid email part before @ sign',
@@ -33,7 +33,7 @@ class Email
];
// the array with the mobile types that are valid
/** @var array<string,string> */
private static $mobile_email_type = [
private static array $mobile_email_type = [
'.*@docomo\.ne\.jp$' => 'keitai_docomo',
// correct are a[2-4], b2, c[1-9], e[2-9], h[2-4], t[1-9]
'.*@([a-z0-9]{2}\.)?ezweb\.ne\.jp$' => 'keitai_kddi_ezweb',
@@ -72,7 +72,7 @@ class Email
];
// short list for mobile email types
/** @var array<string,string> */
private static $mobile_email_type_short = [
private static array $mobile_email_type_short = [
'keitai_docomo' => 'docomo',
'keitai_kddi_ezweb' => 'kddi',
'keitai_kddi' => 'kddi',
@@ -169,10 +169,10 @@ class Email
* @param string $email email string
* @param bool $short default false, if true,
* returns only short type (pc instead of pc_html)
* @return string|bool email type, eg "pc", "docomo", etc,
* @return string|false email type, eg "pc", "docomo", etc,
* false for invalid short type
*/
public static function getEmailType(string $email, bool $short = false)
public static function getEmailType(string $email, bool $short = false): string|false
{
// trip if there is no email address
if (!$email) {
@@ -200,9 +200,9 @@ class Email
* gets the short email type from a long email type
*
* @param string $email_type email string
* @return string|bool short string or false for invalid
* @return string|false short string or false for invalid
*/
public static function getShortEmailType(string $email_type)
public static function getShortEmailType(string $email_type): string|false
{
// check if the short email type exists
if (isset(self::$mobile_email_type_short[$email_type])) {

View File

@@ -11,7 +11,7 @@ namespace CoreLibs\Check;
class Encoding
{
/** @var int<min, -1>|int<1, max>|string */
private static $mb_error_char = '';
private static int|string $mb_error_char = '';
/**
* set error char
@@ -90,27 +90,26 @@ class Encoding
$temp = mb_convert_encoding($string, $to_encoding, $from_encoding);
$compare = mb_convert_encoding($temp, $from_encoding, $to_encoding);
// if string does not match anymore we have a convert problem
if ($string != $compare) {
$failed = [];
// go through each character and find the ones that do not match
for ($i = 0, $iMax = mb_strlen($string, $from_encoding); $i < $iMax; $i++) {
$char = mb_substr($string, $i, 1, $from_encoding);
$r_char = mb_substr($compare, $i, 1, $from_encoding);
// the ord 194 is a hack to fix the IE7/IE8
// bug with line break and illegal character
if (
(($char != $r_char && (!self::$mb_error_char ||
in_array(self::$mb_error_char, ['none', 'long', 'entity']))) ||
($char != $r_char && $r_char == self::$mb_error_char && self::$mb_error_char)) &&
ord($char) != 194
) {
$failed[] = $char;
}
}
return $failed;
} else {
if ($string == $compare) {
return false;
}
$failed = [];
// go through each character and find the ones that do not match
for ($i = 0, $iMax = mb_strlen($string, $from_encoding); $i < $iMax; $i++) {
$char = mb_substr($string, $i, 1, $from_encoding);
$r_char = mb_substr($compare, $i, 1, $from_encoding);
// the ord 194 is a hack to fix the IE7/IE8
// bug with line break and illegal character
if (
(($char != $r_char && (!self::$mb_error_char ||
in_array(self::$mb_error_char, ['none', 'long', 'entity']))) ||
($char != $r_char && $r_char == self::$mb_error_char && self::$mb_error_char)) &&
ord($char) != 194
) {
$failed[] = $char;
}
}
return $failed;
}
}

View File

@@ -51,6 +51,23 @@ class File
// return lines in file
return $lines;
}
/**
* get the mime type of a file via finfo
* if file not found, throws exception
* else returns '' for any other finfo read problem
*
* @param string $read_file File to read, relative or absolute path
* @return string
*/
public static function getMimeType(string $read_file): string
{
$finfo = new \finfo(FILEINFO_MIME_TYPE);
if (!is_file($read_file)) {
throw new \UnexpectedValueException('[getMimeType] File not found: ' . $read_file);
}
return $finfo->file($read_file) ?: '';
}
}
// __END__

View File

@@ -1,6 +1,8 @@
<?php
/*
* NOTE: this is deprecated and all moved \CoreLibs\Security\Password
*
* core password set, check and rehash check wrapper functions
*/
@@ -8,6 +10,8 @@ declare(strict_types=1);
namespace CoreLibs\Check;
use CoreLibs\Security\Password as PasswordNew;
class Password
{
/**
@@ -15,13 +19,16 @@ class Password
*
* @param string $password password
* @return string hashed password
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordSet
*/
public static function passwordSet(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
return password_hash($password, PASSWORD_DEFAULT);
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordSet',
E_USER_DEPRECATED
);
return PasswordNew::passwordSet($password);
}
/**
@@ -30,14 +37,16 @@ class Password
* @param string $password password
* @param string $hash password hash
* @return bool true or false
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordVerify
*/
public static function passwordVerify(string $password, string $hash): bool
{
if (password_verify($password, $hash)) {
return true;
} else {
return false;
}
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordVerify',
E_USER_DEPRECATED
);
return PasswordNew::passwordVerify($password, $hash);
}
/**
@@ -45,14 +54,16 @@ class Password
*
* @param string $hash password hash
* @return bool true or false
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordRehashCheck
*/
public static function passwordRehashCheck(string $hash): bool
{
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
return true;
} else {
return false;
}
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordRehashCheck',
E_USER_DEPRECATED
);
return PasswordNew::passwordRehashCheck($hash);
}
}

View File

@@ -177,6 +177,113 @@ class ArrayHandler
return false;
}
/**
* search for one or many keys in array and return matching values
* If flat is set to true, return flat array with found values only
* If prefix is turned on each found group will be prefixed with the
* search key
*
* @param array<mixed> $array array to search in
* @param array<mixed> $needles keys to find in array
* @param bool $flat [false] Turn on flat output
* @param bool $prefix [false] Prefix found with needle key
* @return array<mixed> Found values
*/
public static function arraySearchKey(
array $array,
array $needles,
bool $flat = false,
bool $prefix = false
): array {
$iterator = new \RecursiveArrayIterator($array);
$recursive = new \RecursiveIteratorIterator(
$iterator,
\RecursiveIteratorIterator::SELF_FIRST
);
$hit_list = [];
if ($prefix === true) {
$hit_list = array_fill_keys($needles, []);
}
$key_path = [];
$prev_depth = 0;
foreach ($recursive as $key => $value) {
if ($prev_depth > $recursive->getDepth()) {
// remove all trailing to ne depth
$diff = $prev_depth - $recursive->getDepth();
array_splice($key_path, -$diff, $diff);
}
$prev_depth = $recursive->getDepth();
if ($flat === false) {
$key_path[$recursive->getDepth()] = $key;
}
if (in_array($key, $needles, true)) {
ksort($key_path);
if ($flat === true) {
$hit = $value;
} else {
$hit = [
'value' => $value,
'path' => $key_path
];
}
if ($prefix === true) {
$hit_list[$key][] = $hit;
} else {
$hit_list[] = $hit;
}
}
}
return $hit_list;
}
/**
* main wrapper function for next/prev key
*
* @param array<mixed> $array array to search in
* @param int|string $key key for next/prev
* @param bool $next [=true] if to search next or prev
* @return int|string|null Next/prev key or null for end/first
*/
private static function arrayGetKey(array $array, int|string $key, bool $next = true): int|string|null
{
$keys = array_keys($array);
if (($position = array_search($key, $keys, true)) === false) {
return null;
}
$next_position = $next ? $position + 1 : $position - 1;
if (!isset($keys[$next_position])) {
return null;
}
return $keys[$next_position];
}
/**
* Get previous array key from an array
* null on not found
*
* @param array<mixed> $array
* @param int|string $key
* @return int|string|null Next key, or null for not found
*/
public static function arrayGetPrevKey(array $array, int|string $key): int|string|null
{
return self::arrayGetKey($array, $key, false);
}
/**
* Get next array key from an array
* null on not found
*
* @param array<mixed> $array
* @param int|string $key
* @return int|string|null Next key, or null for not found
*/
public static function arrayGetNextKey(array $array, int|string $key): int|string|null
{
return self::arrayGetKey($array, $key, true);
}
/**
* correctly recursive merges as an array as array_merge_recursive
* just glues things together
@@ -186,14 +293,13 @@ class ArrayHandler
* bool key flag: true: handle keys as string or int
* default false: all keys are string
*
* @return array<mixed>|false merged array
* @return array<mixed> merged array
*/
public static function arrayMergeRecursive(): array|false
public static function arrayMergeRecursive(): array
{
// croak on not enough arguemnts (we need at least two)
if (func_num_args() < 2) {
trigger_error(__FUNCTION__ . ' needs two or more array arguments', E_USER_WARNING);
return false;
throw new \ArgumentCountError(__FUNCTION__ . ' needs two or more array arguments');
}
// default key is not string
$key_is_string = false;
@@ -206,15 +312,13 @@ class ArrayHandler
}
// check that arrays count is at least two, else we don't have enough to do anything
if (count($arrays) < 2) {
trigger_error(__FUNCTION__ . ' needs two or more array arguments', E_USER_WARNING);
return false;
throw new \ArgumentCountError(__FUNCTION__ . ' needs two or more array arguments');
}
$merged = [];
while ($arrays) {
$array = array_shift($arrays);
if (!is_array($array)) {
trigger_error(__FUNCTION__ . ' encountered a non array argument', E_USER_WARNING);
return false;
throw new \TypeError(__FUNCTION__ . ' encountered a non array argument');
}
if (!$array) {
continue;
@@ -405,6 +509,48 @@ 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
);
}
}
// __END__

View File

@@ -105,53 +105,292 @@ class DateTime
bool $show_micro = true
): string {
// check if the timestamp has any h/m/s/ms inside, if yes skip
if (!preg_match("/(h|m|s|ms)/", (string)$timestamp)) {
list($timestamp, $ms) = array_pad(explode('.', (string)round((float)$timestamp, 4)), 2, null);
// if negative remember
$negative = false;
if ((int)$timestamp < 0) {
$negative = true;
}
$timestamp = abs((float)$timestamp);
$timegroups = [86400, 3600, 60, 1];
$labels = ['d', 'h', 'm', 's'];
$time_string = '';
// if timestamp is zero, return zero string
if ($timestamp == 0) {
$time_string = '0s';
} else {
for ($i = 0, $iMax = count($timegroups); $i < $iMax; $i++) {
$output = floor((float)$timestamp / $timegroups[$i]);
$timestamp = (float)$timestamp % $timegroups[$i];
// output has days|hours|min|sec
if ($output || $time_string) {
$time_string .= $output . $labels[$i] . (($i + 1) != count($timegroups) ? ' ' : '');
}
}
}
// only add ms if we have an ms value
if ($ms !== null) {
// if we have ms and it has leading zeros, remove them, but only if it is nut just 0
$ms = preg_replace("/^0+(\d+)$/", '${1}', $ms);
if (!is_string($ms) || empty($ms)) {
$ms = '0';
}
// add ms if there
if ($show_micro) {
$time_string .= ' ' . $ms . 'ms';
} elseif (!$time_string) {
$time_string .= $ms . 'ms';
}
}
if ($negative) {
$time_string = '-' . $time_string;
if (preg_match("/(h|m|s|ms)/", (string)$timestamp)) {
return (string)$timestamp;
}
// split to 6 (nano seconds)
list($timestamp, $ms) = array_pad(explode('.', (string)round((float)$timestamp, 6)), 2, null);
// if micro seconds is on and we have none, set to 0
if ($show_micro && $ms === null) {
$ms = 0;
}
// if negative remember
$negative = false;
if ((int)$timestamp < 0) {
$negative = true;
}
$timestamp = abs((float)$timestamp);
$timegroups = [86400, 3600, 60, 1];
$labels = ['d', 'h', 'm', 's'];
$time_string = '';
// if timestamp is zero, return zero string
if ($timestamp == 0) {
// if no seconds and we have no microseconds either, show no micro seconds
if ($ms == 0) {
$ms = null;
}
$time_string = '0s';
} else {
$time_string = $timestamp;
for ($i = 0, $iMax = count($timegroups); $i < $iMax; $i++) {
$output = floor((float)$timestamp / $timegroups[$i]);
$timestamp = (float)$timestamp % $timegroups[$i];
// output has days|hours|min|sec
if ($output || $time_string) {
$time_string .= $output . $labels[$i] . (($i + 1) != count($timegroups) ? ' ' : '');
}
}
}
// only add ms if we have an ms value
if ($ms !== null) {
// prefix the milliseoncds with 0. and round it max 3 digits and then convert to int
$ms = round((float)('0.' . $ms), 3) * 1000;
// add ms if there
if ($show_micro) {
$time_string .= ' ' . $ms . 'ms';
} elseif (!$time_string) {
$time_string .= $ms . 'ms';
}
}
if ($negative) {
$time_string = '-' . $time_string;
}
return (string)$time_string;
}
/**
* update timeStringFormat with year and month support
*
* The following flags have to be set to be timeStringFormat compatible.
* Not that on seconds overflow this method will throw an exception, timeStringFormat returned -1s
* show_only_days: true,
* skip_zero: false,
* skip_last_zero: false,
* truncate_nanoseconds: true,
* truncate_zero_seconds_if_microseconds: false
*
* @param int|float $seconds Seconds to convert, maxium 6 decimals,
* else \UnexpectedValueException will be thrown
* if days too large or years too large \LengthException is thrown
* @param string $truncate_after [=''] Truncate after which time name, will not round, hard end
* values are parts names or interval short names (y, d, f, ...)
* if illegal value \UnexpectedValueException is thrown
* @param bool $natural_seperator [=false] use ',' and 'and', if off use space
* @param bool $name_space_seperator [=false] add a space between the number and the time name
* @param bool $show_microseconds [=true] show microseconds
* @param bool $short_time_name [=true] use the short time names (eg s instead of seconds)
* @param bool $skip_last_zero [=true] skip all trailing zero values, eg 5m 0s => 5m
* @param bool $skip_zero [=true] do not show zero values anywhere, eg 1h 0m 20s => 1h 20s
* @param bool $show_only_days [=false] do not show years or months, show only days
* if truncate after is set to year or month
* throws \UnexpectedValueException
* @param bool $auto_fix_microseconds [=false] if the micro seconds decimals are more than 6, round them
* on defaul throw \UnexpectedValueException
* @param bool $truncate_nanoseconds [=false] if microseconds decimals >3 then normal we show 123.4ms
* cut the .4 is set to true
* @param bool $truncate_zero_seconds_if_microseconds [=true] if we have 0.123 seconds then if true no seconds
* will be shown
* @return string
* @throws \UnexpectedValueException if seconds has more than 6 decimals
* if truncate has an illegal value
* if truncate is set to year or month and show_only_days is turned on
* @throws \LengthException if seconds is too large and show_days_only is selected and days is negetive
* or if years is negativ
*/
public static function intervalStringFormat(
int|float $seconds,
string $truncate_after = '',
bool $natural_seperator = false,
bool $name_space_seperator = false,
bool $show_microseconds = true,
bool $short_time_name = true,
bool $skip_last_zero = true,
bool $skip_zero = true,
bool $show_only_days = false,
bool $auto_fix_microseconds = false,
bool $truncate_nanoseconds = false,
bool $truncate_zero_seconds_if_microseconds = true,
): string {
// auto fix long seconds, else \UnexpectedValueException will be thrown on error
// check if we have float and -> round to 6
if ($auto_fix_microseconds === true && is_float($seconds)) {
$seconds = round($seconds, 6);
}
// flag negative + set abs
$negative = $seconds < 0 ? '-' : '';
$seconds = abs($seconds);
// create base time
$date_now = new \DateTime("@0");
try {
$date_seconds = new \DateTime("@$seconds");
} catch (\Exception $e) {
throw new \UnexpectedValueException(
'Seconds value is invalid, too large or more than six decimals: ' . $seconds,
1,
$e
);
}
$interval = date_diff($date_now, $date_seconds);
// if show_only_days and negative but input postive alert that this has to be done in y/m/d ...
if ($interval->y < 0) {
throw new \LengthException('Input seconds value is too large for years output: ' . $seconds, 2);
} elseif ($interval->days < 0 && $show_only_days === true) {
throw new \LengthException('Input seconds value is too large for days output: ' . $seconds, 3);
}
$parts = [
'years' => 'y',
'months' => 'm',
'days' => 'd',
'hours' => 'h',
'minutes' => 'i',
'seconds' => 's',
'microseconds' => 'f',
];
$short_name = [
'years' => 'y', 'months' => 'm', 'days' => 'd',
'hours' => 'h', 'minutes' => 'm', 'seconds' => 's',
'microseconds' => 'ms'
];
// $skip = false;
if (!empty($truncate_after)) {
// if truncate after not in key or value in parts
if (!in_array($truncate_after, array_keys($parts)) && !in_array($truncate_after, array_values($parts))) {
throw new \UnexpectedValueException(
'truncate_after has an invalid value: ' . $truncate_after,
4
);
}
// if truncate after is y or m and we have show_only_days, throw exception
if ($show_only_days === true && in_array($truncate_after, ['y', 'years', 'm', 'months'])) {
throw new \UnexpectedValueException(
'If show_only_days is turned on, the truncate_after cannot be years or months: '
. $truncate_after,
5
);
}
// $skip = true;
}
$formatted = [];
$zero_formatted = [];
$value_set = false;
$add_zero_seconds = false;
foreach ($parts as $time_name => $part) {
if (
// skip for micro seconds
($show_microseconds === false && $part == 'f') ||
// skip for if days only and we have year or month
($show_only_days === true && in_array($part, ['y', 'm']))
) {
continue;
}
$add_value = 0;
if ($show_only_days === true && $part == 'd') {
$value = $interval->days;
} else {
$value = $interval->$part;
}
// print "-> V: $value | $part, $time_name"
// . " | Set: " . ($value_set ? 'Y' : 'N') . ", SkipZ: " . ($skip_zero ? 'Y' : 'N')
// . " | SkipLZ: " . ($skip_last_zero ? 'Y' : 'N')
// . " | " . ($value != 0 ? 'Not zero' : 'ZERO') . "<br>";
if ($value != 0) {
if ($part == 'f') {
if ($truncate_nanoseconds === true) {
$value = round($value, 3);
}
$value *= 1000;
// anything above that is nano seconds?
}
if ($value) {
$value_set = true;
}
$add_value = 1;
} elseif (
$value == 0 &&
$value_set === true && (
$skip_last_zero === false ||
$skip_zero === false
)
) {
$add_value = 2;
}
// echo "ADD VALUE: $add_value<br>";
if ($add_value) {
// build format
$format = "$value";
if ($name_space_seperator) {
$format .= " ";
}
if ($short_time_name) {
$format .= $short_name[$time_name];
} elseif ($value == 1) {
$format .= substr($time_name, 0, -1);
} else {
$format .= $time_name;
}
if ($add_value == 1) {
if (count($zero_formatted) && $skip_zero === false) {
$formatted = array_merge($formatted, $zero_formatted);
}
$zero_formatted = [];
$formatted[] = $format;
} elseif ($add_value == 2) {
$zero_formatted[] = $format;
}
}
// if seconds is zero
if (
$part == 's' && $value == 0 &&
$show_microseconds === true &&
$truncate_zero_seconds_if_microseconds === false
) {
$add_zero_seconds = true;
}
// stop after a truncate is matching
if ($part == $truncate_after || $truncate_after == $time_name) {
break;
}
}
// add all zero entries if we have skip last off
if (count($zero_formatted) && $skip_last_zero === false) {
$formatted = array_merge($formatted, $zero_formatted);
}
// print "=> F: " . print_r($formatted, true)
// . " | Z: " . print_r($zero_list, true)
// . " | ZL: " . print_r($zero_last_list, true)
// . "<br>";
if (count($formatted) == 0) {
// if we have truncate on, then we assume nothing was found
if (!empty($truncate_after)) {
if (in_array($truncate_after, array_values($parts))) {
$truncate_after = array_flip($parts)[$truncate_after];
}
$time_name = $truncate_after;
} else {
$time_name = 'seconds';
}
return '0' . ($name_space_seperator ? ' ' : '')
. ($short_time_name ? $short_name[$time_name] : $time_name);
} elseif (count($formatted) == 1) {
return $negative .
($add_zero_seconds ?
'0'
. ($name_space_seperator ? ' ' : '')
. ($short_time_name ? $short_name['seconds'] : 'seconds')
. ' '
: ''
)
. $formatted[0];
} elseif ($natural_seperator === false) {
return $negative . implode(' ', $formatted);
} else {
$str = implode(', ', array_slice($formatted, 0, -1));
if (!empty($formatted[count($formatted) - 1])) {
$str .= ' and ' . (string)array_pop($formatted);
}
return $negative . $str;
}
}
/**
* does a reverse of the timeStringFormat and converts the string from
* xd xh xm xs xms to a timestamp.microtime format
@@ -162,37 +401,36 @@ class DateTime
public static function stringToTime(string|int|float $timestring): string|int|float
{
$timestamp = 0;
if (preg_match("/(d|h|m|s|ms)/", (string)$timestring)) {
$timestring = (string)$timestring;
// pos for preg match read + multiply factor
$timegroups = [2 => 86400, 4 => 3600, 6 => 60, 8 => 1];
$matches = [];
// if start with -, strip and set negative
$negative = false;
if (preg_match("/^-/", $timestring)) {
$negative = true;
$timestring = substr($timestring, 1);
}
// preg match: 0: full string
// 2, 4, 6, 8 are the to need values
preg_match("/^((\d+)d ?)?((\d+)h ?)?((\d+)m ?)?((\d+)s ?)?((\d+)ms)?$/", $timestring, $matches);
// multiply the returned matches and sum them up. the last one (ms) is added with .
foreach ($timegroups as $i => $time_multiply) {
if (isset($matches[$i]) && is_numeric($matches[$i])) {
$timestamp += (float)$matches[$i] * $time_multiply;
}
}
if (isset($matches[10]) && is_numeric($matches[10])) {
$timestamp .= '.' . $matches[10];
}
if ($negative) {
// cast to flaot so we can do a negative multiplication
$timestamp = (float)$timestamp * -1;
}
return $timestamp;
} else {
if (!preg_match("/(d|h|m|s|ms)/", (string)$timestring)) {
return $timestring;
}
$timestring = (string)$timestring;
// pos for preg match read + multiply factor
$timegroups = [2 => 86400, 4 => 3600, 6 => 60, 8 => 1];
$matches = [];
// if start with -, strip and set negative
$negative = false;
if (preg_match("/^-/", $timestring)) {
$negative = true;
$timestring = substr($timestring, 1);
}
// preg match: 0: full string
// 2, 4, 6, 8 are the to need values
preg_match("/^((\d+)d ?)?((\d+)h ?)?((\d+)m ?)?((\d+)s ?)?((\d+)ms)?$/", $timestring, $matches);
// multiply the returned matches and sum them up. the last one (ms) is added with .
foreach ($timegroups as $i => $time_multiply) {
if (isset($matches[$i]) && is_numeric($matches[$i])) {
$timestamp += (float)$matches[$i] * $time_multiply;
}
}
if (isset($matches[10]) && is_numeric($matches[10])) {
$timestamp .= '.' . $matches[10];
}
if ($negative) {
// cast to flaot so we can do a negative multiplication
$timestamp = (float)$timestamp * -1;
}
return $timestamp;
}
/**
@@ -323,36 +561,36 @@ class DateTime
*
* @param string $start_date start date string in YYYY-MM-DD
* @param string $end_date end date string in YYYY-MM-DD
* @return int|bool false on error
* or int -1 (s<e)/0 (s=e)/1 (s>e) as difference
* @return int int -1 (s<e)/0 (s=e)/1 (s>e) as difference
* @throws \UnexpectedValueException On empty start/end values
*/
public static function compareDate(string $start_date, string $end_date): int|bool
public static function compareDate(string $start_date, string $end_date): int
{
// pre check for empty or wrong
if ($start_date == '--' || $end_date == '--' || !$start_date || !$end_date) {
return false;
if ($start_date == '--' || $end_date == '--' || empty($start_date) || empty($end_date)) {
throw new \UnexpectedValueException('Start or End date not set or are just "--"', 1);
}
// if invalid, quit
if (($start_timestamp = strtotime($start_date)) === false) {
return false;
throw new \UnexpectedValueException("Error parsing start date through strtotime()", 2);
}
if (($end_timestamp = strtotime($end_date)) === false) {
return false;
throw new \UnexpectedValueException("Error parsing end date through strtotime()", 3);
}
$comp = 0;
// convert anything to Y-m-d and then to timestamp
// this is to remove any time parts
$start_timestamp = strtotime(date('Y-m-d', $start_timestamp));
$end_timestamp = strtotime(date('Y-m-d', $end_timestamp));
// compare, or end with false
if ($start_timestamp < $end_timestamp) {
return -1;
$comp = -1;
} elseif ($start_timestamp == $end_timestamp) {
return 0;
$comp = 0;
} elseif ($start_timestamp > $end_timestamp) {
return 1;
} else {
return false;
$comp = 1;
}
return $comp;
}
/**
@@ -366,32 +604,32 @@ class DateTime
*
* @param string $start_datetime start date/time in YYYY-MM-DD HH:mm:ss
* @param string $end_datetime end date/time in YYYY-MM-DD HH:mm:ss
* @return int|bool false for error
* or -1 (s<e)/0 (s=e)/1 (s>e) as difference
* @return int -1 (s<e)/0 (s=e)/1 (s>e) as difference
* @throws \UnexpectedValueException On empty start/end values
*/
public static function compareDateTime(string $start_datetime, string $end_datetime): int|bool
public static function compareDateTime(string $start_datetime, string $end_datetime): int
{
// pre check for empty or wrong
if ($start_datetime == '--' || $end_datetime == '--' || !$start_datetime || !$end_datetime) {
return false;
if ($start_datetime == '--' || $end_datetime == '--' || empty($start_datetime) || empty($end_datetime)) {
throw new \UnexpectedValueException('Start or end timestamp not set or are just "--"', 1);
}
// quit if invalid timestamp
if (($start_timestamp = strtotime($start_datetime)) === false) {
return false;
throw new \UnexpectedValueException("Error parsing start timestamp through strtotime()", 2);
}
if (($end_timestamp = strtotime($end_datetime)) === false) {
return false;
throw new \UnexpectedValueException("Error parsing end timestamp through strtotime()", 3);
}
$comp = 0;
// compare, or return false
if ($start_timestamp < $end_timestamp) {
return -1;
$comp = -1;
} elseif ($start_timestamp == $end_timestamp) {
return 0;
$comp = 0;
} elseif ($start_timestamp > $end_timestamp) {
return 1;
} else {
return false;
$comp = 1;
}
return $comp;
}
/**
@@ -437,9 +675,9 @@ class DateTime
foreach ($period as $dt) {
$curr = $dt->format('D');
if ($curr == 'Sat' || $curr == 'Sun') {
$days[2] ++;
$days[2]++;
} else {
$days[1] ++;
$days[1]++;
}
}
if ($return_named === true) {
@@ -452,6 +690,31 @@ class DateTime
return $days;
}
}
/**
* check if a weekend day (sat/sun) is in the given date range
* Can have time too, but is not needed
*
* @param string $start_date Y-m-d
* @param string $end_date Y-m-d
* @return bool True for has weekend, False for has not
*/
public static function dateRangeHasWeekend(
string $start_date,
string $end_date,
): bool {
$dd_start = new \DateTime($start_date);
$dd_end = new \DateTime($end_date);
if (
// starts with a weekend
$dd_start->format('N') >= 6 ||
// start day plus diff will be 6 and so fall into a weekend
((int)$dd_start->format('w') + $dd_start->diff($dd_end)->days) >= 6
) {
return true;
}
return false;
}
}
// __END__

View File

@@ -37,7 +37,7 @@ class Byte
* BYTE_FORMAT_ADJUST: sprintf adjusted two 2 decimals
* BYTE_FORMAT_SI: use 1000 instead of 1024
* @return string converted byte number (float) with suffix
* @throws \Exception 1: no valid flag set
* @throws \InvalidArgumentException 1: no valid flag set
*/
public static function humanReadableByteFormat(string|int|float $bytes, int $flags = 0): string
{
@@ -63,7 +63,7 @@ class Byte
$si = false;
}
if ($flags > 7) {
throw new \Exception("Invalid flags parameter: $flags", 1);
throw new \InvalidArgumentException("Invalid flags parameter: $flags", 1);
}
// si or normal
@@ -119,7 +119,7 @@ class Byte
* @param int $flags bitwise flag with use space turned on
* BYTE_FORMAT_SI: use 1000 instead of 1024
* @return string|int|float converted value or original value
* @throws \Exception 1: no valid flag set
* @throws \InvalidArgumentException 1: no valid flag set
*/
public static function stringByteFormat(string|int|float $number, int $flags = 0): string|int|float
{
@@ -130,7 +130,7 @@ class Byte
$si = false;
}
if ($flags != 0 && $flags != 4) {
throw new \Exception("Invalid flags parameter: $flags", 1);
throw new \InvalidArgumentException("Invalid flags parameter: $flags", 1);
}
// matches in regex
$matches = [];

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
{
/**
@@ -28,64 +31,52 @@ class Colors
* @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,
* false for invalid color
* @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,
int $green,
int $blue,
bool $hex_prefix = true
): string|bool {
$hex_color = '';
if ($hex_prefix === true) {
$hex_color = '#';
}
foreach (['red', 'green', 'blue'] as $color) {
// if not valid, abort
if ($$color < 0 || $$color > 255) {
return false;
}
// pad left with 0
$hex_color .= str_pad(dechex($$color), 2, '0', STR_PAD_LEFT);
}
return $hex_color;
): string {
return (new Coordinates\RGB([$red, $green, $blue]))->returnAsHex($hex_prefix);
}
/**
* converts a hex RGB color to the int numbers
*
* @param string $hexStr RGB hexstring
* @param string $hex_string RGB hexstring
* @param bool $return_as_string flag to return as string
* @param string $seperator string seperator: default: ","
* @return string|array<string,float|int>|bool false on error or array with RGB
* or a string with the seperator
* @return string|array<string,float|int> array with RGB
* 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 $hexStr,
string $hex_string,
bool $return_as_string = false,
string $seperator = ','
): string|array|bool {
$hexStr = preg_replace("/[^0-9A-Fa-f]/", '', $hexStr); // Gets a proper hex string
if (!is_string($hexStr)) {
return false;
}
): string|array {
$rgbArray = [];
if (strlen($hexStr) == 6) {
// If a proper hex code, convert using bitwise operation.
// No overhead... faster
$colorVal = hexdec($hexStr);
$rgbArray['r'] = 0xFF & ($colorVal >> 0x10);
$rgbArray['g'] = 0xFF & ($colorVal >> 0x8);
$rgbArray['b'] = 0xFF & $colorVal;
} elseif (strlen($hexStr) == 3) {
// If shorthand notation, need some string manipulations
$rgbArray['r'] = hexdec(str_repeat(substr($hexStr, 0, 1), 2));
$rgbArray['g'] = hexdec(str_repeat(substr($hexStr, 1, 1), 2));
$rgbArray['b'] = hexdec(str_repeat(substr($hexStr, 2, 1), 2));
} else {
// Invalid hex color code
return false;
// 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;
@@ -97,46 +88,21 @@ class Colors
* 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<int|float>|bool Hue, Sat, Brightness/Value
* false for input value error
* @param int $red red 0-255
* @param int $green green 0-255
* @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|bool
public static function rgb2hsb(int $red, int $green, int $blue): array
{
// check that rgb is from 0 to 255
foreach (['red', 'green', 'blue'] as $c) {
if ($$c < 0 || $$c > 255) {
return false;
}
$$c = $$c / 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()
);
}
/**
@@ -144,85 +110,21 @@ class Colors
* converts HSB/V to RGB values RGB is full INT
* if HSB/V value is invalid, sets this value to 0
*
* @param float $H hue 0-360 (int)
* @param float $S saturation 0-100 (int)
* @param float $V brightness/value 0-100 (int)
* @return array<int>|bool 0 red/1 green/2 blue array as 0-255
* false for input value error
* @param float $H hue 0-360 (int)
* @param float $S saturation 0-100 (int)
* @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|bool
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) {
return false;
}
if ($S < 0 || $S > 100) {
return false;
}
if ($V < 0 || $V > 100) {
return false;
}
// 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()
);
}
/**
@@ -230,115 +132,42 @@ class Colors
* return:
* array with hue (0-360), saturation (0-100%) and luminance (0-100%)
*
* @param int $red red 0-255
* @param int $green green 0-255
* @param int $blue blue 0-255
* @return array<float>|bool hue/sat/luminance
* false for input value error
* @param int $red red 0-255
* @param int $green green 0-255
* @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|bool
public static function rgb2hsl(int $red, int $green, int $blue): array
{
// check that rgb is from 0 to 255
foreach (['red', 'green', 'blue'] as $c) {
if ($$c < 0 || $$c > 255) {
return false;
}
$$c = $$c / 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()
);
}
/**
* converts an HSL to RGB
* if HSL value is invalid, set this value to 0
*
* @param float $hue hue: 0-360 (degrees)
* @param float $sat saturation: 0-100
* @param float $lum luminance: 0-100
* @return array<int,float|int>|bool red/blue/green 0-255 each
* @param float $hue hue: 0-360 (degrees)
* @param float $sat saturation: 0-100
* @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|bool
public static function hsl2rgb(float $hue, float $sat, float $lum): array
{
if ($hue == 360) {
$hue = 0;
}
if ($hue < 0 || $hue > 359) {
return false;
}
if ($sat < 0 || $sat > 100) {
return false;
}
if ($lum < 0 || $lum > 100) {
return false;
}
// 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

@@ -26,8 +26,12 @@ class SetVarTypeMain
?string $default = null,
bool $to_null = false
): ?string {
if (is_string($val)) {
return $val;
if (
$val === null ||
is_scalar($val) ||
$val instanceof \Stringable
) {
return (string)$val;
}
if ($to_null === false) {
return (string)$default;
@@ -39,6 +43,7 @@ class SetVarTypeMain
* Will convert input data to string if possible.
* Runs for string/int/float/bool/null
* Will skip array/object/resource/callable/etc and use default for that
* Note: this is pretty much the same as setStrMain because string is easy
*
* @param mixed $val Input variable
* @param string|null $default Default value
@@ -47,7 +52,7 @@ class SetVarTypeMain
*/
protected static function makeStrMain(
mixed $val,
string $default = null,
?string $default = null,
bool $to_null = false
): ?string {
// int/float/string/bool/null, everything else is ignored
@@ -71,6 +76,7 @@ class SetVarTypeMain
/**
* If input variable is int, return it, else return default value. If to_null
* is true then null as return is allowed, else only int is returned
* Note, if float is sent in, int is returned
*
* @param mixed $val Input variable
* @param int|null $default Default value
@@ -82,8 +88,8 @@ class SetVarTypeMain
?int $default = null,
bool $to_null = false
): ?int {
if (is_int($val)) {
return $val;
if (is_numeric($val)) {
return (int)$val;
}
if ($to_null === false) {
return (int)$default;
@@ -107,7 +113,7 @@ class SetVarTypeMain
*/
protected static function makeIntMain(
mixed $val,
int $default = null,
?int $default = null,
bool $to_null = false
): ?int {
// if we can filter it to a valid int, we can convert it
@@ -129,6 +135,7 @@ class SetVarTypeMain
/**
* If input is float return it, else set to default value. If to_null is set
* to true, allow null return
* Note if an int is sent in, float is returned
*
* @param mixed $val Input variable
* @param float|null $default Default value
@@ -140,8 +147,8 @@ class SetVarTypeMain
?float $default = null,
bool $to_null = false
): ?float {
if (is_float($val)) {
return $val;
if (is_numeric($val)) {
return (float)$val;
}
if ($to_null === false) {
return (float)$default;
@@ -160,7 +167,7 @@ class SetVarTypeMain
*/
protected static function makeFloatMain(
mixed $val,
float $default = null,
?float $default = null,
bool $to_null = false
): ?float {
if (

View File

@@ -16,16 +16,22 @@ class Html
/**
* full wrapper for html entities
*
* uses default params as: ENT_QUOTES | ENT_HTML5
* switches from ENT_HTML401 to ENT_HTML5 as we assume all our pages have <!DOCTYPE html>
* removed: ENT_SUBSTITUTE -> wrong characters will be replaced with space
* encodes in UTF-8
* does not double encode
*
* @param mixed $string string to html encode
* @param int $flags [default: ENT_QUOTES | ENT_HTML5]
* @return mixed if string, encoded, else as is (eg null)
*/
public static function htmlent(mixed $string): mixed
public static function htmlent(mixed $string, int $flags = ENT_QUOTES | ENT_HTML5): mixed
{
if (is_string($string)) {
return htmlentities($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
} else {
return $string;
return htmlentities($string, $flags, 'UTF-8', false);
}
return $string;
}
/**
@@ -54,14 +60,10 @@ class Html
*/
public static function checked(array|string $haystack, string $needle, int $type = 0): ?string
{
if (is_array($haystack)) {
if (in_array($needle, $haystack)) {
return $type ? 'checked' : 'selected';
}
} else {
if ($haystack == $needle) {
return $type ? 'checked' : 'selected';
}
if (is_array($haystack) && in_array($needle, $haystack)) {
return $type ? 'checked' : 'selected';
} elseif (!is_array($haystack) && $haystack == $needle) {
return $type ? 'checked' : 'selected';
}
return null;
}

View File

@@ -48,8 +48,26 @@ class Json
return (array)$json;
}
/**
* convert array to json
* Will set empty json {} on false/error
* Error can be read with jsonGetLastError
* Deos not throw errors
*
* @param array<mixed> $data
* @param int $flags json_encode flags as is
* @return string JSON string or '{}' if false
*/
public static function jsonConvertArrayTo(array $data, int $flags = 0): string
{
$json_string = json_encode($data, $flags) ?: '{}';
self::$json_last_error = json_last_error();
return (string)$json_string;
}
/**
* returns human readable string for json errors thrown in jsonConvertToArray
* Source: https://www.php.net/manual/en/function.json-last-error.php
*
* @param bool $return_string [default=false] if set to true
* it will return the message string and not
@@ -80,6 +98,15 @@ class Json
case JSON_ERROR_UTF8:
$json_error_string = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
case JSON_ERROR_RECURSION:
$json_error_string = 'One or more recursive references in the value to be encoded';
break;
case JSON_ERROR_INF_OR_NAN:
$json_error_string = 'One or more NAN or INF values in the value to be encoded';
break;
case JSON_ERROR_UNSUPPORTED_TYPE:
$json_error_string = ' A value of a type that cannot be encoded was given';
break;
case JSON_ERROR_INVALID_PROPERTY_NAME:
$json_error_string = 'A key starting with \u0000 character was in the string';
break;

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

@@ -12,7 +12,7 @@ namespace CoreLibs\Convert;
class MimeAppName
{
/** @var array<string,string> */
private static $mime_apps = [];
private static array $mime_apps = [];
/**
* constructor: init mime list

View File

@@ -15,6 +15,7 @@ class MimeEncode
/**
* wrapper function for mb mime convert
* for correct conversion with long strings
* NOTE: This is only a wrapper for mb_encode_mimeheader to stay compatible
*
* @param string $string string to encode
* @param string $encoding target encoding
@@ -29,38 +30,9 @@ class MimeEncode
$current_internal_encoding = mb_internal_encoding();
// set internal encoding, so the mimeheader encode works correctly
mb_internal_encoding($encoding);
// if a subject, make a work around for the broken mb_mimencode
$pos = 0;
// after 36 single bytes characters,
// if then comes MB, it is broken
// has to 2 x 36 < 74 so the mb_encode_mimeheader
// 74 hardcoded split does not get triggered
$split = 36;
$_string = '';
while ($pos < mb_strlen($string, $encoding)) {
$output = mb_strimwidth($string, $pos, $split, "", $encoding);
$pos += mb_strlen($output, $encoding);
// if the strinlen is 0 here, get out of the loop
if (!mb_strlen($output, $encoding)) {
$pos += mb_strlen($string, $encoding);
}
$_string_encoded = mb_encode_mimeheader($output, $encoding);
// only make linebreaks if we have mime encoded code inside
// the space only belongs in the second line
if ($_string && preg_match("/^=\?/", $_string_encoded)) {
$_string .= $line_break . " ";
} elseif (
// hack for plain text with space at the end
mb_strlen($output, $encoding) == $split &&
mb_substr($output, -1, 1, $encoding) == " "
) {
// if output ends with space, add one more
$_string_encoded .= " ";
}
$_string .= $_string_encoded;
}
// strip out any spaces BEFORE a line break
$string = str_replace(" " . $line_break, $line_break, $_string);
// use the internal convert to mime header
// it works from PHP 8.2 on
$string = mb_encode_mimeheader($string, $encoding, 'B', $line_break);
// before we end, reset internal encoding
mb_internal_encoding($current_internal_encoding);
// return mime encoded string

View File

@@ -35,7 +35,7 @@ class SetVarTypeNull extends Extends\SetVarTypeMain
* @param string|null $default Default override value
* @return string|null Input value as string or default as string/null
*/
public static function makeStr(mixed $val, string $default = null): ?string
public static function makeStr(mixed $val, ?string $default = null): ?string
{
return SetVarTypeMain::makeStrMain($val, $default, true);
}
@@ -60,7 +60,7 @@ class SetVarTypeNull extends Extends\SetVarTypeMain
* @param int|null $default Default override value
* @return int|null Input value as int or default as int/null
*/
public static function makeInt(mixed $val, int $default = null): ?int
public static function makeInt(mixed $val, ?int $default = null): ?int
{
return SetVarTypeMain::makeIntMain($val, $default, true);
}
@@ -84,7 +84,7 @@ class SetVarTypeNull extends Extends\SetVarTypeMain
* @param float|null $default Default override value
* @return float|null Input value as float or default as float/null
*/
public static function makeFloat(mixed $val, float $default = null): ?float
public static function makeFloat(mixed $val, ?float $default = null): ?float
{
return SetVarTypeMain::makeFloatMain($val, $default, true);
}

View File

@@ -118,6 +118,34 @@ class Strings
return $value;
}
}
/**
* Strip any duplicated slahes from a path
* eg: //foo///bar/foo.inc -> /foo/bar/foo.inc
*
* @param string $path Path to strip slashes from
* @return string Clean path, on error returns original path
*/
public static function stripMultiplePathSlashes(string $path): string
{
return preg_replace(
'#/+#',
'/',
$path
) ?? $path;
}
/**
* Remove UTF8 BOM Byte string from line
* Note: this is often found in CSV files exported from Excel at the first row, first element
*
* @param string $text
* @return string
*/
public static function stripUTF8BomBytes(string $text): string
{
return trim($text, pack('H*', 'EFBBBF'));
}
}
// __END__

View File

@@ -14,7 +14,7 @@ namespace CoreLibs\Create;
class Email
{
/** @var array<string> allowed list for encodings that can do KV folding */
private static $encoding_kv_allowed = [
private static array $encoding_kv_allowed = [
'UTF-8',
'EUC-JP',
'SJIS',
@@ -25,7 +25,7 @@ class Email
'JIS-ms',
];
/** @var string normaly this does not need to be changed */
private static $mb_convert_kana_mode = 'KV';
private static string $mb_convert_kana_mode = 'KV';
/**
* create mime encoded email part for to/from emails.
@@ -137,7 +137,7 @@ class Email
* @param bool $kv_folding If set to true and a valid encoding,
* do KV folding
* @param bool $test test flag, default off
* @param \CoreLibs\Debug\Logging|null $log Logging class,
* @param \CoreLibs\Logging\Logging|null $log Logging class,
* only used if test flag is true
* @return int 2 test only, no sent
* 1 for ok,
@@ -156,7 +156,7 @@ class Email
string $encoding = 'UTF-8',
bool $kv_folding = false,
bool $test = false,
?\CoreLibs\Debug\Logging $log = null
?\CoreLibs\Logging\Logging $log = null
): int {
/** @var array<string> */
$to_emails = [];
@@ -259,11 +259,11 @@ class Email
$mail_delivery_status = 2;
}
// log if an log instance exists
if ($log instanceof \CoreLibs\Debug\Logging) {
if ($log instanceof \CoreLibs\Logging\Logging) {
// build debug strings: convert to UTF-8 if not utf-8
$log->debug('SEND EMAIL', 'HEADERS: ' . $log->prAr($headers) . ', '
$log->debug('SEND EMAIL', 'HEADERS: ' . \CoreLibs\Debug\Support::prAr($headers) . ', '
. 'ENCODING: ' . $encoding . ', '
. 'KV FOLDING: ' . $log->prBl($kv_folding) . ', '
. 'KV FOLDING: ' . \CoreLibs\Debug\Support::prBl($kv_folding) . ', '
. 'TO: ' . $to_email . ', '
. 'SUBJECT: ' . $out_subject . ', '
. 'BODY: ' . ($encoding == 'UTF-8' ?

View File

@@ -10,6 +10,7 @@ namespace CoreLibs\Create;
class Hash
{
public const DEFAULT_HASH = 'adler32';
public const STANDARD_HASH_LONG = 'ripemd160';
public const STANDARD_HASH_SHORT = 'adler32';
@@ -58,7 +59,7 @@ class Hash
/**
* replacemend for __crc32b call (alternate)
* defaults to adler 32
* allowed crc32b, adler32, fnv132, fnv1a32, joaat
* allowed: any in hash algos list, default to adler 32
* all that create 8 char long hashes
*
* @param string $string string to hash
@@ -67,15 +68,15 @@ class Hash
*/
public static function __hash(
string $string,
string $hash_type = self::STANDARD_HASH_SHORT
string $hash_type = self::DEFAULT_HASH
): string {
// if not empty, check if in valid list
if (
!in_array(
$hash_type,
['crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat']
)
empty($hash_type) ||
!in_array($hash_type, hash_algos())
) {
$hash_type = 'adler32';
// fallback to default hash type if none set or invalid
$hash_type = self::DEFAULT_HASH;
}
return hash($hash_type, $string);
}

View File

@@ -12,13 +12,13 @@ class RandomKey
{
// key generation
/** @var string */
private static $key_range = '';
private static string $key_range = '';
/** @var int */
private static $one_key_length;
private static int $one_key_length;
/** @var int */
private static $key_length = 4; // default key length
private static int $key_length = 4; // default key length
/** @var int */
private static $max_key_length = 256; // max allowed length
private static int $max_key_length = 256; // max allowed length
/**
* if launched as class, init random key data first
@@ -100,7 +100,9 @@ class RandomKey
public static function randomKeyGen(int $key_length = -1): string
{
// init random key strings if not set
if (!is_numeric(self::$one_key_length)) {
if (
!isset(self::$one_key_length)
) {
self::initRandomKeyData();
}
$use_key_length = 0;

View File

@@ -15,20 +15,111 @@ namespace CoreLibs\Create;
class Session
{
/** @var string list for errors */
private $session_intern_error_str = '';
/** @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);
}
/**
@@ -39,49 +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
/**
* Return set error string, empty if none set
* Error strings are only set in the startSession method
* auto rotate session id
*
* @return string Last error string
* @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
*/
public function getErrorStr(): string
private function sessionRegenerateSessionId()
{
return $this->session_intern_error_str;
// 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
*
@@ -108,64 +250,135 @@ 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()) {
$this->session_intern_error_str = '[SESSION] No sessions in php cli';
return false;
throw new \RuntimeException('[SESSION] No sessions in php cli', 1);
}
// if session are OFF
if ($this->getSessionStatus() === PHP_SESSION_DISABLED) {
$this->session_intern_error_str = '[SESSION] Sessions are disabled';
return false;
throw new \RuntimeException('[SESSION] Sessions are disabled', 2);
}
// 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)) {
$this->session_intern_error_str = '[SESSION] Invalid session name: ' . $session_name;
return false;
}
$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()) {
$this->session_intern_error_str = '[SESSION] Failed to activate session';
return false;
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())) {
$this->session_intern_error_str = '[SESSION] getSessionId did not return a session id';
}
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;
}
/**
@@ -193,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
@@ -206,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')
@@ -236,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]);
}
@@ -305,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

@@ -1,5 +1,12 @@
<?php
/**
* Create uniqIds
*
* If convert ID to hash:
* https://github.com/vinkla/hashids
*/
declare(strict_types=1);
namespace CoreLibs\Create;
@@ -7,10 +14,39 @@ namespace CoreLibs\Create;
class Uids
{
// what to use as a default hash if non ise set and no DEFAULT_HASH is defined
public const DEFAULT_HASH = 'sha256';
/** @var int */
public const DEFAULT_UNNIQ_ID_LENGTH = 64;
/** @var string */
public const STANDARD_HASH_LONG = 'ripemd160';
/** @var string */
public const STANDARD_HASH_SHORT = 'adler32';
/**
* Create unique id, lower length is for
*
* @param int $length Length for uniq id, min is 4 characters
* Uneven lengths will return lower bound (9 -> 8)
* @param bool $force_length [default=false] if set to true and we have
* uneven length, then we shorten to this length
* @return string Uniq id
*/
private static function uniqIdL(int $length = 64, bool $force_length = false): string
{
$uniqid_length = ($length < 4) ? 4 : $length;
if ($force_length) {
$uniqid_length++;
}
/** @var int<1,max> make sure that internal this is correct */
$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) {
return $uniqid;
}
return substr($uniqid, 0, $length);
}
/**
* creates psuedo random uuid v4
* Code take from class here:
@@ -20,67 +56,74 @@ 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);
// 0-1: 32 bits for "time_low"
// 2: 16 bits for "time_mid"
// 3: 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
// 4: 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
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
// 5-7: 48 bits for "node"
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* TODO: make this a proper uniq ID creation
* add uuidv4 subcall to the uuid function too
* creates a uniq id
* regex validate uuid v4
*
* @param string $type uniq id type, currently md5 or sha256 allowed
* if not set will use DEFAULT_HASH if set
* @return string uniq id
* @param string $uuidv4
* @return bool
*/
public static function uniqId(string $type = ''): string
public static function validateUuuidv4(string $uuidv4): bool
{
$uniq_id = '';
switch ($type) {
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
*
* @param int|string $length Either length in int, or fallback type for length
* for string type md5 (32), sha256 (64)
* STANDARD_HASH_LONG: ripemd160 (40)
* STANDARD_HASH_SHORT: adler32 (8)
* It is recommended to use the integer
* @param bool $force_length [default=false] if set to true and we have
* uneven length, then we shorten to this length
* @return string Uniq id
*/
public static function uniqId(
int|string $length = self::DEFAULT_UNNIQ_ID_LENGTH,
bool $force_length = false
): string {
if (is_int($length)) {
return self::uniqIdL($length, $force_length);
}
switch ($length) {
case 'md5':
$uniq_id = md5(uniqid((string)rand(), true));
$length = 32;
break;
case self::DEFAULT_HASH:
$uniq_id = hash(self::DEFAULT_HASH, uniqid((string)rand(), true));
case 'sha256':
$length = 64;
break;
case self::STANDARD_HASH_LONG:
$uniq_id = hash(self::STANDARD_HASH_LONG, uniqid((string)rand(), true));
$length = 40;
break;
case self::STANDARD_HASH_SHORT:
$uniq_id = hash(self::STANDARD_HASH_SHORT, uniqid((string)rand(), true));
$length = 8;
break;
default:
// if not empty, check if in valid list
if (
!empty($type) &&
in_array($type, hash_algos())
) {
$hash = $type;
} else {
// fallback to default hash type if none set or invalid
$hash = self::DEFAULT_HASH;
}
$uniq_id = hash($hash, uniqid((string)rand(), true));
$length = 64;
break;
}
return $uniq_id;
return self::uniqIdL($length);
}
/**

View File

@@ -39,56 +39,56 @@ class ArrayIO extends \CoreLibs\DB\IO
{
// main calss variables
/** @var array<mixed> */
public $table_array; // the array from the table to work on
private array $table_array; // the array from the table to work on
/** @var string */
public $table_name; // the table_name
private string $table_name; // the table_name
/** @var string */
public $pk_name = ''; // the primary key from this table
private string $pk_name = ''; // the primary key from this table
/** @var int|string|null */
public $pk_id; // the PK id
private int|string|null $pk_id; // the PK id
// security values
/** @var int base acl for current page */
private $base_acl_level = 0;
private int $base_acl_level = 0;
/**
* constructor for the array io class, set the
* primary key name automatically (from array)
*
* @param array<mixed> $db_config db connection config
* phpcs:ignore
* @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string,db_debug_replace_placeholder?:bool} $db_config db connection config
* @param array<mixed> $table_array table array config
* @param string $table_name table name string
* @param \CoreLibs\Debug\Logging $log Logging class
* @param \CoreLibs\Logging\Logging $log Logging class
* @param int $base_acl_level Set base acl level, if needed
* @param int $acl_admin Flag if this is an admin ACL access level
* @throws \RuntimeException Missing table array or table name entry
*/
public function __construct(
array $db_config,
array $table_array,
string $table_name,
\CoreLibs\Debug\Logging $log,
\CoreLibs\Logging\Logging $log,
int $base_acl_level = 0,
int $acl_admin = 0
) {
// instance db_io class
parent::__construct($db_config, $log);
// more error vars for this class
$this->error_string['1999'] = 'No table array or table name set';
$this->error_string['1998'] = 'No table name set';
$this->error_string['1999'] = 'No table array set';
$this->error_string['1021'] = 'No Primary Key given';
$this->error_string['1022'] = 'Could not run Array Query';
$this->table_array = $table_array;
$this->table_name = $table_name;
// error abort if no table array or no table name
if (empty($table_array) || empty($table_name)) {
$this->__dbError(1999, false, 'MAJOR ERROR: Core settings missing');
}
$this->setTableArray($table_array);
$this->setTableName($table_name);
// set primary key for given table_array
foreach ($this->table_array as $key => $value) {
if (!empty($value['pk'])) {
$this->pk_name = $key;
if (empty($value['pk'])) {
continue;
}
$this->setPkName($key);
break;
}
$this->dbArrayIOSetAcl($base_acl_level, $acl_admin);
}
@@ -101,6 +101,144 @@ class ArrayIO extends \CoreLibs\DB\IO
parent::__destruct();
}
/**
* Set the overall table array
*
* @param array<mixed> $table_array
* @return void
* @throws \RuntimeException 1999 for empty table array
*/
public function setTableArray(array $table_array): void
{
$this->table_array = $table_array;
if (empty($this->table_array)) {
$this->__dbError(1999, false, 'MAJOR ERROR: Core settings missing: table_arrry');
throw new \RuntimeException('MAJOR ERROR: Core settings missing: table_array', 1999);
}
}
/**
* return full table array, or [] if empty
* of reset is set to true, will reset array first
*
* @param bool $reset [=false] run a reset before returning
* @return array<mixed>
*/
public function getTableArray(bool $reset = false): array
{
if (!$reset) {
return $this->table_array ?? [];
}
$table_array = $this->table_array ?? [];
reset($table_array);
return $table_array;
}
/**
* get a table array entry under the key with element pos
*
* @param string $key
* @param string $pos
* @return mixed
*/
public function getTableArrayEntry(string $key, string $pos): mixed
{
return $this->table_array[$key][$pos] ?? null;
}
/**
* set a new value at key with pos
*
* @param mixed $value
* @param string $key
* @param string $pos
* @return void
*/
public function setTableArrayEntry(mixed $value, string $key, string $pos): void
{
$this->table_array[$key][$pos] = $value;
}
/**
* unset entry at key with pos
*
* @param string $key
* @param string $pos
* @return void
*/
public function unsetTableArrayEntry(string $key, string $pos): void
{
unset($this->table_array[$key][$pos]);
}
/**
* Set table name
*
* @param string $table_name
* @return void
* @throws \RuntimeException 1998 for empty table name
*/
public function setTableName(string $table_name): void
{
$this->table_name = $table_name;
if (empty($this->table_name)) {
$this->__dbError(1998, false, 'MAJOR ERROR: Core settings missing: table_name');
throw new \RuntimeException('MAJOR ERROR: Core settings missing: table_name', 1998);
}
}
/**
* Return table name or empty string if not net
*
* @return string
*/
public function getTableName(): string
{
return $this->table_name ?? '';
}
/**
* Set primary key name
*
* @param string $pk_name
* @return void
*/
public function setPkName(string $pk_name): void
{
$this->pk_name = $pk_name;
}
/**
* get primary key name
*
* @return string
*/
public function getPkName(): string
{
return $this->pk_name;
}
/**
* set primary key id, can be null for not yet set
*
* @param int|string|null $pk_id
* @return void
*/
public function setPkId(int|string|null $pk_id): void
{
$this->pk_id = $pk_id;
}
/**
* return primary key id, or null if not set
*
* @return int|string|null
*/
public function getPkId(): int|string|null
{
return $this->pk_id ?? null;
}
/**
* set the base acl level and admin acl flag
* This is needed for table array ACL checks
@@ -195,8 +333,8 @@ class ArrayIO extends \CoreLibs\DB\IO
public function dbCheckPkSet(): bool
{
// if pk_id is set, overrule ...
if ($this->pk_id) {
$this->table_array[$this->pk_name]['value'] = $this->pk_id;
if (!empty($this->getPkId())) {
$this->table_array[$this->pk_name]['value'] = $this->getPkId();
}
// if not set ... produce error
if (!$this->table_array[$this->pk_name]['value']) {
@@ -236,14 +374,14 @@ 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()) {
return $this->table_array;
}
if ($acl_limit === true && $this->base_acl_level < 100) {
$this->log->debug('DB DELETE ERROR', 'ACL Limit on, Delete, '
$this->log->error('DB DELETE ERROR: ACL Limit on, Delete, '
. 'but base ACL level of 100 not met: ' . $this->base_acl_level);
return $this->table_array;
}
@@ -284,7 +422,7 @@ class ArrayIO extends \CoreLibs\DB\IO
$q .= ' AND ' . $q_where;
}
// if 0, error
$this->pk_id = null;
$this->setPkId(null);
if (!$this->dbExec($q)) {
$this->__dbError(1022);
}
@@ -302,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()) {
@@ -371,7 +509,7 @@ class ArrayIO extends \CoreLibs\DB\IO
}
}
// possible dbFetchArray errors ...
$this->pk_id = $this->table_array[$this->pk_name]['value'];
$this->setPkId($this->table_array[$this->pk_name]['value']);
} else {
$this->__dbError(1022);
}
@@ -394,10 +532,6 @@ class ArrayIO extends \CoreLibs\DB\IO
if (count($table_array)) {
$this->table_array = $table_array;
}
// PK ID check
// if ($this->pk_id && !$this->table_array[$this->pk_name]["value"]) {
// $this->table_array[$this->pk_name]["value"]=$this->pk_id;
// }
// checken ob PKs gesetzt, wenn alle -> update, wenn keiner -> insert, wenn ein paar -> ERROR!
if (!$this->table_array[$this->pk_name]['value']) {
$insert = 1;
@@ -406,7 +540,7 @@ class ArrayIO extends \CoreLibs\DB\IO
}
// early abort for new write with not enough ACL level
if ($insert && $acl_limit === true && $this->base_acl_level < 100) {
$this->log->debug('DB WRITE ERROR', 'ACL Limit on, Insert, '
$this->log->error('DB WRITE ERROR: ACL Limit on, Insert, '
. 'but base ACL level of 100 not met: ' . $this->base_acl_level);
return $this->table_array;
}
@@ -579,7 +713,7 @@ class ArrayIO extends \CoreLibs\DB\IO
} // while ...
if (empty($q_data)) {
$this->log->debug('DB WRITE ERROR', 'No data to write, possible through ACL');
$this->log->error('DB WRITE ERROR: No data to write, possible through ACL');
return $this->table_array;
}
@@ -587,7 +721,7 @@ class ArrayIO extends \CoreLibs\DB\IO
// get it at the end, cause now we can be more sure of no double IDs, etc
reset($this->table_array);
// create select part & addition FK part
foreach ($this->table_array as $column => $data_array) {
foreach (array_keys($this->table_array) as $column) {
// check FK ...
if (
isset($this->table_array[$column]['fk']) &&
@@ -621,16 +755,11 @@ class ArrayIO extends \CoreLibs\DB\IO
$q .= ' AND ' . $q_where;
}
// set pk_id ... if it has changed or so
$this->pk_id = $this->table_array[$this->pk_name]['value'];
$this->setPkId($this->table_array[$this->pk_name]['value']);
} else {
$q = 'INSERT INTO ' . $this->table_name . ' ';
$q .= '(' . $q_vars . ') ';
$q .= 'VALUES (' . $q_data . ')';
// write primary key too
// if ($q_data)
// $q .= ", ";
// $q .= $this->pk_name." = ".$this->table_array[$this->pk_name]['value']." ";
// $this->pk_id = $this->table_array[$this->pk_name]['value'];
}
// return success or not
if (!$this->dbExec($q)) {
@@ -643,7 +772,7 @@ class ArrayIO extends \CoreLibs\DB\IO
$insert_id = 0;
}
$this->table_array[$this->pk_name]['value'] = $insert_id;
$this->pk_id = $insert_id;
$this->setPkId($insert_id);
}
// return the table if needed
return $this->table_array;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/6/9
* DESCRIPTION:
* DB Options for convert type
*
* off: no conversion (all string)
* on: int/bool only
* json: json/jsonb to array
* numeric: any numeric or float to float
* bytes: decode bytea to string
*/
declare(strict_types=1);
namespace CoreLibs\DB\Options;
enum Convert: int
{
/** do not convert */
case off = 0;
/** convert only int/bool */
case on = 1;
/** also convert json to php array */
case json = 2;
/** also convert any float/real/numeric to php float */
case numeric = 4;
/** also decode bytea string to php string */
case bytea = 8;
/**
* get internal name from string value
*
* @param non-empty-string $name
* @return self
*/
public static function fromName(string $name): self
{
return match ($name) {
'Off', 'off', 'OFF', 'convert_off', 'CONVERT_OFF' => self::off,
'On', 'on', 'ON', 'convert_on', 'CONVERT_ON' => self::on,
'Json', 'json', 'JSON', 'convert_json', 'CONVERT_JSON' => self::json,
'Numeric', 'numeric', 'NUMERIC', 'convert_numeric', 'CONVERT_NUMERIC' => self::numeric,
'Bytea', 'bytea', 'BYTEA', 'convert_bytea', 'CONVERT_BYTEA' => self::bytea,
default => self::off,
};
}
/**
* Get internal name from int value
*
* @param int $value
* @return self
*/
public static function fromValue(int $value): self
{
return self::from($value);
}
}
// __END__

View File

@@ -209,10 +209,17 @@ interface SqlFunctions
/**
* Undocumented function
*
* @param \PgSql\Result|false $cursor
* @return string
* @return array{0:string,1:string}
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): string;
public function __dbPrintLastError(): array;
/**
* Undocumented function
*
* @param \PgSql\Result|false $cursor
* @return array{0:string,1:string}
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): array;
/**
* Undocumented function
@@ -278,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
*
@@ -285,6 +308,13 @@ interface SqlFunctions
*/
public function __dbVersion(): string;
/**
* Undocumented function
*
* @return int
*/
public function __dbVersionNumeric(): int;
/**
* Undocumented function
*
@@ -299,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
*
@@ -336,6 +374,14 @@ interface SqlFunctions
* @return string
*/
public function __dbGetEncoding(): string;
/**
* Undocumented function
*
* @param string $query
* @return int
*/
public function __dbCountQueryParams(string $query): int;
}
// __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 */
@@ -59,9 +61,9 @@ namespace CoreLibs\DB\SQL;
class PgSQL implements Interface\SqlFunctions
{
/** @var string */
private $last_error_query;
private string $last_error_query;
/** @var \PgSql\Connection|false */
private $dbh;
private \PgSql\Connection|false $dbh = false;
/**
* queries last error query and returns true or false if error was set
@@ -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)) {
@@ -532,18 +540,37 @@ class PgSQL implements Interface\SqlFunctions
return $this->dbh;
}
/**
* Returns last error for active cursor
*
* @return array{0:string,1:string} prefix, error string
*/
public function __dbPrintLastError(): array
{
if (is_bool($this->dbh)) {
return ['', ''];
}
if (!empty($error_message = pg_last_error($this->dbh))) {
return [
'-PostgreSQL-Error-Last-',
$error_message
];
}
return ['', ''];
}
/**
* reads the last error for this cursor and returns
* html formatted string with error name
*
* @param \PgSql\Result|false $cursor cursor
* or null
* @return string error string
* or null
* @return array{0:string,1:string} prefix, error string
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): string
public function __dbPrintError(\PgSql\Result|false $cursor = false): array
{
if (is_bool($this->dbh)) {
return '';
return ['', ''];
}
// run the query again for the error result here
if ((is_bool($cursor)) && $this->last_error_query) {
@@ -552,10 +579,12 @@ class PgSQL implements Interface\SqlFunctions
$cursor = pg_get_result($this->dbh);
}
if ($cursor && $error_str = pg_result_error($cursor)) {
return '-PostgreSQL-Error- '
. $error_str;
return [
'-PostgreSQL-Error-',
$error_str
];
} else {
return '';
return ['', ''];
}
}
@@ -872,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;
@@ -945,6 +976,34 @@ class PgSQL implements Interface\SqlFunctions
{
return $this->__dbShow('client_encoding');
}
/**
* Count placeholder queries. $ only
*
* @param string $query
* @return int
*/
public function __dbCountQueryParams(string $query): int
{
$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_PLACEHOLDERS,
$query,
$matches
);
return count(array_unique(array_filter($matches[3])));
}
}
// __END__

View File

@@ -0,0 +1,347 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/10/10
* DESCRIPTION:
* Convert placeholders in query from PDO style ? or :named to \PG style $number
* pr the other way around
*/
declare(strict_types=1);
namespace CoreLibs\DB\Support;
class ConvertPlaceholder
{
// NOTE for missing: range */+ are not iplemented in the regex below, but - is for now
// NOTE some combinations are allowed, but the query will fail before this
/** @var string split regex, entries before $ group */
private const PATTERN_QUERY_SPLIT =
'\?\?|' // UNKNOWN: double ??, is this to avoid something?
. '[\(,]|' // for ',' and '(' mostly in INSERT or ANY()
. '[<>=]|' // general set for <, >, = in any query with any combination
. '\^@|' // text search for start from text with ^@
. '\|\||' // concats two elements
. '&&|' // array overlap
. '\-\|\-|' // range overlap for array
. '[^-]-{1}|' // single -, used in JSON too
. '->|->>|#>|#>>|@>|<@|@@|@\?|\?{1}|\?\||\?&|#-|' // JSON searches, Array searchs, etc
. 'THEN|ELSE' // command parts (CASE)
;
/** @var string the main regex including the pattern query split */
private const PATTERN_ELEMENT = '(?:\'.*?\')?\s*(?:' . self::PATTERN_QUERY_SPLIT . ')\s*';
/** @var string comment regex
* anything that starts with -- and ends with a line break but any character that is not line break inbetween */
private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)*\s*';
/** @var string parts to ignore in the SQL */
private const PATTERN_IGNORE =
// digit -> ignore
'\d+|'
// other string -> ignore
. '(?:\'.*?\')|';
/** @var string named parameters */
private const PATTERN_NAMED = '(:\w+)';
/** @var string question mark parameters */
private const PATTERN_QUESTION_MARK = '(?:(?:\?\?)?\s*(\?{1}))';
/** @var string numbered parameters */
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_ELEMENT . ')'
. self::PATTERN_COMMENT
. '('
. self::PATTERN_IGNORE
. self::PATTERN_NAMED
. ')'
. '/s';
/** @var string replace regex for question mark (?) entries */
public const REGEX_REPLACE_QUESTION_MARK = '/'
. '(' . self::PATTERN_ELEMENT . ')'
. self::PATTERN_COMMENT
. '('
. self::PATTERN_IGNORE
. self::PATTERN_QUESTION_MARK
. ')'
. '/s';
/** @var string replace regex for numbered ($n) entries */
public const REGEX_REPLACE_NUMBERED = '/'
. '(' . self::PATTERN_ELEMENT . ')'
. self::PATTERN_COMMENT
. '('
. self::PATTERN_IGNORE
. self::PATTERN_NUMBERED
. ')'
. '/s';
/** @var string the main lookup query for all placeholders */
public const REGEX_LOOKUP_PLACEHOLDERS = '/'
// prefix string part, must match towards
// seperator for ( = , ? - [and json/jsonb in pg doc section 9.15]
. self::PATTERN_ELEMENT
. self::PATTERN_COMMENT
// match for replace part
. '(?:'
// ignore parts
. self::PATTERN_IGNORE
// :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';
/**
* Convert PDO type query with placeholders to \PG style and vica versa
* For PDO to: ? and :named
* For \PG to: $number
*
* If the query has a mix of ?, :named or $numbrer the \OutOfRangeException exception
* will be thrown
*
* If the convert_to is either pg or pdo, nothing will be changed
*
* 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 string $convert_to Either pdo or pg, will be converted to lower case for check
* @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,
string $convert_to = 'pg'
): array {
$convert_to = strtolower($convert_to);
$matches = [];
// matches:
// 1: :named
// 2: ? question mark
// 3: $n numbered
$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]);
/** @var array<string> 2: open ? */
$qmark_matches = array_filter($matches[2]);
/** @var array<string> 3: $n matches */
$numbered_matches = array_filter($matches[3]);
// count matches
$count_named = count(array_unique($named_matches));
$count_qmark = count($qmark_matches);
$count_numbered = count(array_unique($numbered_matches));
// throw exception if mixed found
if (
($count_named && $count_qmark) ||
($count_named && $count_numbered) ||
($count_qmark && $count_numbered)
) {
throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200);
}
// // 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 = [];
// 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':
// 0: full
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part :named
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_NAMED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = '$' . $pos;
// skip params setup if param list is empty
if (!$empty_params) {
$params_new[] = $params[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params list',
210
);
}
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
211
)
);
},
$converted_placeholders['original']['query']
);
break;
case 'question_mark':
if (!$empty_params) {
// order and data stays the same
$params_new = $params ?? [];
}
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part ?
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_QUESTION_MARK,
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
);
},
$converted_placeholders['original']['query']
);
break;
case 'numbered':
// 0: full
// 1: pre part
// 2: keep part UNLESS '3' is set
// 3: replace part $numbered
$pos = 0;
$query_new = preg_replace_callback(
self::REGEX_REPLACE_NUMBERED,
function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) {
// only count up if $match[3] is not yet in lookup table
if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) {
$pos++;
$params_lookup[$matches[3]] = ':' . $pos . '_named';
// skip params setup if param list is empty
if (!$empty_params) {
$params_new[] = $params[($pos - 1)] ??
throw new \RuntimeException(
'Cannot lookup ' . ($pos - 1) . ' in params list',
220
);
}
}
// add the connectors back (1), and the data sets only if no replacement will be done
return $matches[1] . (
empty($matches[3]) ?
$matches[2] :
$params_lookup[$matches[3]] ??
throw new \RuntimeException(
'Cannot lookup ' . $matches[3] . ' in params lookup list',
221
)
);
},
$converted_placeholders['original']['query']
);
break;
}
return [
'params_lookup' => $params_lookup,
'query' => $query_new ?? '',
'params' => $params_new,
];
}
}
// __END__

View File

@@ -12,9 +12,9 @@ namespace CoreLibs\Debug;
class FileWriter
{
/** @var string */
private static $debug_filename = 'debug_file.log'; // where to write output
private static string $debug_filename = 'debug_file.log'; // where to write output
/** @var string */
private static $debug_folder;
private static string $debug_folder;
/**
* Set a debug log folder, if not set BASE+LOG folders are set
@@ -63,7 +63,7 @@ class FileWriter
*
* @param string $string string to write to the file
* @param boolean $enter default true, if set adds a linebreak \n at the end
* @return bool True for log written, false for not wirrten
* @return bool True for log written, false for not written
*/
public static function fdebug(string $string, bool $enter = true): bool
{
@@ -75,9 +75,9 @@ class FileWriter
empty(self::$debug_folder) &&
defined('BASE') && defined('LOG')
) {
/** @deprecated Do not use this anymore, define path with fsetFolder */
/** @deprecated Do not use this anymore, define path with festFolder */
trigger_error(
'fsetFolder must be set first. Setting via LOG_FILE_ID and LOg constants is deprecated',
'fsetFolder must be set first. Setting via LOG_FILE_ID and LOG constants is deprecated',
E_USER_DEPRECATED
);
self::$debug_folder = BASE . LOG;

View File

@@ -1,895 +1,26 @@
<?php
/*
* Debug support functions
*
* These are if there is any debug to print out at all at the end
 * debug_output_all - general yes no
 * It's recommended to use the method "debug_for" to turn on of the array vars
 * debug_output - turn on for one level (Array)
 * debug_output_not - turn off for one level (array)
 *
 * Print out the debug at thend of the html
 * echo_output_all
 * echo_output
 * echo_output_not
 *
 * Write debug to file
 * print_output_all
 * print_output
 * print_output_not
* This is a wrapper placeholder for
* \CoreLibs\Logging\Logger
*/
declare(strict_types=1);
namespace CoreLibs\Debug;
use CoreLibs\Debug\Support;
use CoreLibs\Create\Uids;
use CoreLibs\Get\System;
use CoreLibs\Convert\Html;
class Logging
/**
* @deprecated Use \CoreLibs\Logger\Logging
*/
class Logging extends \CoreLibs\Logging\Logging
{
// options
/** @var array<mixed> */
private $options = [];
// page and host name
/** @var string */
private $page_name;
/** @var string */
private $host_name;
/** @var int */
private $host_port;
// internal error reporting vars
/** @var array<mixed> */
private $error_msg = []; // the "connection" to the outside errors
// debug output prefix
/** @var string */
private $error_msg_prefix = ''; // prefix to the error string (the class name)
// debug flags
/** @var array<mixed> */
private $debug_output = []; // if this is true, show debug on desconstructor
/** @var array<mixed> */
private $debug_output_not = [];
/** @var bool */
private $debug_output_all = false;
/** @var array<mixed> */
private $echo_output = []; // errors: echo out, default is 1
/** @var array<mixed> */
private $echo_output_not = [];
/** @var bool */
private $echo_output_all = false;
/** @var array<mixed> */
private $print_output = []; // errors: print to file, default is 0
/** @var array<mixed> */
private $print_output_not = [];
/** @var bool */
private $print_output_all = false;
// debug flags/settings
/** @var string */
private $running_uid = ''; // unique ID set on class init and used in logging as prefix
// log file name
/** @var string */
private $log_folder = '';
/** @var string */
private $log_file_name_ext = 'log'; // use this for date rotate
/** @var string */
private $log_file_name = '';
/** @var int */
private $log_max_filesize = 0; // set in kilobytes
/** @var string */
private $log_print_file = 'error_msg##LOGID####LEVEL####CLASS####PAGENAME####DATE##';
/** @var string */
private $log_file_unique_id; // a unique ID set only once for call derived from this class
/** @var string */
private $log_file_date = ''; // Y-m-d file in file name
/** @var bool */
private $log_print_file_date = true; // if set add Y-m-d and do automatic daily rotation
/** @var string */
private $log_file_id = ''; // a alphanumeric name that has to be set as global definition
/** @var bool */
private $log_per_level = false; // set, it will split per level (first parameter in debug call)
/** @var bool */
private $log_per_class = false; // set, will split log per class
/** @var bool */
private $log_per_page = false; // set, will split log per called file
/** @var bool */
private $log_per_run = false; // create a new log file per run (time stamp + unique ID)
// script running time
/** @var float */
private $script_starttime;
/**
* Init logger
*
* global vars that can be used
* - BASE
* - LOG
* - LOG_FILE_ID
* options array layout
* - log_folder:
* - print_file_date:
* - file_id:
* - unique_id:
* - log_per_level:
* - log_per_class:
* - log_per_page:
* - log_per_run:
* - debug_all:
* - echo_all:
* - print_all:
* - debug (array):
* - echo (array):
* - print (array):
* - debug_not (array):
* - echo_not (array):
* - print_not (array):
*
* @param array<mixed> $options Array with settings options
*/
public function __construct(array $options = [])
{
// copy the options over
$this->options = $options;
// set log folder from options
$this->log_folder = $this->options['log_folder'] ?? '';
// legacy flow, check must set constants
if (empty($this->log_folder) && defined('BASE') && defined('LOG')) {
/** @deprecated Do not use this anymore, define path on class load */
trigger_error(
'options: log_folder must be set. Setting via BASE and LOG constants is deprecated',
E_USER_DEPRECATED
);
// make sure this is writeable, else skip
$this->log_folder = BASE . LOG;
}
// fallback + notice
if (empty($this->log_folder)) {
/* trigger_error(
'options or constant not set or folder not writable. fallback to: ' . getcwd(),
E_USER_NOTICE
); */
$this->log_folder = getcwd() . DIRECTORY_SEPARATOR;
}
// if folder is not writeable, abort
if (!is_writeable($this->log_folder)) {
trigger_error(
'Folder: ' . $this->log_folder . ' is not writeable for logging',
E_USER_ERROR
);
}
// check if log_folder has a trailing /
if (substr($this->log_folder, -1, 1) != DIRECTORY_SEPARATOR) {
$this->log_folder .= DIRECTORY_SEPARATOR;
}
// running time start for script
$this->script_starttime = microtime(true);
// set per run UID for logging
$this->running_uid = Uids::uniqIdShort();
// set the page name
$this->page_name = System::getPageName();
// set host name
list($this->host_name , $this->host_port) = System::getHostName();
// add port to host name if not port 80
if ($this->host_port != 80) {
$this->host_name .= ':' . $this->host_port;
}
// can be overridden with basicSetLogFileId later
if (!empty($this->options['file_id'])) {
$this->setLogId($this->options['file_id']);
} elseif (!empty($GLOBALS['LOG_FILE_ID'])) {
/** @deprecated Do not use this anymore, define file_id on class load */
trigger_error(
'options: file_id must be set. Setting via LOG_FILE_ID global variable is deprecated',
E_USER_DEPRECATED
);
// legacy flow, should be removed and only set via options
$this->setLogId($GLOBALS['LOG_FILE_ID']);
// TODO trigger deprecation error
// trigger_error(
// 'Debug\Logging: Do not use globals LOG_FILE_ID to set log id for Logging',
// E_USER_DEPRECATED
// );
} elseif (defined('LOG_FILE_ID')) {
/** @deprecated Do not use this anymore, define file_id on class load */
trigger_error(
'options: file_id must be set. Setting via LOG_FILE_ID constant is deprecated',
E_USER_DEPRECATED
);
// legacy flow, should be removed and only set via options
$this->setLogId(LOG_FILE_ID);
// trigger deprecation error
// trigger_error(
// 'Debug\Logging: Do not use constant LOG_FILE_ID to set log id for Logging',
// E_USER_DEPRECATED
// );
}
// init the log levels
$this->initLogLevels();
}
// *** PRIVATE ***
/**
* init the basic log levels based on global set variables
*
* @return void
*/
private function initLogLevels(): void
{
// if given via parameters, only for all
// globals overrule given settings, for one (array), eg $ECHO['db'] = 1;
foreach (['debug', 'echo', 'print'] as $type) {
// include or exclude (off) from output
foreach (['on', 'off'] as $flag) {
$in_type = $type;
if ($flag == 'off') {
$in_type .= '_not';
}
$up_type = strtoupper($in_type);
if (
isset($this->options[$in_type]) &&
is_array($this->options[$in_type])
) {
$this->setLogLevel($type, $flag, $this->options[$in_type]);
} elseif (
isset($GLOBALS[$up_type]) &&
is_array($GLOBALS[$up_type])
) {
// TODO trigger deprecation error
$this->setLogLevel($type, $flag, $GLOBALS[$up_type]);
}
}
}
// TODO remove all $GLOBALS call and only use options
// all overrule
$this->setLogLevelAll(
'debug',
$this->options['debug_all'] ??
// for user login, should be handled outside like globals
$_SESSION['DEBUG_ALL'] ?? // DEPRECATED
$GLOBALS['DEBUG_ALL'] ?? // DEPRECATED
false
);
$this->setLogLevelAll(
'print',
$this->options['print_all'] ??
// for user login, should be handled outside like globals
$_SESSION['DEBUG_ALL'] ?? // DEPRECATED
$GLOBALS['PRINT_ALL'] ?? // DEPRECATED
false
);
$this->setLogLevelAll(
'echo',
$this->options['echo_all'] ??
$GLOBALS['ECHO_ALL'] ?? // DEPRECATED
false
);
// GLOBAL rules for log writing
// add file date is default on
$this->setGetLogPrintFileDate(
$this->options['print_file_date'] ??
$GLOBALS['LOG_PRINT_FILE_DATE'] ?? // DEPRECATED
true
);
// all other logging file name flags are off
$this->setLogPer(
'level',
$this->options['per_level'] ??
$GLOBALS['LOG_PER_LEVEL'] ?? // DEPRECATED
false
);
$this->setLogPer(
'class',
$this->options['per_class'] ??
$GLOBALS['LOG_PER_CLASS'] ?? // DEPRECATED
false
);
$this->setLogPer(
'page',
$this->options['per_page'] ??
$GLOBALS['LOG_PER_PAGE'] ?? // DEPRECATED
false
);
$this->setLogPer(
'run',
$this->options['per_run'] ??
$GLOBALS['LOG_PER_RUN'] ?? // DEPRECATED
false
);
// set log per date
if ($this->setGetLogPrintFileDate()) {
$this->log_file_date = date('Y-m-d');
}
// set per run ID
if ($this->log_per_run) {
$this->setLogUniqueId();
}
}
/**
* checks if we have a need to work on certain debug output
* Needs debug/echo/print ad target for which of the debug flag groups we check
* also needs level string to check in the per level output flag check.
* In case we have invalid target it will return false
*
* @param string $target target group to check debug/echo/print
* @param string $level level to check in detailed level flag
* @return bool true on access allowed or false on no access
*/
private function doDebugTrigger(string $target, string $level): bool
{
$access = false;
// check if we do debug, echo or print
if (
(
$this->getLogLevel($target, 'on', $level) ||
$this->getLogLevelAll($target)
) &&
!$this->getLogLevel($target, 'off', $level)
) {
$access = true;
}
return $access;
}
/**
* writes error msg data to file for current level
*
* @param string $level the level to write
* @param string $error_string error string to write
* @return bool True if message written, FAlse if not
*/
private function writeErrorMsg(string $level, string $error_string): bool
{
// only write if write is requested
if (
!($this->doDebugTrigger('debug', $level) &&
$this->doDebugTrigger('print', $level))
) {
return false;
}
// init base file path
$fn = $this->log_folder . $this->log_print_file . '.' . $this->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;
} else {
$rpl_string = '';
}
$fn = str_replace('##LOGID##', $rpl_string, $fn); // log id (like a log file prefix)
if ($this->log_per_run) {
$rpl_string = '_' . $this->log_file_unique_id; // add 8 char unique string
} elseif ($this->setGetLogPrintFileDate()) {
$rpl_string = '_' . $this->log_file_date; // add date to file
} else {
$rpl_string = '';
}
$fn = str_replace('##DATE##', $rpl_string, $fn); // create output filename
// write per level
$rpl_string = !$this->log_per_level ? '' :
// normalize level, replace all non alphanumeric characters with -
'_' . (
// if return is only - then set error string
preg_match(
"/^-+$/",
$level_string = preg_replace("/[^A-Za-z0-9-_]/", '-', $level) ?? ''
) ?
'INVALID-LEVEL-STRING' :
$level_string
);
$fn = str_replace('##LEVEL##', $rpl_string, $fn); // create output filename
// set per class, but don't use get_class as we will only get self
$rpl_string = !$this->log_per_class ? '' : '_'
// set sub class settings
. str_replace('\\', '-', Support::getCallerClass());
$fn = str_replace('##CLASS##', $rpl_string, $fn); // create output filename
// if request to write to one file
$rpl_string = !$this->log_per_page ?
'' :
'_' . System::getPageName(System::NO_EXTENSION);
$fn = str_replace('##PAGENAME##', $rpl_string, $fn); // create output filename
// write to file
// first check if max file size is is set and file is bigger
if (
$this->log_max_filesize > 0 &&
((filesize($fn) / 1024) > $this->log_max_filesize)
) {
// for easy purpose, rename file only to attach timestamp, nur sequence numbering
rename($fn, $fn . '.' . date("YmdHis"));
}
$this->log_file_name = $fn;
$fp = fopen($this->log_file_name, 'a');
if ($fp !== false) {
fwrite($fp, $error_string);
fclose($fp);
return true;
} else {
echo "<!-- could not open file: " . $this->log_file_name . " //-->";
return false;
}
}
// *** PUBLIC ***
/**
* Temporary method to read all class variables for testing purpose
*
* @param string $name what variable to return
* @return mixed can be anything, bool, string, int, array
*/
public function getSetting(string $name): mixed
{
// for debug purpose only
return $this->{$name};
}
/**
* sets the internal log file prefix id
* string must be a alphanumeric string
* if non valid string is given it returns the previous set one only
*
* @param string $string log file id string value
* @return string returns the set log file id string
* @deprecated Use $log->setLogId()
*/
public function basicSetLogId(string $string): string
{
return $this->setLogId($string);
}
/**
* sets the internal log file prefix id
* string must be a alphanumeric string
* if non valid string is given it returns the previous set one only
*
* @param string $string log file id string value
* @return string returns the set log file id string
*/
public function setLogId(string $string): string
{
if (preg_match("/^[\w\-]+$/", $string)) {
$this->log_file_id = $string;
}
return $this->log_file_id;
}
/**
* return current set log file id
* @return string
*/
public function getLogId(): string
{
return $this->log_file_id;
}
/**
* old name for setLogLevel
*
* @param string $type debug, echo, print
* @param string $flag on/off
* array $array of levels to turn on/off debug
* @return bool Return false if type or flag is invalid
* @deprecated Use setLogLevel
*/
public function debugFor(string $type, string $flag): bool
{
/** @phan-suppress-next-line PhanTypeMismatchArgumentReal, PhanParamTooFew @phpstan-ignore-next-line */
return $this->setLogLevel(...[func_get_args()]);
}
/**
* set log level settings for All types
* if invalid type, skip
*
* @param string $type Type to get: debug, echo, print
* @param bool $set True or False
* @return bool Return false if type invalid
*/
public function setLogLevelAll(string $type, bool $set): bool
{
// skip set if not valid
if (!in_array($type, ['debug', 'echo', 'print'])) {
return false;
}
$this->{$type . '_output_all'} = $set;
return true;
}
/**
* get the current log level setting for All level blocks
*
* @param string $type Type to get: debug, echo, print
* @return bool False on failure, or the boolean flag from the all var
*/
public function getLogLevelAll(string $type): bool
{
// type check for debug/echo/print
if (!in_array($type, ['debug', 'echo', 'print'])) {
return false;
}
return $this->{$type . '_output_all'};
}
/**
* passes list of level names, to turn on debug
* eg $foo->debugFor('print', 'on', ['LOG', 'DEBUG', 'INFO']);
*
* @param string $type debug, echo, print
* @param string $flag on/off
* @param array<mixed> $debug_on Array of levels to turn on/off debug
* To turn off a level set 'Level' => false,
* If not set, switches to on
* @return bool Return false if type or flag invalid
* also false if debug array is empty
*/
public function setLogLevel(string $type, string $flag, array $debug_on): bool
{
// abort if not valid type
if (!in_array($type, ['debug', 'echo', 'print'])) {
return false;
}
// invalid flag type
if (!in_array($flag, ['on', 'off'])) {
return false;
}
if (count($debug_on) >= 1) {
foreach ($debug_on as $level => $set) {
$switch = $type . '_output' . ($flag == 'off' ? '_not' : '');
if (!is_bool($set)) {
$level = $set;
$set = true;
}
$this->{$switch}[$level] = $set;
}
} else {
return false;
}
return true;
}
/**
* return the log level for the array type normal and not (disable)
*
* @param string $type debug, echo, print
* @param string $flag on/off
* @param string|null $level if not null then check if this array entry is set
* else return false
* @return array<mixed>|bool if $level is null, return array, else boolean true/false
*/
public function getLogLevel(string $type, string $flag, ?string $level = null): array|bool
{
// abort if not valid type
if (!in_array($type, ['debug', 'echo', 'print'])) {
return false;
}
// invalid flag type
if (!in_array($flag, ['on', 'off'])) {
return false;
}
$switch = $type . '_output' . ($flag == 'off' ? '_not' : '');
// log level direct check must be not null or not empty string
if (!empty($level)) {
return $this->{$switch}[$level] ?? false;
}
// array
return $this->{$switch};
}
/**
* set flags for per log level type
* - level: set per sub group level
* - class: split by class
* - page: split per page called
* - run: for each run
*
* @param string $type Type to get: level, class, page, run
* @param bool $set True or False
* @return bool Return false if type invalid
*/
public function setLogPer(string $type, bool $set): bool
{
if (!in_array($type, ['level', 'class', 'page', 'run'])) {
return false;
}
$this->{'log_per_' . $type} = $set;
// if per run set unique id
if ($type == 'run' && $set == true) {
$this->setLogUniqueId();
}
return true;
}
/**
* return current set log per flag in bool
*
* @param string $type Type to get: level, class, page, run
* @return bool True of false for turned on or off
*/
public function getLogPer(string $type): bool
{
if (!in_array($type, ['level', 'class', 'page', 'run'])) {
return false;
}
return $this->{'log_per_' . $type};
}
/**
* Sets a unique id based on current date (y/m/d, h:i:s) and a unique id (8 chars)
* if override is set to true it will be newly set, else if already set nothing changes
*
* @param bool $override True to force new set
* @return void
*/
public function setLogUniqueId(bool $override = false): void
{
if (!$this->log_file_unique_id || $override == true) {
$this->log_file_unique_id =
date('Y-m-d_His') . '_U_'
. substr(hash('sha1', uniqid((string)mt_rand(), true)), 0, 8);
}
}
/**
* Return current set log file unique id,
* empty string for not set
*
* @return string
*/
public function getLogUniqueId(): string
{
return $this->log_file_unique_id;
}
/**
* Set or get the log file date extension flag
* if null or empty parameter gets current flag
*
* @param boolean|null $set Set the date suffix for log files
* If set to null return current set
* @return boolean Current set flag
*/
public function setGetLogPrintFileDate(?bool $set = null): bool
{
if ($set !== null) {
$this->log_print_file_date = $set;
}
return $this->log_print_file_date;
}
/**
* Return current set log file name
*
* @return string Filename set set after the last time debug was called
*/
public function getLogFileName(): string
{
return $this->log_file_name;
}
/**
* A replacement for the \CoreLibs\Debug\Support::printAr
* But this does not wrap it in <pre></pre>
* It uses some special code sets so we can convert that to pre flags
* for echo output {##HTMLPRE##} ... {##/HTMLPRE##}
* Do not use this without using it in a string in debug function
*
* @param array<mixed> $a Array to format
* @return string print_r formated
*/
public function prAr(array $a): string
{
return '##HTMLPRE##' . print_r($a, true) . '##/HTMLPRE##';
}
/**
* Convert bool value to string value
*
* @param bool $bool Bool value to be transformed
* @param string $true Override default string 'true'
* @param string $false Override default string 'false'
* @return string $true or $false string for true/false bool
*/
public function prBl(
bool $bool,
string $true = 'true',
string $false = 'false'
): string {
return $bool ? $true : $false;
}
/**
* write debug data to error_msg array
*
* @param string $level id for error message, groups messages together
* @param string $string the actual error message
* @param bool $strip default on false, if set to true,
* all html tags will be stripped and <br> changed to \n
* this is only used for debug output
* @param string $prefix Attach some block before $string.
* Will not be stripped even
* when strip is true
* if strip is false, recommended to add that to $string
* @return bool True if logged, false if not logged
*/
public function debug(
string $level,
string $string,
bool $strip = false,
string $prefix = ''
): bool {
$status = false;
// must be debug on and either echo or print on
if (
!$this->doDebugTrigger('debug', $level) ||
(
// if debug is on, either print or echo must be set to on
!$this->doDebugTrigger('print', $level) &&
!$this->doDebugTrigger('echo', $level)
)
) {
return $status;
}
// get the last class entry and wrie that
$class = Support::getCallerClass();
// get timestamp
$timestamp = Support::printTime();
// same string put for print (no html data inside)
// write to file if set
$status = $this->writeErrorMsg(
$level,
'[' . $timestamp . '] '
. '[' . $this->host_name . '] '
. '[' . System::getPageName(System::FULL_PATH) . '] '
. '[' . $this->running_uid . '] '
. '{' . $class . '} '
. '<' . $level . '> - '
// strip the htmlpre special tags if exist
. str_replace(
['##HTMLPRE##', '##/HTMLPRE##'],
'',
// if stripping all html, etc is requested, only for write error msg
($strip ?
// find any <br> and replace them with \n
// strip rest of html elements (base only)
preg_replace(
"/(<\/?)(\w+)([^>]*>)/",
'',
str_replace('<br>', "\n", $prefix . $string)
) :
$prefix . $string
) ?: ''
)
. "\n"
);
// write to error level msg array if there is an echo request
if ($this->doDebugTrigger('echo', $level)) {
// init if not set
if (!isset($this->error_msg[$level])) {
$this->error_msg[$level] = [];
}
// HTML string
$this->error_msg[$level][] = '<div>'
. '[<span style="font-weight: bold; color: #5e8600;">' . $timestamp . '</span>] '
. '[<span style="font-weight: bold; color: #c56c00;">' . $level . '</span>] '
. '[<span style="color: #b000ab;">' . $this->host_name . '</span>] '
. '[<span style="color: #08b369;">' . $this->page_name . '</span>] '
. '[<span style="color: #0062A2;">' . $this->running_uid . '</span>] '
. '{<span style="font-style: italic; color: #928100;">' . $class . '</span>} - '
// as is prefix, allow HTML
. $prefix
// we replace special HTMLPRE with <pre> entries
. str_replace(
['##HTMLPRE##', '##/HTMLPRE##'],
['<pre>', '</pre>'],
Html::htmlent($string)
)
. "</div><!--#BR#-->";
$status = true;
}
return $status;
}
/**
* for ECHO ON only
* returns error data as string so it can be echoed out
*
* @param string $header_prefix prefix string for header
* @return string error msg for all levels
*/
public function printErrorMsg(string $header_prefix = ''): string
{
$string_output = '';
// if not debug && echo on, do not return anything
if (
!$this->getLogLevelAll('debug') ||
!$this->getLogLevelAll('echo')
) {
return $string_output;
}
if ($this->error_msg_prefix) {
$header_prefix = $this->error_msg_prefix;
}
$script_end = microtime(true) - $this->script_starttime;
foreach ($this->error_msg as $level => $temp_debug_output) {
if ($this->doDebugTrigger('debug', $level)) {
if ($this->doDebugTrigger('echo', $level)) {
$string_output .= '<div style="font-size: 12px;">'
. '[<span style="font-style: italic; color: #c56c00;">' . $level . '</span>] '
. ($header_prefix ? "<b>**** " . Html::htmlent($header_prefix) . " ****</br>\n" : '')
. '</div>'
. join('', $temp_debug_output);
} // echo it out
} // do printout
} // for each level
// create the output wrapper around
// so we have a nice formated output per class
if ($string_output) {
$string_prefix = '<div style="text-align: left; padding: 5px; font-size: 10px; '
. 'font-family: sans-serif; border-top: 1px solid black; '
. 'border-bottom: 1px solid black; margin: 10px 0 10px 0; '
. 'background-color: white; color: black;">'
. '<div style="font-size: 12px;">{<span style="font-style: italic; color: #928100;">'
. Support::getCallerClass() . '</span>}</div>';
$string_output = $string_prefix . $string_output
. '<div><span style="font-style: italic; color: #108db3;">Script Run Time:</span> '
. $script_end . '</div>'
. '</div>';
}
// }
return $string_output;
}
/**
* for ECHO ON only
* unsests the error message array
* can be used if writing is primary to file
* if no level given resets all
*
* @param string $level optional level
* @return void has no return
*/
public function resetErrorMsg(string $level = ''): void
{
if (!$level) {
$this->error_msg = [];
} elseif (isset($this->error_msg[$level])) {
unset($this->error_msg[$level]);
}
}
/**
* for ECHO ON only
* Get current error message array
*
* @return array<mixed> error messages collected
*/
public function getErrorMsg(): array
{
return $this->error_msg;
}
/**
* for ECHO ON only
* merges the given error array with the one from this class
* only merges visible ones
*
* @param array<mixed> $error_msg error array
* @return void has no return
*/
public function mergeErrors(array $error_msg = []): void
{
array_push($this->error_msg, ...$error_msg);
parent::__construct($options);
}
}

911
src/Debug/LoggingLegacy.php Normal file
View File

@@ -0,0 +1,911 @@
<?php
/**
* THIS IS LEGACY LOGGING AND WILL BE FULLY REMOVED IN FUTURE VERSION.
* use \CoreLibs\Logger\Logging instead
* for the need to reference old:
* use CoreLibs\Debug\LoggingLegacy as Logging;
*/
/*
* Debug support functions
*
* These are if there is any debug to print out at all at the end
 * debug_output_all - general yes no
 * It's recommended to use the method "debug_for" to turn on of the array vars
 * debug_output - turn on for one level (Array)
 * debug_output_not - turn off for one level (array)
 *
 * Print out the debug at thend of the html
 * echo_output_all
 * echo_output
 * echo_output_not
 *
 * Write debug to file
 * print_output_all
 * print_output
 * print_output_not
*/
declare(strict_types=1);
namespace CoreLibs\Debug;
use CoreLibs\Debug\Support;
use CoreLibs\Create\Uids;
use CoreLibs\Get\System;
use CoreLibs\Convert\Html;
class LoggingLegacy
{
// options
/** @var array<mixed> */
private $options = [];
// page and host name
/** @var string */
private $page_name;
/** @var string */
private $host_name;
/** @var int */
private $host_port;
// internal error reporting vars
/** @var array<mixed> */
private $error_msg = []; // the "connection" to the outside errors
// debug output prefix
/** @var string */
private $error_msg_prefix = ''; // prefix to the error string (the class name)
// debug flags/settings
/** @var string */
private $running_uid = ''; // unique ID set on class init and used in logging as prefix
// log file name
/** @var string */
private $log_folder = '';
/** @var string */
private $log_file_name_ext = 'log'; // use this for date rotate
/** @var string */
private $log_file_name = '';
/** @var int */
private $log_max_filesize = 0; // set in kilobytes
/** @var string */
private $log_print_file = 'error_msg{LOGID}{LEVEL}{CLASS}{PAGENAME}{DATE_RUNID}';
/** @var string */
private $log_file_unique_id; // a unique ID set only once for call derived from this class
/** @var string */
private $log_file_date = ''; // Y-m-d file in file name
/** @var bool */
private $log_print_file_date = true; // if set add Y-m-d and do automatic daily rotation
/** @var string */
private $log_file_id = ''; // a alphanumeric name that has to be set as global definition
/** @var bool */
private $log_per_level = false; // set, it will split per level (first parameter in debug call)
/** @var bool */
private $log_per_class = false; // set, will split log per class
/** @var bool */
private $log_per_page = false; // set, will split log per called file
/** @var bool */
private $log_per_run = false; // create a new log file per run (time stamp + unique ID)
// script running time
/** @var float */
private $script_starttime;
/** @var string[] current log levels */
private $log_levels = ['debug', 'echo', 'print'];
/** @var string[] log group per what for writing to file */
private $log_grouping = ['level', 'class', 'page', 'run'];
// debug flags [they must exist or we get a warning]
/** @var array<mixed> */
private $debug_output = []; // if this is true, show debug on desconstructor
/** @var array<mixed> */
private $debug_output_not = [];
/** @var bool */
private $debug_output_all = false;
/** @var array<mixed> */
private $echo_output = []; // errors: echo out, default is 1
/** @var array<mixed> */
private $echo_output_not = [];
/** @var bool */
private $echo_output_all = false;
/** @var array<mixed> */
private $print_output = []; // errors: print to file, default is 0
/** @var array<mixed> */
private $print_output_not = [];
/** @var bool */
private $print_output_all = false;
/**
* Init logger
*
* global vars that can be used
* - BASE
* - LOG
* - LOG_FILE_ID
* options array layout
* - log_folder:
* - file_id:
* - unique_id:
* - print_file_date:
* - log_per_level:
* - log_per_class:
* - log_per_page:
* - log_per_run:
* - debug_all:
* - echo_all:
* - print_all:
* - debug (array):
* - echo (array):
* - print (array):
* - debug_not (array):
* - echo_not (array):
* - print_not (array):
*
* @param array<mixed> $options Array with settings options
*/
public function __construct(array $options = [])
{
// copy the options over
$this->options = $options;
// set log folder from options
$this->log_folder = $this->options['log_folder'] ?? '';
// legacy flow, check must set constants
if (empty($this->log_folder) && defined('BASE') && defined('LOG')) {
/** @deprecated Do not use this anymore, define path on class load */
trigger_error(
'options: log_folder must be set. Setting via BASE and LOG constants is deprecated',
E_USER_DEPRECATED
);
// make sure this is writeable, else skip
$this->log_folder = BASE . LOG;
}
// fallback + notice
if (empty($this->log_folder)) {
/* trigger_error(
'options or constant not set or folder not writable. fallback to: ' . getcwd(),
E_USER_NOTICE
); */
$this->log_folder = getcwd() . DIRECTORY_SEPARATOR;
}
// if folder is not writeable, abort
if (!is_writeable($this->log_folder)) {
trigger_error(
'Folder: ' . $this->log_folder . ' is not writeable for logging',
E_USER_ERROR
);
}
// check if log_folder has a trailing /
if (substr($this->log_folder, -1, 1) != DIRECTORY_SEPARATOR) {
$this->log_folder .= DIRECTORY_SEPARATOR;
}
// running time start for script
$this->script_starttime = microtime(true);
// set per run UID for logging
$this->running_uid = Uids::uniqIdShort();
// set the page name
$this->page_name = System::getPageName();
// set host name
list($this->host_name , $this->host_port) = System::getHostName();
// add port to host name if not port 80
if ($this->host_port != 80) {
$this->host_name .= ':' . $this->host_port;
}
// can be overridden with basicSetLogFileId later
if (!empty($this->options['file_id'])) {
$this->setLogId($this->options['file_id']);
} elseif (!empty($GLOBALS['LOG_FILE_ID'])) {
/** @deprecated Do not use this anymore, define file_id on class load */
trigger_error(
'options: file_id must be set. Setting via LOG_FILE_ID global variable is deprecated',
E_USER_DEPRECATED
);
// legacy flow, should be removed and only set via options
$this->setLogId($GLOBALS['LOG_FILE_ID']);
// TODO trigger deprecation error
// trigger_error(
// 'Debug\Logging: Do not use globals LOG_FILE_ID to set log id for Logging',
// E_USER_DEPRECATED
// );
} elseif (defined('LOG_FILE_ID')) {
/** @deprecated Do not use this anymore, define file_id on class load */
trigger_error(
'options: file_id must be set. Setting via LOG_FILE_ID constant is deprecated',
E_USER_DEPRECATED
);
// legacy flow, should be removed and only set via options
$this->setLogId((string)LOG_FILE_ID);
// trigger deprecation error
// trigger_error(
// 'Debug\Logging: Do not use constant LOG_FILE_ID to set log id for Logging',
// E_USER_DEPRECATED
// );
}
// init the log levels
$this->initLogLevels();
}
// *** PRIVATE ***
/**
* init the basic log levels based on global set variables
*
* @return void
*/
private function initLogLevels(): void
{
// if given via parameters, only for all
// globals overrule given settings, for one (array), eg $ECHO['db'] = 1;
foreach ($this->log_levels as $type) {
// include or exclude (off) from output
foreach (['on', 'off'] as $flag) {
$in_type = $type;
if ($flag == 'off') {
$in_type .= '_not';
}
$up_type = strtoupper($in_type);
if (
isset($this->options[$in_type]) &&
is_array($this->options[$in_type])
) {
$this->setLogLevel($type, $flag, $this->options[$in_type]);
} elseif (
isset($GLOBALS[$up_type]) &&
is_array($GLOBALS[$up_type])
) {
// TODO trigger deprecation error
$this->setLogLevel($type, $flag, $GLOBALS[$up_type]);
}
}
}
// TODO remove all $GLOBALS call and only use options
// all overrule
$this->setLogLevelAll(
'debug',
$this->options['debug_all'] ??
// for user login, should be handled outside like globals
$_SESSION['DEBUG_ALL'] ?? // DEPRECATED
$GLOBALS['DEBUG_ALL'] ?? // DEPRECATED
false
);
$this->setLogLevelAll(
'print',
$this->options['print_all'] ??
// for user login, should be handled outside like globals
$_SESSION['DEBUG_ALL'] ?? // DEPRECATED
$GLOBALS['PRINT_ALL'] ?? // DEPRECATED
false
);
$this->setLogLevelAll(
'echo',
$this->options['echo_all'] ??
$GLOBALS['ECHO_ALL'] ?? // DEPRECATED
false
);
// GLOBAL rules for log writing
// add file date is default on
$this->setGetLogPrintFileDate(
$this->options['print_file_date'] ??
$GLOBALS['LOG_PRINT_FILE_DATE'] ?? // DEPRECATED
true
);
// all other logging file name flags are off
$this->setLogPer(
'level',
$this->options['per_level'] ??
$GLOBALS['LOG_PER_LEVEL'] ?? // DEPRECATED
false
);
$this->setLogPer(
'class',
$this->options['per_class'] ??
$GLOBALS['LOG_PER_CLASS'] ?? // DEPRECATED
false
);
$this->setLogPer(
'page',
$this->options['per_page'] ??
$GLOBALS['LOG_PER_PAGE'] ?? // DEPRECATED
false
);
$this->setLogPer(
'run',
$this->options['per_run'] ??
$GLOBALS['LOG_PER_RUN'] ?? // DEPRECATED
false
);
// set log per date
if ($this->setGetLogPrintFileDate()) {
$this->log_file_date = date('Y-m-d');
}
// set per run ID
if ($this->log_per_run) {
$this->setLogUniqueId();
}
}
/**
* checks if we have a need to work on certain debug output
* Needs debug/echo/print ad target for which of the debug flag groups we check
* also needs level string to check in the per level output flag check.
* In case we have invalid target it will return false
*
* @param string $target target group to check debug/echo/print
* @param string $level level to check in detailed level flag
* @return bool true on access allowed or false on no access
*/
private function doDebugTrigger(string $target, string $level): bool
{
$access = false;
// check if we do debug, echo or print
if (
(
$this->getLogLevel($target, 'on', $level) ||
$this->getLogLevelAll($target)
) &&
!$this->getLogLevel($target, 'off', $level)
) {
$access = true;
}
return $access;
}
/**
* writes error msg data to file for current level
*
* @param string $level the level to write
* @param string $error_string error string to write
* @return bool True if message written, False if not
*/
private function writeErrorMsg(string $level, string $error_string): bool
{
// only write if write is requested
if (
!($this->doDebugTrigger('debug', $level) &&
$this->doDebugTrigger('print', $level))
) {
return false;
}
// init base file path
$fn = $this->log_folder . $this->log_print_file . '.' . $this->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;
} else {
$rpl_string = '';
}
$fn = str_replace('{LOGID}', $rpl_string, $fn); // log id (like a log file prefix)
// if run id, we auto add ymd, so we ignore the log file date
if ($this->log_per_run) {
$rpl_string = '_' . $this->log_file_unique_id; // add 8 char unique string
} elseif ($this->setGetLogPrintFileDate()) {
$rpl_string = '_' . $this->log_file_date; // add date to file
} else {
$rpl_string = '';
}
$fn = str_replace('{DATE_RUNID}', $rpl_string, $fn); // create output filename
// write per level
$rpl_string = !$this->log_per_level ? '' :
// normalize level, replace all non alphanumeric characters with -
'_' . (
// if return is only - then set error string
preg_match(
"/^-+$/",
$level_string = preg_replace("/[^A-Za-z0-9-_]/", '-', $level) ?? ''
) ?
'INVALID-LEVEL-STRING' :
$level_string
);
$fn = str_replace('{LEVEL}', $rpl_string, $fn); // create output filename
// set per class, but don't use get_class as we will only get self
$rpl_string = !$this->log_per_class ? '' : '_'
// set sub class settings
. str_replace('\\', '-', Support::getCallerTopLevelClass());
$fn = str_replace('{CLASS}', $rpl_string, $fn); // create output filename
// if request to write to one file
$rpl_string = !$this->log_per_page ?
'' :
'_' . System::getPageName(System::NO_EXTENSION);
$fn = str_replace('{PAGENAME}', $rpl_string, $fn); // create output filename
// write to file
// first check if max file size is is set and file is bigger
if (
$this->log_max_filesize > 0 &&
((filesize($fn) / 1024) > $this->log_max_filesize)
) {
// for easy purpose, rename file only to attach timestamp, nur sequence numbering
rename($fn, $fn . '.' . date("YmdHis"));
}
$this->log_file_name = $fn;
$fp = fopen($this->log_file_name, 'a');
if ($fp !== false) {
fwrite($fp, $error_string);
fclose($fp);
return true;
} else {
echo "<!-- could not open file: " . $this->log_file_name . " //-->";
return false;
}
}
// *** PUBLIC ***
/**
* Temporary method to read all class variables for testing purpose
*
* @param string $name what variable to return
* @return mixed can be anything, bool, string, int, array
*/
public function getSetting(string $name): mixed
{
// for debug purpose only
return $this->{$name};
}
/**
* sets the internal log file prefix id
* string must be a alphanumeric string
* if non valid string is given it returns the previous set one only
*
* @param string $string log file id string value
* @return string returns the set log file id string
* @deprecated Use $log->setLogId()
*/
public function basicSetLogId(string $string): string
{
return $this->setLogId($string);
}
/**
* sets the internal log file prefix id
* string must be a alphanumeric string
* if non valid string is given it returns the previous set one only
*
* @param string $string log file id string value
* @return string returns the set log file id string
*/
public function setLogId(string $string): string
{
if (preg_match("/^[\w\-]+$/", $string)) {
$this->log_file_id = $string;
}
return $this->log_file_id;
}
/**
* return current set log file id
* @return string
*/
public function getLogId(): string
{
return $this->log_file_id;
}
/**
* old name for setLogLevel
*
* @param string $type debug, echo, print
* @param string $flag on/off
* array $array of levels to turn on/off debug
* @return bool Return false if type or flag is invalid
* @deprecated Use setLogLevel
*/
public function debugFor(string $type, string $flag): bool
{
/** @phan-suppress-next-line PhanTypeMismatchArgumentReal, PhanParamTooFew @phpstan-ignore-next-line */
return $this->setLogLevel(...[func_get_args()]);
}
/**
* set log level settings for All types
* if invalid type, skip
*
* @param string $type Type to get: debug, echo, print
* @param bool $set True or False
* @return bool Return false if type invalid
*/
public function setLogLevelAll(string $type, bool $set): bool
{
// skip set if not valid
if (!in_array($type, $this->log_levels)) {
return false;
}
$this->{$type . '_output_all'} = $set;
return true;
}
/**
* get the current log level setting for All level blocks
*
* @param string $type Type to get: debug, echo, print
* @return bool False on failure, or the boolean flag from the all var
*/
public function getLogLevelAll(string $type): bool
{
// type check for debug/echo/print
if (!in_array($type, $this->log_levels)) {
return false;
}
return $this->{$type . '_output_all'};
}
/**
* passes list of level names, to turn on debug
* eg $foo->debugFor('print', 'on', ['LOG', 'DEBUG', 'INFO']);
*
* @param string $type debug, echo, print
* @param string $flag on/off
* @param array<mixed> $debug_on Array of levels to turn on/off debug
* To turn off a level set 'Level' => false,
* If not set, switches to on
* @return bool Return false if type or flag invalid
* also false if debug array is empty
*/
public function setLogLevel(string $type, string $flag, array $debug_on): bool
{
// abort if not valid type
if (!in_array($type, $this->log_levels)) {
return false;
}
// invalid flag type
if (!in_array($flag, ['on', 'off'])) {
return false;
}
if (count($debug_on) >= 1) {
foreach ($debug_on as $level => $set) {
$switch = $type . '_output' . ($flag == 'off' ? '_not' : '');
if (!is_bool($set)) {
$level = $set;
$set = true;
}
$this->{$switch}[$level] = $set;
}
} else {
return false;
}
return true;
}
/**
* return the log level for the array type normal and not (disable)
*
* @param string $type debug, echo, print
* @param string $flag on/off
* @param string|null $level if not null then check if this array entry is set
* else return false
* @return array<mixed>|bool if $level is null, return array, else boolean true/false
*/
public function getLogLevel(string $type, string $flag, ?string $level = null): array|bool
{
// abort if not valid type
if (!in_array($type, $this->log_levels)) {
return false;
}
// invalid flag type
if (!in_array($flag, ['on', 'off'])) {
return false;
}
$switch = $type . '_output' . ($flag == 'off' ? '_not' : '');
// log level direct check must be not null or not empty string
if (!empty($level)) {
return $this->{$switch}[$level] ?? false;
}
// array
return $this->{$switch};
}
/**
* set flags for per log level type
* - level: set per sub group level
* - class: split by class
* - page: split per page called
* - run: for each run
*
* @param string $type Type to get: level, class, page, run
* @param bool $set True or False
* @return bool Return false if type invalid
*/
public function setLogPer(string $type, bool $set): bool
{
if (!in_array($type, $this->log_grouping)) {
return false;
}
$this->{'log_per_' . $type} = $set;
// if per run set unique id
if ($type == 'run' && $set == true) {
$this->setLogUniqueId();
}
return true;
}
/**
* return current set log per flag in bool
*
* @param string $type Type to get: level, class, page, run
* @return bool True of false for turned on or off
*/
public function getLogPer(string $type): bool
{
if (!in_array($type, $this->log_grouping)) {
return false;
}
return $this->{'log_per_' . $type};
}
/**
* Sets a unique id based on current date (y/m/d, h:i:s) and a unique id (8 chars)
* if override is set to true it will be newly set, else if already set nothing changes
*
* @param bool $override True to force new set
* @return void
*/
public function setLogUniqueId(bool $override = false): void
{
if (!$this->log_file_unique_id || $override == true) {
$this->log_file_unique_id =
date('Y-m-d_His') . '_U_'
. substr(hash('sha1', uniqid((string)mt_rand(), true)), 0, 8);
}
}
/**
* Return current set log file unique id,
* empty string for not set
*
* @return string
*/
public function getLogUniqueId(): string
{
return $this->log_file_unique_id;
}
/**
* Set or get the log file date extension flag
* if null or empty parameter gets current flag
*
* @param boolean|null $set Set the date suffix for log files
* If set to null return current set
* @return boolean Current set flag
*/
public function setGetLogPrintFileDate(?bool $set = null): bool
{
if ($set !== null) {
$this->log_print_file_date = $set;
}
return $this->log_print_file_date;
}
/**
* Return current set log file name
*
* @return string Filename set set after the last time debug was called
*/
public function getLogFileName(): string
{
return $this->log_file_name;
}
/**
* A replacement for the \CoreLibs\Debug\Support::printAr
* But this does not wrap it in <pre></pre>
* It uses some special code sets so we can convert that to pre flags
* for echo output {##HTMLPRE##} ... {##/HTMLPRE##}
* Do not use this without using it in a string in debug function
*
* @param array<mixed> $a Array to format
* @return string print_r formated
*/
public function prAr(array $a): string
{
return '##HTMLPRE##' . print_r($a, true) . '##/HTMLPRE##';
}
/**
* Convert bool value to string value
*
* @param bool $bool Bool value to be transformed
* @param string $true Override default string 'true'
* @param string $false Override default string 'false'
* @return string $true or $false string for true/false bool
*/
public function prBl(
bool $bool,
string $true = 'true',
string $false = 'false'
): string {
return $bool ? $true : $false;
}
/**
* write debug data to error_msg array
*
* @param string $level id for error message, groups messages together
* @param string $string the actual error message
* @param bool $strip default on false, if set to true,
* all html tags will be stripped and <br> changed to \n
* this is only used for debug output
* @param string $prefix Attach some block before $string.
* Will not be stripped even
* when strip is true
* if strip is false, recommended to add that to $string
* @return bool True if logged, false if not logged
*/
public function debug(
string $level,
string $string,
bool $strip = false,
string $prefix = ''
): bool {
$status = false;
// must be debug on and either echo or print on
if (
!$this->doDebugTrigger('debug', $level) ||
(
// if debug is on, either print or echo must be set to on
!$this->doDebugTrigger('print', $level) &&
!$this->doDebugTrigger('echo', $level)
)
) {
return $status;
}
// get the last class entry and wrie that
$class = Support::getCallerTopLevelClass();
// get timestamp
$timestamp = Support::printTime();
// same string put for print (no html data inside)
// write to file if set
$status = $this->writeErrorMsg(
$level,
'[' . $timestamp . '] '
. '[' . $this->host_name . '] '
. '[' . System::getPageName(System::FULL_PATH) . '] '
. '[' . $this->running_uid . '] '
. '{' . $class . '} '
. '<' . $level . '> - '
// strip the htmlpre special tags if exist
. str_replace(
['##HTMLPRE##', '##/HTMLPRE##'],
'',
// if stripping all html, etc is requested, only for write error msg
($strip ?
// find any <br> and replace them with \n
// strip rest of html elements (base only)
preg_replace(
"/(<\/?)(\w+)([^>]*>)/",
'',
str_replace('<br>', "\n", $prefix . $string)
) :
$prefix . $string
) ?: ''
)
. "\n"
);
// write to error level msg array if there is an echo request
if ($this->doDebugTrigger('echo', $level)) {
// init if not set
if (!isset($this->error_msg[$level])) {
$this->error_msg[$level] = [];
}
// HTML string
$this->error_msg[$level][] = '<div>'
. '[<span style="font-weight: bold; color: #5e8600;">' . $timestamp . '</span>] '
. '[<span style="font-weight: bold; color: #c56c00;">' . $level . '</span>] '
. '[<span style="color: #b000ab;">' . $this->host_name . '</span>] '
. '[<span style="color: #08b369;">' . $this->page_name . '</span>] '
. '[<span style="color: #0062A2;">' . $this->running_uid . '</span>] '
. '{<span style="font-style: italic; color: #928100;">' . $class . '</span>} - '
// as is prefix, allow HTML
. $prefix
// we replace special HTMLPRE with <pre> entries
. str_replace(
['##HTMLPRE##', '##/HTMLPRE##'],
['<pre>', '</pre>'],
Html::htmlent($string)
)
. "</div><!--#BR#-->";
$status = true;
}
return $status;
}
/**
* for ECHO ON only
* returns error data as string so it can be echoed out
*
* @param string $header_prefix prefix string for header
* @return string error msg for all levels
*/
public function printErrorMsg(string $header_prefix = ''): string
{
$string_output = '';
// if not debug && echo on, do not return anything
if (
!$this->getLogLevelAll('debug') ||
!$this->getLogLevelAll('echo')
) {
return $string_output;
}
if ($this->error_msg_prefix) {
$header_prefix = $this->error_msg_prefix;
}
$script_end = microtime(true) - $this->script_starttime;
foreach ($this->error_msg as $level => $temp_debug_output) {
if ($this->doDebugTrigger('debug', $level)) {
if ($this->doDebugTrigger('echo', $level)) {
$string_output .= '<div style="font-size: 12px;">'
. '[<span style="font-style: italic; color: #c56c00;">' . $level . '</span>] '
. ($header_prefix ? "<b>**** " . Html::htmlent($header_prefix) . " ****</br>\n" : '')
. '</div>'
. join('', $temp_debug_output);
} // echo it out
} // do printout
} // for each level
// create the output wrapper around
// so we have a nice formated output per class
if ($string_output) {
$string_prefix = '<div style="text-align: left; padding: 5px; font-size: 10px; '
. 'font-family: sans-serif; border-top: 1px solid black; '
. 'border-bottom: 1px solid black; margin: 10px 0 10px 0; '
. 'background-color: white; color: black;">'
. '<div style="font-size: 12px;">{<span style="font-style: italic; color: #928100;">'
. Support::getCallerTopLevelClass() . '</span>}</div>';
$string_output = $string_prefix . $string_output
. '<div><span style="font-style: italic; color: #108db3;">Script Run Time:</span> '
. $script_end . '</div>'
. '</div>';
}
// }
return $string_output;
}
/**
* for ECHO ON only
* unsests the error message array
* can be used if writing is primary to file
* if no level given resets all
*
* @param string $level optional level
* @return void has no return
*/
public function resetErrorMsg(string $level = ''): void
{
if (!$level) {
$this->error_msg = [];
} elseif (isset($this->error_msg[$level])) {
unset($this->error_msg[$level]);
}
}
/**
* for ECHO ON only
* Get current error message array
*
* @return array<mixed> error messages collected
*/
public function getErrorMsg(): array
{
return $this->error_msg;
}
/**
* for ECHO ON only
* merges the given error array with the one from this class
* only merges visible ones
*
* @param array<mixed> $error_msg error array
* @return void has no return
*/
public function mergeErrors(array $error_msg = []): void
{
array_push($this->error_msg, ...$error_msg);
}
}
// __END__

View File

@@ -13,13 +13,13 @@ use CoreLibs\Convert\Byte;
class MemoryUsage
{
/** @var int */
private static $start_memory = 0;
private static int $start_memory = 0;
/** @var int */
private static $set_memory = 0;
private static int $set_memory = 0;
/** @var int */
private static $previous_memory = 0;
private static int $previous_memory = 0;
/** @var bool */
private static $debug_memory = false;
private static bool $debug_memory = false;
/**
* set memory flag, or return set memory flag

View File

@@ -12,18 +12,18 @@ class RunningTime
{
// hr
/** @var float */
private static $hr_start_time;
private static float $hr_start_time;
/** @var float */
private static $hr_end_time;
private static float $hr_end_time;
/** @var float */
private static $hr_last_time;
private static float $hr_last_time;
// normal
/** @var float */
private static $start_time;
private static float $start_time;
/** @var float */
private static $end_time;
private static float $end_time;
/** @var string */
private static $running_time_string;
private static string $running_time_string;
/**
* sub calculation for running time based on out time.
@@ -79,7 +79,7 @@ class RunningTime
public static function hrRunningTime(string $out_time = 'ms'): float
{
// if start time not set, set start time
if (!self::$hr_start_time) {
if (empty(self::$hr_start_time)) {
self::$hr_start_time = hrtime(true);
self::$hr_last_time = self::$hr_start_time;
$run_time = 0;
@@ -137,7 +137,7 @@ class RunningTime
list($micro, $timestamp) = explode(' ', microtime());
$running_time = 0;
// set start & end time
if (!self::$start_time) {
if (empty(self::$start_time)) {
// always reset running time string on first call
self::$running_time_string = '';
self::$start_time = ((float)$micro + (float)$timestamp);
@@ -149,7 +149,7 @@ class RunningTime
self::$running_time_string .= date('Y-m-d H:i:s', (int)$timestamp);
self::$running_time_string .= ' ' . $micro . ($simple ? ', ' : '<br>');
// if both are set
if (self::$start_time && self::$end_time) {
if (!empty(self::$start_time) && !empty(self::$end_time)) {
$running_time = self::$end_time - self::$start_time;
self::$running_time_string .= ($simple ? 'Run: ' : "<b>Script running time</b>: ") . $running_time . " s";
// reset start & end time after run

View File

@@ -21,7 +21,7 @@ class Support
*/
public static function printTime(int $set_microtime = -1): string
{
list($microtime, $timestamp) = explode(' ', microtime());
[$microtime, $timestamp] = explode(' ', microtime());
$string = date("Y-m-d H:i:s", (int)$timestamp);
// if microtime flag is -1 no round, if 0, no microtime, if >= 1, round that size
if ($set_microtime == -1) {
@@ -34,31 +34,43 @@ class Support
}
/**
* prints a html formatted (pre) array
* prints a html formatted (pre) data
*
* @param array<mixed> $array any array
* @param bool $no_html set to true to use ##HTMLPRE##
* @return string formatted array for output with <pre> tag added
* @param mixed $data any data
* @param bool $no_html default add <pre>
* @return string formatted array for output with <pre> tag added
*/
public static function printAr(array $array, bool $no_html = false): string
public static function printAr(mixed $data, bool $no_html = false): string
{
if ($no_html === false) {
return "<pre>" . print_r($array, true) . "</pre>";
} else {
return '##HTMLPRE##' . print_r($array, true) . '##/HTMLPRE##';
}
return $no_html ?
print_r($data, true) :
'<pre>' . print_r($data, true) . '</pre>';
}
/**
* alternate name for printAr function
*
* @param array<mixed> $array any array
* @param bool $no_html set to true to use ##HTMLPRE##
* @return string formatted array for output with <pre> tag added
* @param mixed $data any array
* @param bool $no_html default add <pre>
* @return string formatted array for output with <pre> tag added
*/
public static function printArray(array $array, bool $no_html = false): string
public static function printArray(mixed $data, bool $no_html = false): string
{
return self::printAr($array, $no_html);
return self::printAr($data, $no_html);
}
/**
* A replacement for the \CoreLibs\Debug\Support::printAr
* But this does not wrap it in <pre></pre>
* Do not use this without using it in a string in debug function
* Note: for full data debug dumps use Support::dumpVar()
*
* @param mixed $data Data to print
* @return string print_r formated
*/
public static function prAr(mixed $data): string
{
return self::printAr($data, true);
}
/**
@@ -66,21 +78,42 @@ class Support
* if $name is set prefix with nae
* default true: true, false: false
*
* @param bool $bool Variable to convert
* @param string $name [default: ''] Prefix name
* @param string $true [default: true] True string
* @param string $false [default: false] False string
* @return string String with converted bool text for debug
* @param bool $bool Variable to convert
* @param string $name [=''] Prefix name
* @param string $true [='true'] True string
* @param string $false [='false'] False string
* @param bool $no_html [=false] if true do not print html
* @return string String with converted bool text for debug
*/
public static function printBool(
bool $bool,
string $name = '',
string $true = 'true',
string $false = 'false',
bool $no_html = false,
): string {
return
(!empty($name) ?
($no_html ?
$name : '<b>' . $name . '</b>') . ': '
: '')
. ($bool ? $true : $false);
}
/**
* Convert bool value to string value. Short name alias for printBool
*
* @param bool $bool Bool value to be transformed
* @param string $true [='true'] Override default string 'true'
* @param string $false [=false'] Override default string 'false'
* @return string $true or $false string for true/false bool
*/
public static function prBl(
bool $bool,
string $true = 'true',
string $false = 'false'
): string {
$string = (!empty($name) ? '<b>' . $name . '</b>: ' : '')
. ($bool ? $true : $false);
return $string;
return self::printBool($bool, '', $true, $false, true);
}
/**
@@ -89,9 +122,10 @@ class Support
* if object return get_class
* for array use printAr function, can be controlled with no_html for
* Debug\Logging compatible output
* Recommended to use Support::dumpVar()
*
* @param mixed $mixed
* @param bool $no_html set to true to use ##HTMLPRE##or html escape
* @param bool $no_html set to true to strip <pre> tags
* @return string
*/
public static function printToString(mixed $mixed, bool $no_html = false): string
@@ -120,34 +154,148 @@ class Support
}
/**
* if there is a need to find out which parent method called a child method,
* eg for debugging, this function does this
* Dumps var data and returns it as string
* var_dump based
* Recommended debug output
*
* call this method in the child method and you get the parent function that called
* @param int $level debug level, default 1
* @return ?string null or the function that called the function
* where this method is called
* @param mixed $data Anything
* @param bool $no_html [=false] If true strip all html tags
* (for text print)
* @return string A text string
*/
public static function getCallerMethod(int $level = 1): ?string
public static function dumpVar(
mixed $data,
bool $no_html = false,
): string {
// dump data
ob_start();
var_dump($data);
$debug_dump = ob_get_clean() ?: '[FAILED TO GET var_dump() data]';
// check if the original caller is dV, if yes, up the caller level for
// the file line get by 1, so we get file + pos from the dV call and
// not this call
$caller_level = 1;
$caller_list = self::getCallerMethodList();
if ($caller_list[0] == 'dV') {
$caller_level = 2;
}
// we need to strip the string in <small></small that is
// "path ... CoreLibs/Debug/Support.php:<number>:
// and replace it with the caller methods and location
$caller_file_number = self::getCallerFileLine($caller_level);
$debug_dump = preg_replace(
'|<small>(/.*:\d+:)</small>|',
'<small>' . $caller_file_number . ':</small>',
$debug_dump
) ?? $debug_dump; // in case of failure keep original
// if strip is ture, remove all HTML tags and convert any html entities back
return $no_html ?
str_replace(
// things to replace in the string if set
['&gt;', '&lt;', '&#13;', '&#10;'],
['>', '<', "\r", "\n"],
strip_tags($debug_dump)
) :
$debug_dump;
}
/**
* exports (dumps) var, in more printable design, but without detail info
*
* @param mixed $data Anything
* @param bool $no_html [=false] If true true do not add <pre> tags
* @return string A text string
*/
public static function exportVar(mixed $data, bool $no_html = false): string
{
return $no_html ?
var_export($data, true) :
'<pre>' . var_export($data, true) . '</pre>';
}
/**
* Return file name and line number where this was called
* One level up
*
* @param int $level [=1] trace level
* @return string|null null or file name:line number
*/
public static function getCallerFileLine(int $level = 1): ?string
{
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print \CoreLibs\Debug\Support::printAr($traces);
// We should check from top down if unset?
// sets the start point here, and in level two (the sub call) we find this
if (isset($traces[$level])) {
return ($traces[$level]['file'] ?? $traces[$level]['function'])
. ':' . ($traces[$level]['line'] ?? '-');
}
return null;
}
/**
* if there is a need to find out which parent method called a child method,
* eg for debugging, this function does this
*
* call this method in the child method and you get the parent function that called
* @param int $level [=1] trace level
* @return string|null null or the function that called the function
* where this method is called
*/
public static function getCallerMethod(int $level = 1): ?string
{
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print "getCallerMethod:<br>" . \CoreLibs\Debug\Support::printAr($traces);
// We should check from top down if unset?
// sets the start point here, and in level two (the sub call) we find this
if (isset($traces[$level])) {
return $traces[$level]['function'];
}
return null;
}
/**
* get the class that first called it and skip the base class
* Companion method to getCallerMethod
*
* @param int $level [=1] trace level
* @return ?string null if class not found
*/
public static function getCallerClass(int $level = 1): ?string
{
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print "getCallerClass:<br>" . \CoreLibs\Debug\Support::printAr($traces);
if (isset($traces[$level])) {
return $traces[$level]['class'] ?? null;
}
return null;
}
/**
* Returns class and method together
*
* @param int $level [=1] travel level
* @return string|null null if trace level not found, else namespace class and method
*/
public static function getCallerClassMethod(int $level = 1): ?string
{
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print "getCallerClass:<br>" . \CoreLibs\Debug\Support::printAr($traces);
if (isset($traces[$level])) {
return ($traces[$level]['class'] ?? '-')
. ($traces[$level]['type'] ?? '')
. $traces[$level]['function'];
}
return null;
}
/**
* Returns array with all methods in the call stack in the order so that last
* called is last in order
* Will start with start_level to skip unwanted from stack
* Defaults to skip level 0 wich is this methid
*
* @param integer $start_level From what level on, as defaul starts with 1
* to exclude self
* @param integer $start_level [=1] From what level on, starts with 1 to exclude self
* @return array<mixed> All method names in list where max is last called
*/
public static function getCallerMethodList(int $start_level = 1): array
@@ -155,39 +303,66 @@ class Support
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$methods = [];
foreach ($traces as $level => $data) {
if ($level >= $start_level) {
if (!empty($data['function'])) {
array_unshift($methods, $data['function']);
}
if ($level < $start_level) {
continue;
}
if (!empty($data['function'])) {
array_unshift($methods, $data['function']);
}
}
return $methods;
}
/**
* Get the full call stack from a certain starting level
* The return string is
* file:line:class->method
*
* Note that '::' is used for static calls
*
* @param int $start_level [=1] starts with 1 to exclude itself
* @return array<string> string with file, line, class and method
*/
public static function getCallStack(int $start_level = 1): array
{
$call_stack = [];
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
foreach ($backtrace as $level => $call_trace) {
if ($level < $start_level) {
continue;
}
$call_stack[] =
($call_trace['file'] ?? 'n/f') . ':'
. ($call_trace['line'] ?? '-') . ':'
. (!empty($call_trace['class']) ?
$call_trace['class'] . ($call_trace['type'] ?? '') :
''
)
. $call_trace['function'];
}
return $call_stack;
}
/**
* Get the current class where this function is called
* Is mostly used in debug log statements to get the class where the debug
* was called
* gets top level class
* loops over the debug backtrace until if finds the first class (from the end)
* loops over the debug backtrace until if finds the first class (from the end)
*
* @return string Class name with namespace
*/
public static function getCallerClass(): string
public static function getCallerTopLevelClass(): string
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// ?? [['class' => get_called_class()]];
// TODO make sure that this doesn't loop forver
$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// print "getCallerClass:<br>" . \CoreLibs\Debug\Support::printAr($traces);
$class = null;
while ($class === null && count($backtrace) > 0) {
// if current is
// [function] => debug
// [class] => CoreLibs\Debug\Logging
// then return
// (OUTSIDE) because it was not called from a class method
// or return file name
$get_class = array_pop($backtrace);
$class = $get_class['class'] ?? null;
// reverse and stop at first set class, this is the top level one
foreach (array_reverse($traces) as $trace) {
$class = $trace['class'] ?? null;
if (!empty($class)) {
break;
}
}
// on null or empty return empty string
return empty($class) ? '' : $class;

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

@@ -54,14 +54,15 @@ class System
/**
* get the host name without the port as given by the SELF var
* if no host name found will set to NOHOST:0
*
* @return array<mixed> host name/port name
* @return array{string,int} host name/port number
*/
public static function getHostName(): array
{
$host = $_SERVER['HTTP_HOST'] ?? 'NOHOST:NOPORT';
list($host_name, $port) = array_pad(explode(':', $host), 2, self::DEFAULT_PORT);
return [$host_name, $port];
$host = $_SERVER['HTTP_HOST'] ?? 'NOHOST:0';
[$host_name, $port] = array_pad(explode(':', $host), 2, self::DEFAULT_PORT);
return [$host_name, (int)$port];
}
/**
@@ -115,6 +116,29 @@ class System
3
) === 'cli' ? true : false;
}
/**
* Collect all IP addresses
* REMOTE_ADDR, HTTP_X_FORWARD_FOR, CLIENT_IP
* and retuns them in an array with index of io source
* if address source has addresses with "," will add "-array" with these as array block
*
* @return array<string,string|array<string>>
*/
public static function getIpAddresses(): array
{
$ip_addr = [];
foreach (['REMOTE_ADDR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP'] as $_ip_source) {
if (!empty($_SERVER[$_ip_source])) {
$ip_addr[$_ip_source] = $_SERVER[$_ip_source];
// same level as ARRAY IF there is a , inside
if (strstr($_SERVER[$_ip_source], ',') !== false) {
$ip_addr[$_ip_source . '-array'] = explode(',', $_SERVER[$_ip_source]);
}
}
}
return $ip_addr;
}
}
// __END__

View File

@@ -29,9 +29,9 @@ namespace CoreLibs\Language\Core;
class CachedFileReader extends \CoreLibs\Language\Core\StringReader
{
/** @var int */
public $error = 0;
public int $error = 0;
/** @var string */
public $fd_str = '';
public string $fd_str = '';
/**
* Undocumented function
@@ -46,7 +46,7 @@ class CachedFileReader extends \CoreLibs\Language\Core\StringReader
if (!is_resource($fd)) {
$this->error = 3; // Cannot read file, probably permissions
} else {
$this->fd_str = fread($fd, filesize($filename) ?: 0) ?: '';
$this->fd_str = fread($fd, filesize($filename) ?: 1) ?: '';
fclose($fd);
}
} else {

View File

@@ -27,13 +27,13 @@ namespace CoreLibs\Language\Core;
class FileReader
{
/** @var int */
public $fr_pos;
public int $fr_pos;
/** @var resource|bool */
public $fr_fd;
public mixed $fr_fd; // no resource type yet
/** @var int */
public $fr_length;
public int $fr_length;
/** @var int */
public $error = 0;
public int $error = 0;
/**
* file read constructor

View File

@@ -41,31 +41,31 @@ class GetTextReader
{
// public:
/** @var int */
public $error = 0; // public variable that holds error code (0 if no error)
public int $error = 0; // public variable that holds error code (0 if no error)
// private:
/** @var int */
private $BYTEORDER = 0; // 0: low endian, 1: big endian
private int $BYTEORDER = 0; // 0: low endian, 1: big endian
/** @var FileReader */
private $STREAM;
private FileReader $STREAM;
/** @var bool */
private $short_circuit = false;
private bool $short_circuit = false;
/** @var bool */
private $enable_cache = false;
private bool $enable_cache = false;
/** @var int */
private $originals = 0; // offset of original table
private int $originals = 0; // offset of original table
/** @var int */
private $translations = 0; // offset of translation table
private int $translations = 0; // offset of translation table
/** @var string */
private $pluralheader = ''; // cache header field for plural forms
private string $pluralheader = ''; // cache header field for plural forms
/** @var int */
private $total = 0; // total string count
private int $total = 0; // total string count
/** @var array<mixed>|null */
private $table_originals = null; // table for original strings (offsets)
private array|null $table_originals = null; // table for original strings (offsets)
/** @var array<mixed>|null */
private $table_translations = null; // table for translated strings (offsets)
private array|null $table_translations = null; // table for translated strings (offsets)
/** @var array<mixed> */
private $cache_translations = []; // original -> translation mapping
private array $cache_translations = []; // original -> translation mapping
/* Methods */
@@ -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

@@ -27,9 +27,9 @@ namespace CoreLibs\Language\Core;
class StringReader
{
/** @var int */
public $sr_pos;
public int $sr_pos;
/** @var string */
public $sr_str;
public string $sr_str;
/**
* constructor for string reader

View File

@@ -128,7 +128,7 @@ class GetLocale
$matches
)
) {
$lang = ($matches['lang'] ?? 'en')
$lang = $matches['lang']
// add country only if set
. (!empty($matches['country']) ? '_' . $matches['country'] : '');
} else {
@@ -235,7 +235,7 @@ class GetLocale
$matches
)
) {
$lang = ($matches['lang'] ?? 'en')
$lang = $matches['lang']
// add country only if set
. (!empty($matches['country']) ? '_' . $matches['country'] : '');
} else {

View File

@@ -35,42 +35,42 @@ class L10n
/** @var string the default fallback encoding if nothing is set */
public const DEFAULT_CHARSET = 'UTF-8';
/** @var string the current locale */
private $locale = '';
private string $locale = '';
/** @var string the SET locale as WHERE the domain file is */
private $locale_set = '';
private string $locale_set = '';
/** @var string the default selected/active domain */
private $domain = '';
private string $domain = '';
/** @var string encoding, as from locale or set from outside */
private $override_encoding = self::DEFAULT_CHARSET;
private string $override_encoding = self::DEFAULT_CHARSET;
/** @var string encoding set during the parse Locale */
private $encoding = '';
private string $encoding = '';
/** @var array<string,array<string,GetTextReader>> locale > domain = translator */
private $domains = [];
private array $domains = [];
/** @var array<string,string> bound paths for domains */
private $paths = ['' => './'];
private array $paths = ['' => './'];
// files
/** @var string the full path to the mo file to loaded */
private $mofile = '';
private string $mofile = '';
/** @var string base path to search level */
private $base_locale_path = '';
private string $base_locale_path = '';
/** @var string dynamic set path to where the mo file is actually */
private $base_content_path = '';
private string $base_content_path = '';
// errors
/** @var bool if load of mo file was unsuccessful */
private $load_failure = false;
private bool $load_failure = false;
// object holders
/** @var FileReader|bool reader class for file reading, false for short circuit */
private $input = false;
private FileReader|bool $input = false;
/** @var GetTextReader reader class for MO data */
private $l10n;
private GetTextReader|null $l10n = null;
/**
* @static
* @var L10n self class
*/
private static $instance;
private static L10n $instance;
/**
* class constructor call for language getstring
@@ -124,7 +124,6 @@ class L10n
*/
public static function getInstance(): L10n
{
/** @phpstan-ignore-next-line */
if (empty(self::$instance)) {
self::$instance = new self();
}
@@ -253,6 +252,13 @@ class L10n
// dummy
$this->l10n = new GetTextReader($this->input);
}
// if this is still null here, we abort
if ($this->l10n === null) {
throw new \RuntimeException(
"Could not create CoreLibs\Language\Core\GetTextReader object",
E_USER_ERROR
);
}
return $this->l10n;
}
@@ -673,6 +679,7 @@ class L10n
// fallback passthrough
if ($this->l10n === null) {
echo $text;
return;
}
echo $this->l10n->translate($text);
}

View File

@@ -0,0 +1,369 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/9/7
* DESCRIPTION:
* General error collection class for output to frontend or to log
*/
declare(strict_types=1);
namespace CoreLibs\Logging;
use CoreLibs\Logging\Logger\MessageLevel;
class ErrorMessage
{
/** @var array<int,array{id:string,level:string,str:string,target:string,target_style:string,highlight:string[]}> */
private array $error_str = [];
/** @var array<string,array{info:string,level:string}> */
private array $jump_targets = [];
/** @var \CoreLibs\Logging\Logging $log */
public \CoreLibs\Logging\Logging $log;
/** @var bool $log_error global flag to log error level message */
private bool $log_error = false;
/** @var bool $log_warning global flat to log warning level messages */
private bool $log_warning = false;
/**
* init ErrorMessage
*
* @param \CoreLibs\Logging\Logging $log
* @param null|bool $log_error [=null], defaults to false if log is not level debug
* @param null|bool $log_warning [=null], defaults to false if log is not level debug
*/
public function __construct(
\CoreLibs\Logging\Logging $log,
?bool $log_error = null,
?bool $log_warning = null
) {
$this->log = $log;
// if log default logging is debug then log_error is default set to true
if ($this->log->loggingLevelIsDebug() && $log_error === null) {
$log_error = true;
} else {
$log_error = $log_error ?? false;
}
$this->log_error = $log_error;
// if log default logging is debug then log_warning is default set to true
if ($this->log->loggingLevelIsDebug() && $log_warning === null) {
$log_warning = true;
} else {
$log_warning = $log_warning ?? false;
}
$this->log_warning = $log_warning;
}
/**
* pushes new error message into the error_str array
* error_id: internal Error ID (should be unique)
* level: error level, can only be ok, info, warn, error, abort, crash
* ok and info are positive response: success
* notice: a debug message for information only
* warn: success, but there might be some things that are not 100% ok
* error: input error or error in executing request
* abort: an internal error happened as mandatory information that normally is
* there is missing, or the ACL level that should normally match does not
* will be logged to "critical"
* crash: system failure or critical system problems (db connection failure)
* will be logged as "alert"
* not set: unkown, will be logged as "emergency"
* target/highlight: id target name for frontend where to attach this message
* highlight is a list of other target points to highlight
* for highlight targets css names are $level without a prefix and should be
* nested in the target element "input .error { ... }"
* jump_target: a target id for to jump and message, is stored in separate jump array
* where the target is unique, first one set is used for info message
* target_style: if not set uses 'error-' $level as css style. applies to targets or main only
*
* @param string $error_id Any internal error ID for this error
* @param string $level Error level in ok/info/warn/error
* @param string $str Error message (out)
* @param string $target alternate attachment point for this error message
* @param string $target_style Alternate color style for the error message
* @param array<string> $highlight Any additional error data as error OR
* highlight points for field highlights
* @param array{}|array{target:string,info?:string} $jump_target with "target" for where to jump and
* "info" for string to show in jump list
* target must be set, if info not set, default message used
* @param string|null $message If abort/crash, non localized $str
* @param array<mixed> $context Additionl info for abort/crash messages
* @param bool|null $log_error [=null] log level 'error' to error, if null use global,
* else set for this call only
* @param bool|null $log_warning [=null] log level 'warning' to warning, if null use global,
* else set for this call only
*/
public function setErrorMsg(
string $error_id,
string $level,
string $str,
string $target = '',
string $target_style = '',
array $highlight = [],
array $jump_target = [],
?string $message = null,
array $context = [],
?bool $log_error = null,
?bool $log_warning = null,
): void {
if ($log_error === null) {
$log_error = $this->log_error;
}
if ($log_warning === null) {
$log_warning = $this->log_warning;
}
$original_level = $level;
$level = MessageLevel::fromName($level)->name;
// if not string set, write message string if set, else level/error id
if (empty($str)) {
$str = $message ?? 'L:' . $level . '|E:' . $error_id;
}
$this->error_str[] = [
'id' => $error_id,
'level' => $level,
'str' => $str,
'target' => $target,
'target_style' => $target_style,
'highlight' => $highlight,
];
// set a jump target
$this->setJumpTarget($jump_target['target'] ?? null, $jump_target['info'] ?? null, $level);
// write to log for abort/crash
switch ($level) {
case 'notice':
$this->log->notice($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
break;
case 'warn':
if ($log_warning) {
$this->log->warning($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
}
break;
case 'error':
if ($log_error) {
$this->log->error($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
}
break;
case 'abort':
$this->log->critical($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
break;
case 'crash':
$this->log->alert($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
break;
case 'unknown':
$this->log->emergency($message ?? $str, array_merge([
'id' => $error_id,
'level' => $original_level,
], $context));
break;
}
}
/**
* pushes new error message into the error_str array
* Note, the parameter order is different and does not need an error id
* This is for backend alerts
*
* @param string $level error level (ok/warn/info/error)
* @param string $str error string
* @param string|null $error_id optional error id for precise error lookup
* @param string $target Alternate id name for output target on frontend
* @param string $target_style Alternate color style for the error message
* @param array<string> $highlight Any additional error data as error OR
* highlight points for field highlights
* @param array{}|array{target:string,info?:string} $jump_target with "target" for where to jump and
* "info" for string to show in jump list
* target must be set, if info not set, default message used
* @param string|null $message If abort/crash, non localized $str
* @param array<mixed> $context Additionl info for abort/crash messages
* @param bool|null $log_error [=null] log level 'error' to error, if null use global,
* else set for this call only
* @param bool|null $log_warning [=null] log level 'warning' to warning, if null use global,
* else set for this call only
*/
public function setMessage(
string $level,
string $str,
?string $error_id = null,
string $target = '',
string $target_style = '',
array $highlight = [],
array $jump_target = [],
?string $message = null,
array $context = [],
?bool $log_error = null,
?bool $log_warning = null,
): void {
$this->setErrorMsg(
$error_id ?? '',
$level,
$str,
$target,
$target_style,
$highlight,
$jump_target,
$message,
$context,
$log_error,
$log_warning
);
}
/**
* Set a jump target. This can be used to jump directly a frontend html block
* with the target id set
*
* @param string|null $target
* @param string|null $info
* @param string $level [='error']
* @return void
*/
public function setJumpTarget(
?string $target,
?string $info,
string $level = 'error',
): void {
if (
empty($target) ||
array_key_exists($target, $this->jump_targets)
// !empty($this->jump_targets[$target])
// also check if this is an alphanumeric string? css id compatible?
) {
return;
}
if (empty($info)) {
$info = 'Jump to: ' . $target;
}
$level = MessageLevel::fromName($level)->name;
$this->jump_targets[$target] = [
'info' => $info,
'level' => $level,
];
}
// *********************************************************************
// GETTERS
// *********************************************************************
/**
* Returns the current set error content from setErrorMsg method
*
* @return array<int,array{id:string,level:string,str:string,target:string,highlight:string[]}> Error messages array
*/
public function getErrorMsg(): array
{
return $this->error_str;
}
/**
* Current set error ids
*
* @return array<string>
*/
public function getErrorIds(): array
{
return array_column($this->error_str, 'id');
}
/**
* Gets the LAST entry in the array list.
* If nothing found returns empty array set
*
* @return array{id:string,level:string,str:string,target:string,target:string,highlight:string[]} Error block
*/
public function getLastErrorMsg(): array
{
return $this->error_str[array_key_last($this->error_str)] ?? [
'level' => '',
'str' => '',
'id' => '',
'target' => '',
'target_string' => '',
'highlight' => [],
];
}
/**
* Return the jump target list
*
* @return array{}|array<int,array{target:string,info:string,level:string}> List of jump targets with info text,
* or empty array if not set
*/
public function getJumpTarget(): array
{
$_jump_target = [];
foreach ($this->jump_targets as $target => $jump) {
$_jump_target[] = array_merge(
$jump,
[
'target' => $target,
]
);
}
return $_jump_target;
}
// *********************************************************************
// FLAG SETTERS
// *********************************************************************
/**
* Set the log error flag
*
* @param bool $flag True to log level error too, False for do not (Default)
* @return void
*/
public function setFlagLogError(bool $flag): void
{
$this->log_error = $flag;
}
/**
* Get the current log error flag
*
* @return bool
*/
public function getFlagLogError(): bool
{
return $this->log_error;
}
/**
* Set the log warning flag
*
* @param bool $flag True to log level warning too, False for do not (Default)
* @return void
*/
public function setFlagLogWarning(bool $flag): void
{
$this->log_warning = $flag;
}
/**
* Get the current log error flag
*
* @return bool
*/
public function getFlagLogWarning(): bool
{
return $this->log_warning;
}
}
// __END__

107
src/Logging/Logger/Flag.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/5/29
* DESCRIPTION:
* Logging options flags for output file name building
*
* per_run: and timestamp + uid will be added
* per_date: ymd will be added (per_run > per_date, cannot be used at the same time)
* per_group: for debug level, group per group id (old level)
* per_page: per file name logging
* per_class: log per class
* per_level: per logging level file split
*/
declare(strict_types=1);
namespace CoreLibs\Logging\Logger;
enum Flag: int
{
/** all off flag */
case all_off = 0;
/** write per run */
case per_run = 1;
/** write per date */
case per_date = 2;
/** was PER_LEVEL, write per group id (debug) */
case per_group = 4;
/** write per page (filename) */
case per_page = 8;
/** write per class */
case per_class = 16;
/** write per log level name */
case per_level = 32;
/**
* get internal name from string value
*
* @param non-empty-string $name
* @return self
*/
public static function fromName(string $name): self
{
return match ($name) {
'Run', 'run', 'per_run', 'PER_RUN' => self::per_run,
'Date', 'date', 'per_date', 'PER_DATE' => self::per_date,
'Group', 'group', 'per_group', 'PER_GROUP' => self::per_group,
'Page', 'page', 'per_page', 'PER_PAGE' => self::per_page,
'Class', 'class', 'per_class', 'PER_CLASS' => self::per_class,
'Level', 'level', 'per_level', 'PER_LEVEL' => self::per_level,
default => self::all_off,
};
}
/**
* Get internal name from int value
*
* @param int $value
* @return self
*/
public static function fromValue(int $value): self
{
return self::from($value);
}
/**
* convert current set level to name (upper case)
*
* @return string
*/
public function getName(): string
{
return strtoupper($this->name);
}
/** @var int[] */
public const VALUES = [
0,
1,
2,
4,
8,
16,
32,
];
/** @var string[] */
public const NAMES = [
'ALL_OFF',
'PER_RUN',
'PER_DATE',
'PER_GROUP',
'PER_PAGE',
'PER_CLASS',
'PER_LEVEL',
];
}
// __END__

View File

@@ -0,0 +1,231 @@
<?php // phpcs:disable Generic.Files.LineLength
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023-05-25
* DESCRIPTION:
* Debug levels
*
* They are based on the Mono log ones
* FC 5424 {@see https://datatracker.ietf.org/doc/html/rfc5424}
*/
declare(strict_types=1);
namespace CoreLibs\Logging\Logger;
use Psr\Log\LogLevel;
/**
* Represents the log levels
*
* Monolog supports the logging levels described by RFC 5424 {@see https://datatracker.ietf.org/doc/html/rfc5424}
* but due to BC the severity values used internally are not 0-7.
*
* To get the level name/value out of a Level there are several options:
*
* - Use ->getName() to get the standard Monolog name which is full uppercased (e.g. "DEBUG")
* - Use ->toPsrLogLevel() to get the standard PSR-3 name which is full lowercased (e.g. "debug")
* - Use ->toRFC5424Level() to get the standard RFC 5424 value (e.g. 7 for debug, 0 for emergency)
* - Use ->name to get the enum case's name which is capitalized (e.g. "Debug")
*
* To get the internal value for filtering, if the includes/isLowerThan/isHigherThan methods are
* not enough, you can use ->value to get the enum case's integer value.
*/
enum Level: int
{
/**
* Detailed debug information
*/
case Debug = 100;
/**
* Interesting events
*
* Examples: User logs in, SQL logs.
*/
case Info = 200;
/**
* Uncommon events
*/
case Notice = 250;
/**
* Exceptional occurrences that are not errors
*
* Examples: Use of deprecated APIs, poor use of an API,
* undesirable things that are not necessarily wrong.
*/
case Warning = 300;
/**
* Runtime errors
*/
case Error = 400;
/**
* Critical conditions
*
* Example: Application component unavailable, unexpected exception.
*/
case Critical = 500;
/**
* Action must be taken immediately
*
* Example: Entire website down, database unavailable, etc.
* This should trigger the SMS alerts and wake you up.
*/
case Alert = 550;
/**
* Urgent alert.
*/
case Emergency = 600;
/**
* @param value-of<self::NAMES>|LogLevel::*|'Debug'|'Info'|'Notice'|'Warning'|'Error'|'Critical'|'Alert'|'Emergency' $name
* @return static
*/
public static function fromName(string $name): self
{
return match ($name) {
'debug', 'Debug', 'DEBUG' => self::Debug,
'info', 'Info', 'INFO' => self::Info,
'notice', 'Notice', 'NOTICE' => self::Notice,
'warning', 'Warning', 'WARNING' => self::Warning,
'error', 'Error', 'ERROR' => self::Error,
'critical', 'Critical', 'CRITICAL' => self::Critical,
'alert', 'Alert', 'ALERT' => self::Alert,
'emergency', 'Emergency', 'EMERGENCY' => self::Emergency,
};
}
/**
* @param value-of<self::VALUES> $value
* @return static
*/
public static function fromValue(int $value): self
{
return self::from($value);
}
/**
* Returns true if the passed $level is higher or equal to $this
*
* @param Level $level
* @return bool
*/
public function includes(Level $level): bool
{
return $this->value <= $level->value;
}
/**
* If level is higher than set one
*
* @param Level $level
* @return bool
*/
public function isHigherThan(Level $level): bool
{
return $this->value > $level->value;
}
/**
* if level is lower than set one
*
* @param Level $level
* @return bool
*/
public function isLowerThan(Level $level): bool
{
return $this->value < $level->value;
}
/**
* Returns the monolog standardized all-capitals name of the level
*
* Use this instead of $level->name which returns the enum case name (e.g. Debug vs DEBUG if you use getName())
*
* @phan-suppress-next-line PhanTypeMismatchDeclaredReturn
* @return value-of<self::NAMES>
*/
public function getName(): string
{
return match ($this) {
self::Debug => 'DEBUG',
self::Info => 'INFO',
self::Notice => 'NOTICE',
self::Warning => 'WARNING',
self::Error => 'ERROR',
self::Critical => 'CRITICAL',
self::Alert => 'ALERT',
self::Emergency => 'EMERGENCY',
};
}
/**
* Returns the PSR-3 level matching this instance
*
* @phpstan-return \Psr\Log\LogLevel::*
*/
public function toPsrLogLevel(): string
{
return match ($this) {
self::Debug => LogLevel::DEBUG,
self::Info => LogLevel::INFO,
self::Notice => LogLevel::NOTICE,
self::Warning => LogLevel::WARNING,
self::Error => LogLevel::ERROR,
self::Critical => LogLevel::CRITICAL,
self::Alert => LogLevel::ALERT,
self::Emergency => LogLevel::EMERGENCY,
};
}
/**
* Returns the RFC 5424 level matching this instance
*
* @phpstan-return int<0, 7>
*/
public function toRFC5424Level(): int
{
return match ($this) {
self::Debug => 7,
self::Info => 6,
self::Notice => 5,
self::Warning => 4,
self::Error => 3,
self::Critical => 2,
self::Alert => 1,
self::Emergency => 0,
};
}
public const VALUES = [
100,
200,
250,
300,
400,
500,
550,
600,
];
public const NAMES = [
'DEBUG',
'INFO',
'NOTICE',
'WARNING',
'ERROR',
'CRITICAL',
'ALERT',
'EMERGENCY',
];
}
// __END__

View File

@@ -0,0 +1,88 @@
<?php // phpcs:disable Generic.Files.LineLength
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023-09-08
* DESCRIPTION:
* Error message return levels
*/
declare(strict_types=1);
namespace CoreLibs\Logging\Logger;
enum MessageLevel: int
{
case ok = 100;
case success = 150; // special for file uploads
case info = 200;
case notice = 250;
case warn = 300;
case error = 400;
case abort = 500;
case crash = 550;
case unknown = 600;
/**
* @param string $name any string name, if not matching use unkown
* @return static
*/
public static function fromName(string $name): self
{
return match (strtolower($name)) {
'ok' => self::ok,
'success' => self::success,
'info' => self::info,
'notice' => self::notice,
'warn', 'warning' => self::warn,
'error' => self::error,
'abort' => self::abort,
'crash' => self::crash,
default => self::unknown,
};
}
/**
* @param int $value
* @return static
*/
public static function fromValue(int $value): self
{
return self::tryFrom($value) ?? self::unknown;
}
/**
* Returns true if the passed $level is higher or equal to $this
*
* @param MessageLevel $level
* @return bool
*/
public function includes(MessageLevel $level): bool
{
return $this->value <= $level->value;
}
/**
* If level is higher than set one
*
* @param MessageLevel $level
* @return bool
*/
public function isHigherThan(MessageLevel $level): bool
{
return $this->value > $level->value;
}
/**
* if level is lower than set one
*
* @param MessageLevel $level
* @return bool
*/
public function isLowerThan(MessageLevel $level): bool
{
return $this->value < $level->value;
}
}
// __END__

1405
src/Logging/Logging.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -194,13 +194,13 @@ class Elements
"/(mailto:)?(\>)?\b([\w\.-]+)@([\w\.\-]+)\.([a-zA-Z]{2,4})\b(\|([^\||^#]+)(#([^\|]+))?\|)?/",
function ($matches) {
return self::createEmail(
$matches[1] ?? '',
$matches[2] ?? '',
$matches[3] ?? '',
$matches[4] ?? '',
$matches[5] ?? '',
$matches[1],
$matches[2],
$matches[3],
$matches[4],
$matches[5],
$matches[7] ?? '',
$matches[9] ?? ''
$matches[9] ?? '',
);
},
$output

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditAccess implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditGroups implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditLanguages implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditMenuGroup implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CoreLibs\Output\Form\TableArrays;
class EditOrder implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor
* @param \CoreLibs\Output\Form\Generate $form base form class
*/
public function __construct(\CoreLibs\Output\Form\Generate $form)
{
$this->form = $form;
$this->form->log->debug('CLASS LOAD', __NAMESPACE__ . __CLASS__);
}
/**
* NOTE: this is a dummy array to just init the Form\Generate class and is not used for anything else
*
* @return array<mixed>
*/
public function setTableArray(): array
{
return [
'table_array' => [
'-'
],
'table_name' => '-',
'load_query' => '',
'show_fields' => [],
];
}
}
// __END__

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditPages implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor
@@ -53,6 +53,7 @@ class EditPages implements Interface\TableArraysInterface
'value' => $_POST['name'] ?? '',
'output_name' => 'Page name',
'mandatory' => 1,
'error_check' => 'unique',
'type' => 'text'
],
'order_number' => [

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditSchemas implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditUsers implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor
@@ -135,30 +135,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',

View File

@@ -7,7 +7,7 @@ namespace CoreLibs\Output\Form\TableArrays;
class EditVisibleGroup implements Interface\TableArraysInterface
{
/** @var \CoreLibs\Output\Form\Generate */
private $form;
private \CoreLibs\Output\Form\Generate $form;
/**
* constructor

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

@@ -23,7 +23,8 @@ class Image
* if empty ROOT is choosen
* @param string $cache_source cache path, if not given TMP is used
* @param bool $clear_cache if set to true, will create thumb all the tame
* @return string|false thumbnail name, or false for error
* @return string thumbnail name
* @throws \RuntimeException no ImageMagick convert command found
*/
public static function createThumbnail(
string $pic,
@@ -33,14 +34,15 @@ class Image
string $path = '',
string $cache_source = '',
bool $clear_cache = false
): string|false {
): string {
// get image type flags
$image_types = [
0 => 'UNKOWN-IMAGE',
1 => 'gif',
2 => 'jpg',
3 => 'png'
];
$return_data = false;
$return_data = '';
$CONVERT = '';
// if CONVERT is not defined, abort
/** @phan-suppress-next-line PhanUndeclaredConstant */
@@ -48,7 +50,7 @@ class Image
/** @phan-suppress-next-line PhanUndeclaredConstant */
$CONVERT = CONVERT;
} else {
return $return_data;
throw new \RuntimeException('CONVERT set binary is not executable or CONVERT is not defined');
}
if (!empty($cache_source)) {
$tmp_src = $cache_source;
@@ -68,72 +70,7 @@ class Image
$pic = $tmp[(count($tmp) - 1)];
}
// does this picture exist and is it a picture
if (file_exists($filename) && is_file($filename)) {
[$width, $height, $type] = getimagesize($filename) ?: [];
$convert_prefix = '';
$create_file = false;
$delete_filename = '';
// check if we can skip the PDF creation: if we have size, if do not have type, we assume type png
if (!$type) {
$check_thumb = $tmp_src . 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[3];
if (!is_file($check_thumb)) {
$create_file = true;
} else {
$type = 3;
}
}
// if type is not in the list, but returns as PDF, we need to convert to JPEG before
if (!$type) {
$output = [];
$return = null;
// is this a PDF, if no, return from here with nothing
$convert_prefix = 'png:';
# TEMP convert to PNG, we then override the file name
$convert_string = $CONVERT . ' ' . $filename . ' ' . $convert_prefix . $filename . '_TEMP';
$status = exec($convert_string, $output, $return);
$filename .= '_TEMP';
// for delete, in case we need to glob
$delete_filename = $filename;
// find file, if we can't find base name, use -0 as the first one (ignore other pages in multiple ones)
if (!is_file($filename)) {
$filename .= '-0';
}
[$width, $height, $type] = getimagesize($filename) ?: [];
}
// if no size given, set size to original
if (!$size_x || $size_x < 1) {
$size_x = $width;
}
if (!$size_y || $size_y < 1) {
$size_y = $height;
}
$thumb = 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[$type];
$thumbnail = $tmp_src . $thumb;
// check if we already have this picture converted
if (!is_file($thumbnail) || $clear_cache == true) {
// convert the picture
if ($width > $size_x) {
$convert_string = $CONVERT . ' -geometry ' . $size_x . 'x ' . $filename . ' ' . $thumbnail;
$status = exec($convert_string, $output, $return);
// get the size of the converted data, if converted
if (is_file($thumbnail)) {
[$width, $height, $type] = getimagesize($thumbnail) ?: [];
}
}
if ($height > $size_y) {
$convert_string = $CONVERT . ' -geometry x' . $size_y . ' ' . $filename . ' ' . $thumbnail;
$status = exec($convert_string, $output, $return);
}
}
if (!is_file($thumbnail)) {
copy($filename, $thumbnail);
}
$return_data = $thumb;
// if we have a delete filename, delete here with glob
if ($delete_filename) {
array_map('unlink', glob($delete_filename . '*') ?: []);
}
} else {
if (!file_exists($filename) || !is_file($filename)) {
if (!empty($dummy) && strstr($dummy, '/') === false) {
// check if we have the "dummy" image flag set
$filename = PICTURES . ICONS . strtoupper($dummy) . ".png";
@@ -141,11 +78,77 @@ class Image
if (!empty($dummy) && file_exists($filename) && is_file($filename)) {
$return_data = $filename;
} else {
$return_data = false;
throw new \RuntimeException('Could not set dummy return file: ' . $dummy . ' in ' . $filename);
}
} else {
$filename = $dummy;
$return_data = $dummy;
}
return $return_data;
}
// resize image
[$width, $height, $type] = getimagesize($filename) ?: [0, 0, 0];
$convert_prefix = '';
$create_file = false;
$delete_filename = '';
// check if we can skip the PDF creation: if we have size, if do not have type, we assume type png
if (!$type) {
$check_thumb = $tmp_src . 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[3];
if (!is_file($check_thumb)) {
$create_file = true;
} else {
$type = 3;
}
}
// if type is not in the list, but returns as PDF, we need to convert to JPEG before
if (!$type) {
$output = [];
$return = null;
// is this a PDF, if no, return from here with nothing
$convert_prefix = 'png:';
# TEMP convert to PNG, we then override the file name
$convert_string = $CONVERT . ' ' . $filename . ' ' . $convert_prefix . $filename . '_TEMP';
$status = exec($convert_string, $output, $return);
$filename .= '_TEMP';
// for delete, in case we need to glob
$delete_filename = $filename;
// find file, if we can't find base name, use -0 as the first one (ignore other pages in multiple ones)
if (!is_file($filename)) {
$filename .= '-0';
}
[$width, $height, $type] = getimagesize($filename) ?: [0, 0, 0];
}
// if no size given, set size to original
if (!$size_x || $size_x < 1) {
$size_x = $width;
}
if (!$size_y || $size_y < 1) {
$size_y = $height;
}
$thumb = 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[$type];
$thumbnail = $tmp_src . $thumb;
// check if we already have this picture converted
if (!is_file($thumbnail) || $clear_cache == true) {
// convert the picture
if ($width > $size_x) {
$convert_string = $CONVERT . ' -geometry ' . $size_x . 'x ' . $filename . ' ' . $thumbnail;
$status = exec($convert_string, $output, $return);
// get the size of the converted data, if converted
if (is_file($thumbnail)) {
[$width, $height, $type] = getimagesize($thumbnail) ?: [0, 0, 0];
}
}
if ($height > $size_y) {
$convert_string = $CONVERT . ' -geometry x' . $size_y . ' ' . $filename . ' ' . $thumbnail;
$status = exec($convert_string, $output, $return);
}
}
if (!is_file($thumbnail)) {
copy($filename, $thumbnail);
}
$return_data = $thumb;
// if we have a delete filename, delete here with glob
if ($delete_filename) {
array_map('unlink', glob($delete_filename . '*') ?: []);
}
return $return_data;
}
@@ -172,7 +175,9 @@ class Image
* set to false to not use (default true)
* to use quick but less nice version
* @param int $jpeg_quality default 80, set image quality for jpeg only
* @return string|false thumbnail with path
* @return string thumbnail with path
* @throws \UnexpectedValueException input values for filename or cache_folder are wrong
* @throws \RuntimeException convert (gd) failed
*/
public static function createThumbnailSimple(
string $filename,
@@ -184,8 +189,9 @@ class Image
bool $use_cache = true,
bool $high_quality = true,
int $jpeg_quality = 80
): string|false {
): string {
$thumbnail = false;
$exception_message = 'Could not create thumbnail';
// $this->debug('IMAGE PREPARE', "FILE: $filename (exists "
// .(string)file_exists($filename)."), WIDTH: $thumb_width, HEIGHT: $thumb_height");
if (
@@ -198,26 +204,31 @@ class Image
E_USER_DEPRECATED
);
// NOTE: we need to depracte this
$cache_folder = BASE . LAYOUT . CONTENT_PATH . CACHE . IMAGES;
$cache_folder = BASE . CONTENT_PATH . LAYOUT . CACHE . IMAGES;
$web_folder = LAYOUT . CACHE . IMAGES;
if (!is_dir($cache_folder)) {
if (false === mkdir($cache_folder)) {
$cache_folder = BASE . LAYOUT . CONTENT_PATH . CACHE;
$cache_folder = BASE . CONTENT_PATH . LAYOUT . CACHE;
$web_folder = LAYOUT . CACHE;
}
}
}
// check that input image exists and is either jpeg or png
// also fail if the basic CACHE folder does not exist at all
if (
!file_exists($filename) ||
!is_dir($cache_folder) ||
!is_writable($cache_folder)
) {
return $thumbnail;
if (!file_exists($filename)) {
// return $thumbnail;
throw new \UnexpectedValueException('Missing image file: ' . $filename);
}
if (!is_dir($cache_folder)) {
// return $thumbnail;
throw new \UnexpectedValueException('Cache folder is not a directory: ' . $cache_folder);
}
if (!is_writable($cache_folder)) {
// return $thumbnail;
throw new \UnexpectedValueException('Cache folder is not writeable: ' . $cache_folder);
}
// $this->debug('IMAGE PREPARE', "FILENAME OK, THUMB WIDTH/HEIGHT OK");
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [];
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [0, 0, null];
$thumbnail_write_path = null;
$thumbnail_web_path = null;
// path set first
@@ -245,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 &&
@@ -277,14 +288,26 @@ 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) {
return false;
throw new \RuntimeException(
'imagecreatetruecolor failed: ' . $thumbnail . ', ' . $filename,
1
);
}
if ($img_type == IMAGETYPE_PNG) {
$imagecolorallocatealpha = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
if ($imagecolorallocatealpha === false) {
return false;
throw new \RuntimeException(
'imagecolorallocatealpha failed: ' . $thumbnail . ', ' . $filename,
2
);
}
// preservere transaprency
imagecolortransparent(
@@ -346,7 +369,10 @@ class Image
imagedestroy($source);
imagedestroy($thumb);
} else {
$thumbnail = false;
throw new \RuntimeException(
'Invalid source image file. Only JPEG/PNG are allowed: ' . $filename,
3
);
}
}
} else {
@@ -360,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
@@ -379,22 +403,28 @@ 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);
if ($thumb === false) {
return false;
throw new \RuntimeException(
'imagecreatetruecolor dummy failed: ' . $thumbnail . ', ' . $filename,
3
);
}
// add outside border px = 5% (rounded up)
// eg 50px -> 2.5px
$gray = imagecolorallocate($thumb, 200, 200, 200);
$white = imagecolorallocate($thumb, 255, 255, 255);
if ($gray === false || $white === false) {
return false;
throw new \RuntimeException(
'imagecolorallocate/imagecolorallocate dummy failed: ' . $thumbnail . ', ' . $filename,
2
);
}
// fill gray background
imagefill($thumb, 0, 0, $gray);
@@ -429,7 +459,11 @@ class Image
// add web path
$thumbnail = $thumbnail_web_path . $thumbnail;
}
// either return false or the thumbnail name + output path web
// if still false -> throw exception
if ($thumbnail === false) {
throw new \RuntimeException($exception_message);
}
// else return the thumbnail name + output path web
return $thumbnail;
}
@@ -440,14 +474,22 @@ class Image
*
* @param string $filename path + filename to rotate. This file must be writeable
* @return void
* @throws \RuntimeException if exit_read_data is not found
* @throws \UnexpectedValueException if file name not writeable or file name not found
*/
public static function correctImageOrientation(string $filename): void
{
// function exists & file is writeable, else do nothing
if (!function_exists('exif_read_data') || !is_writeable($filename)) {
return;
if (!function_exists('exif_read_data')) {
throw new \RuntimeException('Function \'exit_read_data\' does not exist');
}
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [];
if (!file_exists($filename) || !is_file($filename)) {
throw new \UnexpectedValueException('Missing image file: ' . $filename);
}
if (!is_writeable($filename)) {
throw new \UnexpectedValueException('File name is not writeable: ' . $filename);
}
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [0, 0, null];
// add @ to avoid "file not supported error"
$exif = @exif_read_data($filename);
$orientation = null;

View File

@@ -23,13 +23,13 @@ class ProgressBar
// private vars
/** @var string */
public $code; // unique code
public string $code; // unique code
/** @var string */
public $status = 'new'; // current status (new,show,hide)
public string $status = 'new'; // current status (new,show,hide)
/** @var float|int */
public $step = 0; // current step
public float|int $step = 0; // current step
/** @var array<string,null|int|float> */
public $position = [ // current bar position
public array $position = [ // current bar position
'left' => null,
'top' => null,
'width' => null,
@@ -37,43 +37,43 @@ class ProgressBar
];
/** @var int */
public $clear_buffer_size = 1; // we need to send this before the lfush to get browser output
public int $clear_buffer_size = 1; // we need to send this before the lfush to get browser output
/** @var int */
public $clear_buffer_size_init = 1024 * 1024; // if I don't send that junk, it won't send anything
public int $clear_buffer_size_init = 1024 * 1024; // if I don't send that junk, it won't send anything
// public vars
/** @var int */
public $min = 0; // minimal steps
public int $min = 0; // minimal steps
/** @var int */
public $max = 100; // maximal steps
public int $max = 100; // maximal steps
/** @var int */
public $left = 5; // bar position from left
public int $left = 5; // bar position from left
/** @var int */
public $top = 5; // bar position from top
public int $top = 5; // bar position from top
/** @var int */
public $width = 300; // bar width
public int $width = 300; // bar width
/** @var int */
public $height = 25; // bar height
public int $height = 25; // bar height
/** @var int */
public $pedding = 0; // bar pedding
public int $pedding = 0; // bar pedding
/** @var string */
public $color = '#0033ff'; // bar color
public string $color = '#0033ff'; // bar color
/** @var string */
public $bgr_color = '#c0c0c0'; // bar background color
public string $bgr_color = '#c0c0c0'; // bar background color
/** @var string */
public $bgr_color_master = '#ffffff'; // master div background color
public string $bgr_color_master = '#ffffff'; // master div background color
/** @var int */
public $border = 1; // bar border width
public int $border = 1; // bar border width
/** @var string */
public $brd_color = '#000000'; // bar border color
public string $brd_color = '#000000'; // bar border color
/** @var string */
public $direction = 'right'; // direction of motion (right,left,up,down)
public string $direction = 'right'; // direction of motion (right,left,up,down)
/** @var array<string,string|bool|int> */
public $frame = ['show' => false]; // ProgressBar Frame
public array $frame = ['show' => false]; // ProgressBar Frame
/* 'show' => false, # frame show (true/false)
'left' => 200, # frame position from left
'top' => 100, # frame position from top
@@ -86,7 +86,7 @@ class ProgressBar
/** @#var array{string}{string: string|int} */
/** @var mixed[][] */
public $label = []; // ProgressBar Labels
public array $label = []; // ProgressBar Labels
/* 'name' => [ # label name
'type' => 'text', # label type (text,button,step,percent,crossbar)
'value' => 'Please wait ...', # label value
@@ -105,7 +105,7 @@ class ProgressBar
/** @var string */
// output strings
public $prefix_message = '';
public string $prefix_message = '';
/**
* progress bar constructor
@@ -156,7 +156,7 @@ class ProgressBar
{
// avoid divison through 0
if ($this->max - $this->min == 0) {
$this->max ++;
$this->max++;
}
$percent = round(($step - $this->min) / ($this->max - $this->min) * 100);
if ($percent > 100) {
@@ -186,7 +186,7 @@ class ProgressBar
}
// avoid divison through 0
if ($this->max - $this->min == 0) {
$this->max ++;
$this->max++;
}
$pixel = round(($step - $this->min) * ($bar - ($this->pedding * 2)) / ($this->max - $this->min));
if ($step <= $this->min) {

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

@@ -0,0 +1,88 @@
<?php
/**
* very simple symmetric encryption
* better use: https://paragonie.com/project/halite
*
* this is for creating secret keys for
* Security\SymmetricEncryption
*/
declare(strict_types=1);
namespace CoreLibs\Security;
class CreateKey
{
/**
* Create a random key that is a hex string
*
* @return string Hex string key for encrypting
*/
public static function generateRandomKey(): string
{
return self::bin2hex(self::randomKey());
}
/**
* create a random string as binary to encrypt data
* to store it in clear text in some .env file use bin2hex
*
* @return string Binary string for encryption
*/
public static function randomKey(): string
{
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(
#[\SensitiveParameter]
string $hex_key
): string {
return sodium_bin2hex($hex_key);
}
/**
* convert hex string to binary key
*
* @param string $string_key Convery hex key string to binary
* @return string
*/
public static function hex2bin(
#[\SensitiveParameter]
string $string_key
): string {
return sodium_hex2bin($string_key);
}
}
// __END__

64
src/Security/Password.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
/*
* core password set, check and rehash check wrapper functions
*/
declare(strict_types=1);
namespace CoreLibs\Security;
class Password
{
/**
* creates the password hash
*
* @param string $password password
* @return string hashed password
*/
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
return password_hash($password, PASSWORD_DEFAULT);
}
/**
* checks if the entered password matches the hash
*
* @param string $password password
* @param string $hash password hash
* @return bool true or false
*/
public static function passwordVerify(
#[\SensitiveParameter]
string $password,
string $hash
): bool {
if (password_verify($password, $hash)) {
return true;
} else {
return false;
}
}
/**
* checks if the password needs to be rehashed
*
* @param string $hash password hash
* @return bool true or false
*/
public static function passwordRehashCheck(string $hash): bool
{
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
return true;
} else {
return false;
}
}
}
// __END__

View File

@@ -0,0 +1,314 @@
<?php
/**
* very simple symmetric 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 SymmetricEncryption
{
/** @var SymmetricEncryption self instance */
private static SymmetricEncryption $instance;
/** @var ?string bin hex key */
private ?string $key = null;
/**
* init class
* if key not passed, key must be set with createKey
*
* @param string|null $key encryption key
*/
public function __construct(
?string $key = null
) {
if ($key !== null) {
$this->setKey($key);
}
}
/**
* Returns the singleton self object.
* For function wrapper use
*
* @param string|null $key encryption key
* @return SymmetricEncryption object
*/
public static function getInstance(?string $key = null): self
{
// 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
* *************************************************************************/
/**
* create key and check validity
*
* @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(
#[\SensitiveParameter]
?string $key
): string {
if (empty($key)) {
throw new \UnexpectedValueException('Key cannot be empty');
}
try {
$key = CreateKey::hex2bin($key);
} catch (SodiumException $e) {
throw new \UnexpectedValueException('Invalid hex key: ' . $e->getMessage());
}
if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new \RangeException(
'Key is not the correct size (must be '
. SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes long).'
);
}
return $key;
}
/**
* Decryption call
*
* @param string $encrypted Text to decrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string Plain text
* @throws \UnexpectedValueException key cannot be empty
* @throws \UnexpectedValueException decipher message failed
* @throws \UnexpectedValueException invalid key
*/
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');
$plaintext = false;
try {
$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());
}
sodium_memzero($ciphertext);
sodium_memzero($key);
if (!is_string($plaintext)) {
throw new \UnexpectedValueException('Invalid Key');
}
return $plaintext;
}
/**
* Encrypt a message
*
* @param string $message Message to encrypt
* @param ?string $key Mandatory encryption key, will throw exception if empty
* @return string Ciphered text
* @throws \UnexpectedValueException create message failed
*/
private function encryptData(
#[\SensitiveParameter]
string $message,
#[\SensitiveParameter]
?string $key
): string {
$key = $this->createKey($key);
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
try {
$cipher = base64_encode(
$nonce
. sodium_crypto_secretbox(
$message,
$nonce,
$key,
)
);
} catch (SodiumException $e) {
sodium_memzero($message);
sodium_memzero($key);
throw new \UnexpectedValueException("Create encrypted message failed: " . $e->getMessage());
}
sodium_memzero($message);
sodium_memzero($key);
return $cipher;
}
/* ************************************************************************
* MARK: PUBLIC
* *************************************************************************/
/**
* set a new key for encryption
*
* @param string $key
* @return SymmetricEncryption
* @throws \UnexpectedValueException key cannot be empty
*/
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;
}
/**
* Decrypt a message
* static version
*
* @param string $encrypted Message encrypted with safeEncrypt()
* @param string $key Encryption key (as hex string)
* @return string
*/
public static function decryptKey(
#[\SensitiveParameter]
string $encrypted,
#[\SensitiveParameter]
string $key
): string {
return self::getInstance()->decryptData($encrypted, $key);
}
/**
* Decrypt a message
*
* @param string $encrypted Message encrypted with safeEncrypt()
* @return string
*/
public function decrypt(
#[\SensitiveParameter]
string $encrypted
): string {
return $this->decryptData($encrypted, $this->key);
}
/**
* Encrypt a message
* static version
*
* @param string $message Message to encrypt
* @param string $key Encryption key (as hex string)
* @return string
*/
public static function encryptKey(
#[\SensitiveParameter]
string $message,
#[\SensitiveParameter]
string $key
): string {
return self::getInstance()->encryptData($message, $key);
}
/**
* Encrypt a message
*
* @param string $message Message to encrypt
* @return string
*/
public function encrypt(
#[\SensitiveParameter]
string $message
): string {
return $this->encryptData($message, $this->key);
}
}
// __END__

View File

@@ -0,0 +1,275 @@
<?php // phpcs:disable Generic.Files.LineLength
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/6/1
* DESCRIPTION:
* html builder: array
* static build for array lists (not objects)
*
* Recommended to use the Object one or for speed the String Replace
*/
namespace CoreLibs\Template\HtmlBuilder;
use CoreLibs\Template\HtmlBuilder\General\Settings;
use CoreLibs\Template\HtmlBuilder\General\Error;
use CoreLibs\Template\HtmlBuilder\General\HtmlBuilderExcpetion;
class Block
{
/**
* Create Element
*
* @param string $tag
* @param string $id
* @param string $content
* @param array<string> $css,
* @param array<string,string> $options
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
* @throws HtmlBuilderExcpetion
*/
public static function cel(
string $tag,
string $id = '',
string $content = '',
array $css = [],
array $options = []
): array {
if (!preg_match("/^[A-Za-z]+$/", $tag)) {
Error::setError(
'201',
'invalid or empty tag',
['tag' => $tag]
);
throw new HtmlBuilderExcpetion('Invalid or empty tag');
}
return [
'tag' => $tag,
'id' => $id,
'name' => $options['name'] ?? '',
'content' => $content,
'css' => $css,
'options' => $options,
'sub' => [],
];
}
/**
* Search element tree for id and add
* if id is empty add at current
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $base
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $attach
* @param string $id
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function ael(
array $base,
array $attach,
string $id = ''
): array {
// no id or matching id
if (
empty($id) ||
$base['id'] == $id
) {
self::addSub($base, $attach);
return $base;
}
// find id in 'id' in all 'sub'
foreach ($base['sub'] as $el) {
$el = self::ael($el, $attach, $id);
}
return $base;
}
/**
* Add multiple elements to the base element
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $base
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} ...$attach
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function aelx(
array $base,
array ...$attach
): array {
$base = self::addSub($base, ...$attach);
return $base;
}
/**
* Add multiple sub elements to the base element
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $element
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $sub
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function addSub(array $element, array ...$sub): array
{
if (!isset($element['sub'])) {
$element['sub'] = [];
}
array_push($element['sub'], ...$sub);
return $element;
}
/**
* Remove all sub element entries
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $element
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function resetSub(array $element): array
{
$element['sub'] = [];
return $element;
}
// CSS Elements
/**
* Add css entry to the css entries
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $element
* @param string ...$css
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function acssel(array $element, string ...$css): array
{
$element['css'] = array_unique(array_merge($element['css'] ?? [], $css));
return $element;
}
/**
* Remove a css entry entry from the css array
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $element
* @param string ...$css
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function rcssel(array $element, string ...$css): array
{
$element['css'] = array_diff($element['css'] ?? [], $css);
return $element;
}
/**
* Switch CSS entries
* scssel (switch) is not supported
* use rcssel -> acssel
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $element
* @param array<string> $rcss
* @param array<string> $acss
* @return array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}
*/
public static function scssel(array $element, array $rcss, array $acss): array
{
return self::acssel(
self::rcssel($element, ...$rcss),
...$acss
);
}
/**
* Build HTML from the content tree
* alias phfo
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $tree
* @param bool $add_nl [default=false]
* @return string
*/
public static function buildHtml(array $tree, bool $add_nl = false): string
{
if (empty($tree['tag'])) {
return '';
}
// print "D01: " . microtime(true) . "<br>";
$line = '<' . $tree['tag'];
if (!empty($tree['id'])) {
$line .= ' id="' . $tree['id'] . '"';
if (in_array($tree['tag'], Settings::NAME_ELEMENTS)) {
$line .= ' name="'
. (!empty($tree['name']) ? $tree['name'] : $tree['id'])
. '"';
}
}
if (count($tree['css'])) {
$line .= ' class="' . join(' ', $tree['css']) . '"';
}
foreach ($tree['options'] ?? [] as $key => $item) {
if (in_array($key, Settings::SKIP_OPTIONS)) {
continue;
}
$line .= ' ' . $key . '="' . $item . '"';
}
$line .= '>';
if (!empty($tree['content'])) {
$line .= $tree['content'];
}
// sub nodes
foreach ($tree['sub'] ?? [] as $sub) {
if ($add_nl === true) {
$line .= "\n";
}
$line .= self::buildHtml($sub, $add_nl);
if ($add_nl === true) {
$line .= "\n";
}
}
// close line if needed
if (!in_array($tree['tag'], Settings::NO_CLOSE)) {
$line .= '</' . $tree['tag'] . '>';
}
return $line;
}
/**
* Alias for phfo
*
* @param array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>} $tree
* @param bool $add_nl [default=false]
* @return string
*/
public static function phfo(array $tree, bool $add_nl = false): string
{
return self::buildHtml($tree, $add_nl);
}
/**
* Build HTML elements from an array of elements
* alias phfa
*
* @param array<array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}> $list
* @param bool $add_nl [default=false]
* @return string
*/
public static function buildHtmlFromList(array $list, bool $add_nl = false): string
{
$output = '';
foreach ($list as $el) {
$output .= self::buildHtml($el, $add_nl);
}
return $output;
}
/**
* alias for buildHtmlFromList
*
* @param array<array{tag:string,id:string,name:string,content:string,css:array<string>,options:array<string,string>,sub:array<mixed>}> $list array of Elements to build string from
* @param bool $add_nl [default=false] Optional output string line break
* @return string build html as string
*/
public static function printHtmlFromArray(array $list, bool $add_nl = false): string
{
return self::buildHtmlFromList($list, $add_nl);
}
}
// __END__

View File

@@ -0,0 +1,559 @@
<?php
/**
* AUTOR: Clemens Schwaighofer
* CREATED: 2023/6/1
* DESCRIPTION:
* html builder: element
* nested and connected objects
*/
declare(strict_types=1);
namespace CoreLibs\Template\HtmlBuilder;
use CoreLibs\Template\HtmlBuilder\General\Settings;
use CoreLibs\Template\HtmlBuilder\General\Error;
use CoreLibs\Template\HtmlBuilder\General\HtmlBuilderExcpetion;
class Element
{
/** @var string */
private string $tag = '';
/** @var string */
private string $id = '';
/** @var string */
private string $name = '';
/** @var string */
private string $content = '';
/** @var array<string> */
private array $css = [];
/** @var array<string,mixed> */
private array $options = [];
/** @var array<Element> list of elements */
private array $sub = [];
/**
* create new html element
*
* @param string $tag html tag (eg div, button, etc)
* @param string $id html tag id, used also for name if name
* not set in $options
* @param string $content content text inside, eg <div>Content</div>
* if sub elements exist, they are added after content
* @param array<string> $css array of css names, put style in $options
* @param array<string,string> $options Additional element options in
* key = value format
* eg: onClick => 'something();'
* id, css are skipped
* name only set on input/button
* @throws HtmlBuilderExcpetion
*/
public function __construct(
string $tag,
string $id = '',
string $content = '',
array $css = [],
array $options = []
) {
// exit if not valid tag
try {
$this->setTag($tag);
} catch (HtmlBuilderExcpetion $e) {
throw new HtmlBuilderExcpetion('Could not create Element', 0, $e);
}
$this->setId($id);
$this->setName($options['name'] ?? '');
$this->setContent($content);
$this->addCss(...$css);
$this->setOptions($options);
}
/**
* set tag
*
* @param string $tag
* @return void
* @throws HtmlBuilderExcpetion
*/
public function setTag(string $tag): void
{
// tag must be letters only
if (!preg_match("/^[A-Za-z]+$/", $tag)) {
Error::setError(
'201',
'invalid or empty tag',
['tag' => $tag]
);
throw new HtmlBuilderExcpetion('Invalid or empty tag: ' . $tag);
}
$this->tag = $tag;
}
/**
* get the tag name
*
* @return string HTML element tag
*/
public function getTag(): string
{
return $this->tag;
}
/**
* set the element id
*
* @param string $id
* @return void
*/
public function setId(string $id): void
{
// invalid id and name check too
// be strict: [a-zA-Z0-9], -, _
// cannot start with digit, two hyphens or a hyphen with a digit:
// 0abc
// __abc
// _0abc
if (
!empty($id) &&
!preg_match("/^[A-Za-z][\w-]*$/", $id)
) {
Error::setWarning(
'202',
'possible invalid id',
['id' => $id, 'tag' => $this->getTag()]
);
// TODO: shoud throw error
}
$this->id = $id;
}
/**
* get the html tag id
*
* @return string HTML element id
*/
public function getId(): string
{
return $this->id;
}
/**
* Set name for elements
* only for elements that need it (input/button/form)
*
* @param string $name
* @return void
*/
public function setName(string $name): void
{
if (
!empty($name) &&
!preg_match("/^[A-Za-z][\w-]*$/", $name)
) {
Error::setWarning(
'203',
'possible invalid name',
['name' => $name, 'id' => $this->getId(), 'tag' => $this->getTag()]
);
// TODO: shoud throw error
}
$this->name = $name;
}
/**
* get the name if set
*
* @return string Optional HTML name (eg for input)
*/
public function getName(): string
{
return $this->name;
}
/**
* Set new content for element
*
* @param string $content
* @return void
*/
public function setContent(string $content): void
{
$this->content = $content;
}
/**
* get the elment text content (not sub elements)
*
* @return string HTML content text as is
*/
public function getContent(): string
{
return $this->content;
}
/**
* set or update options
*
* @param array<string,mixed> $options
* @return void
*/
public function setOptions(array $options): void
{
foreach ($options as $key => $value) {
if (empty($key)) {
Error::setError(
'110',
'Cannot set option with empty key',
['id' => $this->getId(), 'tag' => $this->getTag()]
);
// TODO: shoud throw error
continue;
}
// if data is null
if ($value === null) {
if (isset($this->options[$key])) {
unset($this->options[$key]);
} else {
Error::setError(
'210',
'Cannot set option with null value',
['id' => $this->getId(), 'tag' => $this->getTag()]
);
}
// TODO: shoud throw error
continue;
}
$this->options[$key] = $value;
}
}
/**
* get the options array
* also holds "name" option
* anything like: style, javascript, value or any other html tag option
* right side can be empty but not null
*
* @return array<string,string> get options as list html option name and value
*/
public function getOptions(): array
{
return $this->options;
}
// Sub Elements
/**
* get the sub elements (array of Elements)
*
* @return array<Element> Array of Elements (that can have sub elements)
*/
public function getSub(): array
{
return $this->sub;
}
/**
* add one or many sub elements (add at the end)
*
* @param Element $sub One or many elements to add
* @return void
* @throws HtmlBuilderExcpetion
*/
public function addSub(Element ...$sub): void
{
foreach ($sub as $_sub) {
// if one of the elements is the same as this class, ignore it
// with this we avoid self reference loop
if ($_sub == $this) {
Error::setError(
'100',
'Cannot assign Element to itself, this would create an infinite loop',
['id' => $this->getId(), 'tag' => $this->getTag()]
);
throw new HtmlBuilderExcpetion('Cannot assign Element to itself, this would create an infinite loop');
}
array_push($this->sub, $_sub);
}
}
/**
* Remove an element from the sub array
* By pos in array or id set on first level
*
* @param int|string $id String id name or int pos number in array
* @return void
*/
public function removeSub(int|string $id): void
{
// find element with id and remove it
// or when number find pos in sub and remove it
if (is_int($id)) {
if (!isset($this->sub[$id])) {
return;
}
unset($this->sub[$id]);
return;
}
// only on first level
foreach ($this->sub as $pos => $el) {
if (
$el->getId() === $id
) {
unset($this->sub[$pos]);
return;
}
}
}
/**
* remove all sub elements
*
* @return void
*/
public function resetSub(): void
{
$this->sub = [];
}
// CSS Elements
/**
* get the current set css elements
*
* @return array<string> list of css element entries
*/
public function getCss(): array
{
return $this->css;
}
/**
* add one or many new css elements
* Note that we can chain: add/remove/reset
*
* @param string ...$css one or more css strings to add
* @return Element Current element for chaining
*/
public function addCss(string ...$css): Element
{
// should do check for empty/invalid css
$_set_css = [];
foreach ($css as $_css) {
if (empty($_css)) {
Error::setError(
'204',
'cannot have empty css string',
);
// TODO: shoud throw error
continue;
}
// -?[_A-Za-z][_A-Za-z0-9-]*
if (!preg_match("/^-?[_A-Za-z][_A-Za-z0-9-]*$/", $_css)) {
Error::setWarning(
'205',
'possible invalid css string',
['css' => $_css, 'id' => $this->id, 'tag' => $this->tag]
);
// TODO: shoud throw error
}
$_set_css[] = $_css;
}
$this->css = array_unique(array_merge($this->css, $_set_css));
return $this;
}
/**
* remove one or more css elements
* Note that we can chain: add/remove/reset
*
* @param string ...$css one or more css strings to remove
* @return Element Current element for chaining
*/
public function removeCss(string ...$css): Element
{
$this->css = array_diff($this->css, $css);
return $this;
}
/**
* unset all css elements
* Note that we can chain: add/remove/reset
*
* @return Element
*/
public function resetCss(): Element
{
$this->css = [];
return $this;
}
// build output from tree
/**
* build html string from the current element tree (self)
* or from the Element tree given as parameter
* if $add_nl is set then new lines are added before each sub element added
* no indet is done (tab or other)
*
* @param Element|null $tree Different Element tree to build
* if not set (null), self is used
* @param bool $add_nl [default=false] Optional output string line breaks
* @return string HTML as string
*/
public function buildHtml(?Element $tree = null, bool $add_nl = false): string
{
// print "D01: " . microtime(true) . "<br>";
if ($tree === null) {
$tree = $this;
}
$line = '<' . $tree->getTag();
if ($tree->getId()) {
$line .= ' id="' . $tree->getId() . '"';
if (in_array($tree->getTag(), Settings::NAME_ELEMENTS)) {
$line .= ' name="'
. (!empty($tree->getName()) ? $tree->getName() : $tree->getId())
. '"';
}
}
if (count($tree->getCss())) {
$line .= ' class="' . join(' ', $tree->getCss()) . '"';
}
foreach ($tree->getOptions() as $key => $item) {
// skip
if (in_array($key, Settings::SKIP_OPTIONS)) {
continue;
}
$line .= ' ' . $key . '="' . $item . '"';
}
$line .= '>';
if (strlen($tree->getContent()) > 0) {
$line .= $tree->getContent();
}
// sub nodes
foreach ($tree->getSub() as $sub) {
if ($add_nl === true) {
$line .= "\n";
}
$line .= $tree->buildHtml($sub, $add_nl);
if ($add_nl === true) {
$line .= "\n";
}
}
// close line if needed
if (!in_array($tree->getTag(), Settings::NO_CLOSE)) {
$line .= '</' . $tree->getTag() . '>';
}
return $line;
}
// this is static
/**
* Builds a single string from an array of elements
* a new line can be added before each new element if $add_nl is set to true
*
* @param array<Element> $list array of Elements, uses buildHtml internal
* @param bool $add_nl [default=false] Optional output string line breaks
* @return string HTML as string
*/
public static function buildHtmlFromList(array $list, bool $add_nl = false): string
{
$output = '';
foreach ($list as $el) {
if (!empty($output) && $add_nl === true) {
$output .= "\n";
}
$output .= $el->buildHtml();
}
return $output;
}
// so we can call builder statically
/**
* Search element tree for id and add
* if id is empty add at element given in parameter $base
*
* @param Element $base Element to attach to
* @param Element $attach Element to attach (single)
* @param string $id Optional id, if empty then attached at the end
* If set will loop through ALL sub elements until
* matching id found. if not found, not added
* @return Element Element with attached sub element
*/
public static function addElementWithId(
Element $base,
Element $attach,
string $id = ''
): Element {
// no id or matching id
if (
empty($id) ||
$base->getId() == $id
) {
$base->addSub($attach);
return $base;
}
// find id in 'id' in all 'sub'
foreach ($base->getSub() as $el) {
self::addElementWithId($el, $attach, $id);
}
return $base;
}
/**
* add one or more elemens to $base
*
* @param Element $base Element to attach to
* @param Element ...$attach Element or Elements to attach
* @return Element Element with attached sub elements
*/
public static function addElement(
Element $base,
Element ...$attach
): Element {
// we must make sure we do not self attach
$base->addSub(...$attach);
return $base;
}
/**
* Static call version for building
* not recommended to be used, rather use "Element->buildHtml()"
* wrapper for buildHtml
*
* @param ?Element $tree Element tree to build
* if not set returns empty string
* @param bool $add_nl [default=false] Optional output string line break
* @return string build html as string
* @deprecated Do not use, use Element->buildHtml() instead
*/
public static function printHtmlFromObject(?Element $tree = null, bool $add_nl = false): string
{
// nothing ->bad
if ($tree === null) {
return '';
}
return $tree->buildHtml(add_nl: $add_nl);
}
/**
* Undocumented function
* wrapper for buildHtmlFromList
*
* @param array<Element> $list array of Elements to build string from
* @param bool $add_nl [default=false] Optional output string line break
* @return string build html as string
*/
public static function printHtmlFromArray(array $list, bool $add_nl = false): string
{
return self::buildHtmlFromList($list, $add_nl);
}
}
// __END__

Some files were not shown because too many files have changed in this diff Show More