From a7451d330ddf9d06ac975e346111f263d357898a Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 2 May 2024 22:54:40 +0000 Subject: [PATCH] Went through todos --- DataStorage/Database/Mapper/WriteMapper.php | 4 + DataStorage/Database/Schema/Builder.php | 4 + Math/Geometry/Shape/D3/Sphere.php | 23 ++++ Math/Number/Numbers.php | 42 ++++++ Model/Message/Reload.php | 134 -------------------- Module/StatusAbstract.php | 52 ++++++-- Security/Guard.php | 77 +++++++++++ System/File/FileUtils.php | 2 + Uri/HttpUri.php | 2 +- Uri/UriFactory.php | 4 +- Utils/IO/Zip/Zip.php | 5 +- Utils/Parser/Markdown/Markdown.php | 3 + tests/Model/Message/ReloadTest.php | 50 -------- 13 files changed, 203 insertions(+), 199 deletions(-) delete mode 100755 Model/Message/Reload.php delete mode 100755 tests/Model/Message/ReloadTest.php diff --git a/DataStorage/Database/Mapper/WriteMapper.php b/DataStorage/Database/Mapper/WriteMapper.php index de51edc61..80aa8a249 100755 --- a/DataStorage/Database/Mapper/WriteMapper.php +++ b/DataStorage/Database/Mapper/WriteMapper.php @@ -29,6 +29,10 @@ use phpOMS\Utils\ArrayUtils; * * @todo Lock data for concurrency (e.g. table row lock or heartbeat) * https://github.com/Karaka-Management/Karaka/issues/152 + * + * @performance Database inserts happen one at a time. + * Try to find a way to optimize this with multiple inserts in one go. + * https://github.com/Karaka-Management/phpOMS/issues/370 */ final class WriteMapper extends DataMapperAbstract { diff --git a/DataStorage/Database/Schema/Builder.php b/DataStorage/Database/Schema/Builder.php index a868a6989..f3e2080fd 100755 --- a/DataStorage/Database/Schema/Builder.php +++ b/DataStorage/Database/Schema/Builder.php @@ -159,6 +159,10 @@ class Builder extends BuilderAbstract * * @return self * + * @todo Allow to create db indices in json files + * e.g. "index": {"idx1": ["col1"], "idx2": ["col1", "col2"] } + * https://github.com/Karaka-Management/phpOMS/issues/355 + * * @since 1.0.0 */ public static function createFromSchema(array $definition, ConnectionAbstract $connection) : self diff --git a/Math/Geometry/Shape/D3/Sphere.php b/Math/Geometry/Shape/D3/Sphere.php index 3ddb8f5a6..90e18f8da 100755 --- a/Math/Geometry/Shape/D3/Sphere.php +++ b/Math/Geometry/Shape/D3/Sphere.php @@ -47,6 +47,8 @@ final class Sphere implements D3ShapeInterface /** * Calculating the distance between two points on a sphere * + * Geocoding + * * @param float $latStart Latitude of start point in deg * @param float $longStart Longitude of start point in deg * @param float $latEnd Latitude of target point in deg @@ -77,6 +79,27 @@ final class Sphere implements D3ShapeInterface return $angle * $radius; } + /** + * Get a bounding box around a lat/lon defined by a distance in meter + * + * @param float $lat Latitude + * @param float $lon Longitude + * @param float $distance Radius in meter + * + * @return array {a:array{lat:float, lon:float}, b:array{lat:float, lon:float}, c:array{lat:float, lon:float}, d:array{lat:float, lon:float}} + * + * @since 1.0.0 + */ + public static function boundingBox(float $lat, float $lon, float $distance) : array + { + return [ + 'a' => ['lat' => $lat + (1 / 111133.0 / 2 * $distance), 'lon' => $lon - (1 / 111320.0 * \cos(\deg2rad($lat - (1 / 111133.0 / 2 * $distance))) * $distance)], + 'b' => ['lat' => $lat + (1 / 111133.0 / 2 * $distance), 'lon' => $lon + (1 / 111320.0 * \cos(\deg2rad($lat - (1 / 111133.0 / 2 * $distance))) * $distance)], + 'c' => ['lat' => $lat - (1 / 111133.0 / 2 * $distance), 'lon' => $lon - (1 / 111320.0 * \cos(\deg2rad($lat - (1 / 111133.0 / 2 * $distance))) * $distance)], + 'd' => ['lat' => $lat - (1 / 111133.0 / 2 * $distance), 'lon' => $lon + (1 / 111320.0 * \cos(\deg2rad($lat - (1 / 111133.0 / 2 * $distance))) * $distance)], + ]; + } + /** * Create sphere by radius * diff --git a/Math/Number/Numbers.php b/Math/Number/Numbers.php index 446ffb60d..af7576006 100755 --- a/Math/Number/Numbers.php +++ b/Math/Number/Numbers.php @@ -124,4 +124,46 @@ final class Numbers return $count; } + + /** + * Remap numbers between 0 and X to 0 and 100 + * + * @param int $number Number to remap + * @param int $max Max possible number + * @param float $exp Exponential modifier + * + * @return float + * + * @since 1.0.0 + */ + public static function remapRangeExponentially(int $number, int $max, float $exp = 1.0) : float + { + if ($number > $max) { + $number = $max; + } + + $exponent = ($number / $max) * $exp; + $mapped = (exp($exponent) - 1) / (exp($exp) - 1) * 100; + + return $mapped; + } + + /** + * Remap numbers between 0 and X to 0 and 100 + * + * @param int $number Number to remap + * @param int $max Max possible number + * + * @return float + * + * @since 1.0.0 + */ + public static function remapRangeLog(int $number, int $max) : float + { + if ($number > $max) { + $number = $max; + } + + return (log($number + 1) / log($max + 1)) * 100; + } } diff --git a/Model/Message/Reload.php b/Model/Message/Reload.php deleted file mode 100755 index 8d73a78fd..000000000 --- a/Model/Message/Reload.php +++ /dev/null @@ -1,134 +0,0 @@ -delay = $delay; - } - - /** - * Set delay. - * - * @param int $delay Delay in ms - * - * @return void - * - * @since 1.0.0 - */ - public function setDelay(int $delay) : void - { - $this->delay = $delay; - } - - /** - * Render message. - * - * @return string - * - * @since 1.0.0 - */ - public function serialize() : string - { - return $this->__toString(); - } - - /** - * {@inheritdoc} - */ - public function unserialize(mixed $raw) : void - { - if (!\is_string($raw)) { - return; - } - - $unserialized = \json_decode($raw, true); - if (!\is_array($unserialized)) { - return; - } - - $this->delay = $unserialized['time'] ?? 0; - } - - /** - * Stringify. - * - * @return string - * - * @since 1.0.0 - */ - public function __toString() - { - return (string) \json_encode($this->toArray()); - } - - /** - * Generate message array. - * - * @return array - * - * @since 1.0.0 - */ - public function toArray() : array - { - return [ - 'type' => self::TYPE, - 'time' => $this->delay, - ]; - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() : mixed - { - return $this->toArray(); - } -} diff --git a/Module/StatusAbstract.php b/Module/StatusAbstract.php index 253935f56..c14a193df 100755 --- a/Module/StatusAbstract.php +++ b/Module/StatusAbstract.php @@ -43,6 +43,18 @@ abstract class StatusAbstract */ public const PATH = ''; + /** + * Routes. + * + * Include consideres the state of the file during script execution. + * This means setting it to empty has no effect if it was not empty before. + * There are also other merging bugs that can happen. + * + * @var array + * @since 1.0.0 + */ + private static array $routes = []; + /** * Deactivate module. * @@ -105,14 +117,17 @@ abstract class StatusAbstract throw new PermissionException($destRoutePath); // @codeCoverageIgnore } - /** @noinspection PhpIncludeInspection */ - $appRoutes = include $destRoutePath; + if (!isset(self::$routes[$destRoutePath])) { + /** @noinspection PhpIncludeInspection */ + self::$routes[$destRoutePath] = include $destRoutePath; + } + /** @noinspection PhpIncludeInspection */ $moduleRoutes = include $srcRoutePath; - $appRoutes = \array_merge_recursive($appRoutes, $moduleRoutes); + self::$routes[$destRoutePath] = \array_merge_recursive(self::$routes[$destRoutePath], $moduleRoutes); - \file_put_contents($destRoutePath, 'getName() . '/' . \basename($file->getName(), '.php')) - || ($appInfo !== null && \basename($file->getName(), '.php') !== $appInfo->getInternalName()) + $appName = \basename($file->getName(), '.php'); + + if (!\is_dir(__DIR__ . '/../../' . $child->getName() . '/' . $appName) + || ($appInfo !== null && $appName !== $appInfo->getInternalName()) ) { continue; } - self::installRoutesHooks(__DIR__ . '/../../' . $child->getName() . '/' . \basename($file->getName(), '.php') . '/' . $type . '.php', $file->getPath()); + self::installRoutesHooks( + __DIR__ . '/../../' . $child->getName() . '/' . $appName . '/' . $type . '.php', + $file->getPath() + ); } } elseif ($child instanceof File) { + $appName = \basename($child->getName(), '.php'); if (!\is_dir(__DIR__ . '/../../' . $child->getName()) - || ($appInfo !== null && \basename($child->getName(), '.php') !== $appInfo->getInternalName()) + || ($appInfo !== null && $appName !== $appInfo->getInternalName()) ) { continue; } - self::installRoutesHooks(__DIR__ . '/../../' . $child->getName() . '/' . $type . '.php', $child->getPath()); + self::installRoutesHooks( + __DIR__ . '/../../' . $child->getName() . '/' . $type . '.php', + $child->getPath() + ); } } } @@ -227,7 +251,10 @@ abstract class StatusAbstract continue; } - self::uninstallRoutesHooks(__DIR__ . '/../../' . $child->getName() . '/' . \basename($file->getName(), '.php') . '/'. $type . '.php', $file->getPath()); + self::uninstallRoutesHooks( + __DIR__ . '/../../' . $child->getName() . '/' . \basename($file->getName(), '.php') . '/'. $type . '.php', + $file->getPath() + ); } } elseif ($child instanceof File) { if (!\is_dir(__DIR__ . '/../../' . $child->getName()) @@ -236,7 +263,10 @@ abstract class StatusAbstract continue; } - self::uninstallRoutesHooks(__DIR__ . '/../../' . $child->getName() . '/'. $type . '.php', $child->getPath()); + self::uninstallRoutesHooks( + __DIR__ . '/../../' . $child->getName() . '/'. $type . '.php', + $child->getPath() + ); } } } diff --git a/Security/Guard.php b/Security/Guard.php index 3376d1f35..1b55e5309 100755 --- a/Security/Guard.php +++ b/Security/Guard.php @@ -114,4 +114,81 @@ final class Guard return true; } + + public static function isSafeFile(string $path) : bool + { + $tmp = \strtolower($path); + if (\str_ends_with($tmp, '.csv')) { + return self::isSafeXml($path); + } elseif (\str_ends_with($tmp, '.xml')) { + return self::isSafeCsv($path); + } + + return true; + } + + public static function isSafeXml(string $path) : bool + { + $maxEntityDepth = 7; + $xml = \file_get_contents($path); + + // Detect injections + $injectionPatterns = [ + '//', + '//', + '//', + '//' + ]; + + foreach ($injectionPatterns as $pattern) { + if (\preg_match($pattern, $xml) !== false) { + return false; + } + } + + $reader = new \XMLReader(); + + $reader->XML($xml); + $reader->setParserProperty(\XMLReader::SUBST_ENTITIES, true); + + $foundBillionLaughsAttack = false; + $entityCount = 0; + + while ($reader->read()) { + if ($reader->nodeType === \XMLReader::ENTITY_REF) { + ++$entityCount; + + if ($entityCount > $maxEntityDepth) { + $foundBillionLaughsAttack = true; + break; + } + } + } + + return !$foundBillionLaughsAttack; + } + + public static function isSafeCsv(string $path) : bool + { + $input = \fopen($path, 'r'); + if (!$input) { + return true; + } + + while (($row = \fgetcsv($input)) !== false) { + foreach ($row as &$cell) { + if (\preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x1F]+/', $cell) !== false) { + return false; + } + + if (\in_array($cell[0] ?? '', ['=', '+', '-', '@'])) { + return false; + } + } + } + + \fclose($input); + + return true; + } } diff --git a/System/File/FileUtils.php b/System/File/FileUtils.php index 748c2db4d..c84c87bf9 100755 --- a/System/File/FileUtils.php +++ b/System/File/FileUtils.php @@ -67,6 +67,8 @@ final class FileUtils * * @return int Extension type * + * @question Consider to move directly to ExtensionType enum and create ::fromExtension() + * * @since 1.0.0 */ public static function getExtensionType(string $extension) : int diff --git a/Uri/HttpUri.php b/Uri/HttpUri.php index cf3ab8472..1b346e437 100755 --- a/Uri/HttpUri.php +++ b/Uri/HttpUri.php @@ -245,7 +245,7 @@ final class HttpUri implements UriInterface return ((!empty($_SERVER['HTTPS'] ?? '') && ($_SERVER['HTTPS'] ?? '') !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') || (($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on') ? 'https' : 'http') - . '://' . ($_SERVER['HTTP_HOST'] ?? ''). ($_SERVER['REQUEST_URI'] ?? ''); + . '://' . ($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? ''); } /** diff --git a/Uri/UriFactory.php b/Uri/UriFactory.php index fe9facc4b..6e53c78e8 100755 --- a/Uri/UriFactory.php +++ b/Uri/UriFactory.php @@ -281,8 +281,8 @@ final class UriFactory ? '#' . \str_replace('\#', '#', $urlStructure['fragment']) : ''); return \str_replace( - ['%5C%7B', '%5C%7D', '%5C%3F', '%5C%23'], - ['{', '}', '?', '#'], + ['%5C%7B', '%5C%7D', '%5C%3F', '%5C%23', '%C2%B0'], + ['{', '}', '?', '#', '°'], $escaped ); } diff --git a/Utils/IO/Zip/Zip.php b/Utils/IO/Zip/Zip.php index bc817de60..d805a46cb 100755 --- a/Utils/IO/Zip/Zip.php +++ b/Utils/IO/Zip/Zip.php @@ -126,7 +126,10 @@ final class Zip implements ArchiveInterface try { $zip = new \ZipArchive(); - if (!$zip->open($source)) { + if (!$zip->open($source) + || $zip->numFiles > 10000 + || (($zip->statIndex(0)['size'] ?? 0) + ($zip->statIndex(1)['size'] ?? 0)) > 2e+9 + ) { return false; // @codeCoverageIgnore } diff --git a/Utils/Parser/Markdown/Markdown.php b/Utils/Parser/Markdown/Markdown.php index 738401d11..f4c982f1b 100755 --- a/Utils/Parser/Markdown/Markdown.php +++ b/Utils/Parser/Markdown/Markdown.php @@ -1274,6 +1274,9 @@ class Markdown return null; } + // @todo Optimize away the child element for spoilers (if reasonable) + // If possible don't forget to adjust scss + // https://github.com/Karaka-Management/phpOMS/issues/367 return [ 'extent' => \strlen($matches[0]), 'element' => [ diff --git a/tests/Model/Message/ReloadTest.php b/tests/Model/Message/ReloadTest.php deleted file mode 100755 index 9f06bb150..000000000 --- a/tests/Model/Message/ReloadTest.php +++ /dev/null @@ -1,50 +0,0 @@ -toArray()['time']); - } - - #[\PHPUnit\Framework\Attributes\Group('framework')] - public function testSetGet() : void - { - $obj = new Reload(5); - - self::assertEquals(['type' => 'reload', 'time' => 5], $obj->toArray()); - self::assertEquals(\json_encode(['type' => 'reload', 'time' => 5]), $obj->serialize()); - self::assertEquals(['type' => 'reload', 'time' => 5], $obj->jsonSerialize()); - - $obj->setDelay(6); - self::assertEquals(['type' => 'reload', 'time' => 6], $obj->toArray()); - - $obj2 = new Reload(); - $obj2->unserialize($obj->serialize()); - self::assertEquals($obj, $obj2); - } -}