diff --git a/composer.json b/composer.json index 385e492..f9b07cf 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "symfony/console": "^6", "symfony/filesystem": "^6", "symfony/finder": "^6", + "symfony/polyfill-mbstring": "^1", "symfony/polyfill-intl-idn": "^1", "symfony/yaml": "^v6", "symfony/http-client": "^6", diff --git a/src/Auth/Generator/HashIdentity.php b/src/Auth/Generator/HashIdentity.php index 7a4a11a..8cc14df 100644 --- a/src/Auth/Generator/HashIdentity.php +++ b/src/Auth/Generator/HashIdentity.php @@ -5,20 +5,16 @@ use ArrayAccess\TrayDigita\Http\ServerRequest; use ArrayAccess\TrayDigita\Util\Filter\Consolidation; +use ArrayAccess\TrayDigita\Util\Generator\RandomString; use SensitiveParameter; -use Throwable; use WhichBrowser\Parser; -use function chr; use function dechex; use function hash_equals; use function hash_hmac; use function hexdec; use function implode; -use function mt_rand; use function preg_match; -use function random_bytes; use function sha1; -use function strlen; use function strtolower; use function strtotime; use function substr; @@ -87,15 +83,7 @@ public function generateUserAgentHash( public function generate(int $id): string { $bytes = 16; - try { - $random = random_bytes($bytes); - } catch (Throwable) { - $random = ''; - while (strlen($random) < $bytes) { - $random .= chr(mt_rand(0, 255)); - } - } - + $random = RandomString::bytes($bytes); $randomKey = sha1($random); // get last 10 hex chars $randomHexNum = hexdec(substr($randomKey, -10)); diff --git a/src/Auth/Google/Authenticator.php b/src/Auth/Google/Authenticator.php index 1d3d70f..8d75214 100644 --- a/src/Auth/Google/Authenticator.php +++ b/src/Auth/Google/Authenticator.php @@ -3,7 +3,7 @@ namespace ArrayAccess\TrayDigita\Auth\Google; -use Throwable; +use ArrayAccess\TrayDigita\Util\Generator\RandomString; use function chr; use function floor; use function hash_hmac; @@ -12,11 +12,9 @@ use function is_numeric; use function max; use function min; -use function mt_rand; use function ord; use function pack; use function pow; -use function random_bytes; use function rawurlencode; use function rtrim; use function sprintf; @@ -117,17 +115,12 @@ public static function randomString( public static function generateRandomCode(int $length = 16, string|int|float $prefix = ''): string { - try { - $random = random_bytes($length); - } catch (Throwable) { - $random = ''; - for ($i = 0; $i < $length; ++$i) { - $random .= chr(mt_rand(0, 256)); - } - } - $length += strlen($prefix); - return substr(rtrim(Converter::base32Encode($prefix . $random), '='), 0, $length); + return substr( + rtrim(Converter::base32Encode($prefix . RandomString::bytes($length)), '='), + 0, + $length + ); } /** diff --git a/src/ComposerCreateProject.php b/src/ComposerCreateProject.php index e2282b8..d275bbb 100644 --- a/src/ComposerCreateProject.php +++ b/src/ComposerCreateProject.php @@ -3,6 +3,7 @@ namespace ArrayAccess\TrayDigita; +use ArrayAccess\TrayDigita\Util\Generator\RandomString; use DirectoryIterator; use Exception; use function chmod; @@ -17,9 +18,7 @@ use function is_dir; use function is_link; use function preg_match; -use function random_bytes; use function realpath; -use function sha1; use function sprintf; use function str_replace; use const DIRECTORY_SEPARATOR; @@ -362,9 +361,9 @@ public static function composerDoCreateProject($event) 'random_nonce_key', ], [ - sha1(random_bytes(16)), - sha1(random_bytes(16)), - sha1(random_bytes(16)), + RandomString::randomHex(64), + RandomString::randomHex(64), + RandomString::randomHex(64), ], $configFile ); diff --git a/src/Console/Command/DatabaseEventGenerator.php b/src/Console/Command/DatabaseEventGenerator.php index 6f94173..3e13348 100644 --- a/src/Console/Command/DatabaseEventGenerator.php +++ b/src/Console/Command/DatabaseEventGenerator.php @@ -8,7 +8,6 @@ use ArrayAccess\TrayDigita\Event\Interfaces\ManagerAllocatorInterface; use ArrayAccess\TrayDigita\Exceptions\InvalidArgument\InteractiveArgumentException; use ArrayAccess\TrayDigita\HttpKernel\BaseKernel; -use ArrayAccess\TrayDigita\Kernel\Decorator; use ArrayAccess\TrayDigita\Kernel\Interfaces\KernelInterface; use ArrayAccess\TrayDigita\Traits\Container\ContainerAllocatorTrait; use ArrayAccess\TrayDigita\Traits\Manager\ManagerAllocatorTrait; diff --git a/src/Console/Command/MiddlewareGenerator.php b/src/Console/Command/MiddlewareGenerator.php index bfe2389..ceadde0 100644 --- a/src/Console/Command/MiddlewareGenerator.php +++ b/src/Console/Command/MiddlewareGenerator.php @@ -8,7 +8,6 @@ use ArrayAccess\TrayDigita\Event\Interfaces\ManagerAllocatorInterface; use ArrayAccess\TrayDigita\Exceptions\InvalidArgument\InteractiveArgumentException; use ArrayAccess\TrayDigita\HttpKernel\BaseKernel; -use ArrayAccess\TrayDigita\Kernel\Decorator; use ArrayAccess\TrayDigita\Kernel\Interfaces\KernelInterface; use ArrayAccess\TrayDigita\Traits\Container\ContainerAllocatorTrait; use ArrayAccess\TrayDigita\Traits\Manager\ManagerAllocatorTrait; diff --git a/src/Console/Command/SchedulerAction.php b/src/Console/Command/SchedulerAction.php index fc8e253..458a72a 100644 --- a/src/Console/Command/SchedulerAction.php +++ b/src/Console/Command/SchedulerAction.php @@ -21,7 +21,6 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableCellStyle; -use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Console/Command/SchedulerGenerator.php b/src/Console/Command/SchedulerGenerator.php index dc28432..a781c11 100644 --- a/src/Console/Command/SchedulerGenerator.php +++ b/src/Console/Command/SchedulerGenerator.php @@ -8,7 +8,6 @@ use ArrayAccess\TrayDigita\Event\Interfaces\ManagerAllocatorInterface; use ArrayAccess\TrayDigita\Exceptions\InvalidArgument\InteractiveArgumentException; use ArrayAccess\TrayDigita\HttpKernel\BaseKernel; -use ArrayAccess\TrayDigita\Kernel\Decorator; use ArrayAccess\TrayDigita\Kernel\Interfaces\KernelInterface; use ArrayAccess\TrayDigita\Traits\Container\ContainerAllocatorTrait; use ArrayAccess\TrayDigita\Traits\Manager\ManagerAllocatorTrait; diff --git a/src/Database/Connection.php b/src/Database/Connection.php index c878c98..46204cc 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -31,7 +31,6 @@ use SensitiveParameter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; -use function floor; use function is_dir; use function is_string; use function is_subclass_of; diff --git a/src/Database/Wrapper/ConnectionWrapper.php b/src/Database/Wrapper/ConnectionWrapper.php index 86e88f5..428a79f 100644 --- a/src/Database/Wrapper/ConnectionWrapper.php +++ b/src/Database/Wrapper/ConnectionWrapper.php @@ -7,12 +7,10 @@ use ArrayAccess\TrayDigita\Event\Interfaces\ManagerIndicateInterface; use ArrayAccess\TrayDigita\Event\Interfaces\ManagerInterface; use ArrayAccess\TrayDigita\Traits\Manager\ManagerDispatcherTrait; -use ArrayAccess\TrayDigita\Util\Filter\ContainerHelper; use Doctrine\DBAL\Driver\Connection as DoctrineConnection; use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware; use Doctrine\DBAL\Driver\Result; use Doctrine\DBAL\Driver\Statement; -use Throwable; class ConnectionWrapper extends AbstractConnectionMiddleware implements ManagerIndicateInterface { diff --git a/src/Database/Wrapper/EntityManagerWrapper.php b/src/Database/Wrapper/EntityManagerWrapper.php index c5780ee..34d91a7 100644 --- a/src/Database/Wrapper/EntityManagerWrapper.php +++ b/src/Database/Wrapper/EntityManagerWrapper.php @@ -15,7 +15,6 @@ use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Internal\Hydration\AbstractHydrator; use Doctrine\Persistence\ObjectRepository; -use Throwable; use function get_object_vars; class EntityManagerWrapper extends EntityManagerDecorator implements ManagerIndicateInterface diff --git a/src/Database/Wrapper/EntityRepositoryWrapper.php b/src/Database/Wrapper/EntityRepositoryWrapper.php index 0696e62..520a4a3 100644 --- a/src/Database/Wrapper/EntityRepositoryWrapper.php +++ b/src/Database/Wrapper/EntityRepositoryWrapper.php @@ -11,7 +11,6 @@ use Doctrine\Common\Collections\Selectable; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; -use Throwable; use function func_get_args; /** diff --git a/src/HttpKernel/Helper/KernelMiddlewareLoader.php b/src/HttpKernel/Helper/KernelMiddlewareLoader.php index 50db115..e00fcdc 100644 --- a/src/HttpKernel/Helper/KernelMiddlewareLoader.php +++ b/src/HttpKernel/Helper/KernelMiddlewareLoader.php @@ -17,9 +17,7 @@ use function ksort; use function trim; use function ucfirst; -use function var_dump; use const SORT_ASC; -use const SORT_DESC; class KernelMiddlewareLoader extends AbstractLoaderNameBased { diff --git a/src/Kernel/HttpKernel.php b/src/Kernel/HttpKernel.php index f5c60c4..10899d2 100644 --- a/src/Kernel/HttpKernel.php +++ b/src/Kernel/HttpKernel.php @@ -30,12 +30,10 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Throwable; -use function ksort; use function memory_get_usage; use function microtime; use function spl_object_hash; use function strtoupper; -use const SORT_DESC; /** * @mixin RouterInterface diff --git a/src/Middleware/AbstractMiddleware.php b/src/Middleware/AbstractMiddleware.php index 74a2cd2..efba074 100644 --- a/src/Middleware/AbstractMiddleware.php +++ b/src/Middleware/AbstractMiddleware.php @@ -14,7 +14,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Throwable; abstract class AbstractMiddleware implements MiddlewareInterface, ManagerIndicateInterface, ContainerIndicateInterface { diff --git a/src/Scheduler/Scheduler.php b/src/Scheduler/Scheduler.php index 6c1cef8..8c98965 100644 --- a/src/Scheduler/Scheduler.php +++ b/src/Scheduler/Scheduler.php @@ -433,7 +433,7 @@ function (Task $a, Task $b) { } /** - * @param ?int $timeout in seconds. if after processed time greater than time scheduler will stopped + * @param ?int $timeout in seconds. if after processed time greater than time scheduler will stop * @return int */ public function run(?int $timeout = null): int diff --git a/src/Traits/Manager/ManagerDispatcherTrait.php b/src/Traits/Manager/ManagerDispatcherTrait.php index 244e151..3a427c9 100644 --- a/src/Traits/Manager/ManagerDispatcherTrait.php +++ b/src/Traits/Manager/ManagerDispatcherTrait.php @@ -3,14 +3,8 @@ namespace ArrayAccess\TrayDigita\Traits\Manager; -use ArrayAccess\TrayDigita\Event\Interfaces\ManagerIndicateInterface; use ArrayAccess\TrayDigita\Event\Interfaces\ManagerInterface; -use ArrayAccess\TrayDigita\Util\Filter\ContainerHelper; -use ReflectionNamedType; -use ReflectionObject; -use ReflectionUnionType; use function debug_backtrace; -use function is_subclass_of; use function reset; use function ucfirst; use const DEBUG_BACKTRACE_IGNORE_ARGS; diff --git a/src/Uploader/Chunk.php b/src/Uploader/Chunk.php index 9a649ec..ded129b 100644 --- a/src/Uploader/Chunk.php +++ b/src/Uploader/Chunk.php @@ -85,6 +85,8 @@ class Chunk implements ManagerAllocatorInterface, ContainerIndicateInterface private int $maxUploadFileSize; + private bool $allowRevertPosition = true; + public function __construct( protected ContainerInterface $container, ?string $storageDir = null @@ -119,6 +121,16 @@ public function __construct( $this->maxUploadFileSize = Consolidation::getMaxUploadSize(); } + public function isAllowRevertPosition(): bool + { + return $this->allowRevertPosition; + } + + public function setAllowRevertPosition(bool $allowRevertPosition): void + { + $this->allowRevertPosition = $allowRevertPosition; + } + public function getContainer(): ?ContainerInterface { return $this->container; diff --git a/src/Uploader/ChunkHandler.php b/src/Uploader/ChunkHandler.php index b97dbef..2041181 100644 --- a/src/Uploader/ChunkHandler.php +++ b/src/Uploader/ChunkHandler.php @@ -19,6 +19,7 @@ use function file_get_contents; use function file_put_contents; use function filesize; +use function fseek; use function is_array; use function is_file; use function is_float; @@ -263,10 +264,11 @@ public function getSize(): int /** * @param string $mode + * @param int $offset * * @return int */ - private function writeResource(string $mode) : int + private function writeResource(string $mode, int $offset) : int { if (file_exists($this->targetCacheFile) && !is_writable($this->targetCacheFile)) { $this->status = self::STATUS_FAIL; @@ -308,14 +310,16 @@ private function writeResource(string $mode) : int if ($uploadedStream->isSeekable()) { $uploadedStream->rewind(); } - + fseek($this->cacheResource, $offset); $this->written = 0; while (!$uploadedStream->eof()) { $this->written += (int) fwrite($this->cacheResource, $uploadedStream->read(2048)); } $isFirst = $this->size === 0; $stat = Consolidation::callbackReduceError(fn () => fstat($this->cacheResource)); - $this->size = $stat ? (int) ($stat['size']??$this->size+$this->written) : ($this->size+$this->written); + $this->size = $stat ? (int) ( + $stat['size']??($offset + $this->written) + ) : ($offset + $this->written); flock($this->cacheResource, LOCK_EX); $written = null; $time = $_SERVER['REQUEST_FLOAT_TIME']??null; @@ -365,41 +369,47 @@ private function writeResource(string $mode) : int json_encode($written, JSON_UNESCAPED_SLASHES) )); } + return $this->written; } /** - * @param ?int $position + * @param ?int $offset * * @return int */ - public function start(?int $position = null): int + public function start(?int $offset = null): int { if ($this->status === self::STATUS_WAITING) { $this->check(); } - $position ??= $this->size; + $offset ??= $this->size; if ($this->status !== self::STATUS_READY) { return $this->written; } - $mode = 'ab+'; - if ($position === 0) { - $mode = 'wb+'; - } elseif ($position !== $this->size) { - throw new InvalidOffsetPositionException( - $position, - $this->size, - $this->processor->chunk->translateContext( - 'Offset upload position is invalid.', - 'chunk-uploader' - ) - ); + $mode = 'wb+'; + if ($offset > 0) { + $allowRevertPosition = $this->processor->chunk->isAllowRevertPosition(); + if (!$allowRevertPosition && $offset !== $this->size + || $allowRevertPosition && $offset > $this->size + ) { + // do delete + $this->deletePartial(); + throw new InvalidOffsetPositionException( + $offset, + $this->size, + $this->processor->chunk->translateContext( + 'Offset upload position is invalid.', + 'chunk-uploader' + ) + ); + } } $this->status = self::STATUS_RESUME; - return $this->writeResource($mode); + return $this->writeResource($mode, $offset); } /** diff --git a/src/Uploader/ChunkProcessor.php b/src/Uploader/ChunkProcessor.php index 72ab374..e672a85 100644 --- a/src/Uploader/ChunkProcessor.php +++ b/src/Uploader/ChunkProcessor.php @@ -19,7 +19,6 @@ use function is_int; use function is_string; use function sprintf; -use function var_dump; final class ChunkProcessor { @@ -129,6 +128,7 @@ public function getHandler() : ChunkHandler return $this->handler; } + $handler = new ChunkHandler($this); if ($this->contentRangeHeader->header) { if (!$this->contentRangeHeader->valid) { throw new InvalidContentRange( @@ -227,6 +227,7 @@ public function getHandler() : ChunkHandler ) ); } elseif ($start !== null && $start <= 0) { + $handler->deletePartial(); throw new InvalidOffsetPositionException( $start, $size, @@ -258,6 +259,6 @@ public function getHandler() : ChunkHandler ); } - return $this->handler = new ChunkHandler($this); + return $this->handler = $handler; } } diff --git a/src/Util/Filter/DataNormalizer.php b/src/Util/Filter/DataNormalizer.php index f33571a..9f05eb4 100644 --- a/src/Util/Filter/DataNormalizer.php +++ b/src/Util/Filter/DataNormalizer.php @@ -119,7 +119,7 @@ public static function normalizeFileName( $contains = false; $string = preg_replace_callback('~[\xc0-\xff]+~', function ($match) use (&$contains) { $contains = true; - return utf8_encode($match[0]); + return StringFilter::sanitizeInvalidUtf8FromString($match[0]); }, $string); if ($contains) { // normalize ascii extended to ascii utf8 diff --git a/src/Util/Filter/StringFilter.php b/src/Util/Filter/StringFilter.php index 9c85c06..f1c3592 100644 --- a/src/Util/Filter/StringFilter.php +++ b/src/Util/Filter/StringFilter.php @@ -4,7 +4,6 @@ namespace ArrayAccess\TrayDigita\Util\Filter; -use ArrayAccess\TrayDigita\Exceptions\Runtime\RuntimeException; use Throwable; use Traversable; use function array_filter; @@ -13,6 +12,7 @@ use function array_unique; use function array_values; use function checkdnsrr; +use function chr; use function explode; use function filter_var; use function function_exists; @@ -28,13 +28,12 @@ use function is_string; use function ltrim; use function mb_strlen; +use function ord; use function parse_url; use function preg_match; use function preg_match_all; use function preg_replace; use function preg_replace_callback; -use function restore_error_handler; -use function set_error_handler; use function sprintf; use function str_contains; use function str_replace; @@ -283,20 +282,72 @@ public static function isHttpUrl(string $str): bool } /** - * Sanitize non utf-8 string - * @param string $data + * alternative on @uses \utf8_encode() + * @param string $string * @return string */ - public static function sanitizeUtf8Encode(string $data): string + public static function utf8Encode(string $string): string { - static $utf8; - if (!is_bool($utf8)) { - $utf8 = function_exists('utf8_encode'); + $string .= $string; + $len = strlen($string); + + for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) { + switch (true) { + case $string[$i] < "\x80": + $string[$j] = $string[$i]; + break; + case $string[$i] < "\xC0": + $string[$j] = "\xC2"; + $string[++$j] = $string[$i]; + break; + default: + $string[$j] = "\xC3"; + $string[++$j] = chr(ord($string[$i]) - 64); + break; + } } - if (!$utf8) { - return $data; + return substr($string, 0, $j); + } + + /** + * alternative on @uses \utf8_decode() + * @param string $string + * @return string + */ + public static function utf8Decode(string $string) : string + { + $len = strlen($string); + for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) { + switch ($string[$i] & "\xF0") { + case "\xC0": + case "\xD0": + $c = (ord($string[$i] & "\x1F") << 6) | ord($string[++$i] & "\x3F"); + $string[$j] = $c < 256 ? chr($c) : '?'; + break; + + case "\xF0": + ++$i; + // no break + + case "\xE0": + $string[$j] = '?'; + $i += 2; + break; + + default: + $string[$j] = $string[$i]; + } } + return substr($string, 0, $j); + } + /** + * Sanitize non utf-8 string + * @param string $data + * @return string + */ + public static function sanitizeUtf8Encode(string $data): string + { $regex = '/( [\xC0-\xC1] # Invalid UTF-8 Bytes | [\xF5-\xFF] # Invalid UTF-8 Bytes @@ -317,9 +368,11 @@ public static function sanitizeUtf8Encode(string $data): string | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2) )/x'; - return preg_replace_callback($regex, function ($e) { - return utf8_encode($e[1]); - }, $data); + return preg_replace_callback( + $regex, + static fn ($e) => StringFilter::utf8Encode($e[1]), + $data + ); } /** @@ -341,27 +394,10 @@ public static function sanitizeInvalidUtf8FromString(string $string): string } if (!function_exists('mb_strlen') || mb_strlen($string, 'UTF-8') !== strlen($string)) { - $result = false; // try to un-serial - try { - // add temporary error handler - set_error_handler(function ($errNo, $errStr) { - throw new RuntimeException( - $errStr, - $errNo - ); - }); - /** - * use trim if possible - * Serialized value could not start & end with white space - */ - /** @noinspection PhpComposerExtensionStubsInspection */ - $result = iconv('windows-1250', 'UTF-8//IGNORE', $string); - } catch (Throwable) { - // pass - } finally { - restore_error_handler(); - } + $result = Consolidation::callbackReduceError( + static fn () => iconv('windows-1250', 'UTF-8//IGNORE', $string) + ); if ($result !== false) { return self::sanitizeUtf8Encode($string); } diff --git a/src/Util/Generator/RandomString.php b/src/Util/Generator/RandomString.php index 9c41459..8b75c53 100644 --- a/src/Util/Generator/RandomString.php +++ b/src/Util/Generator/RandomString.php @@ -3,9 +3,11 @@ namespace ArrayAccess\TrayDigita\Util\Generator; +use Random\Randomizer; use Stringable; use Throwable; use function chr; +use function class_exists; use function function_exists; use function is_bool; use function mt_rand; @@ -41,6 +43,8 @@ public static function char(int $length = 64, ?string $char = self::DEFAULT_CHAR return $randomString; } + private static ?Randomizer $randomizer = null; + /** * @param int $bytes * @return string @@ -52,6 +56,11 @@ public static function bytes(int $bytes) : string if ($bytes < 1) { return ''; } + + if (self::$randomizer || class_exists(Randomizer::class)) { + self::$randomizer ??= new Randomizer(); + return self::$randomizer->getBytes($bytes); + } try { return random_bytes($bytes); } catch (Throwable) { diff --git a/src/View/Twig/TwigExtensions/Miscellaneous.php b/src/View/Twig/TwigExtensions/Miscellaneous.php index be49480..06e2572 100644 --- a/src/View/Twig/TwigExtensions/Miscellaneous.php +++ b/src/View/Twig/TwigExtensions/Miscellaneous.php @@ -7,18 +7,10 @@ use ArrayAccess\TrayDigita\PossibleRoot; use ArrayAccess\TrayDigita\Util\Filter\Consolidation; use ArrayAccess\TrayDigita\Util\Filter\ContainerHelper; -use ArrayAccess\TrayDigita\Util\Filter\DataNormalizer; -use Stringable; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; -use function is_iterable; -use function is_scalar; use function is_string; -use function preg_quote; -use function preg_replace; -use function str_contains; -use const DIRECTORY_SEPARATOR; class Miscellaneous extends AbstractExtension {