Went through todos

This commit is contained in:
Dennis Eichhorn 2024-05-02 22:54:40 +00:00
parent b2d001c0ae
commit a7451d330d
13 changed files with 203 additions and 199 deletions

View File

@ -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
{

View File

@ -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

View File

@ -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
*

View File

@ -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;
}
}

View File

@ -1,134 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package phpOMS\Model\Message
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Model\Message;
use phpOMS\Contract\SerializableInterface;
/**
* Reload class.
*
* @package phpOMS\Model\Message
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
final class Reload implements \JsonSerializable, SerializableInterface
{
/**
* Message type.
*
* @var string
* @since 1.0.0
*/
public const TYPE = 'reload';
/**
* Delay in ms.
*
* @var int
* @since 1.0.0
*/
private int $delay = 0;
/**
* Constructor.
*
* @param int $delay Delay in ms
*
* @since 1.0.0
*/
public function __construct(int $delay = 0)
{
$this->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<string, mixed>
*
* @since 1.0.0
*/
public function toArray() : array
{
return [
'type' => self::TYPE,
'time' => $this->delay,
];
}
/**
* {@inheritdoc}
*/
public function jsonSerialize() : mixed
{
return $this->toArray();
}
}

View File

@ -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<string, 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, '<?php return ' . ArrayParser::serializeArray($appRoutes) . ';', \LOCK_EX);
\file_put_contents($destRoutePath, '<?php return ' . ArrayParser::serializeArray(self::$routes[$destRoutePath]) . ';', \LOCK_EX);
}
/**
@ -150,22 +165,31 @@ abstract class StatusAbstract
if ($child instanceof Directory) {
/** @var File $file */
foreach ($child as $file) {
if (!\is_dir(__DIR__ . '/../../' . $child->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()
);
}
}
}

View File

@ -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 = [
'/<!ENTITY\s+%(\w+)\s+"(.+)">/',
'/<!ENTITY\s+%(\w+)\s+SYSTEM\s+"(.+)">/',
'/<!ENTITY\s+(\w+)\s+"(.+)">/',
'/<!DOCTYPE\s+(.+)\s+SYSTEM\s+"(.+)">/'
];
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;
}
}

View File

@ -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

View File

@ -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'] ?? '');
}
/**

View File

@ -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
);
}

View File

@ -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
}

View File

@ -1274,6 +1274,9 @@ class Markdown
return null;
}
// @todo Optimize away the child <span> 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' => [

View File

@ -1,50 +0,0 @@
<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package tests
* @copyright 2013 Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\tests\phpOMS\Model\Message;
use phpOMS\Model\Message\Reload;
/**
* @internal
*/
#[\PHPUnit\Framework\Attributes\CoversClass(\phpOMS\Model\Message\Reload::class)]
final class ReloadTest extends \PHPUnit\Framework\TestCase
{
#[\PHPUnit\Framework\Attributes\Group('framework')]
public function testDefault() : void
{
$obj = new Reload();
/* Testing default values */
self::assertEquals(0, $obj->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);
}
}