Move scripts to /src subdirectory

This commit is contained in:
2021-03-09 20:09:11 +01:00
parent 06bbaf5f97
commit 2c1f6a0490
6 changed files with 2 additions and 3 deletions

168
src/MDFormatter.php Normal file
View File

@ -0,0 +1,168 @@
<?PHP
/**
* Applies plain text formatting.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Applies plain text formatting.
*/
final class MDFormatter {
/** @var boolean */
public static bool $use_underlined_hl = true;
/**
* Applies markdown formatting for main headline.
*
* @param string $input Headline text.
*
* @return string
*/
public static function formatMarkdownHeadline1(string $input):string {
if (self::$use_underlined_hl === false) {
return '# ' . $input . PHP_EOL . PHP_EOL;
}
$hlLength = mb_strlen($input);
$output = $input . PHP_EOL;
for ($i = 0; $i < $hlLength; $i++) {
$output .= '=';
}
$output .= PHP_EOL . PHP_EOL;
return $output;
}
/**
* Applies markdown formatting for secondary headline.
*
* @param string $input Headline text.
*
* @return string
*/
public static function formatMarkdownHeadline2(string $input):string {
if (self::$use_underlined_hl === false) {
return '## ' . $input . PHP_EOL . PHP_EOL;
}
$hlLength = mb_strlen($input);
$output = $input . PHP_EOL;
for ($i = 0; $i < $hlLength; $i++) {
$output .= '-';
}
$output .= PHP_EOL . PHP_EOL;
return $output;
}
/**
* Applies markdown formatting for tertiary headline.
*
* @param string $input Headline text.
*
* @return string
*/
public static function formatMarkdownHeadline3(string $input):string {
return '### ' . $input . PHP_EOL;
}
/**
* Returns a horizontal rule in markdown.
*
* @return string
*/
public static function formatMarkdownHorizontalRule():string {
return '___' . PHP_EOL;
}
/**
* Applies markdown formatting for a named link.
*
* @param string $link_name Link text.
* @param string $link_url Target URL.
*
* @return string
*/
public static function formatMarkdownNamedLink(string $link_name, string $link_url):string {
return "[{$link_name}]({$link_url})";
}
/**
* Applies markdown formatting for a block quote.
*
* @param string $content Content of the block quote.
*
* @return string
*/
public static function formatMarkdownBlockQuote(string $content):string {
$output = '';
$lines = \explode(PHP_EOL, $content);
foreach ($lines as $line) {
$output .= '> ' . $line . PHP_EOL;
}
return $output;
}
/**
* Applies markdown formatting for a code block.
*
* @param string $content Content of the code block.
* @param string $language Progamming language for syntax highlighting. Optional.
*
* @return string
*/
public static function formatMarkdownCodeBlock(string $content, string $language = ''):string {
$output = '```' . $language . '
' . $content . '
```' . PHP_EOL;
return $output;
}
/**
* Applies markdown formatting for an unordered list item.
*
* @param string $content Content of the list item.
* @param string $delimiter Starting character of a line.
*
* @return string
*/
public static function formatMarkdownUnorderedListItem(string $content, string $delimiter = '-'):string {
if (\strpos($content, PHP_EOL) === false) {
return $delimiter . ' ' . $content . PHP_EOL;
}
$lines = \explode(PHP_EOL, $content);
$output = $delimiter . ' ' . $lines[0] . PHP_EOL;
foreach ($lines as $i => $line) {
if ($i === 0) continue;
$output .= ' ' . $line . PHP_EOL;
}
return $output;
}
}

263
src/MD_JAIL.php Normal file
View File

@ -0,0 +1,263 @@
<?PHP
/**
* Provides class MD_JAIL.
*/
declare(strict_types = 1);
/**
* A class that, once initialized, forces the programmer to make security instructions explicit.
* If an object of the class has been created, not specifying security instructions
* leads to an error.
* A restriction on basic file operations is not practical in an md context because of
* the way transations are loaded through MDTlLoader.
*/
final class MD_JAIL {
const STATUS_NONE = 0;
const STATUS_STARTED = 1;
const STATUS_SPECIFIED = 2; // Determines that everything is fine.
/** @var integer */
private int $_status = self::STATUS_NONE;
/**
* @var integer
* Maximum execution time in seconds.
*/
public int $max_execution_time;
/**
* @var string[]
* Specifies which paths may be used by this script.
*/
private array $_open_basedir = [];
/**
* @var string
* Specifies the maximum RAM the script may use.
*/
public string $memory_limit;
/**
* Static function providing an advisory on how to harden the php.ini or
* .user.ini.
*
* @param array{shell_access_whitelist: string[], sys_function_whitelist: string[], file_function_whitelist: string[], file_uploads: bool, allow_url_fopen: bool, max_input_vars: integer, max_input_nesting_level: integer, post_max_size: string, curl: bool} $requested_resources Requested resources.
*
* @return string
*/
public static function check_server_setup(array $requested_resources):string {
# ini_set("open_basedir", __DIR__ . "/../../");
$disable_functions = [
"dl", # Loads a PHP extension at runtime
"opcache_get_status", # Gets opcache status
"phpinfo", # For obvious reasons
"parse_ini_file", # For obvious reasons
"show_source", "highlight_file", # Aliases; serve the content of a PHP file
"php_uname", # Returns information about the operating system PHP is running on
"phpcredits", # Credits page of PHP authors
"php_strip_whitespace", # Return PHP source with stripped comments and whitespace
"popen", "pclose", # Similar to fopen, but capable of shell access
"virtual", # Perform an Apache sub-request
];
$disable_aliases = [
"ini_set" => "ini_alter", # Alias of ini_set(), ini_set is preferred
"disk_free_space" => "diskfreespace", # Alias of disk_free_space()
"PHP_SAPI" => "php_sapi_name", # Alias of constant PHP_SAPI
"stream_set_write_buffer" => "set_file_buffer", # Alias of stream_set_write_buffer()
];
$shell_access_functions = array_diff(["exec", "passthru", "proc_close", "proc_get_status", "proc_nice", "proc_open", "proc_terminate", "shell_exec", "system"], $requested_resources['shell_access_whitelist']);
$sys_functions = array_diff(["get_current_user", "getmyuid", "getmygid", "getmypid", "getmyinode", "getlastmod", "getenv", "putenv"], $requested_resources['sys_function_whitelist']);
$file_functions = array_diff(["chgrp", "chgrp", "lchgrp", "lchown", "link", "linkinfo", "symlink"], $requested_resources['file_function_whitelist']);
if ($requested_resources['curl'] === false) {
$disable_functions[] = "curl_init";
$disable_functions[] = "curl_exec";
$disable_functions[] = "curl_multi_init";
$disable_functions[] = "curl_multi_exec";
}
$disabledWOAliases = array_merge($disable_functions, $shell_access_functions, $sys_functions, $file_functions);
$output = '## php.ini' . PHP_EOL . PHP_EOL;
$output .= PHP_EOL . "php_value[disable_functions] = \"" . implode(", ", array_merge($disabledWOAliases, $disable_aliases)) . "\"";
if ($requested_resources['file_uploads'] === false) {
$output .= PHP_EOL . "php_value[file_uploads] = 0";
}
if ($requested_resources['allow_url_fopen'] === false) {
$output .= PHP_EOL . "php_value[allow_url_fopen] = 0";
}
$output .= PHP_EOL . PHP_EOL . '## .user.ini' . PHP_EOL;
if ($requested_resources['file_uploads'] === false) {
$output .= PHP_EOL . "upload_max_filesize = 1";
}
if ($requested_resources['max_input_vars'] != ini_get("max_input_vars")) {
$output .= PHP_EOL . "max_input_vars = " . $requested_resources['max_input_vars'];
}
if ($requested_resources['max_input_nesting_level'] != ini_get("max_input_nesting_level")) {
$output .= PHP_EOL . "max_input_nesting_level = " . $requested_resources['max_input_nesting_level'];
}
if ($requested_resources['post_max_size'] != ini_get("post_max_size")) {
$output .= PHP_EOL . "post_max_size = " . $requested_resources['post_max_size'];
}
$output .= PHP_EOL . PHP_EOL . '## PHPStan Directives' . PHP_EOL . PHP_EOL . " disallowedFunctionCalls:" . PHP_EOL;
foreach ($disable_aliases as $to_keep => $disabled) {
$output .= '
-
function: \'' . $disabled . '()\'
message: \'use ' . $to_keep . ' instead\'';
}
foreach ($disabledWOAliases as $disabled) {
$output .= '
- function: \'' . $disabled . '()\'';
}
return $output;
}
/**
* Registers an additional accessible directory for open_basedir.
*
* @param string $dir Directory to register.
*
* @return void
*/
public function register_accessible_dir(string $dir):void {
$this->_open_basedir[] = $dir;
}
/**
* Applies the memory limit setting.
*
* @return void
*/
private function _apply_memory_limit():void {
if (!isset($this->memory_limit)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string.");
}
if (ini_set("memory_limit", $this->memory_limit) === false) {
throw new Exception('Failed to change memory_limit to ' . $this->memory_limit);
}
}
/**
* Applies the maximum execution time setting.
*
* @return void
*/
private function _apply_time_limit():void {
if (!isset($this->max_execution_time)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer.");
}
if (set_time_limit($this->max_execution_time) === false) {
throw new Exception('Failed to change max_execution_time to ' . $this->max_execution_time);
}
}
/**
* Applies basedir restrictions.
*
* @return void
*/
private function _apply_basedir_restriction():void {
if (empty($this->_open_basedir)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->open_basedir = string.");
}
if (ini_set("open_basedir", implode(':', $this->_open_basedir)) === false) {
throw new Exception('Failed to set open_basedir restrictions');
}
}
/**
* Enforces security options previously set.
*
* @return void
*/
public function enforce():void {
// Special instructions on CLI, so as to not disturb PHPUnit
if (PHP_SAPI === 'cli') {
if (!isset($this->memory_limit)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which memory limit the script should hold. Set MD_JAIL->memory_limit = string.");
}
if (!isset($this->max_execution_time)) {
throw new MDJailSecurityOptionNotSetException("It has not been specified, which maximum execution time the script should hold. Set MD_JAIL->max_execution_time = integer.");
}
$this->_status = self::STATUS_SPECIFIED;
$this->__destruct();
return;
}
$this->_apply_memory_limit();
$this->_apply_time_limit();
// Set accessible file paths
$this->_apply_basedir_restriction();
$this->_status = self::STATUS_SPECIFIED;
$this->__destruct();
}
/**
* Setup function. Registers a shutdown function that throws an error
* if the security specifications have not been made.
*
* @return void
*/
public function __construct() {
$this->_status = self::STATUS_STARTED;
}
/**
* Destructor. Throws an exception if the settings have not been set.
*/
public function __destruct() {
if ($this->_status !== self::STATUS_SPECIFIED) {
echo "Security specifications need to be set.";
if (!isset($this->memory_limit)) {
echo "Set memory limit";
}
if (!isset($this->max_execution_time)) {
echo "Set max_execution_time";
}
if (empty($this->_open_basedir)) {
echo "Set open_basedir";
}
throw new MDJailSecurityOptionNotSetException("Security specifications need to be set.");
}
}
}

623
src/MD_STD.php Normal file
View File

@ -0,0 +1,623 @@
<?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;
}
}

88
src/MD_STD_CACHE.php Normal file
View File

@ -0,0 +1,88 @@
<?PHP
/**
* Provides static functions for simple caching.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
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 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 {
$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();
}
/**
* Caches and serves a page through redis. Should be called at the start
* of the script generating a page.
*
* @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 {
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 (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);
}
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
}
}
$redis->close();
return '';
}
}

269
src/MD_STD_IN.php Normal file
View File

@ -0,0 +1,269 @@
<?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 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.");
}
}

148
src/MD_STD_SEC.php Normal file
View File

@ -0,0 +1,148 @@
<?PHP
/**
* Gathers wrappers for handling basic security operations.
*/
declare(strict_types = 1);
/**
* Gathers wrappers for handling basic security operations.
*/
final class MD_STD_SEC {
/**
* Function for retrieving the anti-csrf token or generating it if need be.
*
* @return string
*/
public static function getAntiCsrfToken():string {
if (empty($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf-token'];
}
/**
* Function for validating anti-csrf tokens. Each anti-csrf token is removed
* after use.
*
* @return boolean
*/
public static function validateAntiCsrfToken():bool {
$validity = false;
if (!empty($_POST['csrf-token'])
&& !empty($_SESSION['csrf-token'])
&& hash_equals($_SESSION['csrf-token'], $_POST['csrf-token']) === true
) {
$validity = true;
}
$_SESSION['csrf-token'] = null;
unset($_SESSION['csrf-token']);
return $validity;
}
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;
/**
* Prevent brute force attacks by delaying the login .
*
* @param string $tool_name Identifier of the login.
* @param string $username Username to look for.
*
* @return boolean
*/
public static function preventBruteForce(string $tool_name, string $username):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']));
// Set name of log file
$logfile_common = \sys_get_temp_dir() . "/logins_{$tool_name}.json";
// Ensure the log files exist
if (!\file_exists($logfile_common)) {
\file_put_contents($logfile_common, "[]");
}
// Hash entered username and IP to prevent malicious strings from
// entering the system.
$hash_user = \md5($username);
$hash_ip = \md5($ip);
// Delete the log files if they are too old
$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) {
$loginLog['common'] = ["count" => 0, "time" => \time()];
}
if (empty($loginLog['usr'][$hash_user]) || \time() - $loginLog['usr'][$hash_user]['time'] > 600) {
$loginLog['usr'][$hash_user] = ["count" => 0, "time" => \time()];
}
if (empty($loginLog['ip'][$hash_ip]) || \time() - $loginLog['ip'][$hash_ip]['time'] > 600) {
$loginLog['ip'][$hash_ip] = ["count" => 0, "time" => \time()];
}
// Increase counters and update timers
$loginLog['common']['count']++;
$loginLog['common']['time'] = \time();
$loginLog['usr'][$hash_user]['count']++;
$loginLog['usr'][$hash_user]['time'] = \time();
$loginLog['ip'][$hash_ip]['count']++;
$loginLog['ip'][$hash_ip]['time'] = \time();
// Update the log file
\file_put_contents($logfile_common, \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));
$max_execution_microseconds = \abs((int)\ini_get("max_execution_time")) * 1000000;
// Sleep
\usleep(min($delay_micoseconds, \abs($max_execution_microseconds - 1000000)));
if ($delay_micoseconds > \abs($max_execution_microseconds - 1000000)) {
return false;
}
return true;
}
/**
* 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.
*
* @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\';';
if (!empty($frame_ancestors)) {
$policy .= ' frame-ancestors ' . $frame_ancestors . ';';
}
header($policy);
}
}