Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: PHP Tests

on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.0', '8.1', '8.2', '8.3', '8.4']

steps:
- uses: actions/checkout@v4

- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, openssl
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub Actions workflow only installs the mbstring and openssl extensions. However, the Images class requires the imagick extension (checked in its constructor). Tests that use the Images class will fail in CI unless the imagick extension is added to the workflow, or tests for Images are skipped when the extension is not available.

Copilot uses AI. Check for mistakes.
coverage: none

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run PHPUnit
run: vendor/bin/phpunit --no-coverage
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ bin/phpDocumentor.phar

# Composer
vendor/
composer.lock

# PHPUnit
.phpunit.result.cache
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- **Strict types**: `declare(strict_types=1);` in all PHP library and test files.
- **Vars::arrayContainsSubstring()**: Alias for `arrayInString()` with clearer naming.
- **Tests**: New test classes for Crypto, Files, SQLite, Times, and Random.
- **CI**: GitHub Actions workflow (`.github/workflows/php.yml`) running PHPUnit on PHP 8.0–8.4 for `main` and `dev`.
- **CHANGELOG.md**: This file.

### Changed

- **Error handling**: Replaced `die()` with exceptions in Network, Random, Files, and Debugger (bootstrap `_All.php` still uses `die()` for fatal startup failures).
- `Network::getUserIP()` / `getServerIP()`: throw `RuntimeException` when `$die_if_empty` is true and IP cannot be determined.
- `Random::genStr()`: throw `InvalidArgumentException` for invalid type instead of `die()`.
- `Files::preventDirect()` default callback: throw `RuntimeException("404 Not found")` after setting 404 response code.
- `Debugger::output(..., $die = true)`: echo then throw `RuntimeException` instead of `die()`.
- **Network::ipInRange()**: Removed `echo` on invalid IPs; still returns `null` for invalid input. Fixed check to use `=== false` for `ip2long()` so `0.0.0.0` is valid.
- **Vars::arrayInString()**: Documented behaviour (any element contains needle); added return type `bool`.
- **Images**: Docblocks use `\Imagick` and `\ImagickException`; `blur()` has explicit return type `string`.
- **Composer**: `composer.lock` is no longer ignored so dev and CI use reproducible dependency versions.

### Documentation

- **README**: Get started section documents both Composer and _All.php; module links point to `Docs/classes/*.html`.
- **TEST_SUMMARY.md**: Updated to include new test classes and strict types / exception behaviour.
- **REFACTORING_SUMMARY.md**: Can be updated to reference this changelog for recent improvements.

---

## Previous work (pre-changelog)

- OOP refactor: namespaces, Base/DI, SQL connection injection, identifier validation, Debugger data/HTML separation, visibility and return types (see `REFACTORING_SUMMARY.md` and `OOP_REVIEW.md`).
- Crypto: corrected `hash()` argument order, `verifyhash()` with `hash_equals()`, IV handling with embedded IV in ciphertext.
- Images/SQLite: `parent::__construct()`; SQLite visibility and `$this->res()`.
- SQL::search(): table and column names validated via `validateIdentifier()`.
- README and index.php: fixed doc and test links; bootstrap prefers Composer autoload.
2 changes: 2 additions & 0 deletions PHPUtils/API.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/* ────────────────────────────────────────────────────────────────────────── */
Expand Down
2 changes: 2 additions & 0 deletions PHPUtils/Auth.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

# ──────────────────────────────────────────────────────────────────────────────────────────────── #
Expand Down
2 changes: 2 additions & 0 deletions PHPUtils/Base.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

# ──────────────────────────────────────────────────────────────────────────────────────────────── #
Expand Down
2 changes: 2 additions & 0 deletions PHPUtils/Calendar.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

