diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml new file mode 100644 index 0000000..4d72940 --- /dev/null +++ b/.github/workflows/code_analysis.yaml @@ -0,0 +1,66 @@ +name: Code Analysis + +on: + pull_request: null + push: + branches: + - main + +jobs: + rector_analysis: + name: Rector analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: json, mbstring, pdo, curl, pdo_sqlite + coverage: none + tools: symfony-cli, composer:v2.8 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: composer install --no-progress --ansi + + - run: vendor/bin/rector process -n --no-progress-bar --ansi + + code_analysis: + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4'] + actions: + - + name: Coding Standard + # tip: add "--ansi" to commands in CI to make them full of colors + run: vendor/bin/ecs check src --ansi + + - + name: PHPStan + run: vendor/bin/phpstan analyse --ansi + + - + name: Check composer.json and composer.lock + run: composer validate --strict --ansi + + name: ${{ matrix.actions.name }} - PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + # see https://github.com/shivammathur/setup-php + - uses: shivammathur/setup-php@v2 + with: + # test the lowest version, to make sure checks pass on it + php-version: ${{ matrix.php-version }} + extensions: json, mbstring, pdo, curl, pdo_sqlite + coverage: none + tools: symfony-cli, composer:v2.8 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: composer install --no-progress --ansi + + - run: ${{ matrix.actions.run }} diff --git a/composer.json b/composer.json index 08819d7..4c846e4 100644 --- a/composer.json +++ b/composer.json @@ -10,11 +10,15 @@ } ], "require": { - "php": ">=7.2.9" + "php": ">=8.2" }, "require-dev": { - "bolt/core": "^5.0", - "symplify/easy-coding-standard": "^7.0" + "bolt/core": "^6.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "2.1.31", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "rector/rector": "2.2.7", + "symplify/easy-coding-standard": "^13.0" }, "autoload": { "psr-4": { @@ -24,9 +28,19 @@ "minimum-stability": "dev", "prefer-stable": true, "extra": { + "branch-alias": { + "dev-main": "3.0.x-dev" + }, "entrypoint": "Bolt\\Article\\Extension", "screenshots": [ "screenshots/article.png" ] + }, + "config": { + "allow-plugins": { + "symfony/flex": false, + "drupol/composer-packages": false, + "phpstan/extension-installer": true + } } } diff --git a/config/routes.yaml b/config/routes.yaml index 02bcc1d..8767fa8 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,4 +1,4 @@ article_extension_controllers: resource: '../../vendor/bolt/article/src/Controller/' prefix: '%bolt.backend_url%/async' - type: annotation \ No newline at end of file + type: attribute \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 465f29d..23903c7 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -19,7 +19,7 @@ doctrine: mappings: Article: is_bundle: false - type: annotation + type: attribute dir: '%kernel.project_dir%/vendor/bolt/article/src/Entity' prefix: 'Bolt\Article' alias: Article diff --git a/easy-coding-standard.yml b/easy-coding-standard.yml deleted file mode 100644 index 1a2ec8e..0000000 --- a/easy-coding-standard.yml +++ /dev/null @@ -1,103 +0,0 @@ -imports: - - { resource: 'vendor/symplify/easy-coding-standard/config/set/clean-code.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/common.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/php70.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/php71.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/psr2.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/psr12.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/symfony.yaml' } - - { resource: 'vendor/symplify/easy-coding-standard/config/set/symfony-risky.yaml' } - -services: - # most of these services are taken from symplify.yml - # see https://github.com/Symplify/Symplify/blob/master/ecs.yml - - # PHP 5.5 - Symplify\CodingStandard\Fixer\Php\ClassStringToClassConstantFixer: ~ - - # Control Structures - Symplify\CodingStandard\Fixer\Property\ArrayPropertyDefaultValueFixer: ~ - Symplify\CodingStandard\Fixer\ArrayNotation\StandaloneLineInMultilineArrayFixer: ~ - Symplify\CodingStandard\Fixer\ControlStructure\RequireFollowedByAbsolutePathFixer: ~ - - # Spaces - Symplify\CodingStandard\Fixer\Strict\BlankLineAfterStrictTypesFixer: ~ - PhpCsFixer\Fixer\Operator\ConcatSpaceFixer: - spacing: one - - # Comments - Symplify\CodingStandard\Fixer\Commenting\RemoveSuperfluousDocBlockWhitespaceFixer: ~ - - # Naming - PhpCsFixer\Fixer\PhpUnit\PhpUnitMethodCasingFixer: ~ - - # Debug - Symplify\CodingStandard\Sniffs\Debug\DebugFunctionCallSniff: ~ - Symplify\CodingStandard\Sniffs\Debug\CommentedOutCodeSniff: ~ - - # final classes - PhpCsFixer\Fixer\ClassNotation\FinalInternalClassFixer: ~ - - # multibyte - PhpCsFixer\Fixer\Alias\MbStrFunctionsFixer: ~ - - # psr - PhpCsFixer\Fixer\Basic\Psr0Fixer: ~ - PhpCsFixer\Fixer\Basic\Psr4Fixer: ~ - - PhpCsFixer\Fixer\CastNotation\LowercaseCastFixer: ~ - PhpCsFixer\Fixer\CastNotation\ShortScalarCastFixer: ~ - PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer: ~ - PhpCsFixer\Fixer\Import\NoLeadingImportSlashFixer: ~ - PhpCsFixer\Fixer\Import\OrderedImportsFixer: - importsOrder: - - 'class' - - 'const' - - 'function' - PhpCsFixer\Fixer\LanguageConstruct\DeclareEqualNormalizeFixer: - space: 'none' - PhpCsFixer\Fixer\Operator\NewWithBracesFixer: ~ - PhpCsFixer\Fixer\Basic\BracesFixer: - 'allow_single_line_closure': false - 'position_after_functions_and_oop_constructs': 'next' - 'position_after_control_structures': 'same' - 'position_after_anonymous_constructs': 'same' - - PhpCsFixer\Fixer\ClassNotation\NoBlankLinesAfterClassOpeningFixer: ~ - PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer: - elements: - - 'const' - - 'method' - - 'property' - PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer: ~ - PhpCsFixer\Fixer\FunctionNotation\ReturnTypeDeclarationFixer: ~ - PhpCsFixer\Fixer\Whitespace\NoTrailingWhitespaceFixer: ~ - - PhpCsFixer\Fixer\Semicolon\NoSinglelineWhitespaceBeforeSemicolonsFixer: ~ - PhpCsFixer\Fixer\ArrayNotation\NoWhitespaceBeforeCommaInArrayFixer: ~ - PhpCsFixer\Fixer\ArrayNotation\WhitespaceAfterCommaInArrayFixer: ~ - - #remove useless phpdoc - PhpCsFixer\Fixer\FunctionNotation\PhpdocToReturnTypeFixer: ~ - PhpCsFixer\Fixer\Import\FullyQualifiedStrictTypesFixer: ~ - PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer: ~ - PhpCsFixer\Fixer\Phpdoc\PhpdocLineSpanFixer: - property: single - - #please yoda no - SlevomatCodingStandard\Sniffs\ControlStructures\DisallowYodaComparisonSniff: ~ - -parameters: - cache_directory: var/cache/ecs - skip: - PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer: ~ - PhpCsFixer\Fixer\ControlStructure\YodaStyleFixer: ~ - PhpCsFixer\Fixer\Operator\IncrementStyleFixer: ~ - PhpCsFixer\Fixer\Phpdoc\PhpdocAnnotationWithoutDotFixer: ~ - PhpCsFixer\Fixer\Phpdoc\PhpdocSummaryFixer: ~ - Symplify\CodingStandard\Sniffs\Debug\CommentedOutCodeSniff: ~ #to be removed before beta release - Symplify\CodingStandard\Sniffs\Debug\DebugFunctionCallSniff: ~ #to be removed before beta release - - # Deprecated. Todo: Find replacement - Symplify\CodingStandard\Fixer\ControlStructure\RequireFollowedByAbsolutePathFixer: ~ - Symplify\CodingStandard\Fixer\Property\ArrayPropertyDefaultValueFixer: ~ \ No newline at end of file diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..43705ef --- /dev/null +++ b/ecs.php @@ -0,0 +1,127 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/ecs.php', + ]) + ->withCache('var/cache/ecs') + ->withPreparedSets(psr12: true, common: true, cleanCode: true) + ->withSkip([ + OrderedClassElementsFixer::class => null, + YodaStyleFixer::class => null, + IncrementStyleFixer::class => null, + PhpdocAnnotationWithoutDotFixer::class => null, + PhpdocSummaryFixer::class => null, + PhpdocAlignFixer::class => null, + NativeConstantInvocationFixer::class => null, + NativeFunctionInvocationFixer::class => null, + UnaryOperatorSpacesFixer::class => null, + ArrayOpenerAndCloserNewlineFixer::class => null, + ArrayListItemNewlineFixer::class => null, + ]) + ->withRules([ + StandaloneLineInMultilineArrayFixer::class, + BlankLineAfterStrictTypesFixer::class, + RemoveUselessDefaultCommentFixer::class, + PhpUnitMethodCasingFixer::class, + FinalInternalClassFixer::class, + MbStrFunctionsFixer::class, + LowercaseCastFixer::class, + ShortScalarCastFixer::class, + BlankLineAfterOpeningTagFixer::class, + NoLeadingImportSlashFixer::class, + NewWithBracesFixer::class, + NoBlankLinesAfterClassOpeningFixer::class, + TernaryOperatorSpacesFixer::class, + ReturnTypeDeclarationFixer::class, + NoTrailingWhitespaceFixer::class, + NoSinglelineWhitespaceBeforeSemicolonsFixer::class, + NoWhitespaceBeforeCommaInArrayFixer::class, + WhitespaceAfterCommaInArrayFixer::class, + FullyQualifiedStrictTypesFixer::class, + ]) + ->withConfiguredRule(PhpdocToReturnTypeFixer::class, ['union_types' => false]) + ->withConfiguredRule(NoSuperfluousPhpdocTagsFixer::class, ['remove_inheritdoc' => false]) + ->withConfiguredRule( + ConcatSpaceFixer::class, + ['spacing' => 'one'] + ) + ->withConfiguredRule( + OrderedImportsFixer::class, + [ + 'imports_order' => ['class', 'const', 'function'], + ] + ) + ->withConfiguredRule( + DeclareEqualNormalizeFixer::class, + ['space' => 'none'] + ) + ->withConfiguredRule( + BracesFixer::class, + [ + 'allow_single_line_closure' => false, + 'position_after_functions_and_oop_constructs' => 'next', + 'position_after_control_structures' => 'same', + 'position_after_anonymous_constructs' => 'same', + ] + ) + ->withConfiguredRule( + VisibilityRequiredFixer::class, + [ + 'elements' => ['const', 'method', 'property'], + ] + ) + ->withConfiguredRule( + PhpdocLineSpanFixer::class, + ['property' => 'single'] + ) + ->withConfiguredRule( + ClassAttributesSeparationFixer::class, + ['elements' => ['property' => 'none', 'method' => 'one', 'const' => 'none']] + ); diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..e5f9e2a --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,8 @@ +parameters: + level: 8 + + paths: + - src + + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: true diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..5664b3b --- /dev/null +++ b/rector.php @@ -0,0 +1,26 @@ +withCache('./var/cache/rector', FileCacheStorage::class) + ->withPaths(['./src']) + ->withImportNames() + ->withParallel(timeoutSeconds: 180, jobSize: 10) + ->withPhpSets() + ->withPreparedSets( + typeDeclarations: true, + symfonyCodeQuality: true, + ) + ->withComposerBased( + twig: true, + doctrine: true, + phpunit: true, + symfony: true, + ) + ->withSkip([ + Rector\Symfony\CodeQuality\Rector\Class_\InlineClassRoutePrefixRector::class + ]); diff --git a/src/ArticleConfig.php b/src/ArticleConfig.php index 713e895..40788ec 100644 --- a/src/ArticleConfig.php +++ b/src/ArticleConfig.php @@ -8,8 +8,10 @@ use Bolt\Entity\Content; use Bolt\Extension\ExtensionRegistry; use Bolt\Storage\Query; +use Pagerfanta\PagerfantaInterface; +use RuntimeException; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -18,51 +20,26 @@ class ArticleConfig { private const CACHE_DURATION = 1800; // 30 minutes - /** @var ExtensionRegistry */ - private $registry; + /** @var array|string>> */ + private ?array $config = null; - /** @var UrlGeneratorInterface */ - private $urlGenerator; - - /** @var CsrfTokenManagerInterface */ - private $csrfTokenManager; - - /** @var Config */ - private $boltConfig; - - /** @var Query */ - private $query; - - /** @var array */ - private $config = null; - - /** @var array */ - private $plugins = null; - - /** @var CacheInterface */ - private $cache; - - /** @var Security */ - private $security; + /** @var array */ + private ?array $plugins = null; public function __construct( - ExtensionRegistry $registry, - UrlGeneratorInterface $urlGenerator, - CsrfTokenManagerInterface $csrfTokenManager, - Config $boltConfig, - Query $query, - CacheInterface $cache, - Security $security + private readonly ExtensionRegistry $registry, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly CsrfTokenManagerInterface $csrfTokenManager, + private readonly Config $boltConfig, + private readonly Query $query, + private readonly CacheInterface $cache, + private readonly Security $security ) { - $this->registry = $registry; - $this->urlGenerator = $urlGenerator; - $this->csrfTokenManager = $csrfTokenManager; - $this->boltConfig = $boltConfig; - $this->query = $query; - $this->cache = $cache; - $this->security = $security; } + /** + * @phpstan-ignore missingType.iterableValue (complex type) + */ public function getConfig(): array { if ($this->config) { @@ -76,6 +53,9 @@ public function getConfig(): array return $this->config; } + /** + * @phpstan-ignore missingType.iterableValue (complex type) + */ public function getPlugins(): array { if ($this->plugins) { @@ -93,11 +73,9 @@ public function getPlugins(): array return $this->plugins; } - private function getExtension() - { - return $this->extension = $this->registry->getExtension(Extension::class); - } - + /** + * @phpstan-ignore missingType.iterableValue (complex type) + */ private function getDefaults(): array { $defaults = [ @@ -147,6 +125,9 @@ private function getDefaults(): array return $defaults; } + /** + * @return array + */ private function getDefaultPlugins(): array { return [ @@ -178,15 +159,21 @@ private function getDefaultPlugins(): array ]; } + /** + * @phpstan-ignore missingType.iterableValue (complex type) + */ private function getLinks(): array { - return $this->cache->get('editor_insert_links', function (ItemInterface $item) { + return $this->cache->get('editor_insert_links', function (ItemInterface $item): array { $item->expiresAfter(self::CACHE_DURATION); return $this->getLinksHelper(); }); } + /** + * @phpstan-ignore missingType.iterableValue (complex type) + */ private function getLinksHelper(): array { $amount = 100; @@ -197,7 +184,11 @@ private function getLinksHelper(): array ]; $contentTypes = $this->boltConfig->get('contenttypes')->where('viewless', false)->keys()->implode(','); - $records = $this->query->getContentForTwig($contentTypes, $params)->setMaxPerPage($amount); + /** @var Content[]|PagerfantaInterface $records */ + $records = $this->query->getContentForTwig($contentTypes, $params) ?? []; + if ($records instanceof PagerfantaInterface) { + $records->setMaxPerPage($amount); + } $links = [ '___' => [ @@ -206,7 +197,6 @@ private function getLinksHelper(): array ], ]; - /** @var Content $record */ foreach ($records as $record) { $extras = $record->getExtras(); @@ -224,4 +214,12 @@ private function getLinksHelper(): array ], ]; } + + private function getExtension(): Extension + { + /** @var Extension|null $extension */ + $extension = $this->registry->getExtension(Extension::class); + + return $extension ?? throw new RuntimeException('Redactor extension not registered'); + } } diff --git a/src/ArticleInjectorWidget.php b/src/ArticleInjectorWidget.php index c7d1fd2..aeb87f7 100644 --- a/src/ArticleInjectorWidget.php +++ b/src/ArticleInjectorWidget.php @@ -8,6 +8,7 @@ use Bolt\Widget\Injector\RequestZone; use Bolt\Widget\Injector\Target; use Bolt\Widget\TwigAwareInterface; +use Symfony\Component\HttpFoundation\Request; class ArticleInjectorWidget extends BaseWidget implements TwigAwareInterface { @@ -21,12 +22,15 @@ public function __construct() { } + /** + * @phpstan-ignore missingType.iterableValue (not used here) + */ public function run(array $params = []): ?string { $request = $this->getExtension()->getRequest(); // Only produce output when editing or creating a Record, with GET method. if (! in_array($request->get('_route'), ['bolt_content_edit', 'bolt_content_new', 'bolt_content_duplicate'], true) || - ($this->getExtension()->getRequest()->getMethod() !== 'GET')) { + ($this->getExtension()->getRequest()->getMethod() !== Request::METHOD_GET)) { return null; } diff --git a/src/Controller/Images.php b/src/Controller/Images.php index 574582b..e95587c 100644 --- a/src/Controller/Images.php +++ b/src/Controller/Images.php @@ -10,62 +10,44 @@ use Bolt\Controller\CsrfTrait; use Bolt\Twig\TextExtension; use Bolt\Utils\ThumbnailHelper; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; +use Illuminate\Support\Collection; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Tightenco\Collect\Support\Collection; +use Symfony\Component\Security\Http\Attribute\IsGranted; -/** - * @Security("is_granted('list_files:files')") - */ +#[IsGranted('list_files:files')] class Images implements AsyncZoneInterface { use CsrfTrait; - /** @var Config */ - private $config; - - /** @var Request */ - private $request; - - /** @var ThumbnailHelper */ - private $thumbnailHelper; - - /** @var ArticleConfig */ - private $articleConfig; - - public function __construct(Config $config, CsrfTokenManagerInterface $csrfTokenManager, RequestStack $requestStack, UrlGeneratorInterface $urlGenerator, ThumbnailHelper $thumbnailHelper, ArticleConfig $articleConfig) - { - $this->config = $config; + public function __construct( + private readonly Config $config, + private readonly ThumbnailHelper $thumbnailHelper, + private readonly ArticleConfig $articleConfig, + CsrfTokenManagerInterface $csrfTokenManager, + ) { $this->csrfTokenManager = $csrfTokenManager; - $this->request = $requestStack->getCurrentRequest(); - $this->thumbnailHelper = $thumbnailHelper; - $this->articleConfig = $articleConfig; } - /** - * @Route("/article_images", name="bolt_article_images", methods={"GET"}) - */ + #[Route('/article_images', name: 'bolt_article_images', methods: [Request::METHOD_GET])] public function getImagesList(Request $request): JsonResponse { try { $this->validateCsrf('bolt_article'); - } catch (InvalidCsrfTokenException $e) { + } catch (InvalidCsrfTokenException) { return new JsonResponse([ 'error' => true, 'message' => 'Invalid CSRF token', ], Response::HTTP_FORBIDDEN); } - $locationName = $this->request->query->get('location', 'files'); - $type = $this->request->query->get('type', ''); + $locationName = $request->query->getString('location', 'files'); + $type = $request->query->getString('type'); $path = $this->config->getPath($locationName, true); @@ -74,6 +56,12 @@ public function getImagesList(Request $request): JsonResponse return new JsonResponse($files); } + /** + * @return Collection + */ private function getImageFilesIndex(string $path, string $type): Collection { $glob = '*.{' . implode(',', self::getImageTypes()) . '}'; @@ -90,22 +78,20 @@ private function getImageFilesIndex(string $path, string $type): Collection return new Collection($files); } - /** - * @Route("/article_files", name="bolt_article_files", methods={"GET"}) - */ + #[Route('/article_files', name: 'bolt_article_files', methods: [Request::METHOD_GET])] public function getFilesList(Request $request): JsonResponse { try { $this->validateCsrf('bolt_article'); - } catch (InvalidCsrfTokenException $e) { + } catch (InvalidCsrfTokenException) { return new JsonResponse([ 'error' => true, 'message' => 'Invalid CSRF token', ], Response::HTTP_FORBIDDEN); } - $locationName = $this->request->query->get('location', 'files'); - $type = $this->request->query->get('type', ''); + $locationName = $request->query->getString('location', 'files'); + $type = $request->query->getString('type'); $path = $this->config->getPath($locationName, true); @@ -114,6 +100,13 @@ public function getFilesList(Request $request): JsonResponse return new JsonResponse($files); } + /** + * @return Collection + */ private function getFilesIndex(string $path, string $type): Collection { $fileTypes = $this->config->getFileTypes()->toArray(); @@ -145,7 +138,10 @@ private function findFiles(string $path, ?string $glob = null): Finder return $finder; } - + + /** + * @return string[] + */ private static function getImageTypes(): array { return ['gif', 'png', 'jpg', 'jpeg', 'svg', 'avif', 'webp']; diff --git a/src/Controller/Upload.php b/src/Controller/Upload.php index 877a23f..971e1c3 100644 --- a/src/Controller/Upload.php +++ b/src/Controller/Upload.php @@ -10,57 +10,39 @@ use Bolt\Controller\CsrfTrait; use Bolt\Twig\TextExtension; use Cocur\Slugify\Slugify; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sirius\Upload\Handler; use Sirius\Upload\Result\File; +use Symfony\Component\Filesystem\Path; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Filesystem\Path; +use Symfony\Component\Security\Http\Attribute\IsGranted; +use Throwable; -/** - * @Security("is_granted('upload')") - */ +#[IsGranted('upload')] class Upload implements AsyncZoneInterface { use CsrfTrait; - /** @var Config */ - private $config; - - /** @var TextExtension */ - private $textExtension; - - /** @var Request */ - private $request; - - /** @var ArticleConfig */ - private $articleConfig; - - public function __construct(Config $config, CsrfTokenManagerInterface $csrfTokenManager, TextExtension $textExtension, RequestStack $requestStack, ArticleConfig $articleConfig) - { - $this->config = $config; + public function __construct( + private readonly Config $config, + private readonly TextExtension $textExtension, + private readonly ArticleConfig $articleConfig, + CsrfTokenManagerInterface $csrfTokenManager, + ) { $this->csrfTokenManager = $csrfTokenManager; - $this->textExtension = $textExtension; - $this->request = $requestStack->getCurrentRequest(); - $this->articleConfig = $articleConfig; } - /** - * @Route("/bolt_article_image_upload", name="bolt_article_image_upload", methods={"POST"}) - */ + #[Route('/bolt_article_image_upload', name: 'bolt_article_image_upload', methods: [Request::METHOD_POST])] public function handleImageUpload(Request $request): JsonResponse { return $this->handleUpload($request, 'image'); } - /** - * @Route("/bolt_article_file_upload", name="bolt_article_file_upload", methods={"POST"}) - */ + #[Route('/bolt_article_file_upload', name: 'bolt_article_file_upload', methods: [Request::METHOD_POST])] public function handleFileUpload(Request $request): JsonResponse { return $this->handleUpload($request, 'file'); @@ -70,15 +52,15 @@ private function handleUpload(Request $request, string $type = 'image'): JsonRes { try { $this->validateCsrf('bolt_article'); - } catch (InvalidCsrfTokenException $e) { + } catch (InvalidCsrfTokenException) { return new JsonResponse([ 'error' => true, 'message' => 'Invalid CSRF token', ], Response::HTTP_FORBIDDEN); } - $locationName = $this->request->query->get('location', ''); - $path = $this->request->query->get('path', ''); + $locationName = $request->query->getString('location'); + $path = $request->query->getString('path'); $target = $this->config->getPath($locationName, true, $path); @@ -112,9 +94,7 @@ private function handleUpload(Request $request, string $type = 'image'): JsonRes 'Upload file' ); - $uploadHandler->setSanitizerCallback(function ($name) { - return $this->sanitiseFilename($name); - }); + $uploadHandler->setSanitizerCallback($this->sanitiseFilename(...)); try { /** @var File $result */ @@ -124,7 +104,7 @@ private function handleUpload(Request $request, string $type = 'image'): JsonRes // later on, should we do a `Request::createFromGlobals();` // @see: https://github.com/bolt/core/issues/2027 $_FILES = []; - } catch (\Throwable $e) { + } catch (Throwable) { return new JsonResponse([ 'error' => true, 'message' => 'Ensure the upload does NOT exceed the maximum filesize of ' . $this->textExtension->formatBytes($maxSize) . ', and that the destination folder (on the webserver) is writable.', diff --git a/src/Entity/ArticleField.php b/src/Entity/ArticleField.php index 23a19da..7c46cdb 100644 --- a/src/Entity/ArticleField.php +++ b/src/Entity/ArticleField.php @@ -10,9 +10,7 @@ use Doctrine\ORM\Mapping as ORM; use Twig\Markup; -/** - * @ORM\Entity - */ +#[ORM\Entity] class ArticleField extends Field implements Excerptable, FieldInterface { public const TYPE = 'article'; @@ -20,7 +18,7 @@ class ArticleField extends Field implements Excerptable, FieldInterface /** * Override getTwigValue to render field as html */ - public function getTwigValue() + public function getTwigValue(): Markup { $value = parent::getTwigValue(); diff --git a/src/Extension.php b/src/Extension.php index 7f57841..65236a2 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -22,7 +22,9 @@ public function initialize(): void public function install(): void { + /** @var string $projectDir */ $projectDir = $this->getContainer()->getParameter('kernel.project_dir'); + /** @var string $public */ $public = $this->getContainer()->getParameter('bolt.public_folder'); $source = dirname(__DIR__) . '/assets/'; diff --git a/src/TwigExtension.php b/src/TwigExtension.php index 0a9ffd3..67dc7c3 100644 --- a/src/TwigExtension.php +++ b/src/TwigExtension.php @@ -11,12 +11,9 @@ class TwigExtension extends AbstractExtension { - /** @var ArticleConfig */ - private $articleConfig; - - public function __construct(ArticleConfig $articleConfig) - { - $this->articleConfig = $articleConfig; + public function __construct( + private readonly ArticleConfig $articleConfig + ) { } public function getFunctions(): array @@ -26,8 +23,8 @@ public function getFunctions(): array ]; return [ - new TwigFunction('article_settings', [$this, 'articleSettings'], $safe), - new TwigFunction('article_includes', [$this, 'articleIncludes'], $safe), + new TwigFunction('article_settings', $this->articleSettings(...), $safe), + new TwigFunction('article_includes', $this->articleIncludes(...), $safe), ]; }