Move scripts to /src subdirectory
This commit is contained in:
168
src/MDFormatter.php
Normal file
168
src/MDFormatter.php
Normal 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
263
src/MD_JAIL.php
Normal 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
623
src/MD_STD.php
Normal 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
88
src/MD_STD_CACHE.php
Normal 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
269
src/MD_STD_IN.php
Normal 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
148
src/MD_STD_SEC.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user