# ──────────────────────────────────────────────────────────────────────────────────────────────── #
Expand Down
95 changes: 56 additions & 39 deletions PHPUtils/Crypto.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/* ────────────────────────────────────────────────────────────────────────── */
Expand All @@ -13,70 +15,85 @@
* @package PHPUtils
*/
class Crypto extends Base {

/**
* Generates a random IV for the given encryption method
* Generates a random IV for the given encryption method (hex-encoded for storage).
*
* @param mixed $method The encryption method to use
* @return string The generated IV
* @param string $method The encryption method to use
* @return string The generated IV as hex string
*/
function genIV(string $method) {
public function genIV(string $method): string {
$len = openssl_cipher_iv_length($method);
$bytes = openssl_random_pseudo_bytes($len);
return bin2hex($bytes);
}

/**
* Encrypt a string using a password, and optionally an IV
* Encrypt a string using a password, and optionally an IV.
* When $iv is true, a random IV is used and prepended to the ciphertext (base64(iv_raw . ciphertext)).
*
* @param mixed $str The string to encrypt
* @param mixed $password The password to use
* @param mixed $method The encryption method to use. Defaults to aes-256-cbc
* @param mixed $iv Whether to use an IV or not. Defaults to false
* @return string The encrypted string
* @param string $str The string to encrypt
* @param string $password The password to use
* @param string $method The encryption method. Defaults to aes-256-cbc
* @param bool $iv Whether to use a random IV (prepended to output). Defaults to false
* @return string The encrypted string (when iv=true, base64 of IV + ciphertext)
*/
function encryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', bool $iv = false) {
$iv = ($iv ? $this->genIV($method) : '');
return openssl_encrypt($str,$method,$password,iv:$iv);
public function encryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', bool $iv = false): string {
if ($iv) {
$ivRaw = hex2bin($this->genIV($method));
$encrypted = openssl_encrypt($str, $method, $password, iv: $ivRaw);
return base64_encode($ivRaw . $encrypted);
Comment on lines +44 to +45
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the encryptwithpw method, when $iv is true, the encrypted string returned by openssl_encrypt is already base64-encoded by default (unless the OPENSSL_RAW_DATA option is used). Concatenating the raw IV bytes with a base64 string and then base64-encoding the result creates incorrect data. The decryptwithpw method expects raw bytes concatenated before base64 encoding.

This should use OPENSSL_RAW_DATA option to get raw encrypted bytes, then concatenate with the raw IV, then base64 encode:

$encrypted = openssl_encrypt($str, $method, $password, OPENSSL_RAW_DATA, $ivRaw);

Copilot uses AI. Check for mistakes.
}
return openssl_encrypt($str, $method, $password);
}


/**
* Decrypt a string using a password, and optionally an IV
* Decrypt a string using a password, and optionally an IV.
* When $iv is empty and the string was produced with iv=true, the embedded IV is used.
* When $iv is non-empty, it is treated as hex and used as the IV (ciphertext only in $str).
*
* @param mixed $str The string to decrypt
* @param mixed $password The password to use
* @param mixed $method The encryption method to use. Defaults to aes-256-cbc
* @param mixed $iv Whether to use an IV or not. Defaults to ''
* @return string The decrypted string
* @param string $str The string to decrypt (base64 when IV was embedded, or raw/base64 ciphertext when $iv provided)
* @param string $password The password to use
* @param string $method The encryption method. Defaults to aes-256-cbc
* @param string $iv Optional hex-encoded IV when not embedded. Defaults to ''
* @return string|false The decrypted string or false on failure
*/
function decryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', string $iv = '') {
return openssl_decrypt($str,$method,$password,iv:$iv);
public function decryptwithpw(string $str, string $password, string $method = 'aes-256-cbc', string $iv = ''): string|false {
$ivLen = openssl_cipher_iv_length($method);
if ($iv !== '') {
$ivRaw = hex2bin($iv);
$ciphertext = base64_decode($str, true) ?: $str;
return openssl_decrypt($ciphertext, $method, $password, iv: $ivRaw);
}
$decoded = base64_decode($str, true);
if ($decoded !== false && strlen($decoded) > $ivLen) {
$ivRaw = substr($decoded, 0, $ivLen);
$ciphertext = substr($decoded, $ivLen);
return openssl_decrypt($ciphertext, $method, $password, iv: $ivRaw);
}
return openssl_decrypt($str, $method, $password);
Comment on lines +61 to +74
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decryptwithpw method needs to be consistent with the encryption. When $iv is provided as a parameter, the code should use OPENSSL_RAW_DATA for decryption to match the expected format. When auto-detecting embedded IV, it assumes raw bytes were concatenated before base64 encoding, but this won't work correctly if openssl_encrypt returns base64 by default.

Copilot uses AI. Check for mistakes.
}

/**
* hash
* Hash a string using the given algorithm.
*
* @param mixed $str The string to hash
* @param mixed $hash The hash method to use. Defaults to sha512
* @param string $str The string to hash
* @param string $hash The hash algorithm. Defaults to sha512
* @return string The hashed string
*/
function hash(string $str, string $hash = 'sha512') {
return hash($str, $hash);
public function hash(string $str, string $hash = 'sha512'): string {
return hash($hash, $str);
}

/**
* verifyhash
*
* Verifies a hash
*
* @param mixed $str The string to verify
* @param mixed $hash The hash to verify against
* @param mixed $hashmethod The hash method to use. Defaults to sha512
*
* @return bool Whether the hash is valid or not
* Verify a string against a hash (timing-safe comparison).
*
* @param string $str The string to verify
* @param string $hash The hash to verify against
* @param string $hashmethod The hash method. Defaults to sha512
* @return bool Whether the hash is valid
*/
function verifyhash(string $str, string $hash, string $hashmethod = 'sha512') {
return hash($hashmethod, $str) == $hash;
public function verifyhash(string $str, string $hash, string $hashmethod = 'sha512'): bool {
return hash_equals($hash, hash($hashmethod, $str));
}
}
10 changes: 7 additions & 3 deletions PHPUtils/Debugger.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/* ────────────────────────────────────────────────────────────────────────── */
Expand Down Expand Up @@ -130,11 +132,13 @@ public function formatAsHtml(array $data): string {
* @param mixed $die
* @return void
*/
public function output(mixed $txt, string $type = 'info', bool $die = false) {
public function output(mixed $txt, string $type = 'info', bool $die = false): void {
$formatted = $this->format($txt, $type);
if ($die) {
die($this->format($txt, $type));
echo $formatted;
throw new \RuntimeException("Debugger::output terminated with die=true.");
}
echo $this->format($txt, $type);
echo $formatted;
}


Expand Down
6 changes: 4 additions & 2 deletions PHPUtils/Files.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/* ────────────────────────────────────────────────────────────────────────── */
Expand Down Expand Up @@ -258,9 +260,9 @@ public function preventDirect(array $exceptions = [], mixed &$pagevar = null, ?c

# default callback function
if ($callback == null) {
$callback = function() {
$callback = function(): never {
http_response_code(404);
die("404 Not found");
throw new \RuntimeException("404 Not found");
};
}

Expand Down
2 changes: 2 additions & 0 deletions PHPUtils/Funcs.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/**
Expand Down
25 changes: 14 additions & 11 deletions PHPUtils/Images.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

/**
Expand All @@ -26,23 +28,24 @@ class Images extends Base {
* Initializes the Images class and checks if the Imagick extension is enabled.
* If the extension is not enabled, it terminates the script execution.
*/
public function __construct() {
public function __construct(?Debugger $debugger = null, ?Vars $vars = null, bool $verbose = true) {
parent::__construct($debugger, $vars, $verbose);
if (!extension_loaded('imagick')) {
die("Imagick extension is not enabled. To use the class `Images` you must have Imagick extension enabled.");
throw new \RuntimeException("Imagick extension is not enabled. To use the class Images you must have the Imagick extension enabled.");
}
}

/**
* Blur an image.
*
*
* @param string $imagePath The path to the image file.
* @param float $radius The blur radius.
* @param float $sigma The blur sigma.
* @param int $channel The channel(s) to apply the blur to.
* @param int $channel The channel(s) to apply the blur to (\Imagick::CHANNEL_*).
* @return string The path of the blurred image.
* @throws ImagickException If an error occurs while blurring the image.
* @throws \ImagickException If an error occurs while blurring the image.
*/
function blur(string $imagePath, float $radius = 10, float $sigma = 25, int $channel = imagick::CHANNEL_ALL) {
public function blur(string $imagePath, float $radius = 10, float $sigma = 25, int $channel = \Imagick::CHANNEL_ALL): string {
try {
// Create a temporary file
$tmpDir = dirname($imagePath);
Expand All @@ -66,8 +69,8 @@ function blur(string $imagePath, float $radius = 10, float $sigma = 25, int $cha
});

return $tmpRelativePath; // return the path of the temporary file
} catch (ImagickException $e) {
die("Error: " . $e->getMessage());
} catch (\ImagickException $e) {
throw new \RuntimeException("Error blurring image: " . $e->getMessage(), 0, $e);
}
}

Expand All @@ -83,7 +86,7 @@ function blur(string $imagePath, float $radius = 10, float $sigma = 25, int $cha
* @param string $color The color of the text.
* @throws ImagickException If an error occurs while adding the text to the image.
*/
function textToImage(string $text, string $imagePath, string $fontPath, int $fontSize, int $x, int $y, string $color = 'black') {
public function textToImage(string $text, string $imagePath, string $fontPath, int $fontSize, int $x, int $y, string $color = 'black') {
$draw = new ImagickDraw();
$draw->setFont($fontPath);
$draw->setFontSize($fontSize);
Expand All @@ -106,7 +109,7 @@ function textToImage(string $text, string $imagePath, string $fontPath, int $fon
* @param int $width The width of the image.
* @return string The HTML <img> snippet.
*/
function renderImage(string $imagePath, int $height = 200, int $width = 200) {
return '<img src="'.$imagePath.' height="'.$height.'" width="'.$width.'">';
public function renderImage(string $imagePath, int $height = 200, int $width = 200) {
return '<img src="'.$imagePath.'" height="'.$height.'" width="'.$width.'">';
}
}
2 changes: 2 additions & 0 deletions PHPUtils/Navigation.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace PHPUtils;

# ──────────────────────────────────────────────────────────────────────────────────────────────── #
Expand Down
Loading
Loading