Compare commits
101 Commits
06bbaf5f97
...
master
Author | SHA1 | Date | |
---|---|---|---|
5433811176
|
|||
b9d82d672a
|
|||
9948ee5d3d
|
|||
be437fcf5f
|
|||
fa2985463f
|
|||
bce4268a70
|
|||
92c942aab3
|
|||
6174c5454e
|
|||
a57036789e
|
|||
fc727932ca
|
|||
40d83ce5b0
|
|||
cbc66c4140
|
|||
11faeaa7e7
|
|||
fb008e1b59
|
|||
cd46a3ec73
|
|||
94dfa17290
|
|||
d4918dd893
|
|||
fb1372d193
|
|||
63ac1b296e
|
|||
8c1050f40a
|
|||
2bea372973
|
|||
69e6850c16
|
|||
8006695093
|
|||
db31822a3f
|
|||
0fb368b96d
|
|||
66e704de47
|
|||
a03f072a69
|
|||
d83ed2d0eb
|
|||
2c58e0554b
|
|||
1f2f63c9af
|
|||
c9dce8f782
|
|||
0c0d059dd3
|
|||
5d80f82040
|
|||
5c2c1a47cc
|
|||
ae12cfdf0f
|
|||
2176e7312b
|
|||
3ece870f0c
|
|||
b143845aea
|
|||
88458df949
|
|||
9c49afe416
|
|||
6426947e60
|
|||
a8599667c9
|
|||
bb36388e7e
|
|||
66a5b77b51
|
|||
23232f4e6a
|
|||
c38f0146dc
|
|||
bbac217aa0
|
|||
ced5a65122
|
|||
52aeedd31e
|
|||
f477401114
|
|||
c362aa1283
|
|||
65aaea4097
|
|||
7c02bbb8ad
|
|||
6a7b8bd8fd
|
|||
8d7b270f6f
|
|||
9507387c8a
|
|||
589161219f
|
|||
e18b649250
|
|||
5bb863ffc9
|
|||
1b63951b44
|
|||
93991225fe
|
|||
321609306d
|
|||
2a333c1de6
|
|||
c689f7568f
|
|||
dc86540da2
|
|||
e50f1f0526
|
|||
e2ada291f7
|
|||
aa7a3c5012
|
|||
3f37dd7a9e
|
|||
86c8235dae
|
|||
8f5174e90d
|
|||
80af1ef260
|
|||
eb869071b8
|
|||
e19e0c875c
|
|||
245d161805
|
|||
a1e6d7773b
|
|||
d35e3ed003
|
|||
9113cad57e
|
|||
143a4680e2
|
|||
a6ebab3e03
|
|||
80ab3216d5
|
|||
2071b57053
|
|||
5e7313f166
|
|||
1c5d451619
|
|||
7fb5ad8ced
|
|||
01ac23229b
|
|||
d53303e617
|
|||
6adf0ee0a2
|
|||
dbbdf4f230
|
|||
f030adba20
|
|||
980c408631
|
|||
a06a6ed41d
|
|||
20c33437c9
|
|||
63d6154d40
|
|||
d03befe483
|
|||
fe0a8ba83b
|
|||
3b5f20aa96
|
|||
56f4fdc88a
|
|||
919ffdb1b5
|
|||
36bdb36986
|
|||
2c1f6a0490
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/.phpunit.cache/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
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
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
623
MD_STD.php
623
MD_STD.php
@ -1,623 +0,0 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Provides type-safe overrides of default PHP functions.
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Standard class providing overrides of default PHP functions as static
|
||||
* functions.
|
||||
*/
|
||||
final class MD_STD {
|
||||
/**
|
||||
* Wrapper around file_get_contents, that provides catches errors on it and returns
|
||||
* with type safety.
|
||||
*
|
||||
* @param string $filename Filepath of the file to read.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function file_get_contents(string $filename):string {
|
||||
|
||||
if (\substr($filename, 0, 4) !== 'http' && !\file_exists($filename)) {
|
||||
throw new MDFileDoesNotExist("There is no file {$filename}");
|
||||
}
|
||||
|
||||
$contents = \file_get_contents($filename);
|
||||
|
||||
if (\is_bool($contents)) {
|
||||
throw new MDFileIsNotReadable("File {$filename} is not readable");
|
||||
}
|
||||
|
||||
return $contents;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real path of a relative file path. Throws an error rather than
|
||||
* returning the default false.
|
||||
*
|
||||
* @param string $path File path to convert.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function realpath(string $path):string {
|
||||
|
||||
$output = \realpath($path);
|
||||
if (!\is_string($output)) {
|
||||
throw new MDFileDoesNotExist("The file {$path} does not exist or is not readable.");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around mkdir, that throws an exception, if the folder cannot be
|
||||
* created.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.mkdir.php
|
||||
*
|
||||
* @param string $pathname The directory path.
|
||||
* @param integer $mode Permissions.
|
||||
* @param boolean $recursive Allows the creation of nested directories
|
||||
* specified in the pathname.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function mkdir(string $pathname, int $mode = 0775, bool $recursive = false):void {
|
||||
|
||||
// One reason, to throw an error, is that the folder already exists.
|
||||
if (\is_dir($pathname)) {
|
||||
return;
|
||||
}
|
||||
if (\mkdir($pathname, $mode, $recursive) === false) {
|
||||
throw new Exception("Failed to create directory: $pathname");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around unlink, that throws an exception if the file failed to be
|
||||
* removed.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.unlink.php
|
||||
*
|
||||
* @param string $filename File path.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function unlink(string $filename):void {
|
||||
|
||||
if (\unlink($filename) === false) {
|
||||
throw new Exception("Failed to delete: $filename");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets contents of a folder.
|
||||
*
|
||||
* @param string $filepath Directory path.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function scandir(string $filepath):array {
|
||||
|
||||
if (!\is_dir($filepath) || ($output = \scandir($filepath)) === false) {
|
||||
throw new MDFileDoesNotExist("There is no file {$filepath}");
|
||||
}
|
||||
|
||||
// Remove unwanted files from list
|
||||
$output = \array_diff($output, ['.', '..', '.git']);
|
||||
|
||||
// Return array values, to make it a list rather than an associative array.
|
||||
// This should be done in a separate line, as it observably leads to a
|
||||
// significant reduction in the used RAM.
|
||||
return \array_values($output);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type safe wrapper around ob_get_clean(): Gets the current buffer
|
||||
* contents and delete current output buffer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function ob_get_clean():string {
|
||||
|
||||
$output = \ob_get_clean();
|
||||
if ($output === false) {
|
||||
throw new MDOutputBufferNotStarted("Output buffer was not started");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function checking if a string starts with another.
|
||||
*
|
||||
* @param string $haystack String to check.
|
||||
* @param 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.
|
||||
*
|
||||
* @param string $haystack String to check.
|
||||
* @param string[] $needles Array containing potential start values of $haystack.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function startsWithAny(string $haystack, array $needles):bool {
|
||||
|
||||
$output = false;
|
||||
foreach ($needles as $needle) {
|
||||
$output = self::startsWith($haystack, $needle);
|
||||
if ($output == true) {
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe(r) wrapper around preg_replace.
|
||||
*
|
||||
* @param string $pattern The pattern to search for. It can be either a string or an array with strings.
|
||||
* @param string $replacement To replace with.
|
||||
* @param string $subject The string or an array with strings to search and replace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function preg_replace_str(string $pattern, string $replacement, string $subject):string {
|
||||
|
||||
$output = \preg_replace($pattern, $replacement, $subject);
|
||||
if ($output === null) {
|
||||
throw new Exception("Error replacing in $subject: Replacing $pattern with $replacement");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper around json_encode.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.json-encode.php
|
||||
*
|
||||
* @param array<mixed> $value The value being encoded. Can be any type except a resource.
|
||||
* @param integer $options Bitmask consisting of JSON_FORCE_OBJECT, JSON_HEX_QUOT ...
|
||||
* @param integer $depth Depth of coding.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function json_encode(array $value, int $options = 0, int $depth = 512):string {
|
||||
|
||||
$output = \json_encode($value, $options, $depth);
|
||||
if ($output === false) {
|
||||
throw new Exception("JSON output could not be generated");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper around strtotime().
|
||||
*
|
||||
* @param string $datetime String to convert.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function strtotime(string $datetime):int {
|
||||
|
||||
$output = \strtotime($datetime);
|
||||
if ($output === false) {
|
||||
throw new MDInvalidInputDate("Invalid input date {$datetime}.");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a curl request with the given presets.
|
||||
*
|
||||
* @param string $url URL to query.
|
||||
* @param integer $timeout Timeout in milliseconds.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public static function curl_init(string $url, int $timeout) {
|
||||
|
||||
$curl = \curl_init();
|
||||
|
||||
\curl_setopt($curl, CURLOPT_URL, $url);
|
||||
\curl_setopt($curl, CURLOPT_HEADER, false);
|
||||
\curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, $timeout); //timeout in seconds
|
||||
\curl_setopt($curl, CURLOPT_TIMEOUT_MS, $timeout); //timeout in seconds
|
||||
\curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
// \curl_setopt($curl, CURLOPT_COOKIESESSION, true);
|
||||
\curl_setopt($curl, CURLOPT_AUTOREFERER, true);
|
||||
\curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.2) Gecko/20100101 Firefox/10.0.2');
|
||||
|
||||
/*
|
||||
if (!file_exists(__DIR__ . '/../../curled.txt')) {
|
||||
touch (__DIR__ . '/../../curled.txt');
|
||||
}
|
||||
file_put_contents(__DIR__ . '/../../curled.txt', $url . PHP_EOL, FILE_APPEND);
|
||||
*/
|
||||
|
||||
return $curl;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for curling contents from the web.
|
||||
*
|
||||
* @param string $url URL to query.
|
||||
* @param integer $timeout Timeout in milliseconds.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function runCurl(string $url, int $timeout = 1200):string {
|
||||
|
||||
$curl = self::curl_init($url, $timeout);
|
||||
\curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$result = \curl_exec($curl);
|
||||
|
||||
// if ($err = curl_errno($curl)) echo $err;
|
||||
// if ($errmsg = curl_error($curl)) echo $errmsg;
|
||||
|
||||
\curl_close($curl);
|
||||
if (\is_bool($result)) {
|
||||
return "";
|
||||
}
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for curling multiple pages from the web at ones and returning their contents.
|
||||
* Adapted from hushuilong's comment at https://www.php.net/manual/de/function.curl-multi-init.php#105252.
|
||||
*
|
||||
* @param array<string> $urls URL to query.
|
||||
* @param integer $timeout Timeout in milliseconds.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function runCurlMulti(array $urls, int $timeout = 1200):array {
|
||||
|
||||
if (!($mh = curl_multi_init())) {
|
||||
throw new exception("Failed to set up multi handle");
|
||||
}
|
||||
|
||||
$curl_array = [];
|
||||
foreach($urls as $i => $url) {
|
||||
|
||||
$curl_array[$i] = self::curl_init($url, $timeout);
|
||||
|
||||
curl_setopt($curl_array[$i], CURLOPT_RETURNTRANSFER, true);
|
||||
curl_multi_add_handle($mh, $curl_array[$i]);
|
||||
|
||||
}
|
||||
|
||||
$running = null;
|
||||
do {
|
||||
usleep(10000);
|
||||
curl_multi_exec($mh, $running);
|
||||
} while($running > 0);
|
||||
|
||||
$res = [];
|
||||
foreach($urls as $i => $url) {
|
||||
$res[$i] = curl_multi_getcontent($curl_array[$i]);
|
||||
}
|
||||
|
||||
foreach($urls as $i => $url){
|
||||
curl_multi_remove_handle($mh, $curl_array[$i]);
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
|
||||
return $res;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function lang_getfrombrowser gets the browser language based on HTTP headers.
|
||||
*
|
||||
* @param array<string> $allowed_languages Array containing all the languages for which
|
||||
* there are translations.
|
||||
* @param string $default_language Default language of the instance of MD.
|
||||
* @param string $lang_variable Currently set language variable. Optional.
|
||||
* @param boolean $strict_mode Whether to demand "de-de" (true) or "de" (false) Optional.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function lang_getfrombrowser(array $allowed_languages, string $default_language, string $lang_variable = "", bool $strict_mode = true):string {
|
||||
|
||||
// $_SERVER['HTTP_ACCEPT_LANGUAGE'] verwenden, wenn keine Sprachvariable mitgegeben wurde
|
||||
if ($lang_variable === "") {
|
||||
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
$lang_variable = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
|
||||
}
|
||||
}
|
||||
|
||||
// wurde irgendwelche Information mitgeschickt?
|
||||
if (empty($lang_variable)) {
|
||||
// Nein? => Standardsprache zurückgeben
|
||||
return $default_language;
|
||||
}
|
||||
|
||||
// Den Header auftrennen
|
||||
$accepted_languages = preg_split('/,\s*/', $lang_variable);
|
||||
if (!is_array($accepted_languages)) {
|
||||
return $default_language;
|
||||
}
|
||||
|
||||
// Die Standardwerte einstellen
|
||||
$current_lang = $default_language;
|
||||
$current_q = 0;
|
||||
|
||||
// Nun alle mitgegebenen Sprachen abarbeiten
|
||||
foreach ($accepted_languages as $accepted_language) {
|
||||
|
||||
// Alle Infos über diese Sprache rausholen
|
||||
// phpcs:disable Generic.Strings.UnnecessaryStringConcat
|
||||
$res = \preg_match('/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', $accepted_language, $matches);
|
||||
// phpcs:enable
|
||||
|
||||
// war die Syntax gültig?
|
||||
if (!$res) {
|
||||
// Nein? Dann ignorieren
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sprachcode holen und dann sofort in die Einzelteile trennen
|
||||
$lang_code = \explode('-', $matches[1]);
|
||||
|
||||
// Wurde eine Qualität mitgegeben?
|
||||
if (isset($matches[2])) {
|
||||
// die Qualität benutzen
|
||||
$lang_quality = (float)$matches[2];
|
||||
}
|
||||
else {
|
||||
// Kompabilitätsmodus: Qualität 1 annehmen
|
||||
$lang_quality = 1.0;
|
||||
}
|
||||
|
||||
// Bis der Sprachcode leer ist...
|
||||
// phpcs:disable Squiz.PHP.DisallowSizeFunctionsInLoops
|
||||
while (!empty($lang_code)) {
|
||||
// phpcs:enable
|
||||
// mal sehen, ob der Sprachcode angeboten wird
|
||||
if (\in_array(\strtolower(\join('-', $lang_code)), $allowed_languages, true)) {
|
||||
// Qualität anschauen
|
||||
if ($lang_quality > $current_q) {
|
||||
// diese Sprache verwenden
|
||||
$current_lang = \strtolower(join('-', $lang_code));
|
||||
$current_q = $lang_quality;
|
||||
// Hier die innere while-Schleife verlassen
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wenn wir im strengen Modus sind, die Sprache nicht versuchen zu minimalisieren
|
||||
if ($strict_mode) {
|
||||
// innere While-Schleife aufbrechen
|
||||
break;
|
||||
}
|
||||
// den rechtesten Teil des Sprachcodes abschneiden
|
||||
\array_pop($lang_code);
|
||||
}
|
||||
}
|
||||
|
||||
// die gefundene Sprache zurückgeben
|
||||
return $current_lang;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper around filesize, if output is false, throws an error.
|
||||
*
|
||||
* @param string $filename File name.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function filesize(string $filename):int {
|
||||
|
||||
$output = \filesize($filename);
|
||||
|
||||
if ($output === false) {
|
||||
throw new Exception("Cannot get filesize of file {$filename}");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function human_filesize translates byte-level filesizes to human readable ones.
|
||||
* Thanks to Jeffrey Sambells http://jeffreysambells.com/2012/10/25/human-readable-filesize-php
|
||||
*
|
||||
* @param integer $bytes A file size, e.g. returned from filesize().
|
||||
* @param integer $decimals Number of decimal digits to allow.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function human_filesize(int $bytes, int $decimals = 2):string {
|
||||
|
||||
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
||||
$factor = \floor((\strlen((string)$bytes) - 1) / 3);
|
||||
return \sprintf("%.{$decimals}f", $bytes / \pow(1024, $factor)) . $size[$factor];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper around openssl_random_pseudo_bytes.
|
||||
*
|
||||
* @param integer $length Length.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function openssl_random_pseudo_bytes(int $length):string {
|
||||
|
||||
$output = \openssl_random_pseudo_bytes($length);
|
||||
if ($output === false) {
|
||||
throw new Exception("Failed generating random pseudo bytes using openssl_random_pseudo_bytes");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for minimizing HTML, trimming each line.
|
||||
*
|
||||
* @param string $input Input.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function minimizeHTMLString(string $input):string {
|
||||
|
||||
$input = \explode(PHP_EOL, $input);
|
||||
$output = "";
|
||||
foreach ($input as $line) {
|
||||
$output .= \trim($line) . PHP_EOL;
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This function cuts down a string and adds a period in case it's longer
|
||||
* than length to create a snippet.
|
||||
*
|
||||
* @param string $string Input text to cut down.
|
||||
* @param integer $length Length of the snippet to create.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function createTextSnippet(string $string, int $length = 180):string {
|
||||
|
||||
if (\mb_strlen($string) > $length) {
|
||||
$string = \mb_substr($string, 0, $length);
|
||||
if (($lastWhitespace = \mb_strrpos($string, ' ')) !== false) {
|
||||
$string = \mb_substr($string, 0, $lastWhitespace);
|
||||
}
|
||||
$string .= '...';
|
||||
}
|
||||
return $string;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the finfo functions to get the mime content type of a file.
|
||||
*
|
||||
* @param string $filepath Expected file path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function mime_content_type(string $filepath):string {
|
||||
|
||||
if (!($finfo = \finfo_open(FILEINFO_MIME_TYPE))) {
|
||||
throw new Exception("Cannot open finfo context");
|
||||
}
|
||||
if (!($mime_type = finfo_file($finfo, $filepath))) {
|
||||
throw new MDWrongFileType("Cannot get mime type of file: " . basename($filepath));
|
||||
}
|
||||
\finfo_close($finfo);
|
||||
|
||||
return $mime_type;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file exists, with one of the expected mime types.
|
||||
*
|
||||
* @param string $filepath File path of the file that needs to exist.
|
||||
* @param string[] $accepted_mimetype Mime type the file should have.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ensure_file(string $filepath, array $accepted_mimetype = []):void {
|
||||
|
||||
if (!\file_exists($filepath)) {
|
||||
throw new MDFileDoesNotExist("File " . basename($filepath) . " does not exist");
|
||||
}
|
||||
|
||||
// Check for mime type follows. If no check is to be done, ignore this.
|
||||
if (empty($accepted_mimetype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mime_type = self::mime_content_type($filepath);
|
||||
|
||||
if (!\in_array($mime_type, $accepted_mimetype, true)) {
|
||||
throw new MDWrongFileType("Incorrect mime type of file " . \basename($filepath) . ". Mime type is " . \mime_content_type($filepath) . ", accepted any of ['" . \implode("', '", $accepted_mimetype) . "']");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around exec, to be used with editing functions.
|
||||
* Pipes STDERR to STDOUT and throws an Exception on any error.
|
||||
*
|
||||
* @param string $cmds Commands to run.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function exec_edit(string $cmds):void {
|
||||
|
||||
$error = \shell_exec($cmds . ' 2>&1 1>/dev/null');
|
||||
if (!empty($error)) {
|
||||
throw new \Exception('Shell error: ' . $error . PHP_EOL . PHP_EOL . 'Command was: ' . $cmds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around levenshtein(), that only compares the first 250
|
||||
* characters (levenshtein can't handle more than 255).
|
||||
*
|
||||
* @param string $str1 First string.
|
||||
* @param string $str2 Second string.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function levenshtein(string $str1, string $str2):int {
|
||||
|
||||
if (\strlen($str1) > 250) {
|
||||
$str1 = \substr($str1, 0, 250);
|
||||
}
|
||||
if (\strlen($str2) > 250) {
|
||||
$str2 = \substr($str2, 0, 250);
|
||||
}
|
||||
|
||||
return \levenshtein($str1, $str2);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to color codes.
|
||||
*
|
||||
* @param string $str Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function string_to_color_code(string $str):string {
|
||||
|
||||
$code = substr(dechex(crc32($str)), 0, 6);
|
||||
return $code;
|
||||
|
||||
}
|
||||
}
|
270
MD_STD_IN.php
270
MD_STD_IN.php
@ -1,270 +0,0 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Gathers wrappers for handling inputs.
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Standard class providing overrides of default PHP functions as static
|
||||
* functions.
|
||||
*/
|
||||
final class MD_STD_IN {
|
||||
/**
|
||||
* Validates and sanitizes input integers to be in line with MySQL
|
||||
* autoincrement IDs.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function sanitize_id($input):int {
|
||||
|
||||
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
|
||||
'options' => [
|
||||
'min_range' => 1, // Minimum number of an ID generated.
|
||||
'max_range' => 4294967295 // Max value for MySQL's int data type
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!($input)) {
|
||||
throw new MDpageParameterNotNumericException("Value is not numeric.");
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates input integers to be either valid IDs or 0.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function sanitize_id_or_zero($input):int {
|
||||
|
||||
if ($input === "") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
|
||||
'options' => [
|
||||
'min_range' => 0, // Minimum number of an ID generated.
|
||||
'max_range' => 4294967295 // Max value for MySQL's int data type
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ($input === false) {
|
||||
throw new MDpageParameterNotNumericException("Value is not numeric.");
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* General string sanitization for all purposes. For use of inputs with MySQL's
|
||||
* MATCH AGAINST, use the dedicated sanitization function.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_text($input):string {
|
||||
|
||||
$output = \filter_var($input,
|
||||
FILTER_SANITIZE_STRING,
|
||||
FILTER_FLAG_NO_ENCODE_QUOTES);
|
||||
|
||||
if ($output === false) {
|
||||
return "";
|
||||
}
|
||||
while (strpos($output, " ") !== false) {
|
||||
$output = str_replace(" ", " ", $output);
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* String sanitization for 3 o4 6 characters RGB color codes (sans the leading #).
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_rgb_color($input):string {
|
||||
|
||||
$output = \filter_var($input,
|
||||
FILTER_SANITIZE_STRING,
|
||||
FILTER_FLAG_NO_ENCODE_QUOTES);
|
||||
|
||||
if ($output === false
|
||||
|| ((preg_match('/^[a-zA-Z0-9]{3}$/', $output)) === false && (preg_match('/^[a-zA-Z0-9]{6}$/', $output)) === false)
|
||||
) {
|
||||
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves HTTP input texts from GET or POST variables, whatever is provided.
|
||||
* If neither is given, returns a provided default.
|
||||
*
|
||||
* @param string $var_name Variable name.
|
||||
* @param string $default Default value for the output.
|
||||
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_http_input_text(string $var_name, string $default = "", array $allowed = []):string {
|
||||
|
||||
if (isset($_GET[$var_name])) {
|
||||
$output = self::sanitize_text($_GET[$var_name]);
|
||||
}
|
||||
else if (isset($_POST[$var_name])) {
|
||||
$output = self::sanitize_text($_POST[$var_name]);
|
||||
}
|
||||
else {
|
||||
$output = self::sanitize_text($default);
|
||||
}
|
||||
|
||||
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
|
||||
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves HTTP input texts from POST variables.
|
||||
* If none is given, returns a provided default.
|
||||
*
|
||||
* @param string $var_name Variable name.
|
||||
* @param string $default Default value for the output.
|
||||
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_http_post_text(string $var_name, string $default = "", array $allowed = []):string {
|
||||
|
||||
if (isset($_POST[$var_name])) {
|
||||
$output = self::sanitize_text($_POST[$var_name]);
|
||||
}
|
||||
else {
|
||||
$output = self::sanitize_text($default);
|
||||
}
|
||||
|
||||
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
|
||||
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates a URL. An empty string passes.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_url($input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
$output = \filter_var($input, FILTER_SANITIZE_URL);
|
||||
if (($output = \filter_var($output, FILTER_VALIDATE_URL)) === false) {
|
||||
throw new MDInvalidUrl("Invalid input URL");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates an e-mail address. An empty string passes.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_email($input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
$output = \filter_var($input, FILTER_SANITIZE_EMAIL);
|
||||
if (($output = \filter_var($output, FILTER_VALIDATE_EMAIL)) === false) {
|
||||
throw new MDInvalidEmail("Invalid input email address");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string to a float.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function sanitize_float(string $input):float {
|
||||
|
||||
$output = \str_replace(",", ".", $input);
|
||||
if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
|
||||
throw new MDgenericInvalidInputsException("Input is readable as a floating point value");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates ISBNs. Empty strings are accepted as well.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function validate_isbn(string $input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Remove hyphens
|
||||
$input = trim(strtr($input, ["-" => "", "–" => ""]));
|
||||
|
||||
// ISBN 10
|
||||
if (\mb_strlen($input) === 10) {
|
||||
|
||||
if (\preg_match('/\d{9}[0-9xX]/i', $input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ISBN 13
|
||||
if (\mb_strlen($input) === 10) {
|
||||
|
||||
if (\preg_match('/\d{13}/i', $input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new MDgenericInvalidInputsException("ISBNs must be either 10 or 13 characters long.");
|
||||
|
||||
}
|
||||
}
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Standard functions for museum-digital
|
||||
|
||||
This repository contains the most basic functions for working on museum-digital. Functions are provided as static functions of classes.
|
||||
The (by far) most relevant classes in this repository are:
|
||||
|
||||
- [MD_STD](./src/MD_STD.php)
|
||||
This class mainly provides type-safe wrappers around internal PHP functions. Where the general [`realpath()`](https://www.php.net/manual/en/function.realpath.php) will return `false` if the provided path does not exist, `MD_STD::realpath()` will, e.g., throw an exception or return the absolute path of the input file.
|
||||
- [MD_STD_IN](./src/MD_STD_IN.php)
|
||||
This class is concerned with safely getting input data. It thus mainly covers shorthand functions for validating or sanitizing various types of input data.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for the full license text.
|
500
assets/xsd/Rss2.xsd
Normal file
500
assets/xsd/Rss2.xsd
Normal file
@ -0,0 +1,500 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
XML Schema for RSS v2.0
|
||||
Copyright (C) 2003-2008 Jorgen Thelin
|
||||
|
||||
Microsoft Public License (Ms-PL)
|
||||
|
||||
This license governs use of the accompanying software.
|
||||
If you use the software, you accept this license.
|
||||
If you do not accept the license, do not use the software.
|
||||
|
||||
1. Definitions
|
||||
|
||||
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
|
||||
|
||||
A "contribution" is the original software, or any additions or changes to the software.
|
||||
|
||||
A "contributor" is any person that distributes its contribution under this license.
|
||||
|
||||
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
|
||||
|
||||
2. Grant of Rights
|
||||
|
||||
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
|
||||
|
||||
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
|
||||
|
||||
3. Conditions and Limitations
|
||||
|
||||
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
|
||||
|
||||
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
|
||||
|
||||
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
|
||||
|
||||
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
|
||||
|
||||
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
|
||||
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
elementFormDefault="unqualified"
|
||||
version="2.0.2.16">
|
||||
<xs:annotation>
|
||||
<xs:documentation>XML Schema for RSS v2.0 feed files.</xs:documentation>
|
||||
<xs:documentation>Project home: http://www.codeplex.com/rss2schema/ </xs:documentation>
|
||||
<xs:documentation>Based on the RSS 2.0 specification document at http://cyber.law.harvard.edu/rss/rss.html </xs:documentation>
|
||||
<xs:documentation>Author: Jorgen Thelin</xs:documentation>
|
||||
<xs:documentation>Revision: 16</xs:documentation>
|
||||
<xs:documentation>Date: 01-Nov-2008</xs:documentation>
|
||||
<xs:documentation>Feedback to: http://www.codeplex.com/rss2schema/WorkItem/List.aspx </xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:element name="rss">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="channel" type="RssChannel"/>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="version" type="xs:decimal" use="required" fixed="2.0"/>
|
||||
<xs:anyAttribute namespace="##any"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:complexType name="RssItem">
|
||||
<xs:annotation>
|
||||
<xs:documentation>An item may represent a "story" -- much like a story in a newspaper or magazine; if so its description is a synopsis of the story, and the link points to the full story. An item may also be complete in itself, if so, the description contains the text (entity-encoded HTML is allowed), and the link and title may be omitted.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:sequence>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="title" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The title of the item.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="description" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The item synopsis.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="link" type="xs:anyURI" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL of the item.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="author" type="EmailAddress" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Email address of the author of the item.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="category" type="Category" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Includes the item in one or more categories. </xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="comments" type="xs:anyURI" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>URL of a page for comments relating to the item.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="enclosure" type="Enclosure" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes a media object that is attached to the item.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="guid" type="Guid" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>guid or permalink URL for this entry</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="pubDate" type="Rfc822FormatDate" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Indicates when the item was published.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="source" type="Source" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The RSS channel that the item came from.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Extensibility element.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##any"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="RssChannel">
|
||||
<xs:sequence>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="title" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the channel. It's how people refer to your service. If you have an HTML website that contains the same information as your RSS file, the title of your channel should be the same as the title of your website.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="link" type="xs:anyURI">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL to the HTML website corresponding to the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="description" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Phrase or sentence describing the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="language" type="xs:language" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The language the channel is written in. This allows aggregators to group all Italian language sites, for example, on a single page. A list of allowable values for this element, as provided by Netscape, is here. You may also use values defined by the W3C.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="copyright" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Copyright notice for content in the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="managingEditor" type="EmailAddress" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Email address for person responsible for editorial content.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="webMaster" type="EmailAddress" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Email address for person responsible for technical issues relating to channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="pubDate" type="Rfc822FormatDate" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The publication date for the content in the channel. All date-times in RSS conform to the Date and Time Specification of RFC 822, with the exception that the year may be expressed with two characters or four characters (four preferred).</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="lastBuildDate" type="Rfc822FormatDate" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The last time the content of the channel changed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="category" type="Category" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specify one or more categories that the channel belongs to.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="generator" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A string indicating the program used to generate the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="docs" type="xs:anyURI" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A URL that points to the documentation for the format used in the RSS file. It's probably a pointer to this page. It's for people who might stumble across an RSS file on a Web server 25 years from now and wonder what it is.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="cloud" type="Cloud" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Allows processes to register with a cloud to be notified of updates to the channel, implementing a lightweight publish-subscribe protocol for RSS feeds.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="ttl" type="xs:nonNegativeInteger" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>ttl stands for time to live. It's a number of minutes that indicates how long a channel can be cached before refreshing from the source.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="image" type="Image" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies a GIF, JPEG or PNG image that can be displayed with the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="rating" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The PICS rating for the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="textInput" type="TextInput" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies a text input box that can be displayed with the channel.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="skipHours" type="SkipHoursList" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A hint for aggregators telling them which hours they can skip.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="skipDays" type="SkipDaysList" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A hint for aggregators telling them which days they can skip.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Extensibility element.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:choice>
|
||||
<xs:element name="item" type="RssItem" minOccurs="1" maxOccurs="unbounded">
|
||||
<!--
|
||||
HACK: According to the RSS 2.0 spec, it should strictly be possible to have zero item elements,
|
||||
but this makes the schema non-deterministic with regard to extensibility elements
|
||||
so for the moment we undid bug-fix 10231 and set minOccurs=1 to work around this problem.
|
||||
-->
|
||||
</xs:element>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Extensibility element.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:any>
|
||||
</xs:sequence>
|
||||
<xs:anyAttribute namespace="##any"/>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="SkipHour">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A time in GMT when aggregators should not request the channel data. The hour beginning at midnight is hour zero.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:nonNegativeInteger">
|
||||
<xs:minInclusive value="0"/>
|
||||
<xs:maxInclusive value="23"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="SkipHoursList">
|
||||
<xs:sequence>
|
||||
<xs:element name="hour" type="SkipHour" minOccurs="0" maxOccurs="24"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="SkipDay">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A day when aggregators should not request the channel data.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="Monday"/>
|
||||
<xs:enumeration value="Tuesday"/>
|
||||
<xs:enumeration value="Wednesday"/>
|
||||
<xs:enumeration value="Thursday"/>
|
||||
<xs:enumeration value="Friday"/>
|
||||
<xs:enumeration value="Saturday"/>
|
||||
<xs:enumeration value="Sunday"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="SkipDaysList">
|
||||
<xs:sequence>
|
||||
<xs:element name="day" type="SkipDay" minOccurs="0" maxOccurs="7">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A time in GMT, when aggregators should not request the channel data. The hour beginning at midnight is hour zero.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="Category">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="domain" type="xs:string" use="optional"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="Image">
|
||||
<xs:all>
|
||||
<xs:element name="url" type="xs:anyURI">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL of the image file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="title" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes the image, it's used in the ALT attribute of the HTML <img> tag when the channel is rendered in HTML.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="link" type="xs:anyURI">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL of the site, when the channel is rendered, the image is a link to the site. (Note, in practice the image <title> and <link> should have the same value as the channel's <title> and <link>. </xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="width" type="ImageWidth" default="88" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The width of the image in pixels.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="height" type="ImageHeight" default="31" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The height of the image in pixels.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="description" type="xs:string" minOccurs="0">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Text that is included in the TITLE attribute of the link formed around the image in the HTML rendering.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="ImageHeight">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The height of the image in pixels.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:maxInclusive value="400"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="ImageWidth">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The width of the image in pixels.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:maxInclusive value="144"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="Cloud">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies a web service that supports the rssCloud interface which can be implemented in HTTP-POST, XML-RPC or SOAP 1.1. Its purpose is to allow processes to register with a cloud to be notified of updates to the channel, implementing a lightweight publish-subscribe protocol for RSS feeds.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:attribute name="domain" type="xs:string" use="required"/>
|
||||
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="path" type="xs:string" use="required"/>
|
||||
<xs:attribute name="registerProcedure" type="xs:string" use="required"/>
|
||||
<xs:attribute name="protocol" type="CloudProtocol" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="CloudProtocol">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="xml-rpc"/>
|
||||
<xs:enumeration value="http-post"/>
|
||||
<xs:enumeration value="soap"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="TextInput">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The purpose of this element is something of a mystery! You can use it to specify a search engine box. Or to allow a reader to provide feedback. Most aggregators ignore it.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="title" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The label of the Submit button in the text input area.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="description" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Explains the text input area.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="name" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The name of the text object in the text input area.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element name="link" type="xs:anyURI">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The URL of the CGI script that processes text input requests.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="EmailAddress">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Using the regexp definiton of E-Mail Address by Lucadean from the .NET RegExp Pattern Repository at http://www.3leaf.com/default/NetRegExpRepository.aspx </xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="([a-zA-Z0-9_\-])([a-zA-Z0-9_\-\.]*)@(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}|((([a-zA-Z0-9\-]+)\.)+))([a-zA-Z]{2,}|(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\])"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="Rfc822FormatDate">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A date-time displayed in RFC-822 format.</xs:documentation>
|
||||
<xs:documentation>Using the regexp definiton of rfc-822 date by Sam Ruby at http://www.intertwingly.net/blog/1360.html </xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="(((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)), *)?\d\d? +((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec)) +\d\d(\d\d)? +\d\d:\d\d(:\d\d)? +(([+\-]?\d\d\d\d)|(UT)|(GMT)|(EST)|(EDT)|(CST)|(CDT)|(MST)|(MDT)|(PST)|(PDT)|\w)"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:complexType name="Source">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="url" type="xs:anyURI"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="Enclosure">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="url" type="xs:anyURI" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>URL where the enclosure is located</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="length" type="xs:nonNegativeInteger" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Size in bytes</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="type" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
<xs:documentation>MIME media-type of the enclosure</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="Guid">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="isPermaLink" type="xs:boolean" use="optional" default="true"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
|
||||
<!--
|
||||
TODO:
|
||||
- Need to add regexp pattern for MIME media-type value of tEnclosure/type
|
||||
- Need to add regexp pattern for checking contents of guid is a URL when isPermaLink=true"
|
||||
- Need to add some form of constraint to check on an item that one, or other, or both of title and description are present.
|
||||
However, I'm not sure it is possible to represent these constraints in XML Schema language alone.
|
||||
- Need some way to enforce cardinality constraints preventing repeated elements in channels or items
|
||||
- Unfortunately the bug-fix for issue 10231 made this schema non-deterministic with respect to extensibitity elements.
|
||||
We can't tell whether an extension element in tRssChannel is within the choice or after the item elements.
|
||||
Need to reconsider the solution to bug-fix 10231.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Change Log:
|
||||
Date Revision Description
|
||||
31-Mar-2003 1 Initial version released for comment
|
||||
31-Mar-2003 2 Changes based on feedback from Gudge:
|
||||
- Remove targetNamespace="" and use elemenfFormDefault="unqualified" instead
|
||||
- Use namespace="##other" on <any>'s to create a more deterministic data model.
|
||||
- Added missing xs:documentation inside xs:annotation at the schema level.
|
||||
- Use xs:language for ISO Language Codes in <language> element.
|
||||
- Change guid to a single declaration. This loses some of the checking of the
|
||||
URL when the contents of the guid is a permaLink, so we will need to add
|
||||
that back in with a regexp pattern.
|
||||
14-Apr-2003 3 Changes to solve some element ordering problems.
|
||||
- Use xs:all in place of xs:sequence to support flexible ordering of elements.
|
||||
Although the ordering constraints for elements is not clear from the
|
||||
original specification, the custom and practice seems to be that
|
||||
element ordering is freeform.
|
||||
- Use elemenfFormDefault="qualified" for explicit intent.
|
||||
15-Apr-2003 4 Changes to solve some element ordering problems.
|
||||
- Use xs:choice in place of xs:all as previous usage of <all> was invalid.
|
||||
This creates the problem that unsufficient constraints can be applied
|
||||
by the schema - for example, it can't prevent two title elements for an item.
|
||||
- Use elemenfFormDefault="unqualified" for to get the correct behavious
|
||||
when importing and combining schemas.
|
||||
15-Apr-2003 5 Putting the extensibility element inside the repeating choice solves
|
||||
all problems with element ordering.
|
||||
15-Apr-2003 6 - skipHours and skipDays should contain a nested list of values,
|
||||
not just a single value.
|
||||
- Added version attribute to schema definition.
|
||||
- Corrected type of the cloud element
|
||||
25-Apr-2003 7 - Add regexp for RFC-822 date suggested by Sam Ruby
|
||||
- I had to leave the base type of the tRfc822FormatDate type
|
||||
as xs:string due to the problems with using
|
||||
a pattern with xs:dateTime described at
|
||||
http://www.thearchitect.co.uk/weblog/archives/2003/04/000142.html
|
||||
19-Jun-2003 8 - Fixed a bug the Oxygen XML Editor spotted in the regexp for RFC-822 dates
|
||||
23-Jun-2003 9 - Added legal boilerplate license text for LGPL.
|
||||
- Minor formatting changes.
|
||||
24-Jun-2003 10 - Missing types for item/title and item/description - Spotted by Andreas Schwotzer.
|
||||
01-Jan-2008 11 - Copy made available under the Microsoft Public License (MS-PL).
|
||||
25-May-2008 12 - Bug fix 10231 from Ken Gruven - channel can contain zero or more items.
|
||||
06-Sep-2008 13 - Fixed tab-space whitespace issues. Now always use spaces.
|
||||
- Undid the fix for bug-fix 10231 since it made the schema non-deterministic
|
||||
with respect to extensibility eleemnts in tRssChannel - need to reconsider the fix.
|
||||
08-Sep-2008 14 - Removed 't' prefixes from type names to improve class names
|
||||
that get code-generated from the schema.
|
||||
22-Sep-2008 15 - Move type def for rss element in-line for improved compativility with Java 1.6 tools.
|
||||
01-Nov-2008 16 - Added the missing rating element from the spec to RssChannel.
|
||||
-->
|
||||
|
||||
</xs:schema>
|
19
exceptions/MDCoordinateOutOfRange.php
Normal file
19
exceptions/MDCoordinateOutOfRange.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?PHP
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Custom exception class for coordinates (longitude, latitude) that are out
|
||||
* of range (-90 - 90 for latitudes, -180 to 180 for longitudes).
|
||||
*/
|
||||
final class MDCoordinateOutOfRange extends MDExpectedException {
|
||||
/**
|
||||
* Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function errorMessage() {
|
||||
//error message
|
||||
return 'Parameter: <b>' . $this->getMessage() . '</b> is not a valid, numeric values.';
|
||||
|
||||
}
|
||||
}
|
18
exceptions/MDFailedToCreateDirectory.php
Normal file
18
exceptions/MDFailedToCreateDirectory.php
Normal 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';
|
||||
|
||||
}
|
||||
}
|
18
exceptions/MDFailedToDeleteFile.php
Normal file
18
exceptions/MDFailedToDeleteFile.php
Normal 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';
|
||||
|
||||
}
|
||||
}
|
18
exceptions/MDFilePermissionsException.php
Normal file
18
exceptions/MDFilePermissionsException.php
Normal 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';
|
||||
|
||||
}
|
||||
}
|
@ -12,8 +12,7 @@ final class MDJailSecurityOptionNotSetException extends Exception {
|
||||
*/
|
||||
public function errorMessage() {
|
||||
//error message
|
||||
$errorMsg = 'A security option of MD_JAIL has not been set: <b>' . $this->getMessage() . '</b>).';
|
||||
return $errorMsg;
|
||||
return 'A security option of MD_JAIL has not been set: <b>' . $this->getMessage() . '</b>).';
|
||||
|
||||
}
|
||||
}
|
||||
|
18
exceptions/MDJsonEncodingFailedException.php
Normal file
18
exceptions/MDJsonEncodingFailedException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?PHP
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Reports a failure to encode JSON data.
|
||||
*/
|
||||
final class MDJsonEncodingFailedException extends Exception {
|
||||
/**
|
||||
* Error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function errorMessage() {
|
||||
//error message
|
||||
return 'Failed to encode JSON data.';
|
||||
|
||||
}
|
||||
}
|
0
phpstan-baseline.neon
Normal file
0
phpstan-baseline.neon
Normal file
12
phpstan.neon
Normal file
12
phpstan.neon
Normal 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
14
phpunit.xml
Normal 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>
|
@ -132,12 +132,10 @@ final class MDFormatter {
|
||||
*/
|
||||
public static function formatMarkdownCodeBlock(string $content, string $language = ''):string {
|
||||
|
||||
$output = '```' . $language . '
|
||||
return '```' . $language . '
|
||||
' . $content . '
|
||||
```' . PHP_EOL;
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
@ -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;
|
1036
src/MD_STD.php
Normal file
1036
src/MD_STD.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,27 +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;
|
||||
|
||||
/**
|
||||
* 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 = new Redis();
|
||||
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
|
||||
$redis->set($redisKey, $outputT);
|
||||
$redis->expire($redisKey, $expiry);
|
||||
$redis->close();
|
||||
@ -41,46 +34,44 @@ 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.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function serve_page_through_redis_cache(string $redisKey, int $expiry = 3600):string {
|
||||
public static function serve_page_through_redis_cache(Redis $redis, string $redisKey, int $expiry = 3600):string {
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$redis = new Redis();
|
||||
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
|
||||
if ($redis->ping() !== false) {
|
||||
|
||||
ob_start();
|
||||
|
||||
if (($redisResult = $redis->get($redisKey))) {
|
||||
if ($redisResult = $redis->get($redisKey)) {
|
||||
|
||||
if (strlen($redisResult) > 3 and strpos($redisResult, ' id="errorPage"') === false) {
|
||||
$redis->close();
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
$redis->close();
|
||||
|
||||
return '';
|
||||
|
35
src/MD_STD_DEBUG.php
Normal file
35
src/MD_STD_DEBUG.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Provides basic debugging functions.
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Standard class providing simple and generally applicable
|
||||
* debugging and code improvement functions.
|
||||
*/
|
||||
final class MD_STD_DEBUG {
|
||||
/**
|
||||
* Function simpleBenchmark prints the difference between start time and the time at the exit of script
|
||||
* Should be put very early in the script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function simpleBenchmark():void {
|
||||
|
||||
$start = \microtime(true);
|
||||
\register_shutdown_function(function($start) :void {
|
||||
echo PHP_EOL . '<pre>';
|
||||
echo \microtime(true) - $start . "<br/>";
|
||||
echo 'RAM Usage (Peak): ' . \memory_get_peak_usage() . "<br/>";
|
||||
echo 'RAM Usage: ' . \memory_get_usage() . "<br/>";
|
||||
if ($loadAvg = \sys_getloadavg()) {
|
||||
echo 'Load avg. (last 1 minute): ' . $loadAvg[0] . '<br />';
|
||||
echo 'Load avg. (last 5 minute): ' . $loadAvg[1] . '<br />';
|
||||
echo 'Load avg. (last 15 minute): ' . $loadAvg[2] . '<br />';
|
||||
}
|
||||
echo '</pre>';
|
||||
}, $start);
|
||||
|
||||
}
|
||||
}
|
538
src/MD_STD_IN.php
Normal file
538
src/MD_STD_IN.php
Normal file
@ -0,0 +1,538 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Gathers wrappers for handling inputs.
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Encapsulates functions for handling inputs.
|
||||
*/
|
||||
final class MD_STD_IN {
|
||||
/**
|
||||
* Validates and sanitizes input integers to be in line with MySQL
|
||||
* autoincrement IDs.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return positive-int
|
||||
*/
|
||||
public static function sanitize_id(mixed $input):int {
|
||||
|
||||
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
|
||||
'options' => [
|
||||
'min_range' => 1, // Minimum number of an ID generated.
|
||||
'max_range' => 4294967295 // Max value for MySQL's int data type
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!$input) {
|
||||
throw new MDpageParameterNotNumericException("Value is not numeric.");
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates input integers to be either valid IDs or 0.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return 0|positive-int
|
||||
*/
|
||||
public static function sanitize_id_or_zero(mixed $input):int {
|
||||
|
||||
if ($input === "" || $input === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
|
||||
'options' => [
|
||||
'min_range' => 0, // Minimum number of an ID generated.
|
||||
'max_range' => 4294967295 // Max value for MySQL's int data type
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ($input === false) {
|
||||
throw new MDpageParameterNotNumericException("Value is not numeric.");
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* General string sanitization for all purposes. For use of inputs with MySQL's
|
||||
* MATCH AGAINST, use the dedicated sanitization function.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_text(mixed $input):string {
|
||||
|
||||
$output = \filter_var($input, FILTER_UNSAFE_RAW);
|
||||
|
||||
if ($output === false) {
|
||||
return "";
|
||||
}
|
||||
$output = strip_tags($output);
|
||||
while (strpos($output, " ") !== false) {
|
||||
$output = str_replace(" ", " ", $output);
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* String sanitization for 3 o4 6 characters RGB color codes (sans the leading #).
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public static function sanitize_rgb_color(mixed $input):string {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Valiates a list of entries, ensuring all returned values are valid IDs.
|
||||
*
|
||||
* @param array<mixed> $inputs Input array.
|
||||
*
|
||||
* @return list<positive-int>
|
||||
*/
|
||||
public static function sanitize_id_array(array $inputs):array {
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
$output[] = self::sanitize_id($input);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param non-empty-string $var_name Variable name.
|
||||
* @param string $default Default value for the output.
|
||||
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_http_input_text(string $var_name, string $default = "", array $allowed = []):string {
|
||||
|
||||
if (isset($_GET[$var_name])) {
|
||||
$output = self::sanitize_text($_GET[$var_name]);
|
||||
}
|
||||
else if (isset($_POST[$var_name])) {
|
||||
$output = self::sanitize_text($_POST[$var_name]);
|
||||
}
|
||||
else {
|
||||
$output = self::sanitize_text($default);
|
||||
}
|
||||
|
||||
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
|
||||
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves HTTP input texts from POST variables.
|
||||
* If none is given, returns a provided default.
|
||||
*
|
||||
* @param non-empty-string $var_name Variable name.
|
||||
* @param string $default Default value for the output.
|
||||
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_http_post_text(string $var_name, string $default = "", array $allowed = []):string {
|
||||
|
||||
if (isset($_POST[$var_name])) {
|
||||
$output = self::sanitize_text($_POST[$var_name]);
|
||||
}
|
||||
else {
|
||||
$output = self::sanitize_text($default);
|
||||
}
|
||||
|
||||
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
|
||||
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates a URL. An empty string passes.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_url(mixed $input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
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
|
||||
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");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates an e-mail address. An empty string passes.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_email(mixed $input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) {
|
||||
throw new MDInvalidEmail("Invalid input email address" . ' ' . $input);
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a password (minimum requirements: 8 characters, including
|
||||
* one number and one special char) and returns a list of errors,
|
||||
* if there are any.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function validate_password(string $input):array {
|
||||
|
||||
$errors = [];
|
||||
if (mb_strlen($input) < 8) {
|
||||
$errors[] = 'password_too_short';
|
||||
}
|
||||
|
||||
if (!(\preg_match('@[0-9]@', $input)) && !(\preg_match('@[^\w]@', $input))) {
|
||||
$errors[] = 'password_has_no_number_no_special_char';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes and validates a phone number. An empty string passes.
|
||||
*
|
||||
* @param mixed $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function validate_phone_number(mixed $input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!preg_match("#^[()0-9/ +-]+$#", $input)) {
|
||||
throw new MDgenericInvalidInputsException("Invalid phone number entered.");
|
||||
}
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string to a float.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function sanitize_float(string $input):float {
|
||||
|
||||
$output = \str_replace(",", ".", $input);
|
||||
if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
|
||||
throw new MDgenericInvalidInputsException("Input is not readable as a floating point value");
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a coordinate.
|
||||
*
|
||||
* @param string|integer $input Input string.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function validate_longitude(string|int|float $input):float {
|
||||
|
||||
if (is_string($input)) $output = self::sanitize_float($input);
|
||||
else $output = $input;
|
||||
|
||||
if ($output < -180 || $output > 180) {
|
||||
throw new MDCoordinateOutOfRange("Longitude out of range");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a coordinate.
|
||||
*
|
||||
* @param string|integer $input Input string.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function validate_latitude(string|int|float $input):float {
|
||||
|
||||
if (is_string($input)) $output = self::sanitize_float($input);
|
||||
else $output = $input;
|
||||
|
||||
if ($output < -90 || $output > 90) {
|
||||
throw new MDCoordinateOutOfRange("Latitude out of range");
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates ISBNs. Empty strings are accepted as well.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function validate_isbn(string $input):string {
|
||||
|
||||
if ($input === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Remove hyphens
|
||||
$input = trim(strtr($input, ["-" => "", "–" => "", '—' => '', " " => ""]));
|
||||
|
||||
// ISBN 10
|
||||
if (\mb_strlen($input) === 10) {
|
||||
|
||||
if (\preg_match('/\d{9}[0-9xX]/i', $input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ISBN 13
|
||||
if (\mb_strlen($input) === 13) {
|
||||
|
||||
if (\preg_match('/\d{13}/i', $input)) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new MDgenericInvalidInputsException("ISBNs must be either 10 or 13 characters long.");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $input Input string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function ensureStringIsUtf8(string $input):string {
|
||||
|
||||
// If the input is valid UTF8 from the start, it is simply returned in its
|
||||
// original form.
|
||||
if (\mb_check_encoding($input, 'UTF-8')) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
// To detect and convert the encoding for non-UTF8 strings, the list of
|
||||
// encodings known to PHP's mbstring functions is checked against the input string.
|
||||
// If any encoding matches the string, it will be converted to UTF8 accordingly.
|
||||
$suitableEncodings = [];
|
||||
$encodings = \mb_list_encodings();
|
||||
foreach ($encodings as $encoding) {
|
||||
if (\mb_detect_encoding($input, $encoding, true) !== false) {
|
||||
$suitableEncodings[] = $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
// If ISO-8859-1 is in the list of suitable encodings, try to convert with that.
|
||||
if (\in_array('ISO-8859-1', $suitableEncodings, true)) {
|
||||
if (($converted = \iconv('ISO-8859-1', "UTF-8//TRANSLIT", $input)) !== false) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
|
||||
// If a conversion from ISO-8859-1 doesn't work, just take any of the other ones.
|
||||
$suitableEncodings = \array_reverse($suitableEncodings);
|
||||
foreach ($suitableEncodings as $encoding) {
|
||||
if (($converted = \iconv($encoding, "UTF-8//TRANSLIT", $input)) !== false) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (count($suitableEncodings) === 1) {
|
||||
return mb_convert_encoding($input, 'UTF-8', );
|
||||
}
|
||||
*/
|
||||
|
||||
return $input;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around move_uploaded_file that throws errors in case the upload failed
|
||||
* for an identifiable reason.
|
||||
*
|
||||
* @param non-empty-string $filename Name of the file to upload.
|
||||
* @param non-empty-string $destination Destination to move the file to.
|
||||
* @param array<string> $mime_types Optional array of acceptable mime types. If this is
|
||||
* not empty, the file will be checked for having one
|
||||
* of the given mime types. If it does not, an error
|
||||
* will be thrown.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function move_uploaded_file(string $filename, string $destination, array $mime_types = []):bool {
|
||||
|
||||
MD_STD::ensure_file($filename, $mime_types);
|
||||
if (empty($destDir = dirname($destination))) {
|
||||
return false;
|
||||
}
|
||||
MD_STD::check_is_writable($destDir);
|
||||
|
||||
if (!(\move_uploaded_file($filename, $destination))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
@ -5,9 +5,19 @@
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Class providing static functions with basic security operations.
|
||||
* Gathers wrappers for handling basic security operations.
|
||||
*/
|
||||
final class MD_STD_SEC {
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
*
|
||||
@ -15,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));
|
||||
}
|
||||
@ -45,24 +59,57 @@ final class MD_STD_SEC {
|
||||
|
||||
}
|
||||
|
||||
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
|
||||
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.08;
|
||||
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 1.8;
|
||||
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 4;
|
||||
/**
|
||||
* Computes the delay for preventing brute force attacks.
|
||||
*
|
||||
* @param integer $counter_common General counter.
|
||||
* @param integer $counter_by_user Username-specific counter.
|
||||
* @param integer $counter_by_ip IP-specific counter.
|
||||
* @param integer $max Optional script-specific maximum execution time.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public static function computeAntiBruteForceDelay(int $counter_common, int $counter_by_user, int $counter_by_ip, int $max = 0):int {
|
||||
|
||||
$counter_common = min($counter_common, 40);
|
||||
$counter_by_user = min($counter_by_user, 40);
|
||||
$counter_by_ip = min($counter_by_ip, 40);
|
||||
|
||||
// Calculate delay
|
||||
$delay_micoseconds = \abs(\intval(self::BRUTE_FORCE_DELAY_DEFAULT *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_COMMON ** $counter_common) *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER ** $counter_by_user) *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP ** $counter_by_ip)));
|
||||
|
||||
$max_execution_time = \abs((int)\ini_get("max_execution_time"));
|
||||
if ($max_execution_time !== 0) {
|
||||
$max_execution_microseconds = $max_execution_time * 1000000;
|
||||
$delay_micoseconds = min($delay_micoseconds, \abs($max_execution_microseconds - 1000000));
|
||||
}
|
||||
|
||||
if ($max !== 0 && $max * 1000000 < $delay_micoseconds) {
|
||||
$delay_micoseconds = max($max - 1, 1) * 1000000;
|
||||
}
|
||||
|
||||
return max(0, rand($delay_micoseconds - 100000, $delay_micoseconds));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent brute force attacks by delaying the login .
|
||||
* Prevent brute force attacks by delaying the login.
|
||||
*
|
||||
* @param string $tool_name Identifier of the login.
|
||||
* @param string $username Username to look for.
|
||||
* @param string $tool_name Identifier of the login.
|
||||
* @param string $username Username to look for.
|
||||
* @param integer $max_execution_time Optional maximum execution time to stay below.
|
||||
* In seconds.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function preventBruteForce(string $tool_name, string $username):bool {
|
||||
public static function preventBruteForce(string $tool_name, string $username, int $max_execution_time = 0):bool {
|
||||
|
||||
// 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 = \strval($_SERVER['REMOTE_ADDR'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['HTTP_CLIENT_IP']));
|
||||
$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";
|
||||
@ -81,44 +128,43 @@ final class MD_STD_SEC {
|
||||
$loginLog = \json_decode(MD_STD::file_get_contents($logfile_common), \true) ?: [];
|
||||
|
||||
// Ensure the counters exist and aren't old than 600 seconds / 10 minutes
|
||||
if (empty($loginLog['common']) || \time() - $loginLog['common']['time'] > 600) {
|
||||
if (empty($loginLog['common']) || \time() - $loginLog['common']['time'] > self::REFRESH_TIME_GENERAL) {
|
||||
$loginLog['common'] = ["count" => 0, "time" => \time()];
|
||||
}
|
||||
if (empty($loginLog['usr'][$hash_user]) || \time() - $loginLog['usr'][$hash_user]['time'] > 600) {
|
||||
if (empty($loginLog['usr'][$hash_user]) || \time() - $loginLog['usr'][$hash_user]['time'] > self::REFRESH_TIME_USER) {
|
||||
$loginLog['usr'][$hash_user] = ["count" => 0, "time" => \time()];
|
||||
}
|
||||
if (empty($loginLog['ip'][$hash_ip]) || \time() - $loginLog['ip'][$hash_ip]['time'] > 600) {
|
||||
if (empty($loginLog['ip'][$hash_ip]) || \time() - $loginLog['ip'][$hash_ip]['time'] > self::REFRESH_TIME_IP) {
|
||||
$loginLog['ip'][$hash_ip] = ["count" => 0, "time" => \time()];
|
||||
}
|
||||
|
||||
// Increase counters and update timers
|
||||
$loginLog['common']['count']++;
|
||||
++$loginLog['common']['count'];
|
||||
$loginLog['common']['time'] = \time();
|
||||
$loginLog['usr'][$hash_user]['count']++;
|
||||
++$loginLog['usr'][$hash_user]['count'];
|
||||
$loginLog['usr'][$hash_user]['time'] = \time();
|
||||
$loginLog['ip'][$hash_ip]['count']++;
|
||||
++$loginLog['ip'][$hash_ip]['count'];
|
||||
$loginLog['ip'][$hash_ip]['time'] = \time();
|
||||
|
||||
// Update the log file
|
||||
\file_put_contents($logfile_common, \json_encode($loginLog));
|
||||
\file_put_contents($logfile_common, MD_STD::json_encode($loginLog));
|
||||
|
||||
// Translate counters into delay multipliers
|
||||
$delay_multiplier_common = $loginLog['common']['count'];
|
||||
$delay_multiplier_per_user = $loginLog['usr'][$hash_user]['count'];
|
||||
$delay_multiplier_per_ip = $loginLog['ip'][$hash_ip]['count'];
|
||||
|
||||
// Calculate delay
|
||||
$delay_micoseconds = \intval(self::BRUTE_FORCE_DELAY_DEFAULT *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_COMMON ** $delay_multiplier_common) *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER ** $delay_multiplier_per_user) *
|
||||
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP ** $delay_multiplier_per_ip));
|
||||
$delay_micoseconds = self::computeAntiBruteForceDelay($delay_multiplier_common,
|
||||
$delay_multiplier_per_user,
|
||||
$delay_multiplier_per_ip,
|
||||
$max_execution_time);
|
||||
|
||||
$max_execution_microseconds = \abs((int)\ini_get("max_execution_time")) * 1000000;
|
||||
\error_log("Logged in: " . $username . "; Delay: " . ($delay_micoseconds / 1000000) . " seconds");
|
||||
|
||||
// Sleep
|
||||
\usleep(min($delay_micoseconds, \abs($max_execution_microseconds - 1000000)));
|
||||
\usleep($delay_micoseconds);
|
||||
|
||||
if ($delay_micoseconds > \abs($max_execution_microseconds - 1000000)) {
|
||||
if ($delay_micoseconds > \abs($max_execution_time - 1000000)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -129,14 +175,14 @@ final class MD_STD_SEC {
|
||||
/**
|
||||
* Send CSP headers.
|
||||
*
|
||||
* @param array{default-src: string, connect-src: string, script-src: string, img-src: string, media-src: string, style-src: string, frame-src: string, object-src: string, base-uri: string, form-action: string, frame-ancestors?: string} $directives Directives to send. Font source is always set to 'self', and hence excluded.
|
||||
* @param string $frame_ancestors Frame ancestors directive. Default is to not set it.
|
||||
* @param array{default-src: string, connect-src: string, script-src: string, img-src: string, media-src: string, style-src: string, frame-src: string, object-src: string, base-uri: string, form-action: string, worker-src?: string, frame-ancestors?: string} $directives Directives to send. Font source is always set to 'self', and hence excluded.
|
||||
* @param string $frame_ancestors Frame ancestors directive. Default is to not set it.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function sendContentSecurityPolicy(array $directives, string $frame_ancestors = ""):void {
|
||||
|
||||
$policy = 'Content-Security-Policy: default-src ' . $directives['default-src'] . '; connect-src ' . $directives['connect-src'] . '; script-src ' . $directives['script-src'] . '; img-src ' . $directives['img-src'] . '; media-src ' . $directives['media-src'] . '; style-src ' . $directives['style-src'] . '; font-src \'self\'; frame-src ' . $directives['frame-src'] . '; object-src ' . $directives['object-src'] . '; base-uri ' . $directives['base-uri'] . '; form-action ' . $directives['form-action'] . '; manifest-src \'self\';';
|
||||
$policy = 'Content-Security-Policy: default-src ' . $directives['default-src'] . '; connect-src ' . $directives['connect-src'] . '; script-src ' . $directives['script-src'] . '; img-src ' . $directives['img-src'] . '; media-src ' . $directives['media-src'] . '; style-src ' . $directives['style-src'] . '; font-src \'self\'; frame-src ' . $directives['frame-src'] . '; object-src ' . $directives['object-src'] . '; base-uri ' . $directives['base-uri'] . '; form-action ' . $directives['form-action'] . '; manifest-src \'self\'; worker-src ' . ($directives['worker-src'] ?? '\'self\'') . ';';
|
||||
|
||||
if (!empty($frame_ancestors)) {
|
||||
$policy .= ' frame-ancestors ' . $frame_ancestors . ';';
|
84
src/MD_STD_SORT.php
Normal file
84
src/MD_STD_SORT.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Provides basic helper functions for sorting.
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
/**
|
||||
* Standard class providing basic helper functions for sorting.
|
||||
*/
|
||||
final class MD_STD_SORT {
|
||||
/**
|
||||
* Provides a function usable by usort that sorts first by string occurence,
|
||||
* then by a similarity as provided by an outside array.
|
||||
*
|
||||
* @param string $nameIndex Index of the name column.
|
||||
* @param string $searchValue Value to search for.
|
||||
* @param string $sortIndex Index to sort by in case the name index didn't return a hit.
|
||||
*
|
||||
* @return closure
|
||||
*/
|
||||
public static function sort_by_string_occurence_and_int(string $nameIndex, string $searchValue, string $sortIndex):closure {
|
||||
|
||||
return function (array $a, array $b) use ($nameIndex, $searchValue, $sortIndex) {
|
||||
|
||||
if ($a == $b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($a[$nameIndex] === $searchValue) return -1;
|
||||
if ($b[$nameIndex] === $searchValue) return 1;
|
||||
|
||||
$containsSearchA = stripos($a[$nameIndex], $searchValue);
|
||||
$containsSearchB = stripos($b[$nameIndex], $searchValue);
|
||||
|
||||
if ($containsSearchA !== false and $containsSearchB !== false) {
|
||||
return $a[$sortIndex] > $b[$sortIndex] ? -1 : 1;
|
||||
}
|
||||
if ($containsSearchA !== false) return -1;
|
||||
if ($containsSearchB !== false) return 1;
|
||||
|
||||
return $a[$sortIndex] > $b[$sortIndex] ? -1 : 1;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a function usable by usort that sorts first by string occurence,
|
||||
* then by an integer value provided in another part of the array.
|
||||
*
|
||||
* @param string $nameIndex Index of the name column.
|
||||
* @param string $searchValue Value to search for.
|
||||
* @param array<integer|float> $extSortBy External list to sort by, e.g.
|
||||
* similarities as per levinsthein.
|
||||
* @param string $sortIndex Index to sort by in case the name
|
||||
* index didn't return a hit.
|
||||
*
|
||||
*
|
||||
* @return closure
|
||||
*/
|
||||
public static function sort_by_string_occurence_and_ext_sort_index(string $nameIndex, string $searchValue, array $extSortBy, string $sortIndex):closure {
|
||||
|
||||
return function (array $a, array $b) use ($nameIndex, $searchValue, $extSortBy, $sortIndex) {
|
||||
|
||||
if ($a == $b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($a[$nameIndex] === $searchValue) return -1;
|
||||
if ($b[$nameIndex] === $searchValue) return 1;
|
||||
|
||||
$containsSearchA = stripos($a[$nameIndex], $searchValue);
|
||||
$containsSearchB = stripos($b[$nameIndex], $searchValue);
|
||||
|
||||
if ($containsSearchA !== false and $containsSearchB !== false) {
|
||||
return $extSortBy[$a[$sortIndex]] > $extSortBy[$b[$sortIndex]] ? -1 : 1;
|
||||
}
|
||||
if ($containsSearchA !== false) return -1;
|
||||
if ($containsSearchB !== false) return 1;
|
||||
|
||||
return $extSortBy[$a[$sortIndex]] > $extSortBy[$b[$sortIndex]] ? -1 : 1;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
39
src/MD_STD_STRINGS.php
Normal file
39
src/MD_STD_STRINGS.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
36
src/testing/MD_STD_HTML_TEST.php
Normal file
36
src/testing/MD_STD_HTML_TEST.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
60
src/testing/MD_STD_RSS_TEST.php
Normal file
60
src/testing/MD_STD_RSS_TEST.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?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.
|
||||
*/
|
||||
abstract class MD_STD_RSS_TEST extends TestCase {
|
||||
|
||||
protected string $feed;
|
||||
|
||||
/**
|
||||
* Protected function testRssFeedValidity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function testRssFeedValidity() {
|
||||
|
||||
$domDoc = new DomDocument();
|
||||
self::assertTrue($domDoc->loadXML($this->feed));
|
||||
self::assertTrue($domDoc->schemaValidate(__DIR__ . "/../../assets/xsd/Rss2.xsd"));
|
||||
unset($domDoc);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the availability of RSS feed links and encosures.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function testRssFeedLinksAndEnclosure() {
|
||||
|
||||
if (!($xmlData = simplexml_load_string($this->feed))) {
|
||||
throw new Exception("Failed to load RSS feed string to SimpleXML element");
|
||||
}
|
||||
|
||||
self::assertNotEmpty((string)$xmlData->channel->title);
|
||||
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->image->url), "Path " . $xmlData->channel->image->url . " does not appear to be a reachable URL");
|
||||
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->image->link), "Path " . $xmlData->channel->image->link . " does not appear to be a reachable URL");
|
||||
|
||||
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->item->link), "Path " . $xmlData->channel->item->link . " does not appear to be a reachable URL");
|
||||
|
||||
if (($firstEntryImg = $xmlData->channel->item->enclosure) === null) {
|
||||
throw new Exception("First item does not seem to have an enclosure");
|
||||
}
|
||||
if (($firstEntryImgAttr = $firstEntryImg->attributes()) === null) {
|
||||
throw new Exception("First enclosure does not seem to have attributes");
|
||||
}
|
||||
|
||||
self::assertTrue(MD_STD::checkUrlIsReachable((string)$firstEntryImgAttr->url), "First enclosure does not appear to be a reachable URL (" . $firstEntryImgAttr->url . ")");
|
||||
|
||||
}
|
||||
}
|
145
src/testing/MD_STD_TEST_PROVIDERS.php
Normal file
145
src/testing/MD_STD_TEST_PROVIDERS.php
Normal 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
107
tests/MDFormatterTest.php
Normal 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
692
tests/MD_STD_IN_Test.php
Normal 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));
|
||||
|
||||
}
|
||||
}
|
104
tests/MD_STD_SECTest.php
Normal file
104
tests/MD_STD_SECTest.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?PHP
|
||||
/**
|
||||
* Tests for MD_STD_SEC.
|
||||
*
|
||||
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
|
||||
*/
|
||||
declare(strict_types = 1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
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.
|
||||
*
|
||||
* @small
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testComputeAntiBruteForceDelayDoesNotGoOverMax():void {
|
||||
|
||||
$delay = MD_STD_SEC::computeAntiBruteForceDelay(100, 100, 100);
|
||||
self::assertGreaterThan(0, $delay);
|
||||
# self::assertLessThan(10 * 1000000, $delay); // Smaller than 10 seconds
|
||||
|
||||
$delay_reduced = MD_STD_SEC::computeAntiBruteForceDelay(100, 100, 100, 3);
|
||||
self::assertGreaterThan(0, $delay_reduced);
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
53
tests/MD_STD_STRINGS_Test.php
Normal file
53
tests/MD_STD_STRINGS_Test.php
Normal 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
747
tests/MD_STD_Test.php
Normal 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
25
tests/bootstrap.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user