Compare commits

..

37 Commits

Author SHA1 Message Date
5433811176 Use dedicated exceptions if unlinking failed for reasons other than the
file not existing
2025-03-25 14:57:29 +01:00
b9d82d672a Update years covered in license 2025-01-22 15:20:14 +01:00
9948ee5d3d Add clsas for MD_STD_STRINGS more obscure string operations 2025-01-16 14:21:04 +01:00
be437fcf5f Add function to get next strpos of any of a specified set of needles 2024-12-25 18:41:28 +01:00
fa2985463f Use more explicit type hints for improved static code analysis 2024-09-22 16:52:21 +02:00
bce4268a70 Merge branch 'master' of gitea:museum-digital/MD_STD 2024-09-05 14:25:14 +02:00
92c942aab3 Remove whitespaces in isbns before validating 2024-09-05 14:24:18 +02:00
6174c5454e Clean em dashes from ISBNs during sanitization 2024-08-14 16:41:55 +02:00
a57036789e Fix implicit array creation 2024-07-12 02:56:27 +02:00
fc727932ca Make function in MD_STD_HTML_TEST final 2024-07-12 02:54:26 +02:00
40d83ce5b0 Add class MD_STD_HTML_TEST for validating HTML outputs 2024-07-12 02:31:42 +02:00
cbc66c4140 Improve test coverage for MD_STD_SEC 2024-07-11 15:32:50 +02:00
11faeaa7e7 Improve test coverage 2024-07-11 14:53:05 +02:00
fb008e1b59 Add function for ensuring all input in an array is strings 2024-07-09 16:43:21 +02:00
cd46a3ec73 Add wrapper around file_put_contents 2024-06-24 16:53:01 +02:00
94dfa17290 Fix code smells 2024-06-11 21:57:56 +02:00
d4918dd893 Handle dates outside of strtotime()'s range in date_to_int, int_to_date
Close #9
2024-05-05 00:48:04 +02:00
fb1372d193 Use MD_STD::strtotime() over strtotime() 2024-05-04 01:19:52 +02:00
63ac1b296e Add functions for transferring dates to ints and vice versa 2024-05-03 17:42:32 +02:00
8c1050f40a Add wrapper around MD_STD::strtotime() that will throw an expected
exception
2024-01-30 01:21:51 +01:00
2bea372973 Use phpstan-specific comments for main sanitization + validation
functions
2024-01-14 22:18:48 +01:00
69e6850c16 Write prettier error message in MDFailedToCreateDirectory 2023-11-27 01:31:16 +01:00
8006695093 Throw a specific exception if MD_STD::mkdir fails
Close #8
2023-11-27 01:30:43 +01:00
db31822a3f Use empty() over === false to also return error in case of empty strings 2023-11-10 16:17:20 +01:00
0fb368b96d Extend MD_STD_IN::sanitize_url to automatically set protocol / scheme
names in lowercase
2023-11-09 16:40:28 +01:00
66e704de47 Extend tests for MD_STD_IN considerably, fix some edge cases 2023-11-08 21:24:23 +01:00
a03f072a69 Add function for validating ZIP codes (somewhat) 2023-11-08 02:18:34 +01:00
d83ed2d0eb Improve indentation in phpunit 2023-11-07 22:52:23 +01:00
2c58e0554b Improve coverage of MD_STD_IN 2023-11-07 22:50:18 +01:00
1f2f63c9af Set beStrictAboutOutputDuringTests=true in phpunit.xml 2023-11-06 23:49:27 +01:00
c9dce8f782 Annotate the available tests as @small 2023-11-06 23:26:21 +01:00
0c0d059dd3 Add testsuite for /tests directory in phpunit.xml 2023-11-06 23:03:35 +01:00
5d80f82040 Remove printer class / use testdox by default in phpunit setup 2023-11-06 23:02:44 +01:00
5c2c1a47cc Fully ensure all URL components are present for rewriting 2023-11-05 23:37:28 +01:00
ae12cfdf0f Add tests for MD_STD_IN::sanitize_url() and ensure it supports rewriting
unencoded cyrillic inputs

Close #7
2023-11-05 23:29:14 +01:00
2176e7312b Remove MD_STD_CACHE open_redis_default() 2023-10-05 16:58:48 +02:00
3ece870f0c Require externally set up redis connection for caching + serving full pages via
redis
2023-10-05 16:45:35 +02:00
22 changed files with 2285 additions and 113 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.phpunit.cache/

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 museum-digital
Copyright (c) 2023-2025 museum-digital
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure to create a new directory.
*/
final class MDFailedToCreateDirectory extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Failed to create directory';
}
}

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure to create a new directory.
*/
final class MDFailedToDeleteFile extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Failed to delete file';
}
}

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure due to insufficient permissions in the file system.
*/
final class MDFilePermissionsException extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Insufficient file system permissions';
}
}

0
phpstan-baseline.neon Normal file
View File

12
phpstan.neon Normal file
View File

@ -0,0 +1,12 @@
parameters:
level: 8
bootstrapFiles:
- ./tests/bootstrap.php
paths:
- src
- tests
dynamicConstantNames:
- DATABASENAME
- DATABASENAME_NODA
includes:
- phpstan-baseline.neon

14
phpunit.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" backupGlobals="false" beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" bootstrap="tests/bootstrap.php" cacheResult="false" colors="true" enforceTimeLimit="true" executionOrder="depends,defects" failOnRisky="true" failOnWarning="true" processIsolation="true" stopOnError="true" stopOnFailure="true" stopOnIncomplete="true" stopOnSkipped="true" stopOnRisky="true" testdox="true" timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" cacheDirectory=".phpunit.cache" backupStaticProperties="false" requireCoverageMetadata="false" beStrictAboutCoverageMetadata="false" displayDetailsOnTestsThatTriggerWarnings="true" >
<testsuites>
<testsuite name="tests">
<directory>tests/</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View File

@ -13,9 +13,9 @@ declare(strict_types = 1);
*/
final class MD_JAIL {
const STATUS_NONE = 0;
const STATUS_STARTED = 1;
const STATUS_SPECIFIED = 2; // Determines that everything is fine.
public const STATUS_NONE = 0;
public const STATUS_STARTED = 1;
public const STATUS_SPECIFIED = 2; // Determines that everything is fine.
/** @var integer */
private int $_status = self::STATUS_NONE;

View File

@ -33,6 +33,27 @@ final class MD_STD {
}
/**
* Wrapper around file_put_contents, that provides catches errors on it and returns
* with type safety.
*
* @param non-empty-string $filename Filepath of the file to read.
* @param string $data Data to write.
*
* @return integer
*/
public static function file_put_contents(string $filename, string $data):int {
$status = \file_put_contents($filename, $data);
if ($status === false) {
throw new MDFileIsNotWritable("File {$filename} is not writable");
}
return $status;
}
/**
* Returns the real path of a relative file path. Throws an error rather than
* returning the default false.
@ -71,7 +92,7 @@ final class MD_STD {
return;
}
if (\mkdir($pathname, $mode, $recursive) === false) {
throw new Exception("Failed to create directory: $pathname");
throw new MDFailedToCreateDirectory("Failed to create directory: $pathname");
}
}
@ -89,7 +110,15 @@ final class MD_STD {
public static function unlink(string $filename):void {
if (\unlink($filename) === false) {
throw new Exception("Failed to delete: $filename");
if (!\is_file($filename)) {
throw new MDFileDoesNotExist("Failed to delete $filename, it did not exist");
}
else if (!\is_writable(dirname($filename))) {
throw new MDFilePermissionsException("Failed to delete $filename");
}
else {
throw new MDFailedToDeleteFile("Failed to delete $filename");
}
}
}
@ -152,26 +181,6 @@ final class MD_STD {
}
/**
* Function checking if a string starts with another.
* DEPRECATED. Can be replaced by PHP8's str_starts_with.
*
* @param non-empty-string $haystack String to check.
* @param non-empty-string $needle Potential start of $haystack.
*
* @return boolean
*/
public static function startsWith(string $haystack, string $needle):bool {
if (substr($haystack, 0, \strlen($needle)) === $needle) {
return true;
}
else {
return false;
}
}
/**
* Function checking if a string starts with any input from the input array.
*
@ -303,6 +312,27 @@ final class MD_STD {
}
/**
* Wrapper around self::strtotime() that will return MDParseException (an expected
* exception) rather than MDInvalidInputDate (an unexpected exception).
*
* @param string $datetime String to convert.
*
* @return integer
*/
public static function strtotime_for_public_apis(string $datetime):int {
try {
$output = self::strtotime($datetime);
}
catch (MDInvalidInputDate $e) {
throw new MDParseException($e->getMessage());
}
return $output;
}
/**
* Initializes a curl request with the given presets.
*
@ -424,16 +454,13 @@ final class MD_STD {
*
* @param string $url URL to validate.
*
* @return bool
* @return boolean
*/
public static function checkUrlIsReachable(string $url):bool {
if (empty($url)) {
if (empty($url = MD_STD_IN::sanitize_url($url))) {
throw new MDInvalidUrl("Input URL cannot be empty");
}
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new MDInvalidUrl("URL to check (" . $url . ") does not seem to be a valid URL");
}
$ch = self::curl_init($url, 5000);
curl_setopt_array($ch, [
@ -827,7 +854,11 @@ final class MD_STD {
*/
public static function string_to_color_code(string $str):string {
return \substr(\dechex(\crc32($str)), 0, 6);
$output = \dechex(\crc32($str));
if (\strlen($output) < 6) return '000000';
return \substr($output, 0, 6);
}
@ -869,10 +900,12 @@ final class MD_STD {
$max = count($input);
$offset = 0;
$sizePerEntry = max($size - 1, 1); // Size - 1 is expected, but size below 1 ends in endless loops
while ($offset < $max) {
$cur = array_slice($input, $offset, $size - 1);
$cur = array_slice($input, $offset, $sizePerEntry);
if (!empty($cur)) $output[] = $cur;
$offset += $size - 1;
$offset += $sizePerEntry;
}
return $output;
@ -898,13 +931,106 @@ final class MD_STD {
$max = count($input);
$offset = 0;
$sizePerEntry = max($size - 1, 1); // Size - 1 is expected, but size below 1 ends in endless loops
while ($offset < $max) {
$cur = array_slice($input, $offset, $size - 1);
$cur = array_slice($input, $offset, $sizePerEntry);
if (!empty($cur)) $output[] = $cur;
$offset += $size - 1;
$offset += $sizePerEntry;
}
return $output;
}
/**
* Transfers a date into an integer. 0000-01-01 > 101; 2001-01-01 > 20010101.
* Needed to store negative dates (BC) in MySQL.
*
* @param string $date Date.
*
* @return integer
*/
public static function date_to_int(string $date):int {
$isNegative = substr($date, 0, 1) === '-';
$parts = explode('-', $date);
if ($isNegative === true) return -1 * intval(implode($parts));
else return intval(implode($parts));
}
/**
* Transfers an integer into a date, reversing the effects of date_to_int.
* 0000-01-01 > 101; 2001-01-01 > 20010101.
* Needed to retrieve negative dates (BC) stored in MySQL.
*
* @param integer $date_int Date represented as an integer.
*
* @return string
*/
public static function int_to_date(int $date_int):string {
$dateStr = (string)$date_int;
if (substr($dateStr, 0, 1) === '-') {
$isNegative = true;
$dateStr = substr($dateStr, 1);
}
else $isNegative = false;
if (strlen($dateStr) < 8) {
$dateStr = str_pad($dateStr, 8, "0", STR_PAD_LEFT);
}
$day = substr($dateStr, -2, 2);
$month = substr($dateStr, -4, 2);
$year = substr($dateStr, 0, -4);
return match($isNegative) {
true => '-',
false => '',
} . $year . '-' . $month . '-' . $day;
}
/**
* Finds the next occurence of any of a given set of substrings.
*
* @param string $haystack The string to search in.
* @param non-empty-array<non-empty-string> $needles The strings to search for.
* @param integer $offset If specified, search will
* start this number of
* characters counted from
* the beginning of the string.
* If the offset is negative,
* the search will start this
* number of characters
* counted from the end of
* the string.
*
* @return array{position: integer, needle: string}|array{}
*/
public static function strpos_multi(string $haystack, array $needles, int $offset = 0):array {
$lowest_option = [];
foreach ($needles as $needle) {
if (($pos = strpos($haystack, $needle, $offset)) !== false) {
if (empty($lowest_option)) {
$lowest_option = ['position' => $pos, 'needle' => $needle];
}
else if ($pos < $lowest_option['position']) {
$lowest_option = ['position' => $pos, 'needle' => $needle];
}
}
}
return $lowest_option;
}
}

View File

@ -10,41 +10,20 @@ declare(strict_types = 1);
* Provides caching functions.
*/
final class MD_STD_CACHE {
/** @var string */
public static string $redis_host = '127.0.0.1';
/** @var integer */
public static int $redis_port = 6379;
/**
* Opens a connection to redis.
*
* @return Redis
*/
public static function open_redis_default():Redis {
$redis = new Redis();
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
return $redis;
}
/**
* Shutdown function for caching contents of output buffer.
*
* @param Redis $redis Redis connection.
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
*
* @return void
*/
public static function shutdown_cache_through_redis(string $redisKey, int $expiry = 3600):void {
public static function shutdown_cache_through_redis(Redis $redis, string $redisKey, int $expiry = 3600):void {
$outputT = trim(MD_STD::minimizeHTMLString(MD_STD::ob_get_clean()));
echo $outputT;
$redis = self::open_redis_default();
$redis->set($redisKey, $outputT);
$redis->expire($redisKey, $expiry);
$redis->close();
@ -55,26 +34,18 @@ final class MD_STD_CACHE {
* Caches and serves a page through redis. Should be called at the start
* of the script generating a page.
*
* @param Redis $redis Redis connection.
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
* @param Redis|null $redis Redis connection already opened, if one exists.
* If this parameter is not provided, a separate
* redis connection is opened for this function.
*
* @return string
*/
public static function serve_page_through_redis_cache(string $redisKey, int $expiry = 3600, ?Redis $redis = null):string {
public static function serve_page_through_redis_cache(Redis $redis, string $redisKey, int $expiry = 3600):string {
if (PHP_SAPI === 'cli') {
return '';
}
if ($redis === null) {
$redis = self::open_redis_default();
$closeRedis = true;
}
else $closeRedis = false;
if ($redis->ping() !== false) {
ob_start();
@ -86,26 +57,22 @@ final class MD_STD_CACHE {
return $redisResult;
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
register_shutdown_function(function(Redis $redis, string $redisKey, int $expiry) :void {
self::shutdown_cache_through_redis($redis, $redisKey, $expiry);
}, $redis, $redisKey, $expiry);
}
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
register_shutdown_function(function(Redis $redis, string $redisKey, int $expiry) :void {
self::shutdown_cache_through_redis($redis, $redisKey, $expiry);
}, $redis, $redisKey, $expiry);
}
}
if ($closeRedis === true) {
$redis->close();
}
return '';
}

View File

@ -14,7 +14,7 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return integer
* @return positive-int
*/
public static function sanitize_id(mixed $input):int {
@ -39,11 +39,11 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return integer
* @return 0|positive-int
*/
public static function sanitize_id_or_zero(mixed $input):int {
if ($input === "") {
if ($input === "" || $input === 0) {
return 0;
}
@ -92,14 +92,18 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return string
* @return non-empty-string
*/
public static function sanitize_rgb_color(mixed $input):string {
$output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
if (empty($output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
if ($output === false
|| (preg_match('/^[a-zA-Z0-9]{3}$/', $output) === false && preg_match('/^[a-zA-Z0-9]{6}$/', $output) === false)
$output = \strtoupper($output);
if (!in_array(strlen($output), [3, 6], true)
|| (MD_STD::preg_replace_str('/[A-F0-9]/', '', $output) !== '')
) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
@ -113,7 +117,7 @@ final class MD_STD_IN {
*
* @param array<mixed> $inputs Input array.
*
* @return list<integer>
* @return list<positive-int>
*/
public static function sanitize_id_array(array $inputs):array {
@ -127,6 +131,27 @@ final class MD_STD_IN {
}
/**
* Validates an input array for all entries being strings. Unsets empty values.
* This is especially useful for parsed JSON.
*
* @param array<mixed> $inputs Input array.
*
* @return array<string>
*/
public static function sanitize_text_array(array $inputs):array {
$output = [];
foreach ($inputs as $key => $input) {
if (empty($input)) continue;
$output[$key] = self::sanitize_text($input);
}
return $output;
}
/**
* Retrieves HTTP input texts from GET or POST variables, whatever is provided.
* If neither is given, returns a provided default.
@ -197,13 +222,51 @@ final class MD_STD_IN {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_URL);
if (($output = \filter_var($output, FILTER_VALIDATE_URL)) === false) {
try {
if (($output = \filter_var($input, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL");
}
}
catch (MDInvalidUrl $e) {
if (($parsed = parse_url($input)) === false || empty($parsed['scheme']) || empty($parsed['host']) || empty($parsed['path'])) {
throw new MDInvalidUrl("Invalid input URL");
}
$rewritten = $parsed['scheme'] . '://';
if (!empty($parsed['user']) && !empty($parsed['pass'])) {
$rewritten .= $parsed['user'] . ':' . $parsed['pass'] . '@';
}
$rewritten .= $parsed['host'];
if (!empty($parsed['port'])) $rewritten .= ':' . $parsed['port'];
$rewritten .= str_replace('%2F', '/', urlencode($parsed['path']));
if (!empty($parsed['query'])) {
$rewritten .= '?' . str_replace('%3D', '=', urlencode($parsed['query']));
}
if (($output = \filter_var($rewritten, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL" . \urlencode($input));
}
}
if (empty($output)) return '';
// As per the RFC, URLs should not exceed 2048. Enough real-world ones
// do. But they certainly should not exceed 10000 characters.
if (\strlen($output) > 10000) {
throw new MDInvalidUrl("The entered URL seems to be valid otherwise, but is overly long.");
}
// Check for valid schemes
if (MD_STD::startsWithAny($input, ['https://', 'http://', 'ftp://']) === false) {
try {
if (MD_STD::startsWithAny($output, ['https://', 'http://', 'ftp://']) === false) {
throw new MDInvalidUrl("Invalid input URL" . PHP_EOL . $output . PHP_EOL . strtolower($output));
}
}
catch (MDInvalidUrl $e) {
if (MD_STD::startsWithAny(strtolower($output), ['https://', 'http://', 'ftp://']) === true) {
$output = strtolower(substr($output, 0, 8)) . substr($output, 8);
}
else throw $e;
}
if (\str_contains($output, '.') === false) {
throw new MDInvalidUrl("Invalid input URL");
}
@ -224,9 +287,8 @@ final class MD_STD_IN {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_EMAIL);
if (($output = \filter_var($output, FILTER_VALIDATE_EMAIL)) === false) {
throw new MDInvalidEmail("Invalid input email address");
if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) {
throw new MDInvalidEmail("Invalid input email address" . ' ' . $input);
}
return $output;
@ -289,7 +351,7 @@ final class MD_STD_IN {
$output = \str_replace(",", ".", $input);
if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
throw new MDgenericInvalidInputsException("Input is readable as a floating point value");
throw new MDgenericInvalidInputsException("Input is not readable as a floating point value");
}
return $output;
@ -302,7 +364,7 @@ final class MD_STD_IN {
*
* @return float
*/
public static function validate_longitude(string|int $input):float {
public static function validate_longitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
@ -322,7 +384,7 @@ final class MD_STD_IN {
*
* @return float
*/
public static function validate_latitude(string|int $input):float {
public static function validate_latitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
@ -349,7 +411,7 @@ final class MD_STD_IN {
}
// Remove hyphens
$input = trim(strtr($input, ["-" => "", "" => ""]));
$input = trim(strtr($input, ["-" => "", "" => "", '—' => '', " " => ""]));
// ISBN 10
if (\mb_strlen($input) === 10) {
@ -373,6 +435,27 @@ final class MD_STD_IN {
}
/**
* Validates a ZIP code.
*
* @param string $input Input string.
*
* @return string
*/
public static function validate_zip_code(string $input):string {
if (($input = trim($input)) === "") {
return "";
}
if (\mb_strlen($input) > 7) {
throw new MDgenericInvalidInputsException("ZIP code is too long");
}
return $input;
}
/**
* Returns an UTF8 version of a string.
*

View File

@ -9,14 +9,14 @@ declare(strict_types = 1);
*/
final class MD_STD_SEC {
const REFRESH_TIME_GENERAL = 60; // Time until the comp. with the whole service is cleared.
const REFRESH_TIME_USER = 600; // Time until the comp. with the same username service is cleared.
const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP.
private const REFRESH_TIME_GENERAL = 60; // Time until the comp. with the whole service is cleared.
private const REFRESH_TIME_USER = 600; // Time until the comp. with the same username service is cleared.
private const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP.
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.04;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.0;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.6;
private const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
private const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.04;
private const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.0;
private const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.6;
/**
* Function for retrieving the anti-csrf token or generating it if need be.
@ -25,6 +25,10 @@ final class MD_STD_SEC {
*/
public static function getAntiCsrfToken():string {
if(session_status() !== PHP_SESSION_ACTIVE) {
throw new Exception("Session needs to be started to get csrf token");
}
if (empty($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
@ -105,7 +109,7 @@ final class MD_STD_SEC {
// Unstable but working way to get the user's IP. If the IP is falsified,
// this can't be found out anyway and security is established by _common.
$ip = \filter_var($_SERVER['REMOTE_ADDR'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['HTTP_CLIENT_IP']), \FILTER_VALIDATE_IP) ?: "Failed to find";
$ip = \filter_var($_SERVER['REMOTE_ADDR'] ?? ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? ""), \FILTER_VALIDATE_IP) ?: "Failed to find";
// Set name of log file
$logfile_common = \sys_get_temp_dir() . "/logins_{$tool_name}.json";

39
src/MD_STD_STRINGS.php Normal file
View File

@ -0,0 +1,39 @@
<?PHP
/**
* Gathers wrappers for handling strings.
*/
declare(strict_types = 1);
/**
* Encapsulates functions for handling strings.
*/
final class MD_STD_STRINGS {
/**
* Duplicates words ending in a given set of strings (e.g. dots) that serve as bind chars
* to allow indexing to then allow searching for them in either form.
*
* @param string $input Input string.
*
* @return string
*/
public static function duplicate_words_with_dots_for_indexing(string $input):string {
$charsToDuplicateOn = ',.!-';
$wordsToAdd = [];
$words = explode(' ', $input);
foreach ($words as $word) {
$trimmed = trim($word, $charsToDuplicateOn);
if ($trimmed !== $word) {
$wordsToAdd[] = $trimmed;
}
}
if (empty($wordsToAdd)) {
return $input;
}
return $input . ' ' . implode(' ', $wordsToAdd);
}
}

View File

@ -0,0 +1,36 @@
<?PHP
/**
* Test for ensuring that search HTML pages or components are generated correctly.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
/**
* Tests for the manifest.
*/
abstract class MD_STD_HTML_TEST extends TestCase {
/**
* Validates HTML strings.
*
* @param string $input Input string to validate.
*
* @return void
*/
final protected static function validateHtmlString(string $input):void {
self::assertNotEmpty($input);
libxml_use_internal_errors(true);
$xml = simplexml_load_string('<!DOCTYPE HTML><abc>' . trim(strtr($input, [' async' => ' ', ' defer' => ' '])) . '</abc>');
if (!empty($errors = libxml_get_errors())) {
throw new Exception("Invalid HTML code detected: " . var_export($errors, true) . PHP_EOL . PHP_EOL . $input);
}
self::assertNotEquals(null, $xml);
}
}

View File

@ -0,0 +1,145 @@
<?PHP
/**
* Test for ensuring that search RSS feeds are generated correctly.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
/**
* Tests for the manifest.
*/
final class MD_STD_TEST_PROVIDERS {
/**
* Data provider for returning invalid URLs.
*
* @return array<array{0: string}>
*/
public static function invalid_url_provider():array {
return [
'Space in protocol name' => ["h ttps://www.museum-digital.org"],
'Unwanted protocol' => ["telegram://www.museum-digital.org"],
'String without protocol' => ["www.museum-digital.org"],
'Localhost' => ["http://localhost"],
// As per the RFC, URLs should not exceed 2048. Enough real-world ones
// do. But they certainly should not exceed 10000 characters.
'Overly long URL (> 10000 chars)' => ["https://www.museum-digital.org/" . str_repeat('a', 10000)],
];
}
/**
* Data provider for working URLs.
*
* @return array<array{0: string, 1: string}>
*/
public static function valid_url_provider():array {
return [
'Regular URL without path or query' => ['https://www.museum-digital.org', 'https://www.museum-digital.org'],
'URL with uppercase character in scheme' => ['Https://www.museum-digital.org', 'https://www.museum-digital.org'],
'URL with cyrillic characters, HTML-encoded ' => [
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
],
'URL with cyrillic characters, not HTML-encoded ' => [
'https://sr.wikipedia.org/wiki/Београд',
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
],
'URL with: scheme, user, pass, host, path, query' => [
'https://username:password@sr.wikipedia.org:9000/wiki/Београд?test=hi',
'https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4?test=hi',
],
];
}
/**
* Data provider for working mail addresses.
*
* @return array<array{0: string}>
*/
public static function invalid_email_provider():array {
// Invalid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses
$invalid = [
'plainaddress',
'#@%^%#$@#$@#.com',
'@example.com',
'Joe Smith <email@example.com>',
'email.example.com',
'email@example@example.com',
'.email@example.com',
'email.@example.com',
'email..email@example.com',
'あいうえお@example.com',
'email@example.com (Joe Smith)',
'email@example',
'email@-example.com',
'email@111.222.333.44444',
'email@example..com',
'Abc..123@example.com',
'“(),:;<>[\]@example.com',
'just"not"right@example.com',
'this\ is"really"not\allowed@example.com',
];
$output = [];
foreach ($invalid as $addr) {
$output[$addr] = [
$addr,
];
}
$output['Mail address is too long'] = [str_repeat("a", 10000) . '@example.com'];
return $output;
}
/**
* Data provider for working mail addresses.
*
* @return array<array{0: string, 1: string}>
*/
public static function valid_email_provider():array {
// Valid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses
// Excluding:
//
// 'email@123.123.123.123',
// 'email@[123.123.123.123]',
// '“email”@example.com',
//
// as per PHP's FILTER_VALIDATE_EMAIL
$valid = [
'email@example.com',
'firstname.lastname@example.com',
'email@subdomain.example.com',
'firstname+lastname@example.com',
'1234567890@example.com',
'email@example-one.com',
'_______@example.com',
'email@example.name',
'email@example.museum',
'email@example.co.jp',
'firstname-lastname@example.com',
];
$output = [];
foreach ($valid as $addr) {
$output[$addr] = [
$addr,
$addr,
];
}
return $output;
}
}

107
tests/MDFormatterTest.php Normal file
View File

@ -0,0 +1,107 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_IN.
*/
#[small]
#[CoversClass(\MDFormatter::class)]
final class MDFormatterTest extends TestCase {
/**
* Function for testing formatMarkdownHeadline1().
*
* @return void
*/
public function testFormatMarkdownHeadline1():void {
self::assertEquals("a" . PHP_EOL . "=" . PHP_EOL . PHP_EOL, MDFormatter::formatMarkdownHeadline1("a"));
}
/**
* Function for testing formatMarkdownHeadline2().
*
* @return void
*/
public function testFormatMarkdownHeadline2():void {
self::assertEquals("a" . PHP_EOL . "-" . PHP_EOL . PHP_EOL, MDFormatter::formatMarkdownHeadline2("a"));
}
/**
* Function for testing formatMarkdownHeadline3().
*
* @return void
*/
public function testFormatMarkdownHeadline3():void {
self::assertEquals("### a" . PHP_EOL, MDFormatter::formatMarkdownHeadline3("a"));
}
/**
* Function for testing formatMarkdownHorizontalRule().
*
* @return void
*/
public function testFormatMarkdownHorizontalRule():void {
self::assertEquals("___" . PHP_EOL, MDFormatter::formatMarkdownHorizontalRule());
}
/**
* Function for testing formatMarkdownNamedLink().
*
* @return void
*/
public function testFormatMarkdownNamedLink():void {
self::assertEquals("[a](https://example.com)", MDFormatter::formatMarkdownNamedLink("a", "https://example.com"));
}
/**
* Function for testing formatMarkdownBlockQuote().
*
* @return void
*/
public function testFormatMarkdownBlockQuote():void {
self::assertEquals("> Test" . PHP_EOL . "> test" . PHP_EOL, MDFormatter::formatMarkdownBlockQuote("Test" . PHP_EOL . "test"));
}
/**
* Function for testing formatMarkdownCodeBlock().
*
* @return void
*/
public function testFormatMarkdownCodeBlock():void {
self::assertEquals("```" . PHP_EOL . "Test" . PHP_EOL . "test" . PHP_EOL . "```" . PHP_EOL, MDFormatter::formatMarkdownCodeBlock("Test" . PHP_EOL . "test"));
}
/**
* Function for testing formatMarkdownUnorderedListItem().
*
* @return void
*/
public function testFormatMarkdownUnorderedListItem():void {
self::assertEquals("- a" . PHP_EOL, MDFormatter::formatMarkdownUnorderedListItem("a"));
self::assertEquals("- a" . PHP_EOL . " a" . PHP_EOL, MDFormatter::formatMarkdownUnorderedListItem("a" . PHP_EOL . "a"));
}
}

692
tests/MD_STD_IN_Test.php Normal file
View File

@ -0,0 +1,692 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_IN.
*/
#[small]
#[CoversClass(\MD_STD_IN::class)]
final class MD_STD_IN_Test extends TestCase {
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed, 1: int}>
*/
public static function valid_id_provider():array {
$values = [
[1, 1],
["1", 1],
["1111111", 1111111],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for valid longitudes.
*
* @return array<array{0: mixed, 1: float}>
*/
public static function valid_longitude_provider():array {
$values = [
["1", 1.0],
["12", 12.0],
[12, 12.0],
[12.0, 12.0],
[95, 95.0],
[-95, -95.0],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for invalid longitudes.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function invalid_longitude_provider():array {
$values = [
["test", MDgenericInvalidInputsException::class],
["test1", MDgenericInvalidInputsException::class],
["", MDgenericInvalidInputsException::class],
["1900", MDCoordinateOutOfRange::class],
[1900, MDCoordinateOutOfRange::class],
[-1900, MDCoordinateOutOfRange::class],
[185, MDCoordinateOutOfRange::class],
[-185, MDCoordinateOutOfRange::class],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for valid latitudes.
*
* @return array<array{0: mixed, 1: float}>
*/
public static function valid_latitude_provider():array {
$values = [
["1", 1.0],
["12", 12.0],
[12, 12.0],
[12.0, 12.0],
[85, 85.0],
[-85, -85.0],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for invalid latitudes.
*
* @return array<array{0: mixed, 1: class-string<Throwable>}>
*/
public static function invalid_latitude_provider():array {
$values = [
["test", MDgenericInvalidInputsException::class],
["test1", MDgenericInvalidInputsException::class],
["", MDgenericInvalidInputsException::class],
["1900", MDCoordinateOutOfRange::class],
[1900, MDCoordinateOutOfRange::class],
[-1900, MDCoordinateOutOfRange::class],
[95, MDCoordinateOutOfRange::class],
[-95, MDCoordinateOutOfRange::class],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Function for testing sanitize_id().
*
* @param mixed $to_validate Input to validate.
* @param integer $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_id_provider')]
public function test_sanitize_id_works(mixed $to_validate, int $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_id($to_validate));
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed}>
*/
public static function invalid_id_provider():array {
$output = self::invalid_id_provider_without_zero();
$output['Number 0'] = [0];
return $output;
}
/**
* Function for testing sanitize_id().
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_id_provider')]
public function test_sanitize_id_fails(mixed $to_validate):void {
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id($to_validate);
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed, 1: int}>
*/
public static function valid_id_or_zero_provider():array {
$output = self::valid_id_provider();
$output['Integer 0'] = [0, 0];
$output['String 0'] = [0, 0];
return $output;
}
/**
* Function for testing sanitize_id_or_zero().
*
* @param mixed $to_validate Input to validate.
* @param integer $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_id_or_zero_provider')]
public function test_sanitize_id_or_zero_works(mixed $to_validate, int $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_id_or_zero($to_validate));
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed}>
*/
public static function invalid_id_provider_without_zero():array {
return [
'Number too high' => [1000000000000000000000000000000000000],
'Number with character in the middle' => ["1a2"],
'Number with suffixed string' => ["12a"],
'String character' => ["a"],
];
}
/**
* Function for testing sanitize_id_or_zero().
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_id_provider_without_zero')]
public function test_sanitize_id_or_zero_fails(mixed $to_validate):void {
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_or_zero($to_validate);
}
/**
* Data provider for text with its expected cleaned values.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function text_with_expected_return_value_provider():array {
return [
'Empty string' => ['', ''],
'Integer 0' => [0, '0'],
'String 0' => ['0', '0'],
'Regular string' => ['a', 'a'],
'String with double whitespace in between' => ['a a', 'a a'],
'String to be trimmed (spaces)' => ['a ', 'a'],
'String to be trimmed (newline)' => ['a ' . PHP_EOL, 'a'],
'Empty array' => [[], ''],
'Array with content' => [['test' => 'test'], ''],
];
}
/**
* Function for testing sanitize_text().
*
* @param mixed $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('text_with_expected_return_value_provider')]
public function test_sanitize_text(mixed $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_text($to_validate));
}
/**
* Data provider for working RGB colors.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function valid_rgb_colors_provider():array {
return [
'Three character version' => ['AAA', 'AAA'],
'Three character version (int)' => ['111', '111'],
'Three character version (mixed)' => ['1a1', '1A1'],
'Six character version' => ['AAAAAA', 'AAAAAA'],
'Six character version (int)' => ['111111', '111111'],
'Six character version (mixed)' => ['1a1AAA', '1A1AAA'],
];
}
/**
* Data provider for strings that are not rgb colors.
*
* @return array<array{0: mixed}>
*/
public static function invalid_rgb_colors_provider():array {
$output = [
'Array' => [[]],
'Three characters, but invalid ones' => ['ZZZ'],
'Six characters, but invalid ones' => ['111ZZZ'],
'Three characters, but with spaces' => ['ZZZ '],
];
for ($i = 0; $i++; $i < 10) {
if ($i === 3 || $i === 6) continue;
$output['Valid characters repeated ' . $i . ' times'] = [$i];
}
return $output;
}
/**
* Function for testing sanitize_rgb_color().
*
* @param mixed $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_rgb_colors_provider')]
public function test_sanitize_rgb_color_works(mixed $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_rgb_color($to_validate));
}
/**
* Function for testing sanitize_rgb_color()'s failure modes.
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_rgb_colors_provider')]
public function test_sanitize_rgb_color_fails(mixed $to_validate):void {
self::expectException(MDInvalidColorCode::class);
MD_STD_IN::sanitize_rgb_color($to_validate);
}
/**
* Function for testing sanitize_text_array().
*
* @return void
*/
public function test_sanitize_text_array():void {
self::assertEquals(["1"], MD_STD_IN::sanitize_text_array([1, '']));
self::assertEquals(["1", "2"], MD_STD_IN::sanitize_text_array(["1", 2]));
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([[]]);
}
/**
* Function for testing sanitize_id_array().
*
* @return void
*/
public function test_sanitize_id_array():void {
self::assertEquals([1], MD_STD_IN::sanitize_id_array([1]));
self::assertEquals([1, 2], MD_STD_IN::sanitize_id_array(["1", 2]));
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([0]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([0, 1]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([100000000000000000000000000000]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["1a2"]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["12a"]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["a"]);
}
/**
* Function for testing get_http_input_text().
*
* @return void
*/
public function test_get_http_input_text():void {
$_GET['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_input_text("test"));
unset($_GET['test']);
$_POST['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_input_text("test"));
unset($_POST['test']);
$_POST['test'] = [];
self::assertEquals("", MD_STD_IN::get_http_input_text("test"));
unset($_POST['test']);
self::assertEquals("", MD_STD_IN::get_http_input_text("test"));
self::expectException(MDpageParameterNotFromListException::class);
MD_STD_IN::get_http_input_text("a", "", ['a']);
}
/**
* Function for testing get_http_post_text().
*
* @return void
*/
public function test_get_http_post_text():void {
$_POST['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_post_text("test"));
unset($_POST['test']);
$_POST['test'] = [];
self::assertEquals("", MD_STD_IN::get_http_post_text("test"));
unset($_POST['test']);
self::assertEquals("", MD_STD_IN::get_http_post_text("test"));
self::expectException(MDpageParameterNotFromListException::class);
MD_STD_IN::get_http_post_text("a", "", ['a']);
}
/**
* Function for testing sanitize_url().
*
* @return void
*/
public function test_sanitize_url_with_empty_string():void {
// Ensure empty inputs return empty output
self::assertEquals("", MD_STD_IN::sanitize_url(""));
}
/**
* Function for testing sanitize_url().
*
* @param string $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'valid_url_provider')]
public function test_sanitize_url_works(string $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_url($to_validate));
}
/**
* Function for testing sanitize_url().
*
* @param string $to_validate Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_url_provider')]
public function test_sanitize_url_fails(string $to_validate):void {
self::expectException(MDInvalidUrl::class);
MD_STD_IN::sanitize_url($to_validate);
}
/**
* Function for testing sanitize_email().
*
* @return void
*/
public function test_sanitize_email_empty():void {
self::assertEquals("", MD_STD_IN::sanitize_email(""));
}
/**
* Function for testing sanitize_email().
*
* @param string $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'valid_email_provider')]
public function test_sanitize_email_works(string $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_email($to_validate));
}
/**
* Function for testing sanitize_email() fails when it should.
*
* @param string $to_validate Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_email_provider')]
public function test_sanitize_email_fails(string $to_validate):void {
self::expectException(MDInvalidEmail::class);
MD_STD_IN::sanitize_email($to_validate);
}
/**
* Function for testing validate_password().
*
* @return void
*/
public function test_validate_password():void {
self::assertEquals(['password_too_short', 'password_has_no_number_no_special_char'], MD_STD_IN::validate_password("a"));
self::assertEquals(['password_has_no_number_no_special_char'], MD_STD_IN::validate_password("aaaaaaaaaaaaaaaaaaaa"));
self::assertEquals(['password_too_short'], MD_STD_IN::validate_password("!a323!"));
self::assertEquals([], MD_STD_IN::validate_password("!a324324324123!"));
}
/**
* Function for testing validate_phone_number().
*
* @return void
*/
public function test_validate_phone_number():void {
self::assertEquals("", MD_STD_IN::validate_phone_number(""));
self::assertEquals("+1932-1321123", MD_STD_IN::validate_phone_number("+1932-1321123"));
self::assertEquals("+49 (030) 21321123", MD_STD_IN::validate_phone_number("+49 (030) 21321123"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_phone_number("test@example.org");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_phone_number("+123456789 z");
}
/**
* Function for testing sanitize_float().
*
* @return void
*/
public function test_sanitize_float():void {
self::assertEquals(0, MD_STD_IN::sanitize_float("0"));
self::assertEquals(12, MD_STD_IN::sanitize_float("12"));
self::assertEquals(12.12, MD_STD_IN::sanitize_float("12.12"));
self::assertEquals(12.12, MD_STD_IN::sanitize_float("12,12"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::sanitize_float("test");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::sanitize_float("");
}
/**
* Function for testing validate_longitude().
*
* @param mixed $to_validate Input to validate.
* @param float $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_longitude_provider')]
public function test_validate_longitude_with_valid_entries(mixed $to_validate, float $expected):void {
self::assertEquals($expected, MD_STD_IN::validate_longitude($to_validate));
}
/**
* Function for testing validate_longitude().
*
* @param mixed $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_longitude_provider')]
public function test_validate_longitude_with_invalid_entries(mixed $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD_IN::validate_longitude($to_validate);
}
/**
* Function for testing validate_latitude().
*
* @param mixed $to_validate Input to validate.
* @param float $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_latitude_provider')]
public function test_validate_latitude_with_valid_entries(mixed $to_validate, float $expected):void {
self::assertEquals($expected, MD_STD_IN::validate_latitude($to_validate));
}
/**
* Function for testing validate_latitude().
*
* @param mixed $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_latitude_provider')]
public function test_validate_latitude_with_invalid_entries(mixed $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD_IN::validate_latitude($to_validate);
}
/**
* Function for testing validate_isbn().
*
* @return void
*/
public function test_validate_isbn():void {
self::assertEquals("", MD_STD_IN::validate_isbn(""));
self::assertEquals("0943396042", MD_STD_IN::validate_isbn("0943396042"));
self::assertEquals("0943396042", MD_STD_IN::validate_isbn("0-943396-04-2"));
self::assertEquals("094339604X", MD_STD_IN::validate_isbn("0-943396-04-X"));
self::assertEquals("1230943396042", MD_STD_IN::validate_isbn("1230943396042"));
self::assertEquals("1230943396042", MD_STD_IN::validate_isbn("1230-943396-04-2"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_isbn("X094339604");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_isbn("094339604a");
}
/**
* Function for testing validate_zip_code().
*
* @return void
*/
public function test_validate_zip_code():void {
self::assertEquals("", MD_STD_IN::validate_zip_code(""));
self::assertEquals("1234", MD_STD_IN::validate_zip_code(" 1234"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_zip_code("X094339604");
}
/**
* Function for testing ensureStringIsUtf8().
*
* @return void
*/
public function test_ensureStringIsUtf8():void {
if (empty($convToIso8859 = iconv("UTF-8", 'ISO-8859-1//TRANSLIT', "ä"))) {
throw new Exception("Iconv returned empty result");
}
if (empty($convToIso2022 = iconv("UTF-8", 'ISO-2022-JP//TRANSLIT', "ä"))) {
throw new Exception("Iconv returned empty result");
}
self::assertEquals("ä", MD_STD_IN::ensureStringIsUtf8("ä"));
self::assertEquals("ä", MD_STD_IN::ensureStringIsUtf8($convToIso8859));
self::assertEquals("a", MD_STD_IN::ensureStringIsUtf8($convToIso2022));
}
}

View File

@ -7,19 +7,19 @@
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
require __DIR__ . '/../src/MD_STD_SEC.php';
use PHPUnit\Framework\Attributes\Large;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_SEC.
*/
#[large]
#[CoversClass(\MD_STD_SEC::class)]
final class MD_STD_SECTest extends TestCase {
/**
* Function for testing if the page can be opened using invalid values for objektnum.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
* @group MissingInputs
* @group SafeForProduction
* @small
*
* @return void
*/
@ -34,4 +34,71 @@ final class MD_STD_SECTest extends TestCase {
self::assertLessThan(3 * 1000000, $delay_reduced); // Smaller than 10 seconds
}
/**
* Ensure getAntiCsrfToken does not work without a
* started session.
*
* @return void
*/
public function testGetAntiCsrfTokenFailsWithoutActiveSession():void {
self::expectException(Exception::class);
MD_STD_SEC::getAntiCsrfToken();
}
/**
* Ensure getAntiCsrfToken works.
*
* @return void
*/
public function testGetAntiCsrfTokenWorks():void {
session_start();
self::assertEmpty($_SESSION);
$token = MD_STD_SEC::getAntiCsrfToken();
self::assertNotEmpty($_SESSION['csrf-token']);
self::assertEquals($token, MD_STD_SEC::getAntiCsrfToken());
$_POST = [
'csrf-token' => $token,
];
self::assertTrue(MD_STD_SEC::validateAntiCsrfToken());
}
/**
* Ensure preventBruteForce works.
*
* @return void
*/
public function testPreventBruteForce():void {
self::assertTrue(MD_STD_SEC::preventBruteForce("MD_STD_TEST_SUCCESS", "test_user", 0));
$logFile = \sys_get_temp_dir() . "/logins_MD_STD_TEST_SUCCESS.json";
self::assertFileExists($logFile);
MD_STD::unlink($logFile);
}
/**
* Ensure preventBruteForce returns false on many requests.
*
* @return void
*/
public function testPreventBruteForceReturnsFalseOnManyRequests():void {
for ($i = 0; $i < 10; $i++) {
MD_STD_SEC::preventBruteForce("MD_STD_TEST_FAILURE", "test_user", 3);
}
self::assertFalse(MD_STD_SEC::preventBruteForce("MD_STD_TEST_FAILURE", "test_user", 3));
$logFile = \sys_get_temp_dir() . "/logins_MD_STD_TEST_FAILURE.json";
self::assertFileExists($logFile);
MD_STD::unlink($logFile);
}
}

View File

@ -0,0 +1,53 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_STRINGS.
*/
#[small]
#[CoversClass(\MD_STD_STRINGS::class)]
final class MD_STD_STRINGS_Test extends TestCase {
/**
* Data provider for strings to duplicate words with dots in.
*
* @return array<array{0: string, 1: string}>
*/
public static function duplicated_words_with_dots_provider():array {
$values = [
["hallo test. hallo", "hallo test. hallo test"],
];
$output = [];
foreach ($values as $value) {
$output[$value[0] . ' > ' . $value[1]] = $value;
}
return $output;
}
/**
* Function for testing duplicate_words_with_dots_for_indexing().
*
* @param string $input Input.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('duplicated_words_with_dots_provider')]
public function test_duplicate_words_with_dots_for_indexing(string $input, string $expected):void {
self::assertEquals($expected, MD_STD_STRINGS::duplicate_words_with_dots_for_indexing($input));
}
}

747
tests/MD_STD_Test.php Normal file
View File

@ -0,0 +1,747 @@
<?PHP
/**
* Tests for MD_STD.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD.
*/
#[small]
#[CoversClass(\MD_STD::class)]
final class MD_STD_Test extends TestCase {
/**
* Data provider for returning a tmp file.
*
* @return array<string, array{0: non-empty-string}>
*/
public static function file_name_in_tmp_provider():array {
$temp_file = \tempnam(\sys_get_temp_dir(), 'test_md_std');
if (!$temp_file) throw new Exception("Failed to get tmp dir");
return [
$temp_file => [$temp_file],
];
}
/**
* Data provider for invalid directories.
*
* @return array<non-empty-string, array{0: non-empty-string}>
*/
public static function invalid_directory_provider():array {
$non_existent = __DIR__ . '/' . uniqid();
return [
"File, not directory" => [__FILE__],
"Non-existent directory" => [$non_existent],
];
}
/**
* Data provider for invalid files for file_get_contents.
*
* @return array<string, array{0: string, 1: string}>
*/
public static function invalid_files_for_file_get_contents():array {
return [
'External path: FTP' => ["ftp://test", MDFileDoesNotExist::class], // Invalid external
'Non-existent file' => ["jklsdjfklasdjfklasdkl.jpg", MDFileDoesNotExist::class], // Non-existent file
];
}
/**
* Data provider for invalid files for file_get_contents.
*
* @return array<string, array{0: string, 1: array<string>, 2: string}>
*/
public static function failure_cases_ensure_file():array {
return [
'Non-existent file' => ["jklsdjfklasdjfklasdkl.jpg", [], MDFileDoesNotExist::class], // Non-existent file
'Invalid mime type' => [__FILE__, ["image/jpeg"], MDWrongFileType::class], // Non-existent file
];
}
/**
* Returns sample dates and their equivalent integer values according
* to MD_STD::date_to_int().
*
* @return array<string, array{0: string, 1: integer}>
*/
public static function date_to_int_provider():array {
$values = [
["2022-01-01", 20220101],
["0022-01-01", 220101],
["-0022-01-01", -220101],
["-2022-01-01", -20220101],
["-0001-01-01", -10101],
["0000-01-01", 101],
["100000-01-01", 1000000101],
];
$output = [];
foreach ($values as $value) {
$output[$value[0]] = $value;
}
return $output;
}
/**
* Checks if a file can be read.
*
* @return void
*/
public function test_file_get_contents_works():void {
self::assertNotEmpty(MD_STD::file_get_contents(__DIR__ . '/../phpunit.xml'));
}
/**
* Checks if a file can be written.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_file_put_contents_works(string $temp_file):void {
MD_STD::file_put_contents($temp_file, "Test");
self::assertEquals("Test", MD_STD::file_get_contents($temp_file));
MD_STD::unlink($temp_file);
}
/**
* Check MD_STD::realpath returns absolute file path of an existing file.
*
* @return void
*/
public function test_realpath_works():void {
self::assertEquals(__FILE__, MD_STD::realpath(__FILE__));
}
/**
* Check MD_STD::realpath throws exception on non-existing path.
*
* @param non-empty-string $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_files_for_file_get_contents')]
public function test_realpath_fails(string $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD::realpath($to_validate);
}
/**
* Checks if a file can be read.
*
* @param non-empty-string $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_files_for_file_get_contents')]
public function test_file_get_contents_fails_as_expected(string $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD::file_get_contents($to_validate);
}
/**
* Checks that json_encode works.
*
* @return void
*/
public function test_json_encode_works():void {
self::assertEquals('[0]', MD_STD::json_encode([0]));
}
/**
* Checks that json_encode_object works.
*
* @return void
*/
public function test_json_encode_object_works():void {
self::assertEquals('{"0":0}', MD_STD::json_encode_object((object)[0]));
}
/**
* Checks that mkdir works.
*
* @return void
*/
public function test_mkdir_works():void {
$testDir = sys_get_temp_dir() . '/' . uniqid("_test_");
if (file_exists($testDir)) {
throw new Exception("Test dir already exists");
}
MD_STD::mkdir($testDir);
self::assertTrue(is_dir($testDir));
}
/**
* Checks that nothing happens with mkdir.
*
* @return void
*/
public function test_mkdir_works_with_invalid():void {
MD_STD::mkdir(__DIR__);
self::assertTrue(is_dir(__DIR__));
}
/**
* Checks if nothing happens if a file does not exist.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_unlink_if_exists_passes_if_not_exists(string $temp_file):void {
if (file_exists($temp_file)) {
MD_STD::unlink($temp_file);
}
MD_STD::unlink_if_exists($temp_file);
self::assertFalse(is_file($temp_file));
}
/**
* Checks unlink_if_exists works.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_unlink_if_exists_works(string $temp_file):void {
touch($temp_file);
MD_STD::unlink_if_exists($temp_file);
self::assertFalse(is_file($temp_file));
}
/**
* Checks scandir works.
*
* @return void
*/
public function test_scandir():void {
if (empty($files = scandir(__DIR__))) {
throw new Exception("Running regular scandir() failed on script directory: " . __DIR__);
}
$filesChecked = MD_STD::scandir(__DIR__);
foreach ($filesChecked as $file) {
self::assertTrue(in_array($file, $files, true));
}
foreach (['.', '..', '.git'] as $excluded) {
self::assertFalse(in_array($excluded, $filesChecked, true));
}
}
/**
* Checks invalid directory.
*
* @param non-empty-string $dir Dir name.
*
* @return void
*/
#[DataProvider('invalid_directory_provider')]
public function test_invalid_directory_throws_error_on_scandir(string $dir):void {
self::expectException(MDFileDoesNotExist::class);
MD_STD::scandir($dir);
}
/**
* Checks that startsWithAny works.
*
* @return void
*/
public function test_startsWithAny():void {
self::assertTrue(MD_STD::startsWithAny("abc", ["a"]));
self::assertFalse(MD_STD::startsWithAny("abc", ["b"]));
}
/**
* Checks that stri_contains works.
*
* @return void
*/
public function test_stri_contains():void {
self::assertTrue(MD_STD::stri_contains("abc", "a"));
self::assertTrue(MD_STD::stri_contains("abc", "b"));
self::assertTrue(MD_STD::stri_contains("abc", "C"));
self::assertFalse(MD_STD::stri_contains("abc", "u"));
}
/**
* Checks that stri_contains_any works.
*
* @return void
*/
public function test_stri_contains_any():void {
self::assertTrue(MD_STD::stri_contains_any("abc", ["a"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["b"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["C"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["C", "u"]));
self::assertFalse(MD_STD::stri_contains_any("abc", ["u"]));
self::assertFalse(MD_STD::stri_contains_any("abc", ["u", "z"]));
}
/**
* Checks that strtotime works.
*
* @return void
*/
public function test_strtotime_works():void {
self::assertEquals(strtotime("2024-01-01"), MD_STD::strtotime("2024-01-01"));
}
/**
* Checks that strtotime works.
*
* @return void
*/
public function test_strtotime_fails_as_expected():void {
self::expectException(MDInvalidInputDate::class);
MD_STD::strtotime("test");
}
/**
* Checks that strtotime_for_public_apis works.
*
* @return void
*/
public function test_strtotime_for_pulic_apis_works():void {
self::assertEquals(strtotime("2024-01-01"), MD_STD::strtotime_for_public_apis("2024-01-01"));
}
/**
* Checks that strtotime_for_public_apis works.
*
* @return void
*/
public function test_strtotime_for_pulic_apis_fails_as_expected():void {
self::expectException(MDParseException::class);
MD_STD::strtotime_for_public_apis("test");
}
/**
* Checks that curl_init works.
*
* @return void
*/
public function test_curl_init():void {
self::expectNotToPerformAssertions();
MD_STD::curl_init("https://example.com", 2000);
}
/**
* Checks that runCurl works.
*
* @return void
*/
public function test_runCurl():void {
self::assertNotEmpty(MD_STD::runCurl("https://example.com", headers: ['X-TEST: Test']));
}
/**
* Checks that runCurl works.
*
* @return void
*/
public function test_runCurlMulti():void {
$output = MD_STD::runCurlMulti(["https://example.com" => "https://example.com"], headers: ['X-TEST: Test']);
self::assertEquals(["https://example.com"], array_keys($output));
}
/**
* Checks that checkUrlIsReachable works.
*
* @return void
*/
public function test_checkUrlIsReachable():void {
self::assertTrue(MD_STD::checkUrlIsReachable("https://example.com"));
self::assertFalse(MD_STD::checkUrlIsReachable("https://example.com/404"));
}
/**
* Checks that checkUrlIsReachable fails as expected.
*
* @param string $invalid_url Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_url_provider')]
public function test_checkUrlIsReachable_fails_as_expected(string $invalid_url):void {
self::expectException(MDInvalidUrl::class);
self::assertTrue(MD_STD::checkUrlIsReachable($invalid_url));
}
/**
* Checks that checkUrlIsReachable fails as expected.
*
* @return void
*/
public function test_checkUrlIsReachable_fails_as_expected_on_empty_input():void {
self::expectException(MDInvalidUrl::class);
self::assertTrue(MD_STD::checkUrlIsReachable(""));
}
/**
* Checks that filesize works.
*
* @return void
*/
public function test_filesize():void {
self::assertNotEmpty(MD_STD::filesize(__FILE__));
}
/**
* Checks that human_filesize works.
*
* @return void
*/
public function test_human_filesize_works():void {
self::assertEquals("2.00B", MD_STD::human_filesize(2));
self::assertEquals("2.00kB", MD_STD::human_filesize(2048));
}
/**
* Checks that minimizeHTMLString works.
*
* @return void
*/
public function test_minimizeHTMLString():void {
self::assertEquals("hi" . PHP_EOL . "hi" . PHP_EOL, MD_STD::minimizeHTMLString(" hi" . PHP_EOL . " hi"));
}
/**
* Checks that createTextSnippet works.
*
* @return void
*/
public function test_createTextSnippet():void {
self::assertEquals("Hallo...", MD_STD::createTextSnippet("Hallo Hallo, jkfljksdlajkfas", 8));
}
/**
* Checks that ensure_file works.
*
* @return void
*/
public function test_ensure_file_works():void {
self::expectNotToPerformAssertions();
MD_STD::ensure_file(__FILE__);
}
/**
* Checks that minimizeHTMLString works.
*
* @param non-empty-string $filepath File path.
* @param array<string> $mime_types Mime types expected.
* @param class-string<Throwable> $exception Exception class.
*
* @return void
*/
#[DataProvider('failure_cases_ensure_file')]
public function test_ensure_file_fails_as_expected(string $filepath, array $mime_types, string $exception):void {
self::expectException($exception);
MD_STD::ensure_file($filepath, $mime_types);
}
/**
* Checks that levenshtein works.
*
* @return void
*/
public function test_levensthein_works():void {
self::expectNotToPerformAssertions();
MD_STD::levenshtein(str_repeat("hi", 500), str_repeat("ho", 500));
}
/**
* Checks if dates can be translated to int and back.
*
* @param string $date Date to translate.
* @param integer $expectedInt Expected integer value for it.
*
* @return void
*/
#[DataProvider('date_to_int_provider')]
public function test_date_to_int(string $date, int $expectedInt):void {
$toInt = MD_STD::date_to_int($date);
$toStr = MD_STD::int_to_date($toInt);
self::assertEquals($expectedInt, $toInt);
self::assertEquals($date, $toStr);
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @param non-empty-string $dir Dir name.
*
* @return void
*/
#[DataProvider('invalid_directory_provider')]
public function test_invalid_directory_throws_error_on_check_is_writable(string $dir):void {
self::expectException(MDFileDoesNotExist::class);
MD_STD::check_is_writable($dir);
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @return void
*/
public function test_is_writable_returns_false_without_permissions():void {
self::expectException(MDFileIsNotWritable::class);
MD_STD::check_is_writable("/etc");
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @return void
*/
public function test_remote_mime_content_type_works():void {
self::assertEquals("text/html", MD_STD::remote_mime_content_type("https://example.com"));
}
/**
* Checks string_to_color_code works.
*
* @return void
*/
public function test_string_to_color_code():void {
self::assertEquals(6, strlen(MD_STD::string_to_color_code("")));
self::assertEquals(6, strlen(MD_STD::string_to_color_code("a")));
self::assertEquals(6, strlen(MD_STD::string_to_color_code("dsaf")));
}
/**
* Checks split_int_array_into_sized_parts works.
*
* @return void
*/
public function test_split_int_array_into_sized_parts():void {
self::assertEquals([[0 => 1], [0 => 1], [0 => 1], [0 => 1]], MD_STD::split_int_array_into_sized_parts([1, 1, 1, 1], 1));
}
/**
* Checks split_string_array_into_sized_parts works.
*
* @return void
*/
public function test_split_string_array_into_sized_parts():void {
self::assertEquals([[0 => "1"], [0 => "1"], [0 => "1"], [0 => "1"]], MD_STD::split_string_array_into_sized_parts(["1", "1", "1", "1"], 1));
}
/**
* Checks openssl_random_pseudo_bytes works.
*
* @return void
*/
public function test_openssl_random_pseudo_bytes():void {
self::assertNotEmpty(MD_STD::openssl_random_pseudo_bytes(12));
}
/**
* Checks lang_getfrombrowser works by getting value from HTTP header HTTP_ACCEPT_LANGUAGE.
*
* @return void
*/
public function test_lang_getfrombrowser_via_http_header():void {
$_SERVER = [
'HTTP_ACCEPT_LANGUAGE' => 'de',
];
self::assertEquals("de", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
$_SERVER = [
'HTTP_ACCEPT_LANGUAGE' => 'es_ES',
];
self::assertEquals("en", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
}
/**
* Checks lang_getfrombrowser returns default without any further information being provided.
*
* @return void
*/
public function test_lang_getfrombrowser_get_default():void {
self::assertEquals("en", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
}
/**
* Checks get_user_lang_no_cookie works with GET variable set.
*
* @return void
*/
public function test_get_user_lang_no_cookie_works_from_get_var():void {
$_GET = [
'navlang' => 'de',
];
self::assertEquals("de", MD_STD::get_user_lang_no_cookie(["de", "en"], "en"));
}
/**
* Checks get_user_lang_no_cookie works with GET variable set.
*
* @return void
*/
public function test_get_user_lang_no_cookie_works_without_get():void {
self::assertEquals("en", MD_STD::get_user_lang_no_cookie(["de", "en"], "en"));
}
/**
* Data provider for returning a tmp file.
*
* @return array<string, array{0: non-empty-string, 1: non-empty-array<non-empty-string>, 2: int}>
*/
public static function strpos_multi_provider():array {
return [
"Search quotation markes on when='" => ["when='", ['"', '\''], 5],
"Search quotation markes on when=\"" => ["when=\"", ['"', '\''], 5],
"Search quotation markes on when=\"'" => ["when=\"'", ['"', '\''], 5],
"Search quotation markes on when= (non-existent)" => ["when=", ['"', '\''], -1],
];
}
/**
* Checks unlink_if_exists works.
*
* @param non-empty-string $haystack Haystack.
* @param non-empty-array<non-empty-string> $needles Needles.
* @param integer $expected Expected position.
*
* @return void
*/
#[DataProvider('strpos_multi_provider')]
public function test_strpos_multi(string $haystack, array $needles, int $expected):void {
if ($expected === -1) {
self::assertEmpty(MD_STD::strpos_multi($haystack, $needles));
}
else {
self::assertNotEmpty(MD_STD::strpos_multi($haystack, $needles));
self::assertEquals($expected, MD_STD::strpos_multi($haystack, $needles)['position']);
}
}
}

25
tests/bootstrap.php Normal file
View File

@ -0,0 +1,25 @@
<?PHP
declare(strict_types = 1);
ini_set( 'error_log', '/dev/stdout' );
/**
* Autoloader for musdb.
*
* @param string $className Name of the class to load.
*
* @return void
*/
\spl_autoload_register(function(string $className):void {
// Try using class map as defined through /scripts/buildClassMap.php
foreach (array_merge([__DIR__ . '/../tests', __DIR__ . '/../src', __DIR__ . '/../exceptions', __DIR__ . '/../src/testing', __DIR__ . '/../../MDErrorReporter', __DIR__ . '/../../MDErrorReporter/exceptions', __DIR__ . '/../../MDErrorReporter/exceptions/generic', __DIR__ . '/../../MDErrorReporter/exceptions/updates', __DIR__ . '/../../MDErrorReporter/exceptions/page']) as $classDir) {
if (\file_exists("$classDir/$className.php")) {
include "$classDir/$className.php";
return;
}
}
});