diff --git a/composer.lock b/composer.lock index deacdd055..2315658c0 100644 --- a/composer.lock +++ b/composer.lock @@ -8172,16 +8172,16 @@ }, { "name": "twig/twig", - "version": "v3.14.0", + "version": "v3.14.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" + "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", - "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", + "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", "shasum": "" }, "require": { @@ -8235,7 +8235,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.14.0" + "source": "https://github.com/twigphp/Twig/tree/v3.14.2" }, "funding": [ { @@ -8247,7 +8247,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T17:55:12+00:00" + "time": "2024-11-07T12:36:22+00:00" }, { "name": "vich/uploader-bundle", @@ -9901,16 +9901,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.7", + "version": "1.12.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6a60a4d66142b8156c9da923f1972657bc4748c", + "reference": "f6a60a4d66142b8156c9da923f1972657bc4748c", "shasum": "" }, "require": { @@ -9955,7 +9955,7 @@ "type": "github" } ], - "time": "2024-10-18T11:12:07+00:00" + "time": "2024-11-06T19:06:49+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -13118,16 +13118,16 @@ }, { "name": "vincentlanglet/twig-cs-fixer", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git", - "reference": "4f482ed08d1f95be5799411459ea685f6411ac52" + "reference": "595fe48a3bf43282d21e6930e433d22014f9ecbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/4f482ed08d1f95be5799411459ea685f6411ac52", - "reference": "4f482ed08d1f95be5799411459ea685f6411ac52", + "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/595fe48a3bf43282d21e6930e433d22014f9ecbd", + "reference": "595fe48a3bf43282d21e6930e433d22014f9ecbd", "shasum": "" }, "require": { @@ -13186,7 +13186,7 @@ "homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer", "support": { "issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues", - "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.3.0" + "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.3.1" }, "funding": [ { @@ -13194,7 +13194,7 @@ "type": "github" } ], - "time": "2024-11-05T16:48:31+00:00" + "time": "2024-11-06T16:21:28+00:00" }, { "name": "webmozart/assert", diff --git a/psalm.xml b/psalm.xml index 9c9d79754..91c71ef18 100644 --- a/psalm.xml +++ b/psalm.xml @@ -35,6 +35,11 @@ + + + + + diff --git a/public/css/simple_editor.css b/public/css/simple_editor.css index 7a684ba44..bf1eb9af1 100644 --- a/public/css/simple_editor.css +++ b/public/css/simple_editor.css @@ -76,6 +76,6 @@ padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; + background-position: top calc(0.375em) right calc(0.375em + 0.1875rem); background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } diff --git a/public/js/plugins/plugin-cell-edit.js b/public/js/plugins/plugin-cell-edit.js index 08f39f6b9..84dea9bd5 100644 --- a/public/js/plugins/plugin-cell-edit.js +++ b/public/js/plugins/plugin-cell-edit.js @@ -308,7 +308,7 @@ $(function () { 'tooltipEditClass': 'tooltip-secondary', // the error tooltip class 'tooltipErrorClass': 'tooltip-danger', - // start edit on create + // start edit automatically 'autoEdit': false, // destroy on end edit or on cancel 'autoDispose': false, diff --git a/public/js/plugins/plugin-cell-highlight.js b/public/js/plugins/plugin-cell-highlight.js index 0e3f3efc5..0a074b70e 100644 --- a/public/js/plugins/plugin-cell-highlight.js +++ b/public/js/plugins/plugin-cell-highlight.js @@ -203,7 +203,7 @@ $(function () { let cell = tableIndex[y][x]; // Table matrix is iterated left to right, top to bottom. // It might be that cell has been assigned a value already - // because previous row-cell had a 'rowspan' property, + // because the previous row-cell had a 'rowspan' property, // possibly together with 'colspan'. if (!cell) { cell = columns.eq(cellIndex++); diff --git a/public/js/plugins/plugin-console.js b/public/js/plugins/plugin-console.js index 8d553707b..dfaa25db7 100644 --- a/public/js/plugins/plugin-console.js +++ b/public/js/plugins/plugin-console.js @@ -106,7 +106,7 @@ }, /** - * Check if console is present + * Check if the console is present */ isConsole(method) { return window.console && window.console[method]; diff --git a/public/js/plugins/plugin-input-file.js b/public/js/plugins/plugin-input-file.js index 1ef63bdce..8a9ac9a10 100644 --- a/public/js/plugins/plugin-input-file.js +++ b/public/js/plugins/plugin-input-file.js @@ -251,7 +251,7 @@ $(function () { $img[0].src = event.target.result; file.result = event.target.result; $element.find('.file-input-filename').text(file.name); - // if parent has max-height, using `(max-)height: 100%` on + // if the parent has max-height, using `(max-)height: 100%` on // the child doesn't take padding and border into account if ($preview.css('max-height') !== 'none') { const mh = parseInt($preview.css('max-height'), 10) || 0; diff --git a/public/js/plugins/plugin-rowlink.js b/public/js/plugins/plugin-rowlink.js index 3c25457cd..58de98e94 100644 --- a/public/js/plugins/plugin-rowlink.js +++ b/public/js/plugins/plugin-rowlink.js @@ -1,21 +1,3 @@ -/* ============================================================ - * Bootstrap: rowlink.js v3.1.3 - * http://jasny.github.io/bootstrap/javascript/#rowlink - * ============================================================ - * Copyright 2012-2014 Arnold Daniels - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ /**! compression tag for ftp-deployment */ /** diff --git a/public/js/plugins/plugin-table-editor.js b/public/js/plugins/plugin-table-editor.js index df0de2f4d..c5320f539 100644 --- a/public/js/plugins/plugin-table-editor.js +++ b/public/js/plugins/plugin-table-editor.js @@ -202,7 +202,7 @@ $(function () { * @private */ _isFunction(value) { - return typeof value === 'function' && value !== $.noop; + return typeof value === 'function'; } }; @@ -211,11 +211,11 @@ $(function () { 'inputType': 'text', 'inputClass': 'form-control cell-editor', 'inputCss': {}, - 'onCreateInput': $.noop, - 'onRemoveInput': $.noop, - 'onKeyDown': $.noop, - 'onInput': $.noop, - 'onSave': $.noop, + 'onCreateInput': null, + 'onRemoveInput': null, + 'onKeyDown': null, + 'onInput': null, + 'onSave': null, }; /** diff --git a/public/js/plugins/plugin-theme.js b/public/js/plugins/plugin-theme.js index d47cf2919..e797da3f1 100644 --- a/public/js/plugins/plugin-theme.js +++ b/public/js/plugins/plugin-theme.js @@ -120,7 +120,7 @@ $(function () { if (data) { const $dialog = $(data); $dialog.appendTo($(options.targetId)); - that._initDialog(); + that._initDialog($dialog); that._showDialog(); } }); @@ -182,7 +182,7 @@ $(function () { } const theme = this._getCookieValue(); const selector = this._getInputSelector(); - $dialog.data('old-theme', theme).data('new-theme', false); + $dialog.data('old-theme', theme).data('new-theme', theme); $(selector).each(function () { const $this = $(this); $this.prop('checked', $this.val() === theme); @@ -214,10 +214,19 @@ $(function () { } /** - * Handle the dialog hidden event. + * Handle the theme radio input change event. + * @param {ChangeEvent} e * @private */ - _onDialogHidden() { + _onInputChange(e) { + this._setTheme($(e.currentTarget).val()); + } + + /** + * Handle the dialog hide event. + * @private + */ + _onDialogHide() { const $dialog = this._getDialog(); if (!$dialog) { return; @@ -226,10 +235,10 @@ $(function () { this._setFocus(); const oldTheme = $dialog.data('old-theme'); const newTheme = $dialog.data('new-theme') || this._getTheme(); + this._setTheme(newTheme); if (oldTheme === newTheme) { return; } - this._setTheme(newTheme); this._setCookieValue(newTheme); const $link = $(this._getInputCheckedSelector()); if ($link.length) { @@ -281,20 +290,17 @@ $(function () { /** * Initialize the dialog. + * @param {jQuery} $dialog the dialog to initialize. * @private */ - _initDialog() { - const $dialog = this._getDialog(); - if (!$dialog) { - return; - } + _initDialog($dialog) { const options = this.options; - $(`${options.dialogId} ${options.ok}`) - .on('click', () => this._onDialogAccept()); $dialog.on('show.bs.modal', () => this._onDialogShow()) .on('shown.bs.modal', () => this._onDialogVisible()) - .on('hidden.bs.modal', () => this._onDialogHidden()) - .on('keydown', (e) => this._onDialogKeyDown(e)); + .on('hide.bs.modal', () => this._onDialogHide()) + .on('keydown', (e) => this._onDialogKeyDown(e)) + .on('click', options.ok, () => this._onDialogAccept()) + .on('change', options.input, (e) => this._onInputChange(e)); } /** @@ -325,13 +331,14 @@ $(function () { * @private */ _setTheme(theme) { - if (theme === THEME_AUTO) { + if (!theme || theme === THEME_AUTO) { theme = this._isMediaDark() ? THEME_DARK : THEME_LIGHT; } - document.documentElement.setAttribute(THEME_ATTRIBUTE, theme); + if (this._getTheme() !== theme) { + document.documentElement.setAttribute(THEME_ATTRIBUTE, theme); + } } - /** * Gets the document element theme. * @return {string} the selected theme. diff --git a/public/js/plugins/plugin-treeview.js b/public/js/plugins/plugin-treeview.js index 14e163b6e..02c78845a 100644 --- a/public/js/plugins/plugin-treeview.js +++ b/public/js/plugins/plugin-treeview.js @@ -588,7 +588,7 @@ $(function () { } /** - * Remove handlers proxies. + * Remove handler proxies. * @private */ _removeProxies() { diff --git a/public/js/plugins/plugin-typeahead.js b/public/js/plugins/plugin-typeahead.js index 6507c48c6..cd7e1e3dd 100644 --- a/public/js/plugins/plugin-typeahead.js +++ b/public/js/plugins/plugin-typeahead.js @@ -746,7 +746,7 @@ $(function () { } /** - * Cancel last timer if set. + * Cancel the last timer, if set. * @return {Typeahead} this instance for chaining. * @private */ diff --git a/src/Command/UpdateAssetsCommand.php b/src/Command/UpdateAssetsCommand.php index ddb3cfca5..15bed58d4 100644 --- a/src/Command/UpdateAssetsCommand.php +++ b/src/Command/UpdateAssetsCommand.php @@ -32,6 +32,7 @@ * source: string, * target?: string, * disabled?: bool, + * update?: bool, * prefix?: string, * files: string[]} * @psalm-type CopyEntryType = array{ @@ -122,12 +123,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $version = $plugin['version']; $display = $plugin['display'] ?? $name; if ($this->isPluginDisabled($plugin)) { - $this->writeVerbose(\sprintf('Skip : %s %s', $display, $version), 'fg=gray'); + $this->writeVerbose(\sprintf('Disabled : %s %s', $display, $version), 'fg=gray'); continue; } $files = $plugin['files']; if ([] === $files) { - $this->writeVerbose(\sprintf('Skip : %s %s (No file defined)', $display, $version), 'fg=gray'); + $this->writeVerbose(\sprintf('Skip : %s %s (No file defined)', $display, $version), 'fg=gray'); continue; } @@ -143,8 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $definition = $configuration['sources'][$pluginSource]; $source = $definition['source']; $format = $definition['format']; - - $this->writeVerbose(\sprintf('Install: %s %s', $display, $version)); + $this->writeVerbose(\sprintf('Install : %s %s', $display, $version)); foreach ($files as $file) { $sourceFile = $this->getSourceFile($source, $format, $plugin, $file); $targetFile = $this->getTargetFile($targetTemp, $plugin, $file); @@ -154,16 +154,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int } ++$countPlugins; + if (!$this->isPluginUpdate($plugin)) { + continue; + } + $versionUrl = $definition['versionUrl'] ?? null; $versionPaths = $definition['versionPaths'] ?? null; if (\is_string($versionUrl) && \is_array($versionPaths)) { $this->checkVersion($versionUrl, $versionPaths, $name, $version, $display); - } else { - $this->writeVerbose( - \sprintf('Check : %s %s - No version information.', $display, $version), - 'fg=gray' - ); + continue; } + + $this->writeVerbose( + \sprintf('Check : %s %s - No version information.', $display, $version), + 'fg=gray' + ); } $expected = $this->countFiles($plugins); if ($expected !== $countFiles) { @@ -207,8 +212,7 @@ private function checkAssetVersion(string $filename, string $version): bool if ('' === $content) { return false; } - $result = \preg_match(self::ASSET_VERSION_PATTERN, $content, $matches, \PREG_OFFSET_CAPTURE); - if (1 !== $result) { + if (!StringUtils::pregMatch(self::ASSET_VERSION_PATTERN, $content, $matches, \PREG_OFFSET_CAPTURE)) { return false; } @@ -302,6 +306,10 @@ private function dryRun(array $configuration, string $target): int $this->writeln(\sprintf($pattern, '✗', $display, $version, 'Disabled.'), 'fg=gray'); continue; } + if (!$this->isPluginUpdate($plugin)) { + $this->writeln(\sprintf($pattern, '✗', $display, $version, 'Skip Update.'), 'fg=gray'); + continue; + } $source = $plugin['source']; $definition = $configuration['sources'][$source]; @@ -447,7 +455,15 @@ private function getTargetTemp(string $publicDir): string|false */ private function isPluginDisabled(array $plugin): bool { - return isset($plugin['disabled']) && $plugin['disabled']; + return $plugin['disabled'] ?? false; + } + + /** + * @psalm-param PluginType $plugin + */ + private function isPluginUpdate(array $plugin): bool + { + return $plugin['update'] ?? true; } /** @@ -504,13 +520,14 @@ private function propertyExists(array $var, array|string $properties, bool $log { $properties = (array) $properties; foreach ($properties as $property) { - if (!isset($var[$property])) { - if ($log) { - $this->writeError(\sprintf('Unable to find the property "%s".', $property)); - } - - return false; + if (isset($var[$property])) { + continue; + } + if ($log) { + $this->writeError(\sprintf('Unable to find the property "%s".', $property)); } + + return false; } return true; diff --git a/src/Constraint/PasswordValidator.php b/src/Constraint/PasswordValidator.php index e46932021..4421b56bc 100644 --- a/src/Constraint/PasswordValidator.php +++ b/src/Constraint/PasswordValidator.php @@ -12,6 +12,7 @@ namespace App\Constraint; +use App\Utils\StringUtils; use Symfony\Component\Validator\Constraint; /** @@ -147,7 +148,7 @@ private function checkSpecialChar(Password $constraint, string $value): bool */ private function validateRegex(bool $enabled, string $pattern, string $value, string $message, string $code): bool { - if ($enabled && 1 !== \preg_match($pattern, $value)) { + if ($enabled && !StringUtils::pregMatch($pattern, $value)) { return $this->addViolation($message, $value, $code); } diff --git a/src/Controller/AboutPhpController.php b/src/Controller/AboutPhpController.php index 7f886281c..761ccabae 100644 --- a/src/Controller/AboutPhpController.php +++ b/src/Controller/AboutPhpController.php @@ -20,6 +20,7 @@ use App\Service\PhpInfoService; use App\Spreadsheet\PhpIniDocument; use App\Traits\ArrayTrait; +use App\Utils\StringUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; @@ -78,7 +79,7 @@ private function getApacheVersion(Request $request): bool|string $version = \function_exists('apache_get_version') ? apache_get_version() : $request->server->get('SERVER_SOFTWARE'); - if (\is_string($version) && 1 === \preg_match($regex, $version, $matches)) { + if (\is_string($version) && StringUtils::pregMatch($regex, $version, $matches)) { return $matches['version']; } diff --git a/src/Controller/TestController.php b/src/Controller/TestController.php index edb7bc12f..de8455395 100644 --- a/src/Controller/TestController.php +++ b/src/Controller/TestController.php @@ -606,7 +606,7 @@ private function getCurrencies(): array }, Currencies::getCurrencyCodes()); $currencies = \array_filter( $currencies, - static fn (array $currency): bool => 0 === \preg_match('/\d|\(/', $currency['name']) + static fn (array $currency): bool => !StringUtils::pregMatch('/\d|\(/', $currency['name']) ); \usort( $currencies, diff --git a/src/Form/Extension/AbstractFileTypeExtension.php b/src/Form/Extension/AbstractFileTypeExtension.php index ae961e32e..5c9347409 100644 --- a/src/Form/Extension/AbstractFileTypeExtension.php +++ b/src/Form/Extension/AbstractFileTypeExtension.php @@ -12,6 +12,7 @@ namespace App\Form\Extension; +use App\Utils\StringUtils; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -114,7 +115,7 @@ private function normalizeSize(int|string $size): ?int return (int) $size; } - if (1 === \preg_match('/^(\d++)(' . \implode('|', \array_keys($factors)) . ')$/i', (string) $size, $matches)) { + if (StringUtils::pregMatch('/^(\d++)(' . \implode('|', \array_keys($factors)) . ')$/i', (string) $size, $matches)) { return (int) $matches[1] * $factors[\strtolower($matches[2])]; } diff --git a/src/Listener/VichListener.php b/src/Listener/VichListener.php index e897d779a..2c7a7e8fb 100644 --- a/src/Listener/VichListener.php +++ b/src/Listener/VichListener.php @@ -55,7 +55,7 @@ public function onPostUpload(Event $event): void } // new? - if (1 === \preg_match('/0{6}/m', $file->getFilename())) { + if (StringUtils::pregMatch('/0{6}/m', $file->getFilename())) { $file = $this->rename($mapping, $user, $file); } diff --git a/src/Pdf/Html/AbstractHtmlChunk.php b/src/Pdf/Html/AbstractHtmlChunk.php index 5f3c2c0f5..dcfe93afa 100644 --- a/src/Pdf/Html/AbstractHtmlChunk.php +++ b/src/Pdf/Html/AbstractHtmlChunk.php @@ -347,7 +347,7 @@ private function parseBookmark(string $class): void // level $regex = '/bookmark-(\d+)/'; - if (1 === \preg_match($regex, $class, $matches)) { + if (StringUtils::pregMatch($regex, $class, $matches)) { /** @psalm-var non-negative-int $level */ $level = (int) $matches[1]; $this->bookmarkLevel = $level; diff --git a/src/Pdf/Html/HtmlStyle.php b/src/Pdf/Html/HtmlStyle.php index 3422e5065..4371aae3d 100644 --- a/src/Pdf/Html/HtmlStyle.php +++ b/src/Pdf/Html/HtmlStyle.php @@ -16,6 +16,7 @@ use App\Pdf\Colors\PdfFillColor; use App\Pdf\Colors\PdfTextColor; use App\Pdf\PdfStyle; +use App\Utils\StringUtils; use fpdf\Enums\PdfFontName; use fpdf\Enums\PdfTextAlignment; use fpdf\PdfBorder; @@ -327,7 +328,7 @@ private function updateFont(string $class): self private function updateMargins(string $class): self { - if (1 === \preg_match_all(self::MARGINS_PATTERN, $class, $matches, \PREG_SET_ORDER)) { + if (StringUtils::pregMatchAll(self::MARGINS_PATTERN, $class, $matches, \PREG_SET_ORDER)) { $match = $matches[0]; $value = (float) $match[3]; match ($match[1]) { diff --git a/src/Report/CommandsReport.php b/src/Report/CommandsReport.php index aca05adf4..e77d94505 100644 --- a/src/Report/CommandsReport.php +++ b/src/Report/CommandsReport.php @@ -107,14 +107,14 @@ private function indent(): void private function outputHelp(string $text): void { $text = \strip_tags($text, ''); - $result = \preg_match_all(self::LINK_PATTERN, $text, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE); - if (false === $result || 0 === $result) { + if (!StringUtils::pregMatchAll(self::LINK_PATTERN, $text, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE)) { $this->write($text); return; } $offset = 0; + /** @psalm-var array $match */ foreach ($matches as $match) { // previous chunk $index = $match[0][1]; @@ -246,8 +246,7 @@ private function renderStyledHelp(string $help): void // find classes $help = \str_replace(' target="_blank" rel="noopener noreferrer"', '', $help); - $result = \preg_match_all(self::CLASS_PATTERN, $help, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE); - if (false === $result || 0 === $result) { + if (!StringUtils::pregMatchAll(self::CLASS_PATTERN, $help, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE)) { $this->outputHelp($help); $this->leftMargin = $oldMargin; $this->lineBreak(); @@ -256,6 +255,7 @@ private function renderStyledHelp(string $help): void } $offset = 0; + /** @psalm-var array $match */ foreach ($matches as $match) { // previous chunk $index = $match[0][1]; diff --git a/src/Report/PhpIniReport.php b/src/Report/PhpIniReport.php index 3b54aef39..9ce5334ef 100644 --- a/src/Report/PhpIniReport.php +++ b/src/Report/PhpIniReport.php @@ -75,7 +75,7 @@ private function getCellStyle(string $var): ?PdfStyle { $color = null; $style = PdfFontStyle::REGULAR; - if (1 === \preg_match('/#[\dA-Fa-f]{6}/i', $var)) { + if (StringUtils::pregMatch('/#[\dA-Fa-f]{6}/i', $var)) { $color = PdfTextColor::create($var); } elseif (\in_array( \strtolower($var), diff --git a/src/Service/DiagramService.php b/src/Service/DiagramService.php index da5afcd63..558a374b4 100644 --- a/src/Service/DiagramService.php +++ b/src/Service/DiagramService.php @@ -61,7 +61,7 @@ public function getFiles(): array private function findTitle(string $content, string $name): string { - if (1 === \preg_match(self::TITLE_PATTERN, $content, $matches)) { + if (StringUtils::pregMatch(self::TITLE_PATTERN, $content, $matches)) { return $matches[1]; } diff --git a/src/Service/FontAwesomeImageService.php b/src/Service/FontAwesomeImageService.php index ae5db04b4..7f4e3decb 100644 --- a/src/Service/FontAwesomeImageService.php +++ b/src/Service/FontAwesomeImageService.php @@ -16,6 +16,7 @@ use App\Traits\CacheKeyTrait; use App\Traits\LoggerTrait; use App\Utils\FileUtils; +use App\Utils\StringUtils; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Target; @@ -66,7 +67,7 @@ public function __construct( /** * Gets the icon aliases. * - * @return array the aliases where key is alias name and the value is the existing file + * @return array the aliases where key is the alias name and the value is the existing file */ public function getAliases(): array { @@ -162,8 +163,8 @@ private function convert(string $content): FontAwesomeImage */ private function getTargetSize(string $content): array { - $result = \preg_match(self::VIEW_BOX_PATTERN, $content, $matches); - if (1 !== $result || $matches['width'] === $matches['height']) { + $result = StringUtils::pregMatch(self::VIEW_BOX_PATTERN, $content, $matches); + if (!$result || $matches['width'] === $matches['height']) { return [self::TARGET_SIZE, self::TARGET_SIZE]; } diff --git a/src/Service/PhpInfoService.php b/src/Service/PhpInfoService.php index d0c15ff2b..cb1ee530e 100644 --- a/src/Service/PhpInfoService.php +++ b/src/Service/PhpInfoService.php @@ -24,6 +24,10 @@ class PhpInfoService */ public const REDACTED = '********'; + private const CONVERT = ['yes', 'no', 'enabled', 'disabled', 'on', 'off', 'no value']; + private const DISABLED = ['off', 'no', 'disabled']; + private const ENABLED = ['on', 'yes', 'enabled']; + /** * Gets PHP information as the array. * @@ -49,17 +53,18 @@ public function asArray(int $what = \INFO_ALL): array $result = []; $matchs = null; $regexInfo = '([^<]+)<\/info>'; - $regex3cols = '/' . $regexInfo . '\s*' . $regexInfo . '\s*' . $regexInfo . '/i'; - $regex2cols = '/' . $regexInfo . '\s*' . $regexInfo . '/i'; + $regex2cols = \sprintf('/%1$s\s*%1$s/i', $regexInfo); + $regex3cols = \sprintf('/%1$s\s*%1$s\s*%1$s/i', $regexInfo); $regexLine = '/]*>([^<]+)<\/h2>/i'; + /** @psalm-var array $array */ $array = (array) \preg_split('/(]*>[^<]+<\/h2>)/i', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); foreach ($array as $index => $entry) { - if (1 === \preg_match($regexLine, $entry, $matchs)) { + if (StringUtils::pregMatch($regexLine, $entry, $matchs)) { $name = \trim($matchs[1]); $vals = \explode(StringUtils::NEW_LINE, $array[$index + 1]); foreach ($vals as $val) { - if (1 === \preg_match($regex3cols, $val, $matchs)) { + if (StringUtils::pregMatch($regex3cols, $val, $matchs)) { // 3 columns $match1 = \trim($matchs[1]); $match2 = $this->convert(\trim($matchs[2])); @@ -70,7 +75,7 @@ public function asArray(int $what = \INFO_ALL): array 'master' => $match3, ]; } - } elseif (1 === \preg_match($regex2cols, $val, $matchs)) { + } elseif (StringUtils::pregMatch($regex2cols, $val, $matchs)) { // 2 columns $match1 = \trim($matchs[1]); $match2 = $this->convert(\trim($matchs[2])); @@ -95,39 +100,39 @@ public function asHtml(int $what = \INFO_ALL): string { $info = $this->asText($what); - $info = (string) \preg_replace('%^.*(.*).*$%ms', '$1', $info); - $info = (string) \preg_replace('/(.+?)<\/a>/mi', '

$2

', $info); + $info = $this->pregReplace('%^.*(.*).*$%ms', '$1', $info); + $info = $this->pregReplace('/(.+?)<\/a>/mi', '

$2

', $info); $info = \str_ireplace('background-color: white; text-align: center', '', $info); $info = \str_ireplace('no value', 'No value', $info); $info = \str_ireplace('(none)', 'None', $info); $info = \str_ireplace('', "
", $info); - $info = \str_ireplace(self::REDACTED, '' . self::REDACTED . '', $info); + $info = \str_ireplace(self::REDACTED, \sprintf('%s', self::REDACTED), $info); foreach (['Directive', 'Local Value', 'Master Value'] as $value) { - $info = \str_replace($value, '' . $value . '', $info); + $info = \str_replace($value, \sprintf('%s', $value), $info); } - foreach (['on', 'yes', 'enabled'] as $value) { - $search = '/'; - $info = (string) \preg_replace($search, $replace, $info); + foreach (self::ENABLED as $value) { + $search = \sprintf('/', StringUtils::capitalize($value)); + $info = $this->pregReplace($search, $replace, $info); - $search = '/'; - $info = (string) \preg_replace($search, $replace, $info); + $search = \sprintf('/', StringUtils::capitalize($value)); + $info = $this->pregReplace($search, $replace, $info); } - foreach (['off', 'no', 'disabled'] as $value) { - $search = '/'; - $info = (string) \preg_replace($search, $replace, $info); + foreach (self::DISABLED as $value) { + $search = \sprintf('/', StringUtils::capitalize($value)); + $info = $this->pregReplace($search, $replace, $info); - $search = '/'; - $info = (string) \preg_replace($search, $replace, $info); + $search = \sprintf('/', StringUtils::capitalize($value)); + $info = $this->pregReplace($search, $replace, $info); } - return (string) \preg_replace('/(.+?)<\/table>/is', '', $info, 1); + return $this->pregReplace('/(.+?)<\/table>/is', '', $info, 1); } /** @@ -158,13 +163,13 @@ public function getVersion(): string private function convert(string $var): string|int|float { - if (\in_array(\strtolower($var), ['yes', 'no', 'enabled', 'disabled', 'on', 'off', 'no value'], true)) { + if (\in_array(\strtolower($var), self::CONVERT, true)) { return StringUtils::capitalize($var); } - if (1 === \preg_match('/^-?\d+$/', $var)) { + if (StringUtils::pregMatch('/^-?\d+$/', $var)) { return (int) $var; } - if (1 === \preg_match('/^-?\d+\.\d+$/', $var)) { + if (StringUtils::pregMatch('/^-?\d+\.\d+$/', $var)) { $pos = (int) \strrpos($var, '.'); $decimals = \strlen($var) - $pos - 1; @@ -174,13 +179,21 @@ private function convert(string $var): string|int|float return \str_replace('\\', '/', $var); } + /** + * @param non-empty-string $pattern + */ + private function pregReplace(string $pattern, string $replacement, string $subject, int $limit = -1): string + { + return (string) \preg_replace($pattern, $replacement, $subject, $limit); + } + private function updateContent(string $content): string { - $subst = '$1' . self::REDACTED . '$3'; + $subst = \sprintf('$1%s$3', self::REDACTED); $keys = ['_KEY', '_USER_NAME', 'APP_SECRET', '_PASSWORD', 'MAILER_DSN', 'DATABASE_URL']; foreach ($keys as $key) { - $regex = "/()(.*)(<.*<\/tr>)/mi"; - $content = (string) \preg_replace($regex, $subst, $content); + $regex = \sprintf("/()(.*)(<.*<\/tr>)/mi", $key); + $content = $this->pregReplace($regex, $subst, $content); } $content = \str_replace(['✘ ', '✔ ', '⊕'], '', $content); diff --git a/src/Spreadsheet/PhpIniDocument.php b/src/Spreadsheet/PhpIniDocument.php index 810c4f3b4..6dfeeb79d 100644 --- a/src/Spreadsheet/PhpIniDocument.php +++ b/src/Spreadsheet/PhpIniDocument.php @@ -79,7 +79,7 @@ public function render(): bool private function applyStyle(Worksheet $sheet, int $column, int $row, string $var): self { $color = null; - if (1 === \preg_match('/#[\dA-Fa-f]{6}/i', $var)) { + if (StringUtils::pregMatch('/#[\dA-Fa-f]{6}/i', $var)) { $color = \substr($var, 1); } elseif (\in_array( \strtolower($var), diff --git a/src/Twig/PregExtension.php b/src/Twig/PregExtension.php deleted file mode 100644 index b77f259aa..000000000 --- a/src/Twig/PregExtension.php +++ /dev/null @@ -1,147 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Twig; - -use App\Utils\StringUtils; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; - -/** - * Twig extension for preg_** functions. - */ -class PregExtension extends AbstractExtension -{ - public function getFilters(): array - { - return [ - new TwigFilter('preg_filter', $this->pregFilter(...)), - new TwigFilter('preg_grep', $this->pregGrep(...)), - new TwigFilter('preg_match', $this->pregMatch(...)), - new TwigFilter('preg_quote', $this->pregQuote(...)), - new TwigFilter('preg_replace', $this->pregReplace(...)), - new TwigFilter('preg_split', $this->pregSplit(...)), - ]; - } - - /** - * Perform a regular expression search and replace, returning only matched subjects. - * - * @param string|string[]|null $subject - * @param string|string[] $pattern - * @param string|string[] $replacement - * - * @return string|string[]|null - * - * @psalm-param array|non-empty-string $pattern - */ - protected function pregFilter( - string|array|null $subject, - string|array $pattern, - string|array $replacement, - int $limit = -1 - ): string|array|null { - if (null === $subject) { - return null; - } - - return \preg_filter($pattern, $replacement, $subject, $limit); - } - - /** - * Perform a regular expression match and return an array of entries that match the pattern. - * - * @param string[]|null $subject - * - * @psalm-param int<0,1> $flags - * @psalm-param non-empty-string $pattern - */ - protected function pregGrep(?array $subject, string $pattern, int $flags = 0): array|false - { - if (null === $subject) { - return false; - } - - return \preg_grep($pattern, $subject, $flags); - } - - /** - * Perform a regular expression match. - * - * @psalm-param int-mask<256, 512, 768> $flags - * @psalm-param int $offset - * @psalm-param non-empty-string $pattern - */ - protected function pregMatch(?string $subject, string $pattern, int $flags = 0, int $offset = 0): array|false - { - if (!StringUtils::isString($subject)) { - return false; - } - if (1 === \preg_match($pattern, $subject, $matches, $flags, $offset)) { - return $matches; - } - - return false; - } - - /** - * Quote regular expression characters. - */ - protected function pregQuote(?string $subject, ?string $delimiter = null): ?string - { - if (null === $subject) { - return null; - } - - return \preg_quote($subject, $delimiter); - } - - /** - * Perform a regular expression search and replace. - * - * @param string[]|string|null $subject - * @param string[]|string $pattern - * @param string[]|string $replacement - * - * @return string[]|string|null - * - * @psalm-param array|non-empty-string $pattern - */ - protected function pregReplace( - array|string|null $subject, - array|string $pattern, - array|string $replacement, - int $limit = -1 - ): array|string|null { - if (null === $subject) { - return null; - } - - return \preg_replace($pattern, $replacement, $subject, $limit); - } - - /** - * Split text into an array using a regular expression. - * - * @return string[]|false - * - * @psalm-param non-empty-string $pattern - */ - protected function pregSplit(?string $subject, string $pattern): array|false - { - if (null === $subject) { - return false; - } - - return \preg_split($pattern, $subject); - } -} diff --git a/src/Utils/StringUtils.php b/src/Utils/StringUtils.php index 030c1d1cf..7aec95d00 100644 --- a/src/Utils/StringUtils.php +++ b/src/Utils/StringUtils.php @@ -175,11 +175,60 @@ public static function isString(?string $str): bool return null !== $str && '' !== $str; } + /** + * Perform a regular expression match. + * + * @param string $pattern the pattern to search for + * @param string $subject the input string + * + * @param-out string[] $matches if matches is provided, then it is filled with the results of search. + * + * @param int $flags can be a combination of flags + * @param int $offset to specify the place from which to start the search + * + * @return bool true if the pattern matches the given subject + * + * @psalm-pure + * + * @psalm-param non-empty-string $pattern + * + * @phpstan-param int-mask<256, 512> $flags + */ + public static function pregMatch(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + return 1 === \preg_match($pattern, $subject, $matches, $flags, $offset); + } + + /** + * Perform a global regular expression match. + * + * @param string $pattern the pattern to search for + * @param string $subject the input string + * + * @param-out string[] $matches if matches is provided, then it is filled with the results of search. + * + * @param int $flags can be a combination of flags + * @param int $offset to specify the place from which to start the search + * + * @return bool true if the pattern matches the given subject + * + * @psalm-pure + * + * @psalm-param non-empty-string $pattern + */ + public static function pregMatchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool + { + $result = \preg_match_all($pattern, $subject, $matches, $flags, $offset); + + return \is_int($result) && $result > 0; + } + /** * Replace all occurrences of the pattern string with the replacement string. * * @param array $values an array where key is the pattern, and value is the replacement term * @param string|string[] $subject the string or array being searched and replaced on + * @param int $limit the maximum possible replacements for each pattern in each subject string * * @return string|string[] returns a string or an array with the replaced values * @@ -187,10 +236,10 @@ public static function isString(?string $str): bool * * @psalm-return ($subject is string ? string : string[]) */ - public static function pregReplace(array $values, string|array $subject): string|array + public static function pregReplace(array $values, string|array $subject, int $limit = -1): string|array { /** @psalm-var string|string[] */ - return \preg_replace(\array_keys($values), \array_values($values), $subject); + return \preg_replace(\array_keys($values), \array_values($values), $subject, $limit); } /** diff --git a/src/Word/HtmlWordParser.php b/src/Word/HtmlWordParser.php index 0079fdec0..2f4163584 100644 --- a/src/Word/HtmlWordParser.php +++ b/src/Word/HtmlWordParser.php @@ -183,8 +183,9 @@ private function parseBorders(string $class): string */ private function parseMargins(string $class): string { - if (1 === \preg_match_all(self::MARGINS_PATTERN, $class, $matches, \PREG_SET_ORDER)) { - $value = match ((int) $matches[0][3]) { + if (StringUtils::pregMatchAll(self::MARGINS_PATTERN, $class, $matches, \PREG_SET_ORDER)) { + $match = $matches[0]; + $value = match ((int) $match[3]) { 1 => '4px', // 0.25rem 2 => '8px', // 0.5rem 3 => '16px', // 1.0rem @@ -193,7 +194,7 @@ private function parseMargins(string $class): string default => '0', }; - return match ($matches[0][1]) { + return match ($match[1]) { 't' => \sprintf('margin-top:%s;', $value), 'b' => \sprintf('margin-bottom:%s;', $value), 's' => \sprintf('margin-left:%s;', $value), diff --git a/tests/Controller/AjaxControllerTest.php b/tests/Controller/AjaxControllerTest.php index 21ab11c7f..906119f35 100644 --- a/tests/Controller/AjaxControllerTest.php +++ b/tests/Controller/AjaxControllerTest.php @@ -79,6 +79,16 @@ public function testComputeTaskNegativeItems(): void $this->checkTaskRequest($parameters, Response::HTTP_BAD_REQUEST); } + public function testComputeTaskNoFound(): void + { + $parameters = [ + 'id' => 1_000_000, + 'quantity' => 1.0, + 'items' => [1], + ]; + $this->checkTaskRequest($parameters, Response::HTTP_OK); + } + public function testComputeTaskQuantityEqualZero(): void { $parameters = [ diff --git a/tests/Twig/Fixtures/PregExtension/preg_function.test b/tests/Twig/Fixtures/PregExtension/preg_function.test deleted file mode 100644 index e4e60e4c1..000000000 --- a/tests/Twig/Fixtures/PregExtension/preg_function.test +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -"preg_extension" function test ---TEMPLATE-- -{{ array_values|preg_filter('/\\d/', 'A:$0')|length }} -{{ array_values|preg_grep('/\\d/')|length }} -{{ '1'|preg_match('/\\d/') ? 'ok' : 'ko' }} -{{ 'A'|preg_match('/\\d/') ? 'ok' : 'ko' }} -{{ null_values|preg_match('/\\d/') ? 'ok' : 'ko' }} -{{ '$40'|preg_quote() }} -{{ '1'|preg_replace('/\\d/', '2') }} -{{ 'hypertext language, programming'|preg_split('/[\\s,]+/')|length }} -{{ null_values|preg_grep('/\\d/') }} -{{ null_values|preg_filter('/\\d/', 'A:$0') }} -{{ null_values|preg_replace('/\\d/', '2') }} -{{ null_values|preg_split('/[\\s,]+/') }} -{{ null_values|preg_quote() }} ---DATA-- -return [ - 'array_values' => ['1', '2', '3', '4', 'a', 'b', 'A', 'B'], - 'null_values' => null, -] ---EXPECT-- -4 -4 -ok -ko -ko -\$40 -2 -3 diff --git a/tests/Twig/PhpExtensionTest.php b/tests/Twig/PhpExtensionTest.php deleted file mode 100644 index 6ac19681a..000000000 --- a/tests/Twig/PhpExtensionTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\Twig; - -use App\Twig\PhpExtension; - -class PhpExtensionTest extends IntegrationTestCase -{ - protected function getExtensions(): array - { - return [new PhpExtension()]; - } - - protected function getFixturesDir(): string - { - return __DIR__ . '/Fixtures/PhpExtension'; - } -} diff --git a/tests/Twig/PregExtensionTest.php b/tests/Twig/PregExtensionTest.php deleted file mode 100644 index d3691baff..000000000 --- a/tests/Twig/PregExtensionTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\Twig; - -use App\Twig\PregExtension; - -class PregExtensionTest extends IntegrationTestCase -{ - protected function getExtensions(): array - { - return [new PregExtension()]; - } - - protected function getFixturesDir(): string - { - return __DIR__ . '/Fixtures/PregExtension'; - } -} diff --git a/tests/Utils/StringUtilsTest.php b/tests/Utils/StringUtilsTest.php index d4d84e620..c301bc1dc 100644 --- a/tests/Utils/StringUtilsTest.php +++ b/tests/Utils/StringUtilsTest.php @@ -63,6 +63,31 @@ public static function getIsString(): \Iterator yield ['my home', true]; } + public static function getPregMatch(): \Iterator + { + yield ['/\d+/', '1234', true]; + yield ['/\d+/', 'FAKE', false]; + yield ['/(?J)(?foo)|(?bar)/', 'foo bar', true]; + yield ['/(foo)(bar)(baz)/', 'foobarbaz', true]; + yield ['/(foo)(bar)(baz)/', 'foobaz', false]; + } + + public static function getPregMatchAll(): \Iterator + { + yield ['/\d+/', '1234', true]; + yield ['/\d+/', 'FAKE', false]; + + yield ['/(?J)(?foo)|(?bar)/', 'foo bar', true]; + yield ['/(foo)(bar)(baz)/', 'foobarbaz', true]; + yield ['/(foo)(bar)(baz)/', 'foobaz', false]; + } + + public static function getPregReplace(): \Iterator + { + yield [['/\d+/' => ''], '1234', '']; + yield [['/\d+/' => ''], 'FAKE', 'FAKE']; + } + public static function getShortName(): \Iterator { yield [null, null, true]; @@ -184,6 +209,36 @@ public function testNewLine(): void self::assertSame("\n", StringUtils::NEW_LINE); } + /** + * @psalm-param non-empty-string $pattern + */ + #[DataProvider('getPregMatch')] + public function testPregMatch(string $pattern, string $subject, bool $expected): void + { + $actual = StringUtils::pregMatch($pattern, $subject); + self::assertSame($expected, $actual); + } + + /** + * @psalm-param non-empty-string $pattern + */ + #[DataProvider('getPregMatchAll')] + public function testPregMatchAll(string $pattern, string $subject, bool $expected): void + { + $actual = StringUtils::pregMatchAll($pattern, $subject); + self::assertSame($expected, $actual); + } + + /** + * @psalm-param non-empty-array $values + */ + #[DataProvider('getPregReplace')] + public function testPregReplace(array $values, string $subject, string $expected): void + { + $actual = StringUtils::pregReplace($values, $subject); + self::assertSame($expected, $actual); + } + public function testSlug(): void { $actual = StringUtils::slug('Wôrķšƥáçè ~~sèťtïñğš~~'); @@ -197,6 +252,15 @@ public function testStartWith(string $haystack, string $needle, bool $ignore_cas self::assertSame($expected, $actual); } + public function testUnicode(): void + { + $actual = StringUtils::unicode('fake')->toString(); + self::assertSame('fake', $actual); + + $actual = StringUtils::unicode('fake', true)->toString(); + self::assertSame('fake', $actual); + } + private static function getVarArray(): string { return <<
' . $value . '\s?<\/td>/mi'; - $replace = '' . StringUtils::capitalize($value) . '%s\s?<\/td>/mi', $value); + $replace = \sprintf('%s' . $value . '\s?<\/th>/mi'; - $replace = '' . StringUtils::capitalize($value) . '%s\s?<\/th>/mi', $value); + $replace = \sprintf('%s' . $value . '\s?<\/td>/mi'; - $replace = '' . StringUtils::capitalize($value) . '%s\s?<\/td>/mi', $value); + $replace = \sprintf('%s' . $value . '\s?<\/th>/mi'; - $replace = '' . StringUtils::capitalize($value) . '%s\s?<\/th>/mi', $value); + $replace = \sprintf('%s