diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9da67dbe3..000000000 --- a/.babelrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "modules": false - } - ] - ] -} diff --git a/.eslintrc.json b/.eslintrc.json index 3718a2775..7adfed8f3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,9 @@ { "extends": "airbnb-base", "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false + }, "env": { "browser": true, "es6": true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..998c8abc4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Migrated rules from dependabot.yml +composer.* @Kdecherf @j0k3r @yguedidi diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 61d1b5e76..ecaf623ce 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -24,7 +24,7 @@ If you want to test using an other database than SQLite, uncomment the `postgres ### Using your own PHP server -- Ensure you are running PHP >= 7.4. +- Ensure you are running PHP >= 8.2. - Clone the repository - Launch `composer install` - If you got some errors, fix them (they might be related to some missing PHP extension from your machine) @@ -55,3 +55,7 @@ To run the tests locally run `make test`. To run the PHP formatter run `make fix-cs`. To run the PHPStan static analysis run `make phpstan`. + +To run the JS linter run `make lint-js`. + +To run the SCSS linter run `make lint-scss`. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d36956b95..4a95c66a2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,13 +15,13 @@ updates: patterns: - "*fontsource*" ignore: - - dependency-name: materialize-css + - dependency-name: "@materializecss/materialize" versions: - - "> 0.98.2" + - "> 1.2.2" - package-ecosystem: composer directory: "/" schedule: - interval: daily + interval: weekly time: "04:00" timezone: Europe/Paris open-pull-requests-limit: 10 @@ -35,18 +35,9 @@ updates: phpstan-dependencies: patterns: - "phpstan/*" - reviewers: - - j0k3r - - yguedidi - - Kdecherf ignore: - - dependency-name: lcobucci/jwt - versions: - - ">= 4.2.0" - # until we add support for Symfony 5+ - dependency-name: symfony/* - versions: - - ">= 5.0.0" + update-types: [ "version-update:semver-major" ] - package-ecosystem: github-actions directory: "/" schedule: diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 9b8b6b0fb..adc22452e 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -13,23 +13,29 @@ permissions: jobs: coding-standards: name: "CS Fixer, PHPStan & TwigCS" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "7.4" + php-version: "8.2" tools: cs2pr, pecl extensions: pdo, pdo_mysql, pdo_sqlite, pdo_pgsql, curl, imagick, pgsql, gd, tidy ini-values: "date.timezone=Europe/Paris" env: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Install Node" + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: 'yarn' + - name: "Setup MySQL" run: | sudo systemctl start mysql.service @@ -37,24 +43,48 @@ jobs: cp app/config/tests/parameters_test.mysql.yml app/config/parameters_test.yml - name: "Install dependencies with Composer" + id: composer-install uses: "ramsey/composer-install@v3" with: composer-options: "--optimize-autoloader --prefer-dist" + - name: "Install dependencies with Yarn" + id: yarn-install + run: yarn install + + - name: "Run Composer validate" + if: always() && steps.composer-install.outcome == 'success' + run: "composer validate" + - name: "Run Composer dependency analyser" + if: always() && steps.composer-install.outcome == 'success' run: "bin/composer-dependency-analyser" - name: "Run PHP CS Fixer" + if: always() && steps.composer-install.outcome == 'success' run: "bin/php-cs-fixer fix --verbose --dry-run --format=checkstyle | cs2pr" - name: "Generate test cache for PHPStan" + id: test-cache + if: always() && steps.composer-install.outcome == 'success' run: "php bin/console cache:clear --env=test" - name: "Run PHPStan" + if: always() && steps.test-cache.outcome == 'success' run: "php bin/phpstan analyse --no-progress --error-format=checkstyle | cs2pr" - name: "Run TwigCS" + if: always() && steps.composer-install.outcome == 'success' run: "php bin/twigcs --severity=error --display=blocking --reporter checkstyle app/ src/ | cs2pr" - name: "Run ergebnis/composer-normalize" + if: always() && steps.composer-install.outcome == 'success' run: "composer normalize --dry-run --no-check-lock" + + - name: "Run ESLint" + if: always() && steps.yarn-install.outcome == 'success' + run: "yarn lint:js" + + - name: "Run Stylelint" + if: always() && steps.yarn-install.outcome == 'success' + run: "yarn lint:scss" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b2aa900bd..bde24c3b9 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ env: jobs: phpunit: name: "PHP ${{ matrix.php }} using ${{ matrix.database }}" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest services: rabbitmq: image: rabbitmq:3-alpine @@ -31,9 +31,6 @@ jobs: fail-fast: false matrix: php: - - "7.4" - - "8.0" - - "8.1" - "8.2" - "8.3" - "8.4" @@ -44,7 +41,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" with: fetch-depth: 2 @@ -57,6 +54,12 @@ jobs: extensions: json, pdo, pdo_mysql, pdo_sqlite, pdo_pgsql, curl, imagick, pgsql, gd, tidy ini-values: "date.timezone=Europe/Paris" + - name: "Install Node" + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: 'yarn' + - name: "Setup MySQL" if: "${{ matrix.database == 'mysql' }}" run: | @@ -76,6 +79,12 @@ jobs: with: composer-options: "--optimize-autoloader --prefer-dist" + - name: "Install dependencies with Yarn" + run: yarn install + + - name: "Build assets with Yarn" + run: yarn build:dev + - name: "Prepare database configuration" run: cp app/config/tests/parameters_test.${{ matrix.database }}.yml app/config/parameters_test.yml @@ -84,7 +93,7 @@ jobs: phpunit_no_prefix: name: "PHP ${{ matrix.php }} using ${{ matrix.database }} without prefix" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest services: rabbitmq: image: rabbitmq:3-alpine @@ -107,7 +116,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" with: fetch-depth: 2 @@ -120,6 +129,12 @@ jobs: extensions: json, pdo, pdo_mysql, pdo_sqlite, pdo_pgsql, curl, imagick, pgsql, gd, tidy ini-values: "date.timezone=Europe/Paris" + - name: "Install Node" + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: 'yarn' + - name: "Remove database prefix" run: | pip install --user yq @@ -144,6 +159,12 @@ jobs: with: composer-options: "--optimize-autoloader --prefer-dist" + - name: "Install dependencies with Yarn" + run: yarn install + + - name: "Build assets with Yarn" + run: yarn build:dev + - name: "Prepare database configuration" run: cp app/config/tests/parameters_test.${{ matrix.database }}.yml app/config/parameters_test.yml @@ -152,7 +173,7 @@ jobs: phpunit-without-rmq-redis: name: "PHP ${{ matrix.php }} using ${{ matrix.database }} without Rabbit & Redis" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: fail-fast: false @@ -166,7 +187,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" with: fetch-depth: 2 @@ -179,6 +200,12 @@ jobs: extensions: json, pdo, pdo_mysql, pdo_sqlite, pdo_pgsql, curl, imagick, pgsql, gd, tidy ini-values: "date.timezone=Europe/Paris" + - name: "Install Node" + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: 'yarn' + - name: "Setup MySQL" if: "${{ matrix.database == 'mysql' }}" run: | @@ -198,6 +225,12 @@ jobs: with: composer-options: "--optimize-autoloader --prefer-dist" + - name: "Install dependencies with Yarn" + run: yarn install + + - name: "Build assets with Yarn" + run: yarn build:dev + - name: "Prepare database configuration" run: cp app/config/tests/parameters_test.${{ matrix.database }}.yml app/config/parameters_test.yml diff --git a/.github/workflows/dependabot-automerge-js.yml b/.github/workflows/dependabot-automerge-js.yml index bdba0bc88..eedbac9c3 100644 --- a/.github/workflows/dependabot-automerge-js.yml +++ b/.github/workflows/dependabot-automerge-js.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.2.0 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: '${{ secrets.GITHUB_TOKEN }}' - name: Approve and merge minor updates diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index a9d9201df..829fc6344 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -13,16 +13,16 @@ permissions: jobs: translations: name: "Translations" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: matrix: php: - - "7.4" + - "8.2" steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/upload-release-package.yml b/.github/workflows/upload-release-package.yml index 0fc41360e..9f4b5c428 100644 --- a/.github/workflows/upload-release-package.yml +++ b/.github/workflows/upload-release-package.yml @@ -12,11 +12,11 @@ jobs: strategy: matrix: php: - - "7.4" + - "8.2" steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.gitignore b/.gitignore index 8cd6a2922..b8f1c6f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ web/uploads/ !/web/bundles/.gitkeep /web/assets/images/* !web/assets/images/.gitkeep -/web/wallassets/* +/web/build/* # Build /app/build @@ -60,3 +60,5 @@ specialexport.json web/custom.css .env.local + +yarn-error.log diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 73bc89296..e9e8e70f1 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -37,17 +37,6 @@ return $config // 'psr_autoloading' => true, 'strict_comparison' => true, 'strict_param' => true, - // We override next rule because of current @Symfony ruleSet - // 'parameters' element is breaking PHP 7.4 - // https://cs.symfony.com/doc/rules/control_structure/trailing_comma_in_multiline.html - // TODO: remove this configuration after dropping support of PHP 7.4 - 'trailing_comma_in_multiline' => [ - 'elements' => [ - 'arrays', - 'array_destructuring', - 'match', - ], - ], 'concat_space' => [ 'spacing' => 'one', ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 67aac594d..295b1b04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## Upcoming changes + +* **[BC BREAK]** Convert 403 errors to 404 errors by @yguedidi in https://github.com/wallabag/wallabag/pull/8075 +* `wallassets/` folder renamed to `build/` + +## [2.6.13](https://github.com/wallabag/wallabag/tree/2.6.13) +[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.12...2.6.13) + +### Improvements + +* Add support of Pocket CSV import by @kdecherf and @nicosomb in [https://github.com/wallabag/wallabag/pull/8240](https://github.com/wallabag/wallabag/pull/8240) +* Backport Pocket and Shaarli HTML imports from master by @nicosomb in [https://github.com/wallabag/wallabag/pull/8193](https://github.com/wallabag/wallabag/pull/8193) + +### Fixes + +* Avoid non-validated OTP to be enabled #8139 by @j0k3r in [https://github.com/wallabag/wallabag/pull/8139](https://github.com/wallabag/wallabag/pull/8139) + +### Technical stuff + +* Update j0k3r/php-readability:1.2.13 to fix regression (about latin1 instead of UTF-8 used for entries) by @nicosomb [https://github.com/wallabag/wallabag/pull/8194](https://github.com/wallabag/wallabag/pull/8194) + +## [2.6.12](https://github.com/wallabag/wallabag/tree/2.6.12) +[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.11...2.6.12) + +### Technical stuff + +* Fix changelog by @yguedidi in [https://github.com/wallabag/wallabag/pull/8135](https://github.com/wallabag/wallabag/pull/8135) +* Update dependencies by @yguedidi in [https://github.com/wallabag/wallabag/pull/8136](https://github.com/wallabag/wallabag/pull/8136) + +## [2.6.11](https://github.com/wallabag/wallabag/tree/2.6.11) +[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.10...2.6.11) + +### Security fix +* Protect actions with a CSRF token by @yguedidi in https://github.com/wallabag/wallabag/commit/99c8a06594d6ee7480ce4d041ccff3025b353656 + +### Fixes + +* Fix redirection after action in search results by @nicosomb in [https://github.com/wallabag/wallabag/pull/7827](https://github.com/wallabag/wallabag/pull/7827) +* Fix title tag filter by @nicosomb in [https://github.com/wallabag/wallabag/pull/7846](https://github.com/wallabag/wallabag/pull/7846) +* Change NB_ELEMENTS in pocket importer to 30 by @j0k3r in [https://github.com/wallabag/wallabag/pull/7993](https://github.com/wallabag/wallabag/pull/7993) +* Fix entries counter for annotated entries in the menu by @j0k3r in [https://github.com/wallabag/wallabag/pull/7999](https://github.com/wallabag/wallabag/pull/7999) + +### Technical stuff + +* Prepare 2.6.11 release by @yguedidi in [https://github.com/wallabag/wallabag/pull/8133](https://github.com/wallabag/wallabag/pull/8133) + ## [2.6.10](https://github.com/wallabag/wallabag/tree/2.6.10) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.9...2.6.10) diff --git a/GNUmakefile b/GNUmakefile index 2e7f1b43b..899c0df56 100755 --- a/GNUmakefile +++ b/GNUmakefile @@ -52,6 +52,15 @@ fix-cs: ## Run PHP-CS-Fixer phpstan: ## Run PHPStan @$(PHP_NO_XDEBUG) bin/phpstan analyse +phpstan-baseline: ## Generate PHPStan baseline + @$(PHP_NO_XDEBUG) bin/phpstan analyse --generate-baseline + +lint-js: ## Run ESLint + @$(YARN) lint:js + +lint-scss: ## Run Stylelint + @$(YARN) lint:scss + release: ## Create a package. Need a VERSION parameter (eg: `make release VERSION=master`). ifndef VERSION $(error VERSION is not set) diff --git a/README.md b/README.md index e3ec9b306..df20061ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # wallabag -![CI](https://github.com/wallabag/wallabag/workflows/CI/badge.svg) +[![CI](https://github.com/wallabag/wallabag/actions/workflows/continuous-integration.yml/badge.svg?branch=master)](https://github.com/wallabag/wallabag/actions/workflows/continuous-integration.yml?query=branch%3Amaster) [![Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#wallabag:matrix.org) [![Donation Status](https://img.shields.io/liberapay/goal/wallabag.svg?logo=liberapay)](https://liberapay.com/wallabag/donate) [![Translation status](https://hosted.weblate.org/widgets/wallabag/-/svg-badge.svg)](https://hosted.weblate.org/engage/wallabag/?utm_source=widget) diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index c84d753a3..d060294bb 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -25,13 +25,13 @@ During this documentation, we assume the release is `$LAST_WALLABAG_RELEASE` (li ### Target PHP version `composer.lock` is _always_ built for a particular version, by default the one it is generated (with `composer update`). -If the PHP version used to generate the .lock isn't a widely available one (like PHP 8), a more common one should +If the PHP version used to generate the .lock isn't a widely available one (like latest PHP versions), a more common one should be locally specified in `composer.lock`: ```json "config": { "platform": { - "php": "7.4.29", + "php": "8.2.27", "ext-something": "4.0" } } diff --git a/app/AppKernel.php b/app/AppKernel.php index 7096ff3d9..0b0cf7455 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -1,8 +1,39 @@ getEnvironment(), ['dev', 'test'], true)) { - $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); - $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); - $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); + $bundles[] = new DebugBundle(); + $bundles[] = new WebProfilerBundle(); + $bundles[] = new DoctrineFixturesBundle(); if ('test' === $this->getEnvironment()) { - $bundles[] = new DAMA\DoctrineTestBundle\DAMADoctrineTestBundle(); + $bundles[] = new DAMADoctrineTestBundle(); } if ('dev' === $this->getEnvironment()) { - $bundles[] = new Symfony\Bundle\MakerBundle\MakerBundle(); - $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); + $bundles[] = new MakerBundle(); + $bundles[] = new WebServerBundle(); } } @@ -69,30 +100,16 @@ class AppKernel extends Kernel { $loader->load($this->getProjectDir() . '/app/config/config_' . $this->getEnvironment() . '.yml'); - $loader->load(function ($container) { - if ($container->getParameter('use_webpack_dev_server')) { - $container->loadFromExtension('framework', [ - 'assets' => [ - 'base_url' => 'http://localhost:8080/', - ], - ]); - } else { - $container->loadFromExtension('framework', [ - 'assets' => [ - 'base_url' => $container->getParameter('domain_name'), - ], - ]); - } - }); - - $loader->load(function (ContainerBuilder $container) { + $loader->load(function (ContainerBuilder $container): void { // $container->setParameter('container.autowiring.strict_mode', true); // $container->setParameter('container.dumper.inline_class_loader', true); $container->addObjectResource($this); }); - $loader->load(function (ContainerBuilder $container) { + $loader->load(function (ContainerBuilder $container): void { $this->processDatabaseParameters($container); + $this->defineRedisUrlEnvVar($container); + $this->defineRabbitMqUrlEnvVar($container); }); } @@ -103,19 +120,12 @@ class AppKernel extends Kernel private function processDatabaseParameters(ContainerBuilder $container) { - switch ($container->getParameter('database_driver')) { - case 'pdo_mysql': - $scheme = 'mysql'; - break; - case 'pdo_pgsql': - $scheme = 'pgsql'; - break; - case 'pdo_sqlite': - $scheme = 'sqlite'; - break; - default: - throw new RuntimeException('Unsupported database driver: ' . $container->getParameter('database_driver')); - } + $scheme = match ($container->getParameter('database_driver')) { + 'pdo_mysql' => 'mysql', + 'pdo_pgsql' => 'pgsql', + 'pdo_sqlite' => 'sqlite', + default => throw new RuntimeException('Unsupported database driver: ' . $container->getParameter('database_driver')), + }; $container->setParameter('database_scheme', $scheme); @@ -128,4 +138,45 @@ class AppKernel extends Kernel $container->setParameter('database_port', (string) $container->getParameter('database_port')); $container->setParameter('database_socket', (string) $container->getParameter('database_socket')); } + + private function defineRedisUrlEnvVar(ContainerBuilder $container) + { + $scheme = $container->getParameter('redis_scheme'); + $host = $container->getParameter('redis_host'); + $port = $container->getParameter('redis_port'); + $path = $container->getParameter('redis_path'); + $password = $container->getParameter('redis_password'); + + $url = $scheme . '://'; + + if ($password) { + $url .= $password . '@'; + } + + $url .= $host; + + if ($port) { + $url .= ':' . $port; + } + + $url .= '/' . ltrim($path, '/'); + + $container->setParameter('env(REDIS_URL)', $url); + } + + private function defineRabbitMqUrlEnvVar(ContainerBuilder $container) + { + $host = $container->getParameter('rabbitmq_host'); + $port = $container->getParameter('rabbitmq_port'); + $user = $container->getParameter('rabbitmq_user'); + $password = $container->getParameter('rabbitmq_password'); + + $url = 'amqp://' . $user . ':' . $password . '@' . $host; + + if ($port) { + $url .= ':' . $port; + } + + $container->setParameter('env(RABBITMQ_URL)', $url); + } } diff --git a/app/config/config.yml b/app/config/config.yml index 96412a383..e170430b7 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -5,8 +5,6 @@ imports: - { resource: wallabag.yml } parameters: - # Allows to use the live reload feature for changes in assets - use_webpack_dev_server: false craue_config.cache_adapter.class: Craue\ConfigBundle\CacheAdapter\SymfonyCacheComponentAdapter env(DATABASE_URL): '%database_scheme%://%database_user%:%database_password%@%database_host%:%database_port%/%database_name%?unix_socket=%database_socket%&charset=%database_charset%' @@ -31,9 +29,13 @@ framework: handler_id: session.handler.native_file save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%" cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native fragments: ~ http_method_override: true - assets: ~ + assets: + base_url: '%domain_name%' + json_manifest_path: '%kernel.project_dir%/web/build/manifest.json' mailer: dsn: "%mailer_dsn%" http_client: @@ -47,6 +49,10 @@ framework: X-Accept: 'application/json' request_html_function.client: scope: '.*' + browser.client: + scope: '.*' + verify_host: false + verify_peer: false # Twig Configuration twig: @@ -56,6 +62,7 @@ twig: form_themes: - "@SpiriitFormFilter/Form/form_div_layout.html.twig" globals: + wallabag_url: '%domain_name%' registration_enabled: '%fosuser_registration%' # Doctrine Configuration @@ -72,7 +79,7 @@ doctrine: auto_mapping: true mappings: Wallabag: - type: annotation + type: attribute is_bundle: false dir: '%kernel.project_dir%/src/Entity' prefix: 'Wallabag\Entity' @@ -95,6 +102,8 @@ doctrine_migrations: version_column_name: 'version' version_column_length: 192 executed_at_column_name: 'executed_at' + services: + Doctrine\Migrations\Version\MigrationFactory: Wallabag\Doctrine\MigrationFactoryDecorator fos_rest: param_fetcher_listener: true @@ -190,7 +199,7 @@ fos_user: address: "%from_email%" sender_name: wallabag service: - mailer: Wallabag\Mailer\UserMailer + mailer: fos_user.mailer.twig_symfony fos_oauth_server: db_driver: orm @@ -225,19 +234,11 @@ scheb_two_factor: template: "Authentication/form.html.twig" mailer: Wallabag\Mailer\AuthCodeMailer -rulerz: - targets: - doctrine: true - old_sound_rabbit_mq: connections: default: - host: "%rabbitmq_host%" - port: "%rabbitmq_port%" - user: "%rabbitmq_user%" - password: "%rabbitmq_password%" - vhost: / - lazy: true + url: "%env(RABBITMQ_URL)%" + lazy: true producers: import_pocket: connection: default @@ -304,6 +305,11 @@ old_sound_rabbit_mq: exchange_options: name: 'wallabag.import.pocket_html' type: topic + import_pocket_csv: + connection: default + exchange_options: + name: 'wallabag.import.pocket_csv' + type: topic consumers: import_pocket: connection: default @@ -422,6 +428,15 @@ old_sound_rabbit_mq: name: 'wallabag.import.pocket_html' callback: wallabag.consumer.amqp.pocket_html qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} + import_pocket_csv: + connection: default + exchange_options: + name: 'wallabag.import.pocket_csv' + type: topic + queue_options: + name: 'wallabag.import.pocket_csv' + callback: wallabag.consumer.amqp.pocket_csv + qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} fos_js_routing: routes_to_expose: @@ -449,17 +464,6 @@ sensio_framework_extra: router: annotations: false -httplug: - clients: - wallabag: - factory: Wallabag\Helper\HttpClientFactory - config: - defaults: - timeout: 10 - plugins: ['httplug.plugin.logger'] - discovery: - client: false - # define custom entity so we can override length attribute to fix utf8mb4 issue craue_config: entity_name: Wallabag\Entity\InternalSetting @@ -467,3 +471,8 @@ craue_config: when@dev: maker: root_namespace: 'Wallabag' + +webpack_encore: + output_path: '%kernel.project_dir%/web/build' + script_attributes: + defer: true diff --git a/app/config/config_prod.yml b/app/config/config_prod.yml index 59d2e9e20..709af4551 100644 --- a/app/config/config_prod.yml +++ b/app/config/config_prod.yml @@ -1,10 +1,6 @@ imports: - { resource: config.yml } -framework: - assets: - # json_manifest_path: '%kernel.project_dir%/web/bundles/wallabagcore/manifest.json' - #doctrine: # orm: # metadata_cache_driver: apc diff --git a/app/config/config_test.yml b/app/config/config_test.yml index 3ad233351..ad4a1f641 100644 --- a/app/config/config_test.yml +++ b/app/config/config_test.yml @@ -10,7 +10,7 @@ parameters: framework: test: ~ session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file profiler: collect: false translator: diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index fd46dd933..e41091752 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -47,8 +47,6 @@ parameters: from_email: no-reply@wallabag.org - rss_limit: 50 - # RabbitMQ processing rabbitmq_host: localhost rabbitmq_port: 5672 @@ -57,7 +55,7 @@ parameters: rabbitmq_prefetch_count: 10 # Redis processing - redis_scheme: tcp + redis_scheme: redis redis_host: localhost redis_port: 6379 redis_path: null diff --git a/app/config/routing.yml b/app/config/routing.yml index 80f7b382f..f49561b5b 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -13,6 +13,7 @@ doc-api-json: homepage: path: "/{page}" + methods: GET defaults: _controller: 'Wallabag\Controller\EntryController::showUnreadAction' page : 1 @@ -27,23 +28,27 @@ fos_oauth_server_token: craue_config_settings_modify: path: /settings + methods: [GET, POST] defaults: _controller: 'Craue\ConfigBundle\Controller\SettingsController::modifyAction' fos_js_routing: - resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml" + resource: "@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xml" 2fa_login: path: /2fa + methods: GET defaults: _controller: "scheb_two_factor.form_controller:form" 2fa_login_check: path: /2fa_check + methods: POST # redirect RSS feed to Atom rss_to_atom_unread: path: /{username}/{token}/unread.xml + methods: GET defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' route: unread_feed @@ -51,6 +56,7 @@ rss_to_atom_unread: rss_to_atom_archive: path: /{username}/{token}/archive.xml + methods: GET defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' route: archive_feed @@ -58,6 +64,7 @@ rss_to_atom_archive: rss_to_atom_starred: path: /{username}/{token}/starred.xml + methods: GET defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' route: starred_feed @@ -65,6 +72,7 @@ rss_to_atom_starred: rss_to_atom_all: path: /{username}/{token}/all.xml + methods: GET defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' route: all_feed @@ -72,6 +80,7 @@ rss_to_atom_all: rss_to_atom_tags: path: /{username}/{token}/tags/{slug}.xml + methods: GET defaults: _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' route: tag_feed diff --git a/app/config/security.yml b/app/config/security.yml index b05e54b01..bced6bf20 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -1,5 +1,5 @@ security: - encoders: + password_hashers: FOS\UserBundle\Model\UserInterface: sha512 role_hierarchy: @@ -70,10 +70,9 @@ security: - { path: /(unread|starred|archive|annotated|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/locale, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /tags/(.*).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/feed, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/feed, roles: PUBLIC_ACCESS } - { path: /(unread|starred|archive|annotated).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility - { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/settings, roles: ROLE_SUPER_ADMIN } - - { path: ^/annotations, roles: ROLE_USER } - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } - { path: ^/, roles: ROLE_USER } diff --git a/app/config/services.yml b/app/config/services.yml index 34c1bd13c..620c71087 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -30,6 +30,7 @@ services: $storeArticleHeaders: '@=service(''craue_config'').get(''store_article_headers'')' $supportUrl: '@=service(''craue_config'').get(''wallabag_support_url'')' $fonts: '%wallabag.fonts%' + $defaultIgnoreOriginInstanceRules: '%wallabag.default_ignore_origin_instance_rules%' Wallabag\: resource: '../../src/*' @@ -107,6 +108,14 @@ services: $rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_html_producer' $redisProducer: '@wallabag.producer.redis.pocket_html' + Wallabag\Controller\Import\PocketCsvController: + arguments: + $rabbitMqProducer: '@old_sound_rabbit_mq.import_pocket_csv_producer' + $redisProducer: '@wallabag.producer.redis.pocket_csv' + + Wallabag\Doctrine\MigrationFactoryDecorator: + decorates: doctrine.migrations.migrations_factory + Doctrine\DBAL\Connection: alias: doctrine.dbal.default_connection @@ -187,11 +196,17 @@ services: tags: - { name: doctrine.event_subscriber } + psr18.wallabag.client: + class: Symfony\Component\HttpClient\Psr18Client + arguments: + $client: '@Wallabag\HttpClient\WallabagClient' + Graby\Graby: arguments: $config: error_message: '%wallabag.fetching_error_message%' error_message_title: '%wallabag.fetching_error_message_title%' + $client: '@psr18.wallabag.client' calls: - [ setLogger, [ "@logger" ] ] tags: @@ -201,9 +216,6 @@ services: arguments: $config: {} - wallabag.http_client: - alias: 'httplug.client.wallabag' - Wallabag\SiteConfig\GrabySiteConfigBuilder: tags: - { name: monolog.logger, channel: graby } @@ -212,11 +224,9 @@ services: Wallabag\SiteConfig\SiteConfigBuilder: alias: Wallabag\SiteConfig\GrabySiteConfigBuilder - GuzzleHttp\Cookie\CookieJar: ~ - - Wallabag\Helper\HttpClientFactory: - calls: - - ['addSubscriber', ['@Wallabag\Guzzle\AuthenticatorSubscriber']] + Symfony\Component\BrowserKit\HttpBrowser: + arguments: + $client: '@browser.client' RulerZ\RulerZ: alias: rulerz @@ -225,30 +235,17 @@ services: tags: - { name: rulerz.operator, target: native, operator: matches } - Wallabag\Operator\Doctrine\Matches: - tags: - - { name: rulerz.operator, target: doctrine, operator: matches, inline: true } - Wallabag\Operator\PHP\NotMatches: tags: - { name: rulerz.operator, target: native, operator: notmatches } - Wallabag\Operator\Doctrine\NotMatches: - tags: - - { name: rulerz.operator, target: doctrine, operator: notmatches, inline: true } - Wallabag\Operator\PHP\PatternMatches: tags: - { name: rulerz.operator, target: native, operator: "~" } Predis\Client: arguments: - $parameters: - scheme: '%redis_scheme%' - host: '%redis_host%' - port: '%redis_port%' - path: '%redis_path%' - password: '%redis_password%' + $parameters: '%env(REDIS_URL)%' Wallabag\Event\Subscriber\SQLiteCascadeDeleteSubscriber: tags: @@ -268,16 +265,6 @@ services: $defaultSettings: '%wallabag.default_internal_settings%' $defaultIgnoreOriginInstanceRules: '%wallabag.default_ignore_origin_instance_rules%' - Wallabag\Mailer\UserMailer: - arguments: - $parameters: - template: - confirmation: '%fos_user.registration.confirmation.template%' - resetting: '%fos_user.resetting.email.template%' - from_email: - confirmation: '%fos_user.registration.confirmation.from_email%' - resetting: '%fos_user.resetting.email.from_email%' - Wallabag\Event\Listener\CreateConfigListener: arguments: $itemsOnPage: "%wallabag.items_on_page%" @@ -346,6 +333,10 @@ services: tags: - { name: wallabag.import, alias: pocket_html } + Wallabag\Import\PocketCsvImport: + tags: + - { name: wallabag.import, alias: pocket_csv } + # to factorize the proximity and bypass translation for prev & next pagerfanta.view.default_wallabag: class: Pagerfanta\View\OptionableView diff --git a/app/config/services_rabbit.yml b/app/config/services_rabbit.yml index ff00fc8df..8c83b2123 100644 --- a/app/config/services_rabbit.yml +++ b/app/config/services_rabbit.yml @@ -20,6 +20,7 @@ services: $elcuratorConsumer: '@old_sound_rabbit_mq.import_elcurator_consumer' $shaarliConsumer: '@old_sound_rabbit_mq.import_shaarli_consumer' $pocketHtmlConsumer: '@old_sound_rabbit_mq.import_pocket_html_consumer' + $pocketCsvConsumer: '@old_sound_rabbit_mq.import_pocket_csv_consumer' $omnivoreConsumer: '@old_sound_rabbit_mq.import_omnivore_consumer' wallabag.consumer.amqp.pocket: @@ -86,3 +87,8 @@ services: class: Wallabag\Consumer\AMQPEntryConsumer arguments: $import: '@Wallabag\Import\PocketHtmlImport' + + wallabag.consumer.amqp.pocket_csv: + class: Wallabag\Consumer\AMQPEntryConsumer + arguments: + $import: '@Wallabag\Import\PocketCsvImport' diff --git a/app/config/services_redis.yml b/app/config/services_redis.yml index 2065fe4f5..af9b5ad85 100644 --- a/app/config/services_redis.yml +++ b/app/config/services_redis.yml @@ -212,3 +212,19 @@ services: class: Wallabag\Consumer\RedisEntryConsumer arguments: $import: '@Wallabag\Import\PocketHtmlImport' + + # pocket csv + wallabag.queue.redis.pocket_csv: + class: Simpleue\Queue\RedisQueue + arguments: + $queueName: "wallabag.import.pocket_csv" + + wallabag.producer.redis.pocket_csv: + class: Wallabag\Redis\Producer + arguments: + - "@wallabag.queue.redis.pocket_csv" + + wallabag.consumer.redis.pocket_csv: + class: Wallabag\Consumer\RedisEntryConsumer + arguments: + $import: '@Wallabag\Import\PocketCsvImport' diff --git a/app/config/services_test.yml b/app/config/services_test.yml index c850bf215..e2789b19c 100644 --- a/app/config/services_test.yml +++ b/app/config/services_test.yml @@ -6,6 +6,9 @@ services: # fixtures Wallabag\DataFixtures\: + bind: + $defaultIgnoreOriginInstanceRules: '%wallabag.default_ignore_origin_instance_rules%' + $defaultInternalSettings: '%wallabag.default_internal_settings%' resource: '../../fixtures/*' tags: ['doctrine.fixture.orm'] autowire: true diff --git a/app/config/wallabag.yml b/app/config/wallabag.yml index 3cc1b15c1..a3721f392 100644 --- a/app/config/wallabag.yml +++ b/app/config/wallabag.yml @@ -1,5 +1,5 @@ parameters: - wallabag.version: 2.6.10 + wallabag.version: 2.7.0-dev wallabag.paypal_url: "https://liberapay.com/wallabag/donate" wallabag.languages: en: 'English' @@ -18,6 +18,7 @@ parameters: pt: 'Português' ru: 'Русский' ja: '日本語' + ko: '한국어' zh: '简体中文' uk: 'Українська' hr: 'Hrvatski' @@ -175,5 +176,11 @@ parameters: - 'Montserrat' - 'OpenDyslexicRegular' - 'Oswald' - wallabag.allow_mimetypes: ['application/octet-stream', 'application/json', 'text/plain', 'text/csv', 'text/html'] + wallabag.allow_mimetypes: + - 'application/octet-stream' + - 'application/json' + - 'text/plain' + - 'text/csv' + - 'text/html' + - 'application/vnd.ms-excel' wallabag.resource_dir: "%kernel.project_dir%/web/uploads/import" diff --git a/app/config/webpack/common.js b/app/config/webpack/common.js deleted file mode 100644 index bd4f3845d..000000000 --- a/app/config/webpack/common.js +++ /dev/null @@ -1,37 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const StyleLintPlugin = require('stylelint-webpack-plugin'); - -const projectDir = path.resolve(__dirname, '../../../'); - -module.exports = { - entry: { - material: path.join(projectDir, './assets/material/index.js'), - public: path.join(projectDir, './assets/_global/share.js'), - }, - output: { - filename: '[name].js', - path: path.resolve(projectDir, 'web/wallassets'), - publicPath: '', - }, - plugins: [ - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - 'window.$': 'jquery', - 'window.jQuery': 'jquery', - }), - new StyleLintPlugin({ - configFile: 'stylelint.config.js', - failOnError: false, - quiet: false, - context: 'assets', - files: '**/*.scss', - }), - ], - resolve: { - alias: { - jquery: path.join(projectDir, 'node_modules/jquery/dist/jquery.js'), - }, - }, -}; diff --git a/app/config/webpack/dev.js b/app/config/webpack/dev.js deleted file mode 100644 index 1c614f1a0..000000000 --- a/app/config/webpack/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -const { merge } = require('webpack-merge'); -const ESLintPlugin = require('eslint-webpack-plugin'); -const commonConfig = require('./common'); - -module.exports = merge(commonConfig, { - devtool: 'eval-source-map', - output: { - filename: '[name].dev.js', - }, - mode: 'development', - devServer: { - static: { - directory: './web', - }, - }, - plugins: [ - new ESLintPlugin(), - ], - module: { - rules: [ - { - test: /\.js$/, - exclude: /(node_modules)/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'], - }, - }, - }, - { - test: /\.(s)?css$/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 1, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: ['autoprefixer'], - }, - }, - }, - 'sass-loader', - ], - }, - { - test: /\.(jpg|png|gif|svg|ico|eot|ttf|woff|woff2)$/, - type: 'asset/inline', - }, - ], - }, -}); diff --git a/app/config/webpack/prod.js b/app/config/webpack/prod.js deleted file mode 100644 index 021cfacda..000000000 --- a/app/config/webpack/prod.js +++ /dev/null @@ -1,98 +0,0 @@ -const webpack = require('webpack'); -const { merge } = require('webpack-merge'); -const ESLintPlugin = require('eslint-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); - -const commonConfig = require('./common'); - -module.exports = merge(commonConfig, { - output: { - filename: '[name].js', - }, - mode: 'production', - devtool: 'source-map', - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - parallel: true, - terserOptions: { - output: { - comments: false, - }, - }, - extractComments: false, - }), - ], - }, - plugins: [ - new ESLintPlugin(), - new MiniCssExtractPlugin(), - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: JSON.stringify('production'), - }, - }), - new WebpackManifestPlugin({ - fileName: 'manifest.json', - sort: (file1, file2) => file1.path.localeCompare(file2.path), - }), - ], - module: { - rules: [ - { - test: /\.js$/, - exclude: /(node_modules)/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'], - }, - }, - }, - { - test: /\.(sa|sc|c)ss$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - importLoaders: 1, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: ['autoprefixer'], - }, - }, - }, - 'sass-loader', - ], - }, - { - test: /\.(jpg|png|gif|svg|ico)$/, - include: /node_modules/, - type: 'asset/resource', - generator: { - filename: 'img/[name][ext]', - }, - }, - { - test: /\.(jpg|png|gif|svg|ico)$/, - exclude: /node_modules/, - type: 'asset/resource', - }, - { - test: /\.(eot|ttf|woff|woff2)$/, - type: 'asset/resource', - generator: { - filename: 'fonts/[name][ext]', - }, - }, - ], - }, -}); diff --git a/assets/_global/index.js b/assets/_global/index.js deleted file mode 100644 index 06664e939..000000000 --- a/assets/_global/index.js +++ /dev/null @@ -1,124 +0,0 @@ -/* jQuery */ -import $ from 'jquery'; - -/* Annotations */ -import annotator from 'annotator'; - -import ClipboardJS from 'clipboard'; -import 'mathjax/es5/tex-svg'; - -/* Fonts */ -import 'material-design-icons-iconfont/dist/material-design-icons.css'; -import 'lato-font/css/lato-font.css'; -import 'open-dyslexic/open-dyslexic-regular.css'; -import '@fontsource/atkinson-hyperlegible'; -import '@fontsource/eb-garamond'; -import '@fontsource/montserrat'; -import '@fontsource/oswald'; -import './global.scss'; - -/* Shortcuts */ -import './js/shortcuts/entry'; -import './js/shortcuts/main'; - -/* Highlight */ -import './js/highlight'; - -import { savePercent, retrievePercent } from './js/tools'; - -/* ========================================================================== - Annotations & Remember position - ========================================================================== */ - -$(document).ready(() => { - if ($('#article').length) { - const app = new annotator.App(); - - app.include(annotator.ui.main, { - element: document.querySelector('article'), - }); - - const authorization = { - permits() { return true; }, - }; - app.registry.registerUtility(authorization, 'authorizationPolicy'); - - const x = JSON.parse($('#annotationroutes').html()); - app.include(annotator.storage.http, $.extend({}, x, { - onError(msg, xhr) { - if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) { - annotator.notification.banner('An error occurred', 'error'); - return; - } - $.each(xhr.responseJSON.children, (k, v) => { - if (v.errors) { - $.each(v.errors, (n, errorText) => { - annotator.notification.banner(errorText, 'error'); - }); - } - }); - }, - })); - - app.start().then(() => { - app.annotations.load({ entry: x.entryId }); - }); - - $(window).scroll(() => { - const scrollTop = $(window).scrollTop(); - const docHeight = $(document).height(); - const scrollPercent = (scrollTop) / (docHeight); - const scrollPercentRounded = Math.round(scrollPercent * 100) / 100; - savePercent(x.entryId, scrollPercentRounded); - }); - - retrievePercent(x.entryId); - - $(window).resize(() => { - retrievePercent(x.entryId, true); - }); - } - - document.querySelectorAll('[data-handler=tag-rename]').forEach((item) => { - const current = item; - current.wallabag_edit_mode = false; - current.onclick = (event) => { - const target = event.currentTarget; - - if (target.wallabag_edit_mode === false) { - $(target.parentNode.querySelector('[data-handle=tag-link]')).addClass('hidden'); - $(target.parentNode.querySelector('[data-handle=tag-rename-form]')).removeClass('hidden'); - target.parentNode.querySelector('[data-handle=tag-rename-form] input').focus(); - target.querySelector('.material-icons').innerHTML = 'done'; - - target.wallabag_edit_mode = true; - } else { - target.parentNode.querySelector('[data-handle=tag-rename-form]').submit(); - } - }; - }); - - // mimic radio button because emailTwoFactor is a boolean - $('#update_user_googleTwoFactor').on('change', () => { - $('#update_user_emailTwoFactor').prop('checked', false); - }); - - $('#update_user_emailTwoFactor').on('change', () => { - $('#update_user_googleTwoFactor').prop('checked', false); - }); - - // same mimic for super admin - $('#user_googleTwoFactor').on('change', () => { - $('#user_emailTwoFactor').prop('checked', false); - }); - - $('#user_emailTwoFactor').on('change', () => { - $('#user_googleTwoFactor').prop('checked', false); - }); - - // handle copy to clipboard for developer stuff - const clipboard = new ClipboardJS('.btn'); - clipboard.on('success', (e) => { - e.clearSelection(); - }); -}); diff --git a/assets/_global/js/bookmarklet.js b/assets/_global/js/bookmarklet.js deleted file mode 100644 index e1ee92234..000000000 --- a/assets/_global/js/bookmarklet.js +++ /dev/null @@ -1,4 +0,0 @@ -top['bookmarklet-url@wallabag.org'] = 'bag it!' - + '' - + ''; diff --git a/assets/_global/js/highlight.js b/assets/_global/js/highlight.js deleted file mode 100644 index f6f8349b4..000000000 --- a/assets/_global/js/highlight.js +++ /dev/null @@ -1,8 +0,0 @@ -import 'highlight.js/styles/atom-one-light.css'; -import hljs from 'highlight.js'; - -window.addEventListener('load', () => { - document.querySelectorAll('pre').forEach((element) => { - hljs.highlightElement(element); - }); -}); diff --git a/assets/_global/js/shortcuts/main.js b/assets/_global/js/shortcuts/main.js deleted file mode 100644 index b99fa802a..000000000 --- a/assets/_global/js/shortcuts/main.js +++ /dev/null @@ -1,15 +0,0 @@ -import Mousetrap from 'mousetrap'; - -/* Shortcuts */ - -/* Go to */ -Mousetrap.bind('g u', () => { window.location.href = Routing.generate('homepage'); }); -Mousetrap.bind('g s', () => { window.location.href = Routing.generate('starred'); }); -Mousetrap.bind('g r', () => { window.location.href = Routing.generate('archive'); }); -Mousetrap.bind('g a', () => { window.location.href = Routing.generate('all'); }); -Mousetrap.bind('g t', () => { window.location.href = Routing.generate('tag'); }); -Mousetrap.bind('g c', () => { window.location.href = Routing.generate('config'); }); -Mousetrap.bind('g i', () => { window.location.href = Routing.generate('import'); }); -Mousetrap.bind('g d', () => { window.location.href = Routing.generate('developer'); }); -Mousetrap.bind('?', () => { window.location.href = Routing.generate('howto'); }); -Mousetrap.bind('g l', () => { window.location.href = Routing.generate('fos_user_security_logout'); }); diff --git a/assets/_global/js/tools.js b/assets/_global/js/tools.js deleted file mode 100644 index 7e5a2b271..000000000 --- a/assets/_global/js/tools.js +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'jquery'; -import './shortcuts/main'; -import './shortcuts/entry'; - -/* Allows inline call qr-code call */ -import jrQrcode from 'jr-qrcode'; // eslint-disable-line - -function supportsLocalStorage() { - try { - return 'localStorage' in window && window.localStorage !== null; - } catch (e) { - return false; - } -} - -function savePercent(id, percent) { - if (!supportsLocalStorage()) { return false; } - localStorage[`wallabag.article.${id}.percent`] = percent; - return true; -} - -function retrievePercent(id, resized) { - if (!supportsLocalStorage()) { return false; } - - const bheight = $(document).height(); - const percent = localStorage[`wallabag.article.${id}.percent`]; - const scroll = bheight * percent; - - if (!resized) { - $('html,body').animate({ scrollTop: scroll }, 'fast'); - } - - return true; -} - -export { savePercent, retrievePercent }; diff --git a/assets/_global/share.js b/assets/_global/share.js deleted file mode 100644 index d70786165..000000000 --- a/assets/_global/share.js +++ /dev/null @@ -1 +0,0 @@ -import './share.scss'; diff --git a/assets/bootstrap.js b/assets/bootstrap.js new file mode 100644 index 000000000..c91af8043 --- /dev/null +++ b/assets/bootstrap.js @@ -0,0 +1,11 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge'; + +// Registers Stimulus controllers from controllers.json and in the controllers/ directory +export default startStimulusApp(require.context( + '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', + true, + /\.[jt]sx?$/, +)); + +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 000000000..a1c6e90cf --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,4 @@ +{ + "controllers": [], + "entrypoints": [] +} diff --git a/assets/controllers/add_tag_controller.js b/assets/controllers/add_tag_controller.js new file mode 100644 index 000000000..2afe9c2b6 --- /dev/null +++ b/assets/controllers/add_tag_controller.js @@ -0,0 +1,13 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['input']; + + toggle() { + this.element.classList.toggle('hidden'); + + if (!this.element.classList.contains('hidden')) { + this.inputTarget.focus(); + } + } +} diff --git a/assets/controllers/annotations_controller.js b/assets/controllers/annotations_controller.js new file mode 100644 index 000000000..1fd4308f6 --- /dev/null +++ b/assets/controllers/annotations_controller.js @@ -0,0 +1,57 @@ +import { Controller } from '@hotwired/stimulus'; +import annotator from 'annotator'; + +export default class extends Controller { + static values = { + entryId: Number, + createUrl: String, + updateUrl: String, + destroyUrl: String, + searchUrl: String, + }; + + connect() { + this.app = new annotator.App(); + + this.app.include(annotator.ui.main, { + element: this.element, + }); + + const authorization = { + permits() { return true; }, + }; + this.app.registry.registerUtility(authorization, 'authorizationPolicy'); + + this.app.include(annotator.storage.http, { + prefix: '', + urls: { + create: this.createUrlValue, + update: this.updateUrlValue, + destroy: this.destroyUrlValue, + search: this.searchUrlValue, + }, + entryId: this.entryIdValue, + onError(msg, xhr) { + if (!Object.prototype.hasOwnProperty.call(xhr, 'responseJSON')) { + annotator.notification.banner('An error occurred', 'error'); + return; + } + Object.values(xhr.responseJSON.children).forEach((v) => { + if (v.errors) { + Object.values(v.errors).forEach((errorText) => { + annotator.notification.banner(errorText, 'error'); + }); + } + }); + }, + }); + + this.app.start().then(() => { + this.app.annotations.load({ entry: this.entryIdValue }); + }); + } + + disconnect() { + this.app.destroy(); + } +} diff --git a/assets/controllers/batch_edit_controller.js b/assets/controllers/batch_edit_controller.js new file mode 100644 index 000000000..e15fc41c5 --- /dev/null +++ b/assets/controllers/batch_edit_controller.js @@ -0,0 +1,15 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['item', 'tagAction']; + + toggleSelection(e) { + this.itemTargets.forEach((item) => { + item.checked = e.currentTarget.checked; // eslint-disable-line no-param-reassign + }); + } + + tagSelection() { + this.element.requestSubmit(this.tagActionTarget); + } +} diff --git a/assets/controllers/clipboard_controller.js b/assets/controllers/clipboard_controller.js new file mode 100644 index 000000000..eba297138 --- /dev/null +++ b/assets/controllers/clipboard_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus'; +import ClipboardJS from 'clipboard'; + +export default class extends Controller { + connect() { + this.clipboard = new ClipboardJS(this.element); + + this.clipboard.on('success', (e) => { + e.clearSelection(); + }); + } + + disconnect() { + this.clipboard.destroy(); + } +} diff --git a/assets/controllers/config_controller.js b/assets/controllers/config_controller.js new file mode 100644 index 000000000..804da826a --- /dev/null +++ b/assets/controllers/config_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['previewArticle', 'previewContent', 'font', 'fontSize', 'lineHeight', 'maxWidth']; + + connect() { + this.updatePreview(); + } + + updatePreview() { + this.previewArticleTarget.style.maxWidth = `${this.maxWidthTarget.value}em`; + this.previewContentTarget.style.fontFamily = this.fontTarget.value; + this.previewContentTarget.style.fontSize = `${this.fontSizeTarget.value}em`; + this.previewContentTarget.style.lineHeight = `${this.lineHeightTarget.value}em`; + } +} diff --git a/assets/controllers/dark_theme_controller.js b/assets/controllers/dark_theme_controller.js new file mode 100644 index 000000000..e19b364ea --- /dev/null +++ b/assets/controllers/dark_theme_controller.js @@ -0,0 +1,39 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + connect() { + this.#choose(); + + this.mql = window.matchMedia('(prefers-color-scheme: dark)'); + this.mql.addEventListener('change', this.#choose.bind(this)); + } + + useLight() { + this.element.classList.remove('dark-theme'); + document.cookie = 'theme=light;samesite=Lax;path=/;max-age=31536000'; + } + + useDark() { + this.element.classList.add('dark-theme'); + document.cookie = 'theme=dark;samesite=Lax;path=/;max-age=31536000'; + } + + useAuto() { + document.cookie = 'theme=auto;samesite=Lax;path=/;max-age=0'; + this.#choose(); + } + + #choose() { + const themeCookieExists = document.cookie.split(';').some((cookie) => cookie.trim().startsWith('theme=')); + + if (themeCookieExists) { + return; + } + + if (this.mql.matches) { + this.element.classList.add('dark-theme'); + } else { + this.element.classList.remove('dark-theme'); + } + } +} diff --git a/assets/controllers/entries_navigation_controller.js b/assets/controllers/entries_navigation_controller.js new file mode 100644 index 000000000..52bbb1138 --- /dev/null +++ b/assets/controllers/entries_navigation_controller.js @@ -0,0 +1,58 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['card', 'paginationWrapper']; + + connect() { + this.pagination = this.paginationWrapperTarget.querySelector('.pagination'); + + this.cardIndex = 0; + this.lastCardIndex = this.cardTargets.length - 1; + + /* If we come from next page */ + if (window.location.hash === '#prev') { + this.cardIndex = this.lastCardIndex; + } + + this.currentCard = this.cardTargets[this.cardIndex]; + + this.currentCard.classList.add('z-depth-4'); + } + + selectRightCard() { + if (this.cardIndex >= 0 && this.cardIndex < this.lastCardIndex) { + this.currentCard.classList.remove('z-depth-4'); + this.cardIndex += 1; + this.currentCard = this.cardTargets[this.cardIndex]; + this.currentCard.classList.add('z-depth-4'); + + return; + } + + if (this.pagination && this.pagination.querySelector('a[rel="next"]')) { + window.location.href = this.pagination.querySelector('a[rel="next"]').href; + } + } + + selectLeftCard() { + if (this.cardIndex > 0 && this.cardIndex <= this.lastCardIndex) { + this.currentCard.classList.remove('z-depth-4'); + this.cardIndex -= 1; + this.currentCard = this.cardTargets[this.cardIndex]; + this.currentCard.classList.add('z-depth-4'); + + return; + } + + if (this.pagination && this.pagination.querySelector('a[rel="prev"]')) { + window.location.href = `${this.pagination.querySelector('a[rel="prev"]').href}#prev`; + } + } + + selectCurrentCard() { + const url = this.currentCard.querySelector('a.card-title').href; + if (url) { + window.location.href = url; + } + } +} diff --git a/assets/controllers/fake_radio_controller.js b/assets/controllers/fake_radio_controller.js new file mode 100644 index 000000000..a9426a189 --- /dev/null +++ b/assets/controllers/fake_radio_controller.js @@ -0,0 +1,13 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['emailTwoFactor', 'googleTwoFactor']; + + uncheckGoogle() { + this.googleTwoFactorTarget.checked = false; + } + + uncheckEmail() { + this.emailTwoFactorTarget.checked = false; + } +} diff --git a/assets/controllers/highlight_controller.js b/assets/controllers/highlight_controller.js new file mode 100644 index 000000000..8177b342e --- /dev/null +++ b/assets/controllers/highlight_controller.js @@ -0,0 +1,11 @@ +import { Controller } from '@hotwired/stimulus'; +import 'highlight.js/styles/atom-one-light.css'; +import hljs from 'highlight.js'; + +export default class extends Controller { + connect() { + this.element.querySelectorAll('pre code').forEach((element) => { + hljs.highlightElement(element); + }); + } +} diff --git a/assets/controllers/leftbar_controller.js b/assets/controllers/leftbar_controller.js new file mode 100644 index 000000000..45728a6f0 --- /dev/null +++ b/assets/controllers/leftbar_controller.js @@ -0,0 +1,7 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + toggleAddTagForm() { + this.dispatch('toggleAddTagForm'); + } +} diff --git a/assets/controllers/materialize/collapsible_controller.js b/assets/controllers/materialize/collapsible_controller.js new file mode 100644 index 000000000..b9e60f38c --- /dev/null +++ b/assets/controllers/materialize/collapsible_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + static values = { + accordion: { type: Boolean, default: true }, + }; + + connect() { + this.instance = M.Collapsible.init(this.element, { accordion: this.accordionValue }); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/dropdown_controller.js b/assets/controllers/materialize/dropdown_controller.js new file mode 100644 index 000000000..12a209d16 --- /dev/null +++ b/assets/controllers/materialize/dropdown_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + connect() { + this.instance = M.Dropdown.init(this.element, { + hover: false, + coverTrigger: false, + constrainWidth: false, + }); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/fab_controller.js b/assets/controllers/materialize/fab_controller.js new file mode 100644 index 000000000..1d11f38bd --- /dev/null +++ b/assets/controllers/materialize/fab_controller.js @@ -0,0 +1,32 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + static values = { + edge: { type: String, default: 'left' }, + }; + + connect() { + this.instance = M.FloatingActionButton.init(this.element); + } + + autoDisplay() { + const scrolled = (window.innerHeight + window.scrollY) >= document.body.offsetHeight; + + if (scrolled) { + this.toggleScroll = true; + this.instance.open(); + } else if (this.toggleScroll === true) { + this.toggleScroll = false; + this.instance.close(); + } + } + + click() { + this.dispatch('click'); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/form_select_controller.js b/assets/controllers/materialize/form_select_controller.js new file mode 100644 index 000000000..3a0fcf374 --- /dev/null +++ b/assets/controllers/materialize/form_select_controller.js @@ -0,0 +1,12 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + connect() { + this.instance = M.FormSelect.init(this.element.querySelector('select')); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/sidenav_controller.js b/assets/controllers/materialize/sidenav_controller.js new file mode 100644 index 000000000..c5c9fbd26 --- /dev/null +++ b/assets/controllers/materialize/sidenav_controller.js @@ -0,0 +1,24 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +const mobileMaxWidth = 993; + +export default class extends Controller { + static values = { + edge: { type: String, default: 'left' }, + }; + + connect() { + this.instance = M.Sidenav.init(this.element, { edge: this.edgeValue }); + } + + close() { + if (window.innerWidth < mobileMaxWidth) { + this.instance.close(); + } + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/tabs_controller.js b/assets/controllers/materialize/tabs_controller.js new file mode 100644 index 000000000..312b486d4 --- /dev/null +++ b/assets/controllers/materialize/tabs_controller.js @@ -0,0 +1,12 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + connect() { + this.instance = M.Tabs.init(this.element); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/materialize/toast_controller.js b/assets/controllers/materialize/toast_controller.js new file mode 100644 index 000000000..ba9165473 --- /dev/null +++ b/assets/controllers/materialize/toast_controller.js @@ -0,0 +1,12 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + connect() { + this.instance = M.toast({ text: this.element.innerText }); + } + + disconnect() { + this.instance.dismissAll(); + } +} diff --git a/assets/controllers/materialize/tooltip_controller.js b/assets/controllers/materialize/tooltip_controller.js new file mode 100644 index 000000000..99f2acf3a --- /dev/null +++ b/assets/controllers/materialize/tooltip_controller.js @@ -0,0 +1,12 @@ +import { Controller } from '@hotwired/stimulus'; +import M from '@materializecss/materialize'; + +export default class extends Controller { + connect() { + this.instance = M.Tooltip.init(this.element); + } + + disconnect() { + this.instance.destroy(); + } +} diff --git a/assets/controllers/qrcode_controller.js b/assets/controllers/qrcode_controller.js new file mode 100644 index 000000000..c7818bca7 --- /dev/null +++ b/assets/controllers/qrcode_controller.js @@ -0,0 +1,10 @@ +import { Controller } from '@hotwired/stimulus'; +import jrQrcode from 'jr-qrcode'; + +export default class extends Controller { + static values = { url: String }; + + connect() { + this.element.setAttribute('src', jrQrcode.getQrBase64(this.urlValue)); + } +} diff --git a/assets/controllers/scroll_indicator_controller.js b/assets/controllers/scroll_indicator_controller.js new file mode 100644 index 000000000..1fab9b4ed --- /dev/null +++ b/assets/controllers/scroll_indicator_controller.js @@ -0,0 +1,10 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + updateWidth() { + const referenceHeight = document.body.offsetHeight - window.innerHeight; + const scrollPercent = (window.scrollY / referenceHeight) * 100; + + this.element.style.width = `${scrollPercent}%`; + } +} diff --git a/assets/controllers/scroll_storage_controller.js b/assets/controllers/scroll_storage_controller.js new file mode 100644 index 000000000..2bb6fc14b --- /dev/null +++ b/assets/controllers/scroll_storage_controller.js @@ -0,0 +1,19 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static values = { entryId: Number }; + + connect() { + window.scrollTo({ + top: window.innerHeight * localStorage[`wallabag.article.${this.entryIdValue}.percent`], + behavior: 'smooth', + }); + } + + saveScroll() { + const scrollPercent = window.scrollY / window.innerHeight; + const scrollPercentRounded = Math.round(scrollPercent * 100) / 100; + + localStorage[`wallabag.article.${this.entryIdValue}.percent`] = scrollPercentRounded; + } +} diff --git a/assets/controllers/shortcuts_controller.js b/assets/controllers/shortcuts_controller.js new file mode 100644 index 000000000..921395fca --- /dev/null +++ b/assets/controllers/shortcuts_controller.js @@ -0,0 +1,141 @@ +import { Controller } from '@hotwired/stimulus'; +import Mousetrap from 'mousetrap'; + +export default class extends Controller { + static targets = ['openOriginal', 'markAsFavorite', 'markAsRead', 'deleteEntry', 'showAddUrl', 'showSearch', 'showActions']; + + static outlets = ['entries-navigation']; + + connect() { + /* Go to */ + Mousetrap.bind('g u', () => { + window.location.href = Routing.generate('homepage'); + }); + Mousetrap.bind('g s', () => { + window.location.href = Routing.generate('starred'); + }); + Mousetrap.bind('g r', () => { + window.location.href = Routing.generate('archive'); + }); + Mousetrap.bind('g a', () => { + window.location.href = Routing.generate('all'); + }); + Mousetrap.bind('g t', () => { + window.location.href = Routing.generate('tag'); + }); + Mousetrap.bind('g c', () => { + window.location.href = Routing.generate('config'); + }); + Mousetrap.bind('g i', () => { + window.location.href = Routing.generate('import'); + }); + Mousetrap.bind('g d', () => { + window.location.href = Routing.generate('developer'); + }); + Mousetrap.bind('?', () => { + window.location.href = Routing.generate('howto'); + }); + Mousetrap.bind('g l', () => { + window.location.href = Routing.generate('fos_user_security_logout'); + }); + + /* open original article */ + Mousetrap.bind('o', () => { + if (!this.hasOpenOriginalTarget) { + return; + } + + this.openOriginalTarget.click(); + }); + + /* mark as favorite */ + Mousetrap.bind('f', () => { + if (!this.hasMarkAsFavoriteTarget) { + return; + } + + this.markAsFavoriteTarget.click(); + }); + + /* mark as read */ + Mousetrap.bind('a', () => { + if (!this.hasMarkAsReadTarget) { + return; + } + + this.markAsReadTarget.click(); + }); + + /* delete */ + Mousetrap.bind('del', () => { + if (!this.hasDeleteEntryTarget) { + return; + } + + this.deleteEntryTarget.click(); + }); + + /* Actions */ + Mousetrap.bind('g n', (e) => { + if (!this.hasShowAddUrlTarget) { + return; + } + + e.preventDefault(); + this.showAddUrlTarget.click(); + }); + + Mousetrap.bind('s', (e) => { + if (!this.hasShowSearchTarget) { + return; + } + + e.preventDefault(); + this.showSearchTarget.click(); + }); + + Mousetrap.bind('esc', (e) => { + if (!this.hasShowActionsTarget) { + return; + } + + e.preventDefault(); + this.showActionsTarget.click(); + }); + + const originalStopCallback = Mousetrap.prototype.stopCallback; + + Mousetrap.prototype.stopCallback = (e, element, combo) => { + // allow esc key to be used in input fields of topbar + if (combo === 'esc' && element.dataset.topbarTarget !== undefined) { + return false; + } + + return originalStopCallback(e, element); + }; + + Mousetrap.bind('right', () => { + if (!this.hasEntriesNavigationOutlet) { + return; + } + + this.entriesNavigationOutlet.selectRightCard(); + }); + + Mousetrap.bind('left', () => { + if (!this.hasEntriesNavigationOutlet) { + return; + } + + this.entriesNavigationOutlet.selectLeftCard(); + }); + + Mousetrap.bind('enter', () => { + if (!this.hasEntriesNavigationOutlet) { + return; + } + + this.entriesNavigationOutlet.selectCurrentCard(); + }); + } +} diff --git a/assets/controllers/sticky_nav_controller.js b/assets/controllers/sticky_nav_controller.js new file mode 100644 index 000000000..12b405b0e --- /dev/null +++ b/assets/controllers/sticky_nav_controller.js @@ -0,0 +1,7 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + toggle() { + this.element.classList.toggle('entry-nav-top--sticky'); + } +} diff --git a/assets/controllers/tag_controller.js b/assets/controllers/tag_controller.js new file mode 100644 index 000000000..3f2b6c054 --- /dev/null +++ b/assets/controllers/tag_controller.js @@ -0,0 +1,12 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['link', 'edit', 'form', 'input']; + + showForm() { + this.formTarget.classList.remove('hidden'); + this.editTarget.classList.add('hidden'); + this.linkTarget.classList.add('hidden'); + this.inputTarget.focus(); + } +} diff --git a/assets/controllers/topbar_controller.js b/assets/controllers/topbar_controller.js new file mode 100644 index 000000000..e200893ad --- /dev/null +++ b/assets/controllers/topbar_controller.js @@ -0,0 +1,31 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['addUrl', 'addUrlInput', 'search', 'searchInput', 'actions']; + + showAddUrl() { + this.actionsTarget.style.display = 'none'; + this.addUrlTarget.style.display = 'flex'; + this.searchTarget.style.display = 'none'; + this.addUrlInputTarget.focus(); + } + + submittingUrl(e) { + e.currentTarget.disabled = true; + this.addUrlInputTarget.readOnly = true; + this.addUrlInputTarget.blur(); + } + + showSearch() { + this.actionsTarget.style.display = 'none'; + this.addUrlTarget.style.display = 'none'; + this.searchTarget.style.display = 'flex'; + this.searchInputTarget.focus(); + } + + showActions() { + this.actionsTarget.style.display = 'flex'; + this.addUrlTarget.style.display = 'none'; + this.searchTarget.style.display = 'none'; + } +} diff --git a/assets/_global/img/icons/diaspora-icon--black.png b/assets/img/icons/diaspora-icon--black.png similarity index 100% rename from assets/_global/img/icons/diaspora-icon--black.png rename to assets/img/icons/diaspora-icon--black.png diff --git a/assets/_global/img/icons/linkding.svg b/assets/img/icons/linkding.svg similarity index 100% rename from assets/_global/img/icons/linkding.svg rename to assets/img/icons/linkding.svg diff --git a/assets/_global/img/icons/scuttle.png b/assets/img/icons/scuttle.png similarity index 100% rename from assets/_global/img/icons/scuttle.png rename to assets/img/icons/scuttle.png diff --git a/assets/_global/img/icons/shaarli.png b/assets/img/icons/shaarli.png similarity index 100% rename from assets/_global/img/icons/shaarli.png rename to assets/img/icons/shaarli.png diff --git a/assets/_global/img/icons/unmark-icon--black.png b/assets/img/icons/unmark-icon--black.png similarity index 100% rename from assets/_global/img/icons/unmark-icon--black.png rename to assets/img/icons/unmark-icon--black.png diff --git a/assets/index.js b/assets/index.js new file mode 100755 index 000000000..dbbd06fdc --- /dev/null +++ b/assets/index.js @@ -0,0 +1,19 @@ +import './bootstrap'; + +/* Materialize imports */ +import '@materializecss/materialize/dist/css/materialize.css'; +import '@materializecss/materialize/dist/js/materialize'; + +import 'mathjax/es5/tex-svg'; + +/* Fonts */ +import 'material-design-icons-iconfont/dist/material-design-icons.css'; +import 'lato-font/css/lato-font.css'; +import 'open-dyslexic/open-dyslexic-regular.css'; +import '@fontsource/atkinson-hyperlegible'; +import '@fontsource/eb-garamond'; +import '@fontsource/montserrat'; +import '@fontsource/oswald'; + +/* Theme style */ +import './scss/index.scss'; diff --git a/assets/material/css/sidenav.scss b/assets/material/css/sidenav.scss deleted file mode 100644 index 00e4c5c2a..000000000 --- a/assets/material/css/sidenav.scss +++ /dev/null @@ -1,50 +0,0 @@ -/* ========================================================================== - Side-nav - ========================================================================== */ - -.side-nav { - width: 240px; - - li { - padding: 0; - - &.logo > a:hover { - background: initial; - } - - & > a > i.material-icons.theme-toggle-icon { - float: none; - margin-left: 0; - } - } - - a { - margin: 0; - } - - &.fixed a { - font-size: 13px; - line-height: 44px; - height: 44px; - } - - .collapsible-header, - &.fixed .collapsible-header { - height: 45px; - line-height: 44px; - padding: 0 20px; - } - - > li.logo { - line-height: 0; - text-align: center; - } -} - -.bold > a { - font-weight: bold; -} - -.items-number { - float: right; -} diff --git a/assets/material/index.js b/assets/material/index.js deleted file mode 100755 index 473a55c65..000000000 --- a/assets/material/index.js +++ /dev/null @@ -1,263 +0,0 @@ -import $ from 'jquery'; - -/* Materialize imports */ -import 'materialize-css/dist/css/materialize.css'; -import 'materialize-css/dist/js/materialize'; - -/* Global imports */ -import '../_global/index'; - -/* Tools */ -import { - initExport, initFilters, initRandom, initPreviewText, -} from './js/tools'; - -/* Import shortcuts */ -import './js/shortcuts/main'; -import './js/shortcuts/entry'; - -/* Theme style */ -import './css/index.scss'; - -const mobileMaxWidth = 993; - -(function darkTheme() { - const rootEl = document.querySelector('html'); - const themeDom = { - darkClass: 'dark-theme', - - toggleClass(el) { - return el.classList.toggle(this.darkClass); - }, - - addClass(el) { - return el.classList.add(this.darkClass); - }, - - removeClass(el) { - return el.classList.remove(this.darkClass); - }, - }; - const themeCookie = { - values: { - light: 'light', - dark: 'dark', - }, - - name: 'theme', - - getValue(isDarkTheme) { - return isDarkTheme ? this.values.dark : this.values.light; - }, - - setCookie(isDarkTheme) { - const value = this.getValue(isDarkTheme); - document.cookie = `${this.name}=${value};samesite=Lax;path=/;max-age=31536000`; - }, - - removeCookie() { - document.cookie = `${this.name}=auto;samesite=Lax;path=/;max-age=0`; - }, - - exists() { - return document.cookie.split(';').some((cookie) => cookie.trim().startsWith(`${this.name}=`)); - }, - }; - const preferedColorScheme = { - choose() { - const themeCookieExists = themeCookie.exists(); - if (this.isAvailable() && !themeCookieExists) { - const isPreferedColorSchemeDark = window.matchMedia('(prefers-color-scheme: dark)').matches === true; - if (!themeCookieExists) { - themeDom[isPreferedColorSchemeDark ? 'addClass' : 'removeClass'](rootEl); - } - } - }, - - isAvailable() { - return typeof window.matchMedia === 'function'; - }, - - init() { - if (!this.isAvailable()) { - return false; - } - this.choose(); - window.matchMedia('(prefers-color-scheme: dark)').addListener(() => { - this.choose(); - }); - return true; - }, - }; - - const addDarkThemeListeners = () => { - $(document).ready(() => { - const lightThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="light"]'); - [...lightThemeButtons].map((lightThemeButton) => { - lightThemeButton.addEventListener('click', (e) => { - e.preventDefault(); - themeDom.removeClass(rootEl); - themeCookie.setCookie(false); - }); - return true; - }); - const darkThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="dark"]'); - [...darkThemeButtons].map((darkThemeButton) => { - darkThemeButton.addEventListener('click', (e) => { - e.preventDefault(); - themeDom.addClass(rootEl); - themeCookie.setCookie(true); - }); - return true; - }); - const autoThemeButtons = document.querySelectorAll('.js-theme-toggle[data-theme="auto"]'); - [...autoThemeButtons].map((autoThemeButton) => { - autoThemeButton.addEventListener('click', (e) => { - e.preventDefault(); - themeCookie.removeCookie(); - preferedColorScheme.choose(); - }); - return true; - }); - }); - }; - - preferedColorScheme.init(); - addDarkThemeListeners(); -}()); - -const stickyNav = () => { - const nav = $('.js-entry-nav-top'); - $('[data-toggle="actions"]').click(() => { - nav.toggleClass('entry-nav-top--sticky'); - }); -}; - -const articleScroll = () => { - const articleEl = $('#article'); - if (articleEl.length > 0) { - $(window).scroll(() => { - const s = $(window).scrollTop(); - const d = $(document).height(); - const c = $(window).height(); - const articleElBottom = articleEl.offset().top + articleEl.height(); - const scrollPercent = (s / (d - c)) * 100; - $('.progress .determinate').css('width', `${scrollPercent}%`); - const fixedActionBtn = $('.js-fixed-action-btn'); - const toggleScrollDataName = 'toggle-auto'; - if ((s + c) > articleElBottom) { - fixedActionBtn.data(toggleScrollDataName, true); - fixedActionBtn.openFAB(); - } else if (fixedActionBtn.data(toggleScrollDataName) === true) { - fixedActionBtn.data(toggleScrollDataName, false); - fixedActionBtn.closeFAB(); - } - }); - } -}; - -$(document).ready(() => { - // sideNav - $('.button-collapse').sideNav(); - $('select').material_select(); - $('.collapsible').collapsible({ - accordion: false, - }); - $('.datepicker').pickadate({ - selectMonths: true, - selectYears: 15, - formatSubmit: 'yyyy-mm-dd', - hiddenName: false, - format: 'yyyy-mm-dd', - container: 'body', - }); - - $('.dropdown-trigger').dropdown({ hover: false }); - - initFilters(); - initExport(); - initRandom(); - stickyNav(); - articleScroll(); - initPreviewText(); - - const toggleNav = (toShow, toFocus) => { - $('.nav-panel-actions').hide(100); - $(toShow).show(100); - $(toFocus).focus(); - }; - - $('#nav-btn-add-tag').on('click', () => { - $('.nav-panel-add-tag').toggle(100); - $('.nav-panel-menu').addClass('hidden'); - if (window.innerWidth < mobileMaxWidth) { - $('.side-nav').sideNav('hide'); - } - $('#tag_label').focus(); - return false; - }); - - $('#nav-btn-add').on('click', () => { - toggleNav('.nav-panel-add', '#entry_url'); - return false; - }); - - $('#config_fontsize').on('input', () => { - const value = $('#config_fontsize').val(); - const css = `${value}em`; - $('#preview-content').css('font-size', css); - }); - - $('#config_font').on('change', () => { - const value = $('#config_font').val(); - $('#preview-content').css('font-family', value); - }); - - $('#config_lineHeight').on('input', () => { - const value = $('#config_lineHeight').val(); - const css = `${value}em`; - $('#preview-content').css('line-height', css); - }); - - $('#config_maxWidth').on('input', () => { - const value = $('#config_maxWidth').val(); - const css = `${value}em`; - $('#preview-article').css('max-width', css); - }); - - const materialAddForm = $('.nav-panel-add'); - materialAddForm.on('submit', () => { - materialAddForm.addClass('disabled'); - $('input#entry_url', materialAddForm).prop('readonly', true).trigger('blur'); - }); - - $('#nav-btn-search').on('click', () => { - toggleNav('.nav-panel-search', '#search_entry_term'); - return false; - }); - - $('.close').on('click', (e) => { - $(e.target).parent('.nav-panel-item').hide(100); - $('.nav-panel-actions').show(100); - return false; - }); - - const mainCheckboxes = document.querySelectorAll('[data-js="checkboxes-toggle"]'); - if (mainCheckboxes.length) { - [...mainCheckboxes].forEach((el) => { - el.addEventListener('click', () => { - const checkboxes = document.querySelectorAll(el.dataset.toggle); - [...checkboxes].forEach((checkbox) => { - const checkboxClone = checkbox; - checkboxClone.checked = el.checked; - }); - }); - }); - } - $('form[name="form_mass_action"] input[name="tags"]').on('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - $('form[name="form_mass_action"] button[name="tag"]').trigger('click'); - } - }); -}); diff --git a/assets/material/js/shortcuts/entry.js b/assets/material/js/shortcuts/entry.js deleted file mode 100644 index e19800bd3..000000000 --- a/assets/material/js/shortcuts/entry.js +++ /dev/null @@ -1,26 +0,0 @@ -import Mousetrap from 'mousetrap'; -import $ from 'jquery'; - -$(document).ready(() => { - if ($('#article').length > 0) { - /* open original article */ - Mousetrap.bind('o', () => { - $('ul.side-nav a.original i')[0].click(); - }); - - /* mark as favorite */ - Mousetrap.bind('f', () => { - $('ul.side-nav a.favorite i')[0].click(); - }); - - /* mark as read */ - Mousetrap.bind('a', () => { - $('ul.side-nav a.markasread i')[0].click(); - }); - - /* delete */ - Mousetrap.bind('del', () => { - $('ul.side-nav a.delete i')[0].click(); - }); - } -}); diff --git a/assets/material/js/shortcuts/main.js b/assets/material/js/shortcuts/main.js deleted file mode 100644 index 771cfb783..000000000 --- a/assets/material/js/shortcuts/main.js +++ /dev/null @@ -1,92 +0,0 @@ -import Mousetrap from 'mousetrap'; -import $ from 'jquery'; - -function toggleFocus(cardToToogleFocus) { - if (cardToToogleFocus) { - $(cardToToogleFocus).toggleClass('z-depth-4'); - } -} - -$(document).ready(() => { - const cards = $('#content').find('.card'); - const cardNumber = cards.length; - let cardIndex = 0; - /* If we come from next page */ - if (window.location.hash === '#prev') { - cardIndex = cardNumber - 1; - } - let card = cards[cardIndex]; - const pagination = $('.pagination'); - - /* Show nothing on quickstart */ - if ($('#content > div.quickstart').length > 0) { - return; - } - - /* Show nothing on login/register page */ - if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) { - return; - } - - /* Show nothing on login/register page */ - if ($('#username').length > 0 || $('#fos_user_registration_form_username').length > 0) { - return; - } - - /* Focus current card */ - toggleFocus(card); - - /* Actions */ - Mousetrap.bind('g n', () => { - $('#nav-btn-add').trigger('click'); - return false; - }); - - Mousetrap.bind('s', () => { - $('#nav-btn-search').trigger('click'); - return false; - }); - - Mousetrap.bind('esc', () => { - $('.close').trigger('click'); - }); - - /* Select right card. If there's a next page, go to next page */ - Mousetrap.bind('right', () => { - if (cardIndex >= 0 && cardIndex < cardNumber - 1) { - toggleFocus(card); - cardIndex += 1; - card = cards[cardIndex]; - toggleFocus(card); - return; - } - if (pagination.length > 0 && pagination.find('li.next:not(.disabled)').length > 0 && cardIndex === cardNumber - 1) { - window.location.href = window.location.origin + $(pagination).find('li.next a').attr('href'); - } - }); - - /* Select previous card. If there's a previous page, go to next page */ - Mousetrap.bind('left', () => { - if (cardIndex > 0 && cardIndex < cardNumber) { - toggleFocus(card); - cardIndex -= 1; - card = cards[cardIndex]; - toggleFocus(card); - return; - } - if (pagination.length > 0 && $(pagination).find('li.prev:not(.disabled)').length > 0 && cardIndex === 0) { - window.location.href = `${window.location.origin + $(pagination).find('li.prev a').attr('href')}#prev`; - } - }); - - Mousetrap.bind('enter', () => { - if (typeof card !== 'object') { - return; - } - - const url = $(card).find('.card-title a').attr('href'); - if (typeof url === 'string' && url.length > 0) { - window.location.href = window.location.origin + url; - } - }); -}); diff --git a/assets/material/js/tools.js b/assets/material/js/tools.js deleted file mode 100644 index c497dee38..000000000 --- a/assets/material/js/tools.js +++ /dev/null @@ -1,53 +0,0 @@ -import $ from 'jquery'; - -function initFilters() { - // no display if filters not available - if ($('div').is('#filters')) { - $('#button_filters').show(); - $('.js-filters-action').sideNav({ edge: 'right' }); - $('#clear_form_filters').on('click', () => { - $('#filters input').val(''); - $('#filters :checked').removeAttr('checked'); - - return false; - }); - } -} - -function initExport() { - // no display if export not available - if ($('div').is('#export')) { - $('#button_export').show(); - $('.js-export-action').sideNav({ edge: 'right' }); - } -} - -function initRandom() { - // no display if export (ie: entries) not available - if ($('div').is('#export')) { - $('#button_random').show(); - } -} - -function initPreviewText() { - // no display if preview_text not available - if ($('div').is('#preview-article')) { - const defaultFontFamily = $('#config_font').val(); - const defaultFontSize = $('#config_fontsize').val(); - const defaultLineHeight = $('#config_lineHeight').val(); - const defaultMaxWidth = $('#config_maxWidth').val(); - const previewContent = $('#preview-content'); - - previewContent.css('font-family', defaultFontFamily); - previewContent.css('font-size', `${defaultFontSize}em`); - previewContent.css('line-height', `${defaultLineHeight}em`); - $('#preview-article').css('max-width', `${defaultMaxWidth}em`); - } -} - -export { - initExport, - initFilters, - initRandom, - initPreviewText, -}; diff --git a/assets/material/css/article.scss b/assets/scss/_article.scss similarity index 100% rename from assets/material/css/article.scss rename to assets/scss/_article.scss diff --git a/assets/material/css/cards.scss b/assets/scss/_cards.scss similarity index 93% rename from assets/material/css/cards.scss rename to assets/scss/_cards.scss index b63fad649..ad2fae38b 100644 --- a/assets/material/css/cards.scss +++ b/assets/scss/_cards.scss @@ -154,6 +154,15 @@ a.original:not(.waves-effect) { } } +.card .card-content .card-title, +.card-stacked .card-content .card-title { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + .card-entry-labels li, .card-tag-labels li { margin: 10px 10px 10px auto; @@ -179,6 +188,7 @@ a.original:not(.waves-effect) { .card-entry-tags a, .card-entry-labels a, .card-tag-labels a, +.card-tag-labels button, .card-entry-labels-hidden a, #list .chip a { text-decoration: none; @@ -186,6 +196,14 @@ a.original:not(.waves-effect) { color: #fff; } +.card-tag-labels button { + background: transparent; + border: none; + font-weight: normal; + color: #fff; + cursor: pointer; +} + .card-tag-link { width: calc(100% - 24px); line-height: 1.3; @@ -196,6 +214,7 @@ a.original:not(.waves-effect) { .card-tag-form { display: flex; + align-items: center; min-width: 100px; flex-grow: 1; } diff --git a/assets/material/css/dark_theme.scss b/assets/scss/_dark_theme.scss similarity index 79% rename from assets/material/css/dark_theme.scss rename to assets/scss/_dark_theme.scss index ac38b566d..1f8f71814 100644 --- a/assets/material/css/dark_theme.scss +++ b/assets/scss/_dark_theme.scss @@ -12,9 +12,9 @@ .collapsible-header, .collection, .dropdown-content, - .side-nav, - .side-nav .collapsible-body, - .side-nav.fixed .collapsible-body, + .sidenav, + .sidenav .collapsible-body, + .sidenav.sidenav-fixed .collapsible-body, .tabs { background-color: #131716; } @@ -59,10 +59,13 @@ #article article h5, #article article h6, .dropdown-content li > a, - .nav-panels .input-field input:focus, + .input-field input, + .input-field input:focus, .results-item, - .side-nav li > a, - .side-nav li > a > i.material-icons { + .sidenav li > a, + .sidenav li > a > i.material-icons, + .sidenav li button, + .sidenav li button > i.material-icons { color: #dfdfdf; } @@ -81,14 +84,15 @@ color: #dfdfdf !important; } - .side-nav li.active { + .sidenav li.active { background-color: #2f2f2f; } .mass-action-tags .mass-action-tags-input.mass-action-tags-input, - .side-nav li:not(.logo) > a:hover, - .side-nav .collapsible-header:hover, - .side-nav.fixed .collapsible-header:hover { + .sidenav li:not(.logo) > a:hover, + .sidenav li:not(.logo) button:hover, + .sidenav .collapsible-header:hover, + .sidenav.sidenav-fixed .collapsible-header:hover { background-color: #1d1d1d; } @@ -141,6 +145,12 @@ background-color: transparent; } + .z-depth-4 { + box-shadow: 0 16px 24px 2px rgba(255 255 255 / 14%), + 0 6px 30px 5px rgba(255 255 255 / 12%), + 0 8px 10px -7px rgba(255 255 255 / 20%); + } + @media only screen and (min-width: 992px) { #article { background-color: #101010; diff --git a/assets/material/css/entries.scss b/assets/scss/_entries.scss similarity index 100% rename from assets/material/css/entries.scss rename to assets/scss/_entries.scss diff --git a/assets/material/css/filters.scss b/assets/scss/_filters.scss similarity index 100% rename from assets/material/css/filters.scss rename to assets/scss/_filters.scss diff --git a/assets/material/css/fonts.scss b/assets/scss/_fonts.scss similarity index 100% rename from assets/material/css/fonts.scss rename to assets/scss/_fonts.scss diff --git a/assets/material/css/icons.scss b/assets/scss/_icons.scss similarity index 87% rename from assets/material/css/icons.scss rename to assets/scss/_icons.scss index f2e307434..4b96da0cf 100644 --- a/assets/material/css/icons.scss +++ b/assets/scss/_icons.scss @@ -125,24 +125,24 @@ a.icon-image { } &.diaspora::before { - background: url("../../_global/img/icons/diaspora-icon--black.png") no-repeat center/80%; + background: url("../img/icons/diaspora-icon--black.png") no-repeat center/80%; } &.unmark::before { - background: url("../../_global/img/icons/unmark-icon--black.png") no-repeat center/80%; + background: url("../img/icons/unmark-icon--black.png") no-repeat center/80%; } &.linkding::before { - background: url("../../_global/img/icons/linkding.svg") no-repeat center/80%; + background: url("../img/icons/linkding.svg") no-repeat center/80%; filter: grayscale(1); } &.shaarli::before { - background: url("../../_global/img/icons/shaarli.png") no-repeat center/80%; + background: url("../img/icons/shaarli.png") no-repeat center/80%; } &.scuttle::before { - background: url("../../_global/img/icons/scuttle.png") no-repeat center/80%; + background: url("../img/icons/scuttle.png") no-repeat center/80%; } } diff --git a/assets/material/css/layout.scss b/assets/scss/_layout.scss similarity index 100% rename from assets/material/css/layout.scss rename to assets/scss/_layout.scss diff --git a/assets/_global/global.scss b/assets/scss/_material-icons.scss similarity index 100% rename from assets/_global/global.scss rename to assets/scss/_material-icons.scss diff --git a/assets/material/css/media_queries.scss b/assets/scss/_media_queries.scss similarity index 100% rename from assets/material/css/media_queries.scss rename to assets/scss/_media_queries.scss diff --git a/assets/material/css/nav.scss b/assets/scss/_nav.scss similarity index 85% rename from assets/material/css/nav.scss rename to assets/scss/_nav.scss index a085febd9..a61657c76 100644 --- a/assets/material/css/nav.scss +++ b/assets/scss/_nav.scss @@ -6,17 +6,38 @@ nav { line-height: initial; } +// adapted from anchor styles from node_modules/@materializecss/materialize/sass/components/_navbar.scss +nav ul button { + transition: background-color .3s; + font-size: 1rem; + color: #fff; + display: block; + padding: 0 15px; + cursor: pointer; + background: none; + border: 0; + + &:focus { + background: none; + } + + &:hover { + background-color: rgba(0 0 0 / 10%); + } +} + nav { input { color: #aaa; } + ul button:hover, ul a:hover { background-color: initial; } } -.nav-panel-item .button-collapse { +.nav-panel-item .sidenav-trigger { margin-left: 0; margin-right: 0.5rem; padding-left: 0.5rem; @@ -34,6 +55,7 @@ nav { justify-content: space-between; align-items: center; + button, a { padding: 10px 15px; } @@ -143,14 +165,6 @@ nav { margin: 0 1%; } -.button-filters { - display: none; -} - -.button-export { - display: none; -} - .entry-nav-top--sticky { position: sticky; top: 0; @@ -183,7 +197,7 @@ nav { justify-content: end; } - .button-collapse { + .sidenav-trigger { display: none; } diff --git a/assets/material/css/print.scss b/assets/scss/_print.scss similarity index 100% rename from assets/material/css/print.scss rename to assets/scss/_print.scss diff --git a/assets/scss/_sidenav.scss b/assets/scss/_sidenav.scss new file mode 100644 index 000000000..8f2be7dd3 --- /dev/null +++ b/assets/scss/_sidenav.scss @@ -0,0 +1,85 @@ +/* ========================================================================== + Sidenav + ========================================================================== */ + +.sidenav { + width: 240px; + + li { + padding: 0; + + &.logo > a:hover { + background: initial; + } + + & button > i.material-icons.theme-toggle-icon, + & > a > i.material-icons.theme-toggle-icon { + float: none; + margin-left: 0; + } + } + + a { + margin: 0; + } + + &.sidenav-fixed button, + &.sidenav-fixed a { + font-size: 13px; + line-height: 44px; + height: 44px; + } + + .collapsible-header, + &.sidenav-fixed .collapsible-header { + height: 45px; + line-height: 44px; + padding: 0 20px; + } + + > li.logo { + line-height: 0; + text-align: center; + } +} + +// adapted from anchor styles from node_modules/@materializecss/materialize/sass/components/_sidenav.scss +.sidenav li button { + color: rgba(0 0 0 / 87%); + display: block; + font-size: 14px; + font-weight: 500; + height: 48px; + line-height: 48px; + padding: 0 (16px * 2); + width: 100%; + text-align: left; + + &:hover { + background-color: rgba(0 0 0 / 5%); + } + + & > i, + & > i.material-icons { + float: left; + height: 48px; + line-height: 48px; + margin: 0 (16px * 2) 0 0; + width: 24px; + color: rgba(0 0 0 / 54%); + } +} + +.bold > a, +.bold > button { + font-weight: bold; +} + +.items-number { + float: right; +} + +.button-filters .sidenav-trigger, +.button-export .sidenav-trigger { + display: block; +} diff --git a/assets/material/css/variables.scss b/assets/scss/_variables.scss similarity index 100% rename from assets/material/css/variables.scss rename to assets/scss/_variables.scss diff --git a/assets/material/css/various.scss b/assets/scss/_various.scss similarity index 78% rename from assets/material/css/various.scss rename to assets/scss/_various.scss index ad0703afa..569c46155 100644 --- a/assets/material/css/various.scss +++ b/assets/scss/_various.scss @@ -1,3 +1,5 @@ +@use "variables"; + /* ========================================================================== * Various * ========================================================================== */ @@ -38,3 +40,18 @@ nav .input-field input { .tab { flex: 1; } + +.btn-link { + background: none; + border: 0; + padding: 0; + color: variables.$blue-accent-color; + + &:focus { + background: none; + } +} + +.inline-block { + display: inline-block; +} diff --git a/assets/material/css/index.scss b/assets/scss/index.scss similarity index 91% rename from assets/material/css/index.scss rename to assets/scss/index.scss index c90def53e..81f5b578f 100644 --- a/assets/material/css/index.scss +++ b/assets/scss/index.scss @@ -1,3 +1,4 @@ +@use "material-icons"; @use "variables"; /* Style */ diff --git a/assets/_global/share.scss b/assets/scss/share.scss similarity index 100% rename from assets/_global/share.scss rename to assets/scss/share.scss diff --git a/assets/share.js b/assets/share.js new file mode 100644 index 000000000..b4815d817 --- /dev/null +++ b/assets/share.js @@ -0,0 +1 @@ +import './scss/share.scss'; diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 7a70a775e..d29916219 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -5,8 +5,9 @@ use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; $config = new Configuration(); -return $config +$config ->disableComposerAutoloadPathScan() + ->disableExtensionsAnalysis() ->enableAnalysisOfUnusedDevDependencies() ->addPathToScan(__DIR__ . '/app', false) ->addPathToScan(__DIR__ . '/migrations', false) @@ -22,14 +23,13 @@ return $config 'friendsoftwig/twigcs', 'incenteev/composer-parameter-handler', 'j0k3r/graby-site-config', + 'j0k3r/php-readability', 'laminas/laminas-code', 'lcobucci/jwt', 'mgargano/simplehtmldom', 'mnapoli/piwik-twig-extension', 'ocramius/proxy-manager', 'pagerfanta/twig', - 'php-http/client-common', - 'php-http/httplug', 'php-http/mock-client', 'phpstan/extension-installer', 'phpstan/phpstan', @@ -38,30 +38,22 @@ return $config 'phpstan/phpstan-symfony', 'psr/http-client', 'psr/http-factory', - 'psr/http-message', - 'rulerz-php/doctrine-orm', - 'scheb/2fa-qr-code', + 'rector/rector', 'scheb/2fa-trusted-device', 'shipmonk/composer-dependency-analyser', 'symfony/asset', - 'symfony/browser-kit', 'symfony/css-selector', - 'symfony/doctrine-bridge', 'symfony/google-mailer', 'symfony/intl', 'symfony/phpunit-bridge', - 'symfony/polyfill-php80', - 'symfony/polyfill-php81', 'symfony/proxy-manager-bridge', 'symfony/templating', 'symfony/var-dumper', 'twig/string-extra', ], [ErrorType::UNUSED_DEPENDENCY]) ->ignoreErrorsOnPackages([ - 'guzzlehttp/streams', 'monolog/monolog', 'symfony/filesystem', - 'symfony/http-client', ], [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) ->ignoreErrorsOnPackages([ 'dama/doctrine-test-bundle', @@ -71,4 +63,9 @@ return $config 'symfony/web-profiler-bundle', 'symfony/web-server-bundle', ], [ErrorType::DEV_DEPENDENCY_IN_PROD]) + ->ignoreErrorsOnPackages([ + 'gedmo/doctrine-extensions', + ], [ErrorType::SHADOW_DEPENDENCY]) ; + +return $config; diff --git a/composer.json b/composer.json index f757940f7..95d24cd36 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "issues": "https://github.com/wallabag/wallabag/issues" }, "require": { - "php": ">=7.4", + "php": ">=8.2", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -57,141 +57,147 @@ "ext-tidy": "*", "ext-tokenizer": "*", "ext-xml": "*", - "babdev/pagerfanta-bundle": "^3.8", + "babdev/pagerfanta-bundle": "^4.5", "craue/config-bundle": "^2.7.0", "defuse/php-encryption": "^2.4", - "doctrine/collections": "^1.8", - "doctrine/common": "^3.4.3", - "doctrine/dbal": "^3.8.2", - "doctrine/doctrine-bundle": "^2.11.3", - "doctrine/doctrine-migrations-bundle": "^3.3", + "doctrine/collections": "^2.3", + "doctrine/common": "^3.5.0", + "doctrine/dbal": "^3.9.4", + "doctrine/doctrine-bundle": "^2.13.2", + "doctrine/doctrine-migrations-bundle": "^3.4.1", "doctrine/event-manager": "^1.2", - "doctrine/migrations": "^3.5.5", - "doctrine/orm": "^2.18.1", - "doctrine/persistence": "^3.2", - "egulias/email-validator": "^3.2.6", - "enshrined/svg-sanitize": "^0.21", - "friendsofsymfony/jsrouting-bundle": "^2.8", + "doctrine/migrations": "^3.8.2", + "doctrine/orm": "^2.20.2", + "doctrine/persistence": "^3.4", + "egulias/email-validator": "^4.0.4", + "enshrined/svg-sanitize": "^0.22", + "friendsofsymfony/jsrouting-bundle": "^3.5", "friendsofsymfony/oauth-server-bundle": "dev-master#dc8ff343363cf794d30eb1a123610d186a43f162", - "friendsofsymfony/rest-bundle": "^3.6", - "friendsofsymfony/user-bundle": "^3.2.1", - "guzzlehttp/guzzle": "^5.3.4", - "guzzlehttp/psr7": "^2.6.2", - "guzzlehttp/streams": "^3.0", - "html2text/html2text": "^4.3.1", + "friendsofsymfony/rest-bundle": "^3.8", + "friendsofsymfony/user-bundle": "^3.4.0", + "guzzlehttp/psr7": "^2.7.0", + "html2text/html2text": "^4.3.2", "incenteev/composer-parameter-handler": "^2.2", - "j0k3r/graby": "^2.4.5", - "j0k3r/graby-site-config": "^1.0", + "j0k3r/graby": "^2.4.6", + "j0k3r/graby-site-config": "^1.0.197", + "j0k3r/php-readability": "^1.2.13", "javibravo/simpleue": "^2.1", - "jms/serializer": "^3.29.1", - "jms/serializer-bundle": "^5.4", - "laminas/laminas-code": "^4.7.1", + "jms/serializer": "^3.32.3", + "jms/serializer-bundle": "^5.5.1", + "laminas/laminas-code": "^4.16", "lcobucci/jwt": "^4.3", - "league/html-to-markdown": "^5.1", + "league/html-to-markdown": "^5.1.1", "mgargano/simplehtmldom": "^1.5", "mnapoli/piwik-twig-extension": "^3.0", - "monolog/monolog": "^2.9", - "nelmio/api-doc-bundle": "^4.20.0", - "nelmio/cors-bundle": "^2.4", + "monolog/monolog": "^2.10", + "nelmio/api-doc-bundle": "^4.38.1", + "nelmio/cors-bundle": "^2.5", "ocramius/proxy-manager": "^2.1.1", "pagerfanta/core": "^3.8", - "pagerfanta/doctrine-orm-adapter": "^3.8", - "pagerfanta/twig": "^3.8", - "php-amqplib/php-amqplib": "^3.6.1", - "php-amqplib/rabbitmq-bundle": "^2.14.0", - "php-http/client-common": "^2.7.1", - "php-http/guzzle5-adapter": "^2.0", - "php-http/httplug": "^2.4", - "php-http/httplug-bundle": "^1.32", + "pagerfanta/doctrine-orm-adapter": "^4.7", + "pagerfanta/twig": "^4.7", + "php-amqplib/php-amqplib": "^3.7.3", + "php-amqplib/rabbitmq-bundle": "^2.17.3", "pragmarx/recovery": "^0.2.1", - "predis/predis": "^2.2.2", + "predis/predis": "^3.2.0", "psr/http-client": "^1.0.3", - "psr/http-factory": "^1.0.2", + "psr/http-factory": "^1.1.0", "psr/http-message": "^2.0", "psr/log": "^1.1.4", - "rulerz-php/doctrine-orm": "dev-master", "scheb/2fa-backup-code": "^5.13.2", "scheb/2fa-bundle": "^5.13.2", "scheb/2fa-email": "^5.13.2", "scheb/2fa-google-authenticator": "^5.13.2", - "scheb/2fa-qr-code": "^5.13.2", "scheb/2fa-trusted-device": "^5.13.2", - "scssphp/scssphp": "^1.12.1", + "scssphp/scssphp": "^2.0.1", "sensio/framework-extra-bundle": "^6.2.10", - "sentry/sentry-symfony": "^5.0.1", - "spiriitlabs/form-filter-bundle": "^10.0", - "stof/doctrine-extensions-bundle": "^1.11.0", - "symfony/asset": "^5.4.35", - "symfony/config": "^5.4.35", - "symfony/console": "^5.4.35", - "symfony/dependency-injection": "^5.4.35", - "symfony/doctrine-bridge": "^5.4.35", - "symfony/dom-crawler": "^5.4.35", - "symfony/error-handler": "^5.4.35", - "symfony/event-dispatcher": "^5.4.35", - "symfony/event-dispatcher-contracts": "^2.5.2", - "symfony/expression-language": "^5.4.35", - "symfony/filesystem": "^5.4", - "symfony/finder": "^5.4.35", - "symfony/form": "^5.4.35", - "symfony/framework-bundle": "^5.4.35", - "symfony/google-mailer": "^5.4.35", - "symfony/http-client": "^5.4.35", - "symfony/http-client-contracts": "^2.5", - "symfony/http-foundation": "^5.4.35", - "symfony/http-kernel": "^5.4.35", - "symfony/intl": "^5.4.35", - "symfony/mailer": "^5.4.35", - "symfony/mime": "^5.4.35", + "sentry/sentry-symfony": "^5.2.0", + "spiriitlabs/form-filter-bundle": "^10.0.2", + "stof/doctrine-extensions-bundle": "^1.13.0", + "symfony/asset": "^5.4.45", + "symfony/browser-kit": "^5.4.45", + "symfony/config": "^5.4.46", + "symfony/console": "^5.4.47", + "symfony/dependency-injection": "^5.4.48", + "symfony/doctrine-bridge": "^5.4.48", + "symfony/dom-crawler": "^5.4.48", + "symfony/error-handler": "^5.4.46", + "symfony/event-dispatcher": "^5.4.45", + "symfony/event-dispatcher-contracts": "^2.5.4", + "symfony/expression-language": "^5.4.45", + "symfony/filesystem": "^5.4.45", + "symfony/finder": "^5.4.45", + "symfony/form": "^5.4.45", + "symfony/framework-bundle": "^5.4.45", + "symfony/google-mailer": "^5.4.45", + "symfony/http-client": "^5.4.49", + "symfony/http-client-contracts": "^2.5.5", + "symfony/http-foundation": "^5.4.48", + "symfony/http-kernel": "^5.4.48", + "symfony/intl": "^5.4.47", + "symfony/mailer": "^5.4.45", + "symfony/mime": "^5.4.45", "symfony/monolog-bundle": "^3.10", - "symfony/options-resolver": "^5.4.21", - "symfony/polyfill-php80": "^1.29", - "symfony/polyfill-php81": "^1.29", - "symfony/proxy-manager-bridge": "^5.4.21", - "symfony/routing": "^5.4.35", - "symfony/security-bundle": "^5.4.35", - "symfony/security-core": "^5.4.35", - "symfony/security-http": "^5.4.35", - "symfony/templating": "^5.4.35", - "symfony/translation-contracts": "^2.5.2", - "symfony/twig-bundle": "^5.4.35", - "symfony/validator": "^5.4.35", - "tecnickcom/tcpdf": "^6.6.5", - "twig/extra-bundle": "^3.8", - "twig/string-extra": "^3.8", - "twig/twig": "^3.8.0", + "symfony/options-resolver": "^5.4.45", + "symfony/proxy-manager-bridge": "^5.4.45", + "symfony/routing": "^5.4.48", + "symfony/security-bundle": "^5.4.45", + "symfony/security-core": "^5.4.48", + "symfony/security-http": "^5.4.47", + "symfony/templating": "^5.4.45", + "symfony/translation-contracts": "^2.5.4", + "symfony/twig-bundle": "^5.4.45", + "symfony/validator": "^5.4.48", + "symfony/webpack-encore-bundle": "^1.17.2", + "tecnickcom/tcpdf": "^6.8.2", + "twig/extra-bundle": "^3.20", + "twig/string-extra": "^3.20", + "twig/twig": "^3.20.0", "wallabag/phpepub": "^4.0.10", "wallabag/rulerz": "dev-master", "wallabag/rulerz-bundle": "dev-master", - "willdurand/hateoas": "^3.10", - "willdurand/hateoas-bundle": "^2.6" + "willdurand/hateoas": "^3.12", + "willdurand/hateoas-bundle": "^2.7" }, "require-dev": { - "dama/doctrine-test-bundle": "^8.0.2", - "doctrine/data-fixtures": "^1.7", - "doctrine/doctrine-fixtures-bundle": "^3.5.1", - "ergebnis/composer-normalize": "^2.42.0", - "friendsofphp/php-cs-fixer": "^3.49", - "friendsoftwig/twigcs": "^6.1", + "dama/doctrine-test-bundle": "^8.2.2", + "doctrine/data-fixtures": "^2.0.2", + "doctrine/doctrine-fixtures-bundle": "^3.7.1", + "ergebnis/composer-normalize": "^2.45.0", + "friendsofphp/php-cs-fixer": "^3.70.2", + "friendsoftwig/twigcs": "^6.5", "m6web/redis-mock": "^5.6", - "php-http/mock-client": "^1.6", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.59", - "phpstan/phpstan-doctrine": "^1.3.62", - "phpstan/phpstan-phpunit": "^1.3.16", - "phpstan/phpstan-symfony": "^1.3.7", - "phpunit/phpunit": "^9.6.17", - "shipmonk/composer-dependency-analyser": "^1.7", - "symfony/browser-kit": "^5.4.35", - "symfony/css-selector": "^5.4.35", - "symfony/debug-bundle": "^5.4.35", - "symfony/maker-bundle": "^1.43", - "symfony/phpunit-bridge": "^7.0.3", - "symfony/process": "^5.4", - "symfony/var-dumper": "^5.4.35", - "symfony/web-profiler-bundle": "^5.4.35", + "php-http/mock-client": "^1.6.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.20", + "phpstan/phpstan-doctrine": "^1.5.7", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-symfony": "^1.4.13", + "phpunit/phpunit": "^9.6.22", + "rector/rector": "^1.2", + "shipmonk/composer-dependency-analyser": "^1.8.2", + "symfony/css-selector": "^5.4.45", + "symfony/debug-bundle": "^5.4.45", + "symfony/maker-bundle": "^1.50", + "symfony/phpunit-bridge": "^7.2.0", + "symfony/process": "^5.4.47", + "symfony/var-dumper": "^5.4.48", + "symfony/web-profiler-bundle": "^5.4.48", "symfony/web-server-bundle": "^4.4.44" }, + "replace": { + "symfony/polyfill-php54": "*", + "symfony/polyfill-php55": "*", + "symfony/polyfill-php56": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-php82": "*" + }, "suggest": { "ext-imagick": "To keep GIF animation when downloading image is enabled" }, @@ -222,9 +228,6 @@ "phpstan/extension-installer": true }, "bin-dir": "bin", - "platform": { - "php": "7.4.29" - }, "sort-packages": true }, "extra": { diff --git a/composer.lock b/composer.lock index 601df0444..b5504c753 100644 --- a/composer.lock +++ b/composer.lock @@ -4,56 +4,60 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "455c734a93cefa1a4073788cfd972406", + "content-hash": "5ca96a07db4b52c436fe273e77abd135", "packages": [ { "name": "babdev/pagerfanta-bundle", - "version": "v3.8.0", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/BabDev/PagerfantaBundle.git", - "reference": "39df12df63177fee40c17c6efff8d4110bea3c94" + "reference": "838e486e0aad9a4485eeb9d5ea854872ae23dadc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BabDev/PagerfantaBundle/zipball/39df12df63177fee40c17c6efff8d4110bea3c94", - "reference": "39df12df63177fee40c17c6efff8d4110bea3c94", + "url": "https://api.github.com/repos/BabDev/PagerfantaBundle/zipball/838e486e0aad9a4485eeb9d5ea854872ae23dadc", + "reference": "838e486e0aad9a4485eeb9d5ea854872ae23dadc", "shasum": "" }, "require": { - "pagerfanta/core": "^3.1", - "php": "^7.4 || ^8.0", - "symfony/config": "^4.4 || ^5.4 || ^6.0", - "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.0", - "symfony/deprecation-contracts": "^2.1 || ^3.0", - "symfony/http-foundation": "^4.4 || ^5.4 || ^6.0", - "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0", - "symfony/polyfill-php80": "^1.15", - "symfony/property-access": "^4.4 || ^5.4 || ^6.0", - "symfony/routing": "^4.4 || ^5.4 || ^6.0" + "pagerfanta/core": "^3.7 || ^4.0", + "php": "^8.1", + "psr/container": "^1.0 || ^2.0", + "symfony/config": "^5.4 || ^6.4 || ^7.1", + "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.1", + "symfony/http-foundation": "^5.4 || ^6.4 || ^7.1", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.1", + "symfony/property-access": "^5.4 || ^6.4 || ^7.1", + "symfony/routing": "^5.4 || ^6.4 || ^7.1" }, "conflict": { - "pagerfanta/twig": "<3.1", - "twig/twig": "<1.35 || >=2.0,<2.5", + "jms/serializer": "<3.18", + "jms/serializer-bundle": "<4.2", + "pagerfanta/twig": "<3.7", + "symfony/serializer": "<5.4 || >=6.0,<6.4 || >=7.0,<7.1", + "symfony/translation": "<5.4 || >=6.0,<6.4 || >=7.0,<7.1", + "symfony/twig-bridge": "<5.4 || >=6.0,<6.4 || >=7.0,<7.1", + "symfony/twig-bundle": "<5.4 || >=6.0,<6.4 || >=7.0,<7.1", + "twig/twig": "<2.13", "white-october/pagerfanta-bundle": "*" }, "require-dev": { - "doctrine/annotations": "^1.8", - "jms/serializer": "^3.0", - "jms/serializer-bundle": "^3.0 || ^4.0", - "matthiasnoback/symfony-dependency-injection-test": "^4.3", - "pagerfanta/twig": "^3.1", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "1.10.15", - "phpstan/phpstan-phpunit": "1.3.13", - "phpstan/phpstan-symfony": "1.2.19", - "phpunit/phpunit": "9.6.8", - "symfony/phpunit-bridge": "^5.4 || ^6.0", - "symfony/serializer": "^4.4 || ^5.4 || ^6.0", - "symfony/translation": "^4.4 || ^5.4 || ^6.0", - "symfony/twig-bridge": "^4.4 || ^5.4 || ^6.0", - "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0", - "twig/twig": "^1.35 || ^2.5 || ^3.0" + "jms/serializer": "^3.18", + "jms/serializer-bundle": "^4.2 || ^5.0", + "matthiasnoback/symfony-dependency-injection-test": "^5.0", + "pagerfanta/twig": "^3.7 || ^4.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-symfony": "1.4.11", + "phpunit/phpunit": "9.6.21", + "symfony/phpunit-bridge": "^5.4 || ^6.4 || ^7.1", + "symfony/serializer": "^5.4 || ^6.4 || ^7.1", + "symfony/translation": "^5.4 || ^6.4 || ^7.1", + "symfony/twig-bridge": "^5.4 || ^6.4 || ^7.1", + "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.1", + "twig/twig": "^2.13 || ^3.0" }, "suggest": { "jms/serializer-bundle": "To use the Pagerfanta class with the JMS Serializer", @@ -79,7 +83,7 @@ ], "support": { "issues": "https://github.com/BabDev/PagerfantaBundle/issues", - "source": "https://github.com/BabDev/PagerfantaBundle/tree/v3.8.0" + "source": "https://github.com/BabDev/PagerfantaBundle/tree/v4.5.0" }, "funding": [ { @@ -87,74 +91,20 @@ "type": "github" } ], - "time": "2023-06-01T01:04:54+00:00" - }, - { - "name": "bacon/bacon-qr-code", - "version": "2.0.8", - "source": { - "type": "git", - "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", - "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", - "shasum": "" - }, - "require": { - "dasprid/enum": "^1.0.3", - "ext-iconv": "*", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "phly/keep-a-changelog": "^2.1", - "phpunit/phpunit": "^7 | ^8 | ^9", - "spatie/phpunit-snapshot-assertions": "^4.2.9", - "squizlabs/php_codesniffer": "^3.4" - }, - "suggest": { - "ext-imagick": "to generate QR code images" - }, - "type": "library", - "autoload": { - "psr-4": { - "BaconQrCode\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Ben Scholzen 'DASPRiD'", - "email": "mail@dasprids.de", - "homepage": "https://dasprids.de/", - "role": "Developer" - } - ], - "description": "BaconQrCode is a QR code generator for PHP.", - "homepage": "https://github.com/Bacon/BaconQrCode", - "support": { - "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" - }, - "time": "2022-12-07T17:46:57+00:00" + "time": "2024-11-03T20:23:50+00:00" }, { "name": "beberlei/assert", - "version": "v3.3.2", + "version": "v3.3.3", "source": { "type": "git", "url": "https://github.com/beberlei/assert.git", - "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655" + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655", - "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655", + "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", "shasum": "" }, "require": { @@ -162,7 +112,7 @@ "ext-json": "*", "ext-mbstring": "*", "ext-simplexml": "*", - "php": "^7.0 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", @@ -206,9 +156,9 @@ ], "support": { "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.2" + "source": "https://github.com/beberlei/assert/tree/v3.3.3" }, - "time": "2021-12-16T21:41:27+00:00" + "time": "2024-07-15T13:18:35+00:00" }, { "name": "behat/transliterator", @@ -370,11 +320,11 @@ }, "type": "symfony-bundle", "extra": { - "branch-alias": { - "dev-master": "2.7.x-dev" - }, "symfony": { "require": "~4.4|~5.4|^6.3" + }, + "branch-alias": { + "dev-master": "2.7.x-dev" } }, "autoload": { @@ -411,56 +361,6 @@ }, "time": "2023-08-06T22:32:54+00:00" }, - { - "name": "dasprid/enum", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/DASPRiD/Enum.git", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", - "shasum": "" - }, - "require": { - "php": ">=7.1 <9.0" - }, - "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9", - "squizlabs/php_codesniffer": "*" - }, - "type": "library", - "autoload": { - "psr-4": { - "DASPRiD\\Enum\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Ben Scholzen 'DASPRiD'", - "email": "mail@dasprids.de", - "homepage": "https://dasprids.de/", - "role": "Developer" - } - ], - "description": "PHP 7.1 enum implementation", - "keywords": [ - "enum", - "map" - ], - "support": { - "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" - }, - "time": "2023-08-25T16:18:39+00:00" - }, { "name": "defuse/php-encryption", "version": "v2.4.0", @@ -699,32 +599,34 @@ }, { "name": "doctrine/collections", - "version": "1.8.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e" + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/2b44dd4cbca8b5744327de78bafef5945c7e7b5e", - "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3 || ^1", - "php": "^7.1.3 || ^8.0" + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "doctrine/coding-standard": "^9.0 || ^10.0", - "phpstan/phpstan": "^1.4.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", - "vimeo/psalm": "^4.22" + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + "Doctrine\\Common\\Collections\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -763,9 +665,23 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.8.0" + "source": "https://github.com/doctrine/collections/tree/2.3.0" }, - "time": "2022-09-01T20:12:10+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2025-03-22T10:17:19+00:00" }, { "name": "doctrine/common", @@ -860,40 +776,41 @@ }, { "name": "doctrine/dbal", - "version": "3.9.3", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" + "reference": "3626601014388095d3af9de7e9e958623b7ef005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", - "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3626601014388095d3af9de7e9e958623b7ef005", + "reference": "3626601014388095d3af9de7e9e958623b7ef005", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", "doctrine/event-manager": "^1|^2", "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, + "conflict": { + "doctrine/cache": "< 1.11" + }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.12.6", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "9.6.20", - "psalm/plugin-phpunit": "0.18.4", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0", - "vimeo/psalm": "4.30.0" + "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -953,7 +870,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.3" + "source": "https://github.com/doctrine/dbal/tree/3.10.1" }, "funding": [ { @@ -969,30 +886,33 @@ "type": "tidelift" } ], - "time": "2024-10-10T17:56:43+00:00" + "time": "2025-08-05T12:18:06+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -1012,22 +932,22 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/doctrine-bundle", - "version": "2.13.1", + "version": "2.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "2740ad8b8739b39ab37d409c972b092f632b025a" + "reference": "aac7562c96d117e16cbadfe41bef17d2fc760f74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/2740ad8b8739b39ab37d409c972b092f632b025a", - "reference": "2740ad8b8739b39ab37d409c972b092f632b025a", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/aac7562c96d117e16cbadfe41bef17d2fc760f74", + "reference": "aac7562c96d117e16cbadfe41bef17d2fc760f74", "shasum": "" }, "require": { @@ -1041,7 +961,7 @@ "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^5.4.46 || ^6.4.3 || ^7.0.3", + "symfony/doctrine-bridge": "^5.4.46 || ~6.3.12 || ^6.4.3 || ^7.0.3", "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" @@ -1057,13 +977,14 @@ "doctrine/deprecations": "^1.0", "doctrine/orm": "^2.17 || ^3.0", "friendsofphp/proxy-manager-lts": "^1.0", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", "phpunit/phpunit": "^9.5.26", - "psalm/plugin-phpunit": "^0.18.4", - "psalm/plugin-symfony": "^5", "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/phpunit-bridge": "^6.1 || ^7.0", "symfony/property-info": "^5.4 || ^6.0 || ^7.0", - "symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0", + "symfony/proxy-manager-bridge": "^5.4 || ^6.0", "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", "symfony/string": "^5.4 || ^6.0 || ^7.0", @@ -1072,8 +993,7 @@ "symfony/var-exporter": "^5.4 || ^6.2 || ^7.0", "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "twig/twig": "^1.34 || ^2.12 || ^3.0", - "vimeo/psalm": "^5.15" + "twig/twig": "^1.34 || ^2.12 || ^3.0" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -1118,7 +1038,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.1" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.3" }, "funding": [ { @@ -1134,26 +1054,26 @@ "type": "tidelift" } ], - "time": "2024-11-08T23:27:54+00:00" + "time": "2025-03-16T10:55:20+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.3.1", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0" + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", - "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9", + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9", "shasum": "" }, "require": { "doctrine/doctrine-bundle": "^2.4", "doctrine/migrations": "^3.2", - "php": "^7.2|^8.0", + "php": "^7.2 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" }, @@ -1161,27 +1081,20 @@ "composer/semver": "^3.0", "doctrine/coding-standard": "^12", "doctrine/orm": "^2.6 || ^3", - "doctrine/persistence": "^2.0 || ^3 ", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpstan/phpstan-symfony": "^1.3", - "phpunit/phpunit": "^8.5|^9.5", - "psalm/plugin-phpunit": "^0.18.4", - "psalm/plugin-symfony": "^3 || ^5", + "phpstan/phpstan": "^1.4 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpstan/phpstan-symfony": "^1.3 || ^2", + "phpunit/phpunit": "^8.5 || ^9.5", "symfony/phpunit-bridge": "^6.3 || ^7", - "symfony/var-exporter": "^5.4 || ^6 || ^7", - "vimeo/psalm": "^4.30 || ^5.15" + "symfony/var-exporter": "^5.4 || ^6 || ^7" }, "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\MigrationsBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1210,7 +1123,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.3.1" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.2" }, "funding": [ { @@ -1226,7 +1139,7 @@ "type": "tidelift" } ], - "time": "2024-05-14T20:32:18+00:00" + "time": "2025-03-11T17:36:26+00:00" }, { "name": "doctrine/event-manager", @@ -1413,30 +1326,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -1463,7 +1376,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -1479,32 +1392,31 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "doctrine/lexer", - "version": "2.1.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", - "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.21" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1541,7 +1453,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.1" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1557,51 +1469,52 @@ "type": "tidelift" } ], - "time": "2024-02-05T11:35:39+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "doctrine/migrations", - "version": "3.5.5", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204" + "reference": "cd12028853c418b454602e3fda89e519e9af947b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204", - "reference": "4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/cd12028853c418b454602e3fda89e519e9af947b", + "reference": "cd12028853c418b454602e3fda89e519e9af947b", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/dbal": "^3.5.1", + "doctrine/dbal": "^3.6 || ^4", "doctrine/deprecations": "^0.5.3 || ^1", "doctrine/event-manager": "^1.2 || ^2.0", - "friendsofphp/proxy-manager-lts": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^8.1", "psr/log": "^1.1.3 || ^2 || ^3", - "symfony/console": "^4.4.16 || ^5.4 || ^6.0", - "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" }, "conflict": { - "doctrine/orm": "<2.12" + "doctrine/orm": "<2.12 || >=4" }, "require-dev": { - "doctrine/coding-standard": "^9", - "doctrine/orm": "^2.13", - "doctrine/persistence": "^2 || ^3", + "doctrine/coding-standard": "^13", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpstan/phpstan-symfony": "^1.1", - "phpunit/phpunit": "^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "symfony/process": "^4.4 || ^5.4 || ^6.0", - "symfony/yaml": "^4.4 || ^5.4 || ^6.0" + "fig/log-test": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", @@ -1613,7 +1526,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + "Doctrine\\Migrations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1643,7 +1556,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.5.5" + "source": "https://github.com/doctrine/migrations/tree/3.9.3" }, "funding": [ { @@ -1659,20 +1572,20 @@ "type": "tidelift" } ], - "time": "2023-01-18T12:44:30+00:00" + "time": "2025-08-13T22:04:47+00:00" }, { "name": "doctrine/orm", - "version": "2.20.0", + "version": "2.20.2", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c" + "reference": "19912de9270fa6abb3d25a1a37784af6b818d534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c", - "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c", + "url": "https://api.github.com/repos/doctrine/orm/zipball/19912de9270fa6abb3d25a1a37784af6b818d534", + "reference": "19912de9270fa6abb3d25a1a37784af6b818d534", "shasum": "" }, "require": { @@ -1702,15 +1615,14 @@ "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", "phpstan/extension-installer": "~1.1.0 || ^1.4", - "phpstan/phpstan": "~1.4.10 || 1.12.6", - "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan": "~1.4.10 || 2.0.3", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "vimeo/psalm": "4.30.0 || 5.24.0" + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1760,9 +1672,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.20.0" + "source": "https://github.com/doctrine/orm/tree/2.20.2" }, - "time": "2024-10-11T11:47:24+00:00" + "time": "2025-02-04T19:17:01+00:00" }, { "name": "doctrine/persistence", @@ -1862,26 +1774,26 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.3.0", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "3447381095d32a171fe3a58323749f44dbb5ac7d" + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/3447381095d32a171fe3a58323749f44dbb5ac7d", - "reference": "3447381095d32a171fe3a58323749f44dbb5ac7d", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^8.5 || ^9.6", - "vimeo/psalm": "^4.11" + "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" }, "bin": [ "bin/sql-formatter" @@ -1911,32 +1823,32 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.3.0" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" }, - "time": "2024-05-06T21:49:18+00:00" + "time": "2025-01-24T11:45:48+00:00" }, { "name": "egulias/email-validator", - "version": "3.2.6", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7", - "reference": "e5997fa97e8790cdae03a9cbd5e78e45e3c7bda7", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { - "doctrine/lexer": "^1.2|^2", - "php": ">=7.2", - "symfony/polyfill-intl-idn": "^1.15" + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { - "phpunit/phpunit": "^8.5.8|^9.3.3", - "vimeo/psalm": "^4" + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -1944,7 +1856,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { @@ -1972,7 +1884,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/3.2.6" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -1980,95 +1892,20 @@ "type": "github" } ], - "time": "2023-06-01T07:04:22+00:00" - }, - { - "name": "endroid/qr-code", - "version": "3.9.7", - "source": { - "type": "git", - "url": "https://github.com/endroid/qr-code.git", - "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/endroid/qr-code/zipball/94563d7b3105288e6ac53a67ae720e3669fac1f6", - "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6", - "shasum": "" - }, - "require": { - "bacon/bacon-qr-code": "^2.0", - "khanamiryan/qrcode-detector-decoder": "^1.0.5", - "myclabs/php-enum": "^1.5", - "php": "^7.3||^8.0", - "symfony/options-resolver": "^3.4||^4.4||^5.0", - "symfony/property-access": "^3.4||^4.4||^5.0" - }, - "require-dev": { - "endroid/quality": "^1.5.2", - "setasign/fpdf": "^1.8" - }, - "suggest": { - "ext-gd": "Required for generating PNG images", - "roave/security-advisories": "Avoids installation of package versions with vulnerabilities", - "setasign/fpdf": "Required to use the FPDF writer.", - "symfony/security-checker": "Checks your composer.lock for vulnerabilities" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Endroid\\QrCode\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeroen van den Enden", - "email": "info@endroid.nl" - } - ], - "description": "Endroid QR Code", - "homepage": "https://github.com/endroid/qr-code", - "keywords": [ - "bundle", - "code", - "endroid", - "php", - "qr", - "qrcode" - ], - "support": { - "issues": "https://github.com/endroid/qr-code/issues", - "source": "https://github.com/endroid/qr-code/tree/3.9.7" - }, - "funding": [ - { - "url": "https://github.com/endroid", - "type": "github" - } - ], - "time": "2021-04-20T19:10:54+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "enshrined/svg-sanitize", - "version": "0.21.0", + "version": "0.22.0", "source": { "type": "git", "url": "https://github.com/darylldoyle/svg-sanitizer.git", - "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9" + "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/5e477468fac5c5ce933dce53af3e8e4e58dcccc9", - "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500", + "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500", "shasum": "" }, "require": { @@ -2098,9 +1935,9 @@ "description": "An SVG sanitizer for PHP", "support": { "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", - "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.21.0" + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0" }, - "time": "2025-01-13T09:32:25+00:00" + "time": "2025-08-12T10:13:48+00:00" }, { "name": "fossar/htmlawed", @@ -2192,8 +2029,8 @@ "type": "library", "extra": { "thanks": { - "name": "ocramius/proxy-manager", - "url": "https://github.com/Ocramius/ProxyManager" + "url": "https://github.com/Ocramius/ProxyManager", + "name": "ocramius/proxy-manager" } }, "autoload": { @@ -2243,33 +2080,33 @@ }, { "name": "friendsofsymfony/jsrouting-bundle", - "version": "2.8.0", + "version": "3.5.2", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git", - "reference": "c978fabc6a21a77052ff3fe927b41111ec944f0d" + "reference": "af2ee3a5406a3b57dfe1a0c010d2a29c2fdbfeed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSJsRoutingBundle/zipball/c978fabc6a21a77052ff3fe927b41111ec944f0d", - "reference": "c978fabc6a21a77052ff3fe927b41111ec944f0d", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSJsRoutingBundle/zipball/af2ee3a5406a3b57dfe1a0c010d2a29c2fdbfeed", + "reference": "af2ee3a5406a3b57dfe1a0c010d2a29c2fdbfeed", "shasum": "" }, "require": { - "php": "^7.1|^8.0", - "symfony/console": "~3.4|^4.4.20|^5.0", - "symfony/framework-bundle": "~3.4|^4.4.20|^5.0", - "symfony/serializer": "~3.4|^4.4.20|^5.0", - "willdurand/jsonp-callback-validator": "~1.1" + "php": "^8.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.0.1|^7.0", + "willdurand/jsonp-callback-validator": "~1.1|^2.0" }, "require-dev": { - "symfony/expression-language": "~3.4|^4.4.20|^5.0", - "symfony/phpunit-bridge": "^5.3" + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0" }, "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { @@ -2294,7 +2131,7 @@ "homepage": "https://github.com/friendsofsymfony/FOSJsRoutingBundle/contributors" } ], - "description": "A pretty nice way to expose your Symfony2 routing to client applications.", + "description": "A pretty nice way to expose your Symfony routing to client applications.", "homepage": "http://friendsofsymfony.github.com", "keywords": [ "Js Routing", @@ -2303,9 +2140,9 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues", - "source": "https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/tree/2.8.0" + "source": "https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/tree/3.5.2" }, - "time": "2021-12-15T08:51:04+00:00" + "time": "2024-11-26T15:26:56+00:00" }, { "name": "friendsofsymfony/oauth-server-bundle", @@ -2655,25 +2492,24 @@ }, { "name": "gedmo/doctrine-extensions", - "version": "v3.17.1", + "version": "v3.19.0", "source": { "type": "git", "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900" + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/5b0b8a442d19e6701ae64535dc08f7944e2895d2", + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2", "shasum": "" }, "require": { "behat/transliterator": "^1.2", "doctrine/collections": "^1.2 || ^2.0", - "doctrine/common": "^2.13 || ^3.0", "doctrine/deprecations": "^1.0", "doctrine/event-manager": "^1.2 || ^2.0", - "doctrine/persistence": "^2.2 || ^3.0", + "doctrine/persistence": "^2.2 || ^3.0 || ^4.0", "php": "^7.4 || ^8.0", "psr/cache": "^1 || ^2 || ^3", "psr/clock": "^1", @@ -2681,24 +2517,26 @@ }, "conflict": { "doctrine/annotations": "<1.13 || >=3.0", + "doctrine/common": "<2.13 || >=4.0", "doctrine/dbal": "<3.7 || >=5.0", "doctrine/mongodb-odm": "<2.3 || >=3.0", - "doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=4.0" + "doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0" }, "require-dev": { "doctrine/annotations": "^1.13 || ^2.0", "doctrine/cache": "^1.11 || ^2.0", + "doctrine/common": "^2.13 || ^3.0", "doctrine/dbal": "^3.7 || ^4.0", "doctrine/doctrine-bundle": "^2.3", "doctrine/mongodb-odm": "^2.3", - "doctrine/orm": "^2.14.0 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.14.0", + "doctrine/orm": "^2.20 || ^3.3", + "friendsofphp/php-cs-fixer": "^3.70", "nesbot/carbon": "^2.71 || ^3.0", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-doctrine": "^1.4", - "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan": "^2.1.1", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.3", "phpunit/phpunit": "^9.6", - "rector/rector": "^1.1", + "rector/rector": "^2.0.6", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/phpunit-bridge": "^6.0 || ^7.0", @@ -2760,7 +2598,7 @@ "support": { "email": "gediminas.morkevicius@gmail.com", "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", - "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.19.0", "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" }, "funding": [ @@ -2781,7 +2619,7 @@ "type": "github" } ], - "time": "2024-10-07T22:30:27+00:00" + "time": "2025-02-24T22:12:57+00:00" }, { "name": "grandt/binstring", @@ -2988,75 +2826,18 @@ }, "time": "2015-05-14T08:18:23+00:00" }, - { - "name": "guzzlehttp/guzzle", - "version": "5.3.4", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "b87eda7a7162f95574032da17e9323c9899cb6b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b87eda7a7162f95574032da17e9323c9899cb6b2", - "reference": "b87eda7a7162f95574032da17e9323c9899cb6b2", - "shasum": "" - }, - "require": { - "guzzlehttp/ringphp": "^1.1", - "php": ">=5.4.0", - "react/promise": "^2.2" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/5.3" - }, - "time": "2019-10-30T09:32:00+00:00" - }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -3143,7 +2924,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, "funding": [ { @@ -3159,118 +2940,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" - }, - { - "name": "guzzlehttp/ringphp", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/RingPHP.git", - "reference": "5e2a174052995663dd68e6b5ad838afd47dd615b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/5e2a174052995663dd68e6b5ad838afd47dd615b", - "reference": "5e2a174052995663dd68e6b5ad838afd47dd615b", - "shasum": "" - }, - "require": { - "guzzlehttp/streams": "~3.0", - "php": ">=5.4.0", - "react/promise": "~2.0" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "ext-curl": "Guzzle will use specific adapters if cURL is present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Ring\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", - "support": { - "issues": "https://github.com/guzzle/RingPHP/issues", - "source": "https://github.com/guzzle/RingPHP/tree/1.1.1" - }, - "abandoned": true, - "time": "2018-07-31T13:22:33+00:00" - }, - { - "name": "guzzlehttp/streams", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/streams.git", - "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", - "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Provides a simple abstraction over streams of data", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "Guzzle", - "stream" - ], - "support": { - "issues": "https://github.com/guzzle/streams/issues", - "source": "https://github.com/guzzle/streams/tree/master" - }, - "abandoned": true, - "time": "2014-10-12T19:18:40+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "hoa/compiler", @@ -4394,16 +4064,16 @@ }, { "name": "j0k3r/graby", - "version": "2.4.5", + "version": "2.4.6", "source": { "type": "git", "url": "https://github.com/j0k3r/graby.git", - "reference": "3519a1e8fddec59b61d05461dcb16818a6fd2650" + "reference": "75437c1928bb036dbb07c61dba40ad6aaa2ffb99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/j0k3r/graby/zipball/3519a1e8fddec59b61d05461dcb16818a6fd2650", - "reference": "3519a1e8fddec59b61d05461dcb16818a6fd2650", + "url": "https://api.github.com/repos/j0k3r/graby/zipball/75437c1928bb036dbb07c61dba40ad6aaa2ffb99", + "reference": "75437c1928bb036dbb07c61dba40ad6aaa2ffb99", "shasum": "" }, "require": { @@ -4424,7 +4094,7 @@ "simplepie/simplepie": "^1.7", "smalot/pdfparser": "^1.1", "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0|^7.0", - "true/punycode": "^2.1" + "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", @@ -4467,7 +4137,7 @@ "description": "Graby helps you extract article content from web pages", "support": { "issues": "https://github.com/j0k3r/graby/issues", - "source": "https://github.com/j0k3r/graby/tree/2.4.5" + "source": "https://github.com/j0k3r/graby/tree/2.4.6" }, "funding": [ { @@ -4475,20 +4145,20 @@ "type": "github" } ], - "time": "2024-01-04T08:46:05+00:00" + "time": "2025-02-24T06:22:45+00:00" }, { "name": "j0k3r/graby-site-config", - "version": "1.0.195", + "version": "1.0.202", "source": { "type": "git", "url": "https://github.com/j0k3r/graby-site-config.git", - "reference": "f5e352e1caf7dbb7cc60b3303eb12b33e80a96f7" + "reference": "870ff8f1f35cdbf87ff000b68c7bef5e712fd533" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/j0k3r/graby-site-config/zipball/f5e352e1caf7dbb7cc60b3303eb12b33e80a96f7", - "reference": "f5e352e1caf7dbb7cc60b3303eb12b33e80a96f7", + "url": "https://api.github.com/repos/j0k3r/graby-site-config/zipball/870ff8f1f35cdbf87ff000b68c7bef5e712fd533", + "reference": "870ff8f1f35cdbf87ff000b68c7bef5e712fd533", "shasum": "" }, "require": { @@ -4517,9 +4187,9 @@ "description": "Graby site config files", "support": { "issues": "https://github.com/j0k3r/graby-site-config/issues", - "source": "https://github.com/j0k3r/graby-site-config/tree/1.0.195" + "source": "https://github.com/j0k3r/graby-site-config/tree/1.0.202" }, - "time": "2025-01-01T02:26:53+00:00" + "time": "2025-08-01T07:03:24+00:00" }, { "name": "j0k3r/httplug-ssrf-plugin", @@ -4594,16 +4264,16 @@ }, { "name": "j0k3r/php-readability", - "version": "1.2.10", + "version": "1.2.13", "source": { "type": "git", "url": "https://github.com/j0k3r/php-readability.git", - "reference": "563835730692eb369a73fe4bb9a1b44a603c4ce7" + "reference": "b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/563835730692eb369a73fe4bb9a1b44a603c4ce7", - "reference": "563835730692eb369a73fe4bb9a1b44a603c4ce7", + "url": "https://api.github.com/repos/j0k3r/php-readability/zipball/b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad", + "reference": "b9dde0f4cd46e9fc082bb37f75dc94ecd2f8faad", "shasum": "" }, "require": { @@ -4615,7 +4285,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^2.14", "monolog/monolog": "^1.24|^2.1", - "symfony/phpunit-bridge": "^4.4|^5.3" + "symfony/phpunit-bridge": "^4.4|^5.3|^6.0|^7.0" }, "suggest": { "ext-tidy": "Used to clean up given HTML and to avoid problems with bad HTML structure." @@ -4665,7 +4335,7 @@ ], "support": { "issues": "https://github.com/j0k3r/php-readability/issues", - "source": "https://github.com/j0k3r/php-readability/tree/1.2.10" + "source": "https://github.com/j0k3r/php-readability/tree/1.2.13" }, "funding": [ { @@ -4673,7 +4343,7 @@ "type": "github" } ], - "time": "2022-06-13T04:15:24+00:00" + "time": "2025-06-03T08:02:58+00:00" }, { "name": "javibravo/simpleue", @@ -4738,16 +4408,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -4757,8 +4427,9 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", @@ -4791,9 +4462,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-11-18T16:19:46+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { "name": "jms/metadata", @@ -4861,16 +4532,16 @@ }, { "name": "jms/serializer", - "version": "3.32.1", + "version": "3.32.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "b4285f4197a5b961d365e2c756b144d7af24a7fd" + "reference": "7c88b1b02ff868eecc870eeddbb3b1250e4bd89c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/b4285f4197a5b961d365e2c756b144d7af24a7fd", - "reference": "b4285f4197a5b961d365e2c756b144d7af24a7fd", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/7c88b1b02ff868eecc870eeddbb3b1250e4bd89c", + "reference": "7c88b1b02ff868eecc870eeddbb3b1250e4bd89c", "shasum": "" }, "require": { @@ -4947,15 +4618,19 @@ ], "support": { "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.32.1" + "source": "https://github.com/schmittjoh/serializer/tree/3.32.5" }, "funding": [ { "url": "https://github.com/goetas", "type": "github" + }, + { + "url": "https://github.com/scyzoryck", + "type": "github" } ], - "time": "2024-12-03T22:30:06+00:00" + "time": "2025-05-26T15:55:41+00:00" }, { "name": "jms/serializer-bundle", @@ -5046,88 +4721,31 @@ ], "time": "2024-11-06T12:45:22+00:00" }, - { - "name": "khanamiryan/qrcode-detector-decoder", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git", - "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/khanamiryan/php-qrcode-detector-decoder/zipball/45326fb83a2a375065dbb3a134b5b8a5872da569", - "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 | ^7.5 | ^8.0 | ^9.0", - "rector/rector": "^0.13.6", - "symplify/easy-coding-standard": "^11.0" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Common/customFunctions.php" - ], - "psr-4": { - "Zxing\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT", - "Apache-2.0" - ], - "authors": [ - { - "name": "Ashot Khanamiryan", - "email": "a.khanamiryan@gmail.com", - "homepage": "https://github.com/khanamiryan", - "role": "Developer" - } - ], - "description": "QR code decoder / reader", - "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/", - "keywords": [ - "barcode", - "qr", - "zxing" - ], - "support": { - "issues": "https://github.com/khanamiryan/php-qrcode-detector-decoder/issues", - "source": "https://github.com/khanamiryan/php-qrcode-detector-decoder/tree/1.0.6" - }, - "time": "2022-06-29T09:25:13+00:00" - }, { "name": "laminas/laminas-code", - "version": "4.7.1", + "version": "4.16.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-code.git", - "reference": "91aabc066d5620428120800c0eafc0411e441a62" + "reference": "1793e78dad4108b594084d05d1fb818b85b110af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/91aabc066d5620428120800c0eafc0411e441a62", - "reference": "91aabc066d5620428120800c0eafc0411e441a62", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/1793e78dad4108b594084d05d1fb818b85b110af", + "reference": "1793e78dad4108b594084d05d1fb818b85b110af", "shasum": "" }, "require": { - "php": ">=7.4, <8.2" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "doctrine/annotations": "^1.13.2", + "doctrine/annotations": "^2.0.1", "ext-phar": "*", - "laminas/laminas-coding-standard": "^2.3.0", - "laminas/laminas-stdlib": "^3.6.1", - "phpunit/phpunit": "^9.5.10", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.13.1" + "laminas/laminas-coding-standard": "^3.0.0", + "laminas/laminas-stdlib": "^3.18.0", + "phpunit/phpunit": "^10.5.37", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.15.0" }, "suggest": { "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", @@ -5135,9 +4753,6 @@ }, "type": "library", "autoload": { - "files": [ - "polyfill/ReflectionEnumPolyfill.php" - ], "psr-4": { "Laminas\\Code\\": "src/" } @@ -5167,35 +4782,38 @@ "type": "community_bridge" } ], - "time": "2022-11-21T01:32:31+00:00" + "time": "2024-11-20T13:15:13+00:00" }, { "name": "lcobucci/clock", - "version": "2.0.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/lcobucci/clock.git", - "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3" + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3", - "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" }, "require-dev": { - "infection/infection": "^0.17", - "lcobucci/coding-standard": "^6.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/php-code-coverage": "9.1.4", - "phpunit/phpunit": "9.3.7" + "infection/infection": "^0.29", + "lcobucci/coding-standard": "^11.1.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^11.3.6" }, "type": "library", "autoload": { @@ -5216,7 +4834,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/2.0.x" + "source": "https://github.com/lcobucci/clock/tree/3.3.1" }, "funding": [ { @@ -5228,7 +4846,7 @@ "type": "patreon" } ], - "time": "2020-08-27T18:56:02+00:00" + "time": "2024-09-24T20:45:14+00:00" }, { "name": "lcobucci/jwt", @@ -5393,6 +5011,180 @@ ], "time": "2023-07-12T21:21:09+00:00" }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, { "name": "masterminds/html5", "version": "2.9.0", @@ -5551,16 +5343,16 @@ }, { "name": "monolog/monolog", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215" + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/a30bfe2e142720dfa990d0a7e573997f5d884215", - "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", "shasum": "" }, "require": { @@ -5637,7 +5429,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.9.3" + "source": "https://github.com/Seldaek/monolog/tree/2.10.0" }, "funding": [ { @@ -5649,83 +5441,20 @@ "type": "tidelift" } ], - "time": "2024-04-12T20:52:51+00:00" - }, - { - "name": "myclabs/php-enum", - "version": "1.8.4", - "source": { - "type": "git", - "url": "https://github.com/myclabs/php-enum.git", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.6.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "MyCLabs\\Enum\\": "src/" - }, - "classmap": [ - "stubs/Stringable.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP Enum contributors", - "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" - } - ], - "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", - "keywords": [ - "enum" - ], - "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.4" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", - "type": "tidelift" - } - ], - "time": "2022-08-04T09:53:51+00:00" + "time": "2024-11-12T12:43:37+00:00" }, { "name": "nelmio/api-doc-bundle", - "version": "v4.34.0", + "version": "v4.38.1", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "ff9139576376695d2c3febc23a5e7eab91866d83" + "reference": "fe4247aa059e8c703f57bd10827a8f2de951a5af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/ff9139576376695d2c3febc23a5e7eab91866d83", - "reference": "ff9139576376695d2c3febc23a5e7eab91866d83", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/fe4247aa059e8c703f57bd10827a8f2de951a5af", + "reference": "fe4247aa059e8c703f57bd10827a8f2de951a5af", "shasum": "" }, "require": { @@ -5746,7 +5475,7 @@ "symfony/options-resolver": "^5.4 || ^6.4 || ^7.1", "symfony/property-info": "^5.4.10 || ^6.4 || ^7.1", "symfony/routing": "^5.4 || ^6.4 || ^7.1", - "zircote/swagger-php": "^4.6.1" + "zircote/swagger-php": "^4.11.1 || ^5.0" }, "conflict": { "zircote/swagger-php": "4.8.7" @@ -5799,7 +5528,7 @@ "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "4.x-dev" + "dev-4.x": "4.x-dev" } }, "autoload": { @@ -5826,7 +5555,7 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.34.0" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.38.1" }, "funding": [ { @@ -5834,7 +5563,7 @@ "type": "github" } ], - "time": "2025-01-08T11:43:02+00:00" + "time": "2025-03-07T18:57:31+00:00" }, { "name": "nelmio/cors-bundle", @@ -5898,6 +5627,62 @@ }, "time": "2024-06-24T21:25:28+00:00" }, + { + "name": "nikic/php-parser", + "version": "v4.19.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" + }, + "time": "2024-09-29T15:01:53+00:00" + }, { "name": "pagerfanta/core", "version": "v3.8.0", @@ -5945,29 +5730,26 @@ }, { "name": "pagerfanta/doctrine-orm-adapter", - "version": "v3.8.0", + "version": "v4.7.1", "source": { "type": "git", "url": "https://github.com/Pagerfanta/doctrine-orm-adapter.git", - "reference": "d0865fbfc7f8dd6e4c16f76135f904c60c3af3b5" + "reference": "b3be49948e84b67c023c820abc54ea574a100d5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Pagerfanta/doctrine-orm-adapter/zipball/d0865fbfc7f8dd6e4c16f76135f904c60c3af3b5", - "reference": "d0865fbfc7f8dd6e4c16f76135f904c60c3af3b5", + "url": "https://api.github.com/repos/Pagerfanta/doctrine-orm-adapter/zipball/b3be49948e84b67c023c820abc54ea574a100d5d", + "reference": "b3be49948e84b67c023c820abc54ea574a100d5d", "shasum": "" }, "require": { - "doctrine/orm": "^2.8", - "pagerfanta/core": "^3.0", - "php": "^7.4 || ^8.0", - "symfony/deprecation-contracts": "^2.1 || ^3.0" + "doctrine/orm": "^2.14 || ^3.0", + "pagerfanta/core": "^3.7 || ^4.0", + "php": "^8.1" }, "require-dev": { - "doctrine/annotations": "^1.11.1", - "doctrine/cache": "^1.11 || ^2.0", - "phpunit/phpunit": "^9.6 || ^10.0", - "symfony/cache": "^4.4 || ^5.4 || ^6.0" + "phpunit/phpunit": "^10.5", + "symfony/cache": "^5.4 || ^6.3 || ^7.0" }, "type": "library", "autoload": { @@ -5989,32 +5771,31 @@ "pagerfanta" ], "support": { - "source": "https://github.com/Pagerfanta/doctrine-orm-adapter/tree/v3.8.0" + "source": "https://github.com/Pagerfanta/doctrine-orm-adapter/tree/v4.7.1" }, - "time": "2023-04-15T16:39:14+00:00" + "time": "2024-11-30T19:18:10+00:00" }, { "name": "pagerfanta/twig", - "version": "v3.8.0", + "version": "v4.7.1", "source": { "type": "git", "url": "https://github.com/Pagerfanta/twig.git", - "reference": "19ba831401d7bc5249997c09c574e5c922773b12" + "reference": "f3eb17928c61fe6f72abc822825e1006afae6257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Pagerfanta/twig/zipball/19ba831401d7bc5249997c09c574e5c922773b12", - "reference": "19ba831401d7bc5249997c09c574e5c922773b12", + "url": "https://api.github.com/repos/Pagerfanta/twig/zipball/f3eb17928c61fe6f72abc822825e1006afae6257", + "reference": "f3eb17928c61fe6f72abc822825e1006afae6257", "shasum": "" }, "require": { - "pagerfanta/core": "^3.0", - "php": "^7.4 || ^8.0", - "symfony/polyfill-php80": "^1.15", + "pagerfanta/core": "^3.7 || ^4.0", + "php": "^8.1", "twig/twig": "^2.13 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { @@ -6034,9 +5815,9 @@ "pagerfanta" ], "support": { - "source": "https://github.com/Pagerfanta/twig/tree/v3.8.0" + "source": "https://github.com/Pagerfanta/twig/tree/v4.7.1" }, - "time": "2023-04-15T16:39:14+00:00" + "time": "2024-12-13T15:11:13+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -6157,16 +5938,16 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v3.7.2", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199" + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199", - "reference": "738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59", "shasum": "" }, "require": { @@ -6232,22 +6013,22 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.2" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.3" }, - "time": "2024-11-21T09:21:41+00:00" + "time": "2025-02-18T20:11:13+00:00" }, { "name": "php-amqplib/rabbitmq-bundle", - "version": "2.17.3", + "version": "2.17.4", "source": { "type": "git", "url": "https://github.com/php-amqplib/RabbitMqBundle.git", - "reference": "76ca2c9b9efddad5ade150b656efe10119a81acf" + "reference": "84f6c284daddd350b1b9ba3a4efabcb7c9c03479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/RabbitMqBundle/zipball/76ca2c9b9efddad5ade150b656efe10119a81acf", - "reference": "76ca2c9b9efddad5ade150b656efe10119a81acf", + "url": "https://api.github.com/repos/php-amqplib/RabbitMqBundle/zipball/84f6c284daddd350b1b9ba3a4efabcb7c9c03479", + "reference": "84f6c284daddd350b1b9ba3a4efabcb7c9c03479", "shasum": "" }, "require": { @@ -6311,9 +6092,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/RabbitMqBundle/issues", - "source": "https://github.com/php-amqplib/RabbitMqBundle/tree/2.17.3" + "source": "https://github.com/php-amqplib/RabbitMqBundle/tree/2.17.4" }, - "time": "2025-01-08T09:24:19+00:00" + "time": "2025-08-03T22:35:15+00:00" }, { "name": "php-http/client-common", @@ -6463,74 +6244,6 @@ }, "time": "2024-10-02T11:20:13+00:00" }, - { - "name": "php-http/guzzle5-adapter", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/guzzle5-adapter.git", - "reference": "cce48360b1f8a3467bd94e853e6107aa4532008e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/guzzle5-adapter/zipball/cce48360b1f8a3467bd94e853e6107aa4532008e", - "reference": "cce48360b1f8a3467bd94e853e6107aa4532008e", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^5.1", - "php": "^7.0", - "php-http/discovery": "^1.0", - "php-http/httplug": "^2.0" - }, - "provide": { - "php-http/client-implementation": "1.0", - "psr/http-client-implementation": "1.0" - }, - "require-dev": { - "ext-curl": "*", - "guzzlehttp/ringphp": "^1.1", - "php-http/client-integration-tests": "^2.0", - "phpunit/phpunit": "^6.0 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Adapter\\Guzzle5\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eric GELOEN", - "email": "geloen.eric@gmail.com" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Guzzle 5 HTTP Adapter", - "homepage": "http://httplug.io", - "keywords": [ - "Guzzle", - "http" - ], - "support": { - "issues": "https://github.com/php-http/guzzle5-adapter/issues", - "source": "https://github.com/php-http/guzzle5-adapter/tree/2.0.0" - }, - "abandoned": "php-http/guzzle7-adapter", - "time": "2019-02-05T12:28:45+00:00" - }, { "name": "php-http/httplug", "version": "2.4.1", @@ -6588,169 +6301,6 @@ }, "time": "2024-09-23T11:39:58+00:00" }, - { - "name": "php-http/httplug-bundle", - "version": "1.34.3", - "source": { - "type": "git", - "url": "https://github.com/php-http/HttplugBundle.git", - "reference": "87c61d27c025dd9d699a2208a6d06be58061e433" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/HttplugBundle/zipball/87c61d27c025dd9d699a2208a6d06be58061e433", - "reference": "87c61d27c025dd9d699a2208a6d06be58061e433", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "php-http/client-common": "^1.9 || ^2.0", - "php-http/client-implementation": "^1.0", - "php-http/discovery": "^1.14", - "php-http/httplug": "^2.0", - "php-http/logger-plugin": "^1.1", - "php-http/message": "^1.13", - "php-http/message-factory": "^1.0.2", - "php-http/stopwatch-plugin": "^1.2", - "psr/http-message": "^1.0 || ^2.0", - "symfony/config": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/options-resolver": "^4.4 || ^5.0 || ^6.0 || ^7.0" - }, - "conflict": { - "php-http/cache-plugin": "<1.7.0", - "php-http/curl-client": "<2.0", - "php-http/guzzle6-adapter": "<1.1", - "php-http/socket-client": "<2.0", - "php-http/throttle-plugin": "<1.1" - }, - "require-dev": { - "guzzlehttp/psr7": "^1.7 || ^2.0", - "matthiasnoback/symfony-config-test": "^4.3 || ^5.0", - "matthiasnoback/symfony-dependency-injection-test": "^4.3.1 || ^5.0", - "nyholm/nsa": "^1.1", - "nyholm/psr7": "^1.2.1", - "php-http/cache-plugin": "^1.7", - "php-http/mock-client": "^1.2", - "php-http/promise": "^1.0", - "phpunit/phpunit": "^9.6", - "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/cache": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/dom-crawler": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/http-foundation": "^4.4.19 || ^5.0 || ^6.0 || ^7.0", - "symfony/stopwatch": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/web-profiler-bundle": "^4.4.19 || ^5.0 || ^6.0 || ^7.0", - "twig/twig": "^1.41 || ^2.10 || ^3.0" - }, - "suggest": { - "php-http/cache-plugin": "To configure clients that cache responses", - "php-http/mock-client": "Add this to your require-dev section to mock HTTP responses easily", - "twig/twig": "Add this to your require-dev section when using the WebProfilerBundle" - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "Http\\HttplugBundle\\": "src/" - }, - "exclude-from-classmap": [ - "/Tests/Resources/MyPsr18TestClient.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "David Buchmann", - "email": "mail@davidbu.ch" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "Symfony integration for HTTPlug", - "homepage": "http://httplug.io", - "keywords": [ - "adapter", - "bundle", - "discovery", - "factory", - "http", - "httplug", - "message", - "php-http" - ], - "support": { - "issues": "https://github.com/php-http/HttplugBundle/issues", - "source": "https://github.com/php-http/HttplugBundle/tree/1.34.3" - }, - "time": "2024-09-01T08:25:40+00:00" - }, - { - "name": "php-http/logger-plugin", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/php-http/logger-plugin.git", - "reference": "bf47eb5cb379962d276c94da14861669c2313563" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/logger-plugin/zipball/bf47eb5cb379962d276c94da14861669c2313563", - "reference": "bf47eb5cb379962d276c94da14861669c2313563", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "php-http/client-common": "^1.9 || ^2.0", - "php-http/message": "^1.0", - "psr/log": "^1.0 || ^2 || ^3", - "symfony/polyfill-php73": "^1.17" - }, - "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Client\\Common\\Plugin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "PSR-3 Logger plugin for HTTPlug", - "homepage": "http://httplug.io", - "keywords": [ - "http", - "httplug", - "logger", - "plugin" - ], - "support": { - "issues": "https://github.com/php-http/logger-plugin/issues", - "source": "https://github.com/php-http/logger-plugin/tree/1.3.1" - }, - "time": "2024-09-01T06:51:51+00:00" - }, { "name": "php-http/message", "version": "1.16.2", @@ -6927,64 +6477,6 @@ }, "time": "2024-03-15T13:55:21+00:00" }, - { - "name": "php-http/stopwatch-plugin", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/php-http/stopwatch-plugin.git", - "reference": "11862cfbc719afade4ff407964ab3fbfe9ec2baa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/stopwatch-plugin/zipball/11862cfbc719afade4ff407964ab3fbfe9ec2baa", - "reference": "11862cfbc719afade4ff407964ab3fbfe9ec2baa", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "php-http/client-common": "^1.9 || ^2.0", - "symfony/stopwatch": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "require-dev": { - "guzzlehttp/psr7": "^2.1", - "symfony/phpunit-bridge": "^6.4.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Client\\Common\\Plugin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Symfony Stopwatch plugin for HTTPlug", - "homepage": "http://httplug.io", - "keywords": [ - "http", - "httplug", - "plugin", - "stopwatch" - ], - "support": { - "issues": "https://github.com/php-http/stopwatch-plugin/issues", - "source": "https://github.com/php-http/stopwatch-plugin/tree/1.4.2" - }, - "time": "2023-12-05T14:36:57+00:00" - }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -7162,16 +6654,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.43", + "version": "3.0.46", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", "shasum": "" }, "require": { @@ -7252,7 +6744,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" }, "funding": [ { @@ -7268,20 +6760,20 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:12:59+00:00" + "time": "2025-06-26T16:29:55+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { @@ -7313,9 +6805,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-10-13T11:29:49+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpzip/phpzip", @@ -7519,25 +7011,27 @@ }, { "name": "predis/predis", - "version": "v2.3.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "url": "https://api.github.com/repos/predis/predis/zipball/9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^8.0 || ^9.4" + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ~9.4.4" }, "suggest": { "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" @@ -7559,7 +7053,7 @@ "role": "Maintainer" } ], - "description": "A flexible and feature-complete Redis client for PHP.", + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", "homepage": "http://github.com/predis/predis", "keywords": [ "nosql", @@ -7568,7 +7062,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.3.0" + "source": "https://github.com/predis/predis/tree/v3.2.0" }, "funding": [ { @@ -7576,24 +7070,24 @@ "type": "github" } ], - "time": "2024-11-21T20:00:02+00:00" + "time": "2025-08-06T06:41:24+00:00" }, { "name": "psr/cache", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { @@ -7613,7 +7107,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for caching libraries", @@ -7623,9 +7117,9 @@ "psr-6" ], "support": { - "source": "https://github.com/php-fig/cache/tree/master" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2016-08-06T20:24:11+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { "name": "psr/clock", @@ -7985,25 +7479,25 @@ }, { "name": "psr/simple-cache", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -8018,7 +7512,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for simple caching", @@ -8030,9 +7524,9 @@ "simple-cache" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2017-10-23T01:57:42+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { "name": "ralouphie/getallheaders", @@ -8078,136 +7572,6 @@ }, "time": "2019-03-08T08:55:37+00:00" }, - { - "name": "react/promise", - "version": "v2.11.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "1a8460931ea36dc5c76838fec5734d55c88c6831" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/1a8460931ea36dc5c76838fec5734d55c88c6831", - "reference": "1a8460931ea36dc5c76838fec5734d55c88c6831", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.11.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-16T16:16:50+00:00" - }, - { - "name": "rulerz-php/doctrine-orm", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/rulerz-php/doctrine-orm.git", - "reference": "3ff5c8507e23942fa4af14818ab92f0e91bfaebe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rulerz-php/doctrine-orm/zipball/3ff5c8507e23942fa4af14818ab92f0e91bfaebe", - "reference": "3ff5c8507e23942fa4af14818ab92f0e91bfaebe", - "shasum": "" - }, - "require": { - "doctrine/orm": "^2.4", - "kphoen/rulerz": "dev-master as 1.0.0", - "php": ">=7.1" - }, - "require-dev": { - "behat/behat": "~3.0", - "kphoen/rusty": "dev-master", - "liip/rmt": "^1.2", - "phpunit/phpunit": "^7.1" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "RulerZ\\DoctrineORM\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kévin Gomez", - "email": "contact@kevingomez.fr", - "homepage": "http://www.kevingomez.fr/" - } - ], - "description": "Doctrine ORM compilation target for RulerZ", - "keywords": [ - "doctrine", - "engine", - "orm", - "rule", - "rulerz", - "specification" - ], - "support": { - "issues": "https://github.com/rulerz-php/doctrine-orm/issues", - "source": "https://github.com/rulerz-php/doctrine-orm/tree/master" - }, - "time": "2020-01-03T14:08:51+00:00" - }, { "name": "scheb/2fa-backup-code", "version": "v5.13.2", @@ -8422,56 +7786,6 @@ }, "time": "2022-01-03T10:21:24+00:00" }, - { - "name": "scheb/2fa-qr-code", - "version": "v5.13.2", - "source": { - "type": "git", - "url": "https://github.com/scheb/2fa-qr-code.git", - "reference": "1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-qr-code/zipball/1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf", - "reference": "1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf", - "shasum": "" - }, - "require": { - "endroid/qr-code": "^3.0", - "scheb/2fa-bundle": "self.version" - }, - "type": "library", - "autoload": { - "psr-4": { - "Scheb\\TwoFactorBundle\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Scheb", - "email": "me@christianscheb.de" - } - ], - "description": "Extends scheb/2fa-bundle with the ability to render QR-codes for Google Authenticator or TOTP", - "homepage": "https://github.com/scheb/2fa", - "keywords": [ - "2fa", - "Authentication", - "qr-code", - "symfony", - "two-factor", - "two-step" - ], - "support": { - "source": "https://github.com/scheb/2fa-qr-code/tree/v5.13.2" - }, - "abandoned": true, - "time": "2022-01-03T10:21:24+00:00" - }, { "name": "scheb/2fa-trusted-device", "version": "v5.13.2", @@ -8523,46 +7837,48 @@ }, { "name": "scssphp/scssphp", - "version": "v1.13.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "63d1157457e5554edf00b0c1fabab4c1511d2520" + "reference": "024f92cd9782e3033b41c2d1c66ab1c0e5c12c0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/63d1157457e5554edf00b0c1fabab4c1511d2520", - "reference": "63d1157457e5554edf00b0c1fabab4c1511d2520", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/024f92cd9782e3033b41c2d1c66ab1c0e5c12c0f", + "reference": "024f92cd9782e3033b41c2d1c66ab1c0e5c12c0f", "shasum": "" }, "require": { "ext-ctype": "*", "ext-json": "*", - "php": ">=5.6.0" + "league/uri": "^7.4", + "league/uri-interfaces": "^7.4", + "php": ">=8.1", + "scssphp/source-span": "^1.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.30" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "phpunit/phpunit": "^9.5.6", "sass/sass-spec": "*", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", + "symfony/var-dumper": "^6.3", "thoughtbot/bourbon": "^7.0", "twbs/bootstrap": "~5.0", "twbs/bootstrap4": "4.6.1", "zurb/foundation": "~6.7.0" }, "suggest": { - "ext-iconv": "Can be used as fallback when ext-mbstring is not available", - "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv" + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than the polyfill" }, - "bin": [ - "bin/pscss" - ], "type": "library", "extra": { "bamarni-bin": { - "forward-command": false, - "bin-links": false + "bin-links": false, + "forward-command": false } }, "autoload": { @@ -8597,9 +7913,67 @@ ], "support": { "issues": "https://github.com/scssphp/scssphp/issues", - "source": "https://github.com/scssphp/scssphp/tree/v1.13.0" + "source": "https://github.com/scssphp/scssphp/tree/v2.0.1" }, - "time": "2024-08-17T21:02:11+00:00" + "time": "2025-01-31T12:28:20+00:00" + }, + { + "name": "scssphp/source-span", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/scssphp/source-span.git", + "reference": "f08fc78765e6fb6fa8ca0573fc61b3f8860f0114" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scssphp/source-span/zipball/f08fc78765e6fb6fa8ca0573fc61b3f8860f0114", + "reference": "f08fc78765e6fb6fa8ca0573fc61b3f8860f0114", + "shasum": "" + }, + "require": { + "league/uri": "^7.4", + "league/uri-interfaces": "^7.4", + "php": ">=8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpunit/phpunit": "^9.5.6", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", + "symfony/var-dumper": "^6.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "SourceSpan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "homepage": "https://github.com/stof" + } + ], + "description": "Provides a representation for source code locations and spans.", + "keywords": [ + "parsing" + ], + "support": { + "issues": "https://github.com/scssphp/source-span/issues", + "source": "https://github.com/scssphp/source-span/tree/v1.0.0" + }, + "time": "2024-12-09T23:08:15+00:00" }, { "name": "sensio/framework-extra-bundle", @@ -8681,16 +8055,16 @@ }, { "name": "sentry/sentry", - "version": "4.10.0", + "version": "4.14.2", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "2af937d47d8aadb8dab0b1d7b9557e495dd12856" + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/2af937d47d8aadb8dab0b1d7b9557e495dd12856", - "reference": "2af937d47d8aadb8dab0b1d7b9557e495dd12856", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/bfeec74303d60d3f8bc33701ab3e86f8a8729f17", + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17", "shasum": "" }, "require": { @@ -8754,7 +8128,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.10.0" + "source": "https://github.com/getsentry/sentry-php/tree/4.14.2" }, "funding": [ { @@ -8766,27 +8140,27 @@ "type": "custom" } ], - "time": "2024-11-06T07:44:19+00:00" + "time": "2025-07-21T08:28:29+00:00" }, { "name": "sentry/sentry-symfony", - "version": "5.1.0", + "version": "5.3.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-symfony.git", - "reference": "d8eb611b79cde1016470585a96f47f47d5374a49" + "reference": "f98f641adec07f0d6ef9a63f2e84a97f533a4466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/d8eb611b79cde1016470585a96f47f47d5374a49", - "reference": "d8eb611b79cde1016470585a96f47f47d5374a49", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/f98f641adec07f0d6ef9a63f2e84a97f533a4466", + "reference": "f98f641adec07f0d6ef9a63f2e84a97f533a4466", "shasum": "" }, "require": { "guzzlehttp/psr7": "^2.1.1", "jean85/pretty-package-versions": "^1.5||^2.0", "php": "^7.2||^8.0", - "sentry/sentry": "^4.10.0", + "sentry/sentry": "^4.14.1", "symfony/cache-contracts": "^1.1||^2.4||^3.0", "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0", @@ -8794,9 +8168,7 @@ "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/polyfill-php80": "^1.22", - "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0", - "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", - "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0" + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0" }, "require-dev": { "doctrine/dbal": "^2.13||^3.3||^4.0", @@ -8817,6 +8189,8 @@ "symfony/monolog-bundle": "^3.4", "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0", "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0", "vimeo/psalm": "^4.3||^5.16.0" @@ -8856,7 +8230,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-symfony/issues", - "source": "https://github.com/getsentry/sentry-symfony/tree/5.1.0" + "source": "https://github.com/getsentry/sentry-symfony/tree/5.3.1" }, "funding": [ { @@ -8868,20 +8242,20 @@ "type": "custom" } ], - "time": "2024-11-20T09:43:49+00:00" + "time": "2025-07-30T19:30:27+00:00" }, { "name": "simplepie/simplepie", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/simplepie/simplepie.git", - "reference": "65b095d87bc00898d8fa7737bdbcda93a3fbcc55" + "reference": "a567b8ab9b6145a23e6a9ec2b6b74f56d52f7ad1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplepie/simplepie/zipball/65b095d87bc00898d8fa7737bdbcda93a3fbcc55", - "reference": "65b095d87bc00898d8fa7737bdbcda93a3fbcc55", + "url": "https://api.github.com/repos/simplepie/simplepie/zipball/a567b8ab9b6145a23e6a9ec2b6b74f56d52f7ad1", + "reference": "a567b8ab9b6145a23e6a9ec2b6b74f56d52f7ad1", "shasum": "" }, "require": { @@ -8942,9 +8316,9 @@ ], "support": { "issues": "https://github.com/simplepie/simplepie/issues", - "source": "https://github.com/simplepie/simplepie/tree/1.8.0" + "source": "https://github.com/simplepie/simplepie/tree/1.8.1" }, - "time": "2023-01-20T08:37:35+00:00" + "time": "2024-11-22T16:33:20+00:00" }, { "name": "smalot/pdfparser", @@ -8998,16 +8372,16 @@ }, { "name": "spiriitlabs/form-filter-bundle", - "version": "v10.0.0", + "version": "v10.0.2", "source": { "type": "git", "url": "https://github.com/SpiriitLabs/form-filter-bundle.git", - "reference": "7a1ab5c7f8d1890562f8a3e57796bbb8f84e5be8" + "reference": "78a8aaaf4f0e51d1ba6ffd59e371e137f9517123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SpiriitLabs/form-filter-bundle/zipball/7a1ab5c7f8d1890562f8a3e57796bbb8f84e5be8", - "reference": "7a1ab5c7f8d1890562f8a3e57796bbb8f84e5be8", + "url": "https://api.github.com/repos/SpiriitLabs/form-filter-bundle/zipball/78a8aaaf4f0e51d1ba6ffd59e371e137f9517123", + "reference": "78a8aaaf4f0e51d1ba6ffd59e371e137f9517123", "shasum": "" }, "require": { @@ -9044,9 +8418,9 @@ "symfony" ], "support": { - "source": "https://github.com/SpiriitLabs/form-filter-bundle/tree/v10.0.0" + "source": "https://github.com/SpiriitLabs/form-filter-bundle/tree/v10.0.2" }, - "time": "2024-01-15T14:09:58+00:00" + "time": "2024-08-23T08:46:41+00:00" }, { "name": "spomky-labs/otphp", @@ -9082,9 +8456,9 @@ "type": "library", "extra": { "branch-alias": { - "v10.0": "10.0.x-dev", + "v8.3": "8.3.x-dev", "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" + "v10.0": "10.0.x-dev" } }, "autoload": { @@ -9278,59 +8652,130 @@ "time": "2024-10-22T13:05:35+00:00" }, { - "name": "symfony/cache", - "version": "v5.4.46", + "name": "symfony/browser-kit", + "version": "v5.4.45", "source": { "type": "git", - "url": "https://github.com/symfony/cache.git", - "reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b" + "url": "https://github.com/symfony/browser-kit.git", + "reference": "03cce39764429e07fbab9b989a1182a24578341d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/0fe08ee32cec2748fbfea10c52d3ee02049e0f6b", - "reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/03cce39764429e07fbab9b989a1182a24578341d", + "reference": "03cce39764429e07fbab9b989a1182a24578341d", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-22T13:05:35+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/d038cd3054aeaf1c674022a77048b2ef6376a175", + "reference": "d038cd3054aeaf1c674022a77048b2ef6376a175", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", - "symfony/cache-contracts": "^1.1.7|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "symfony/cache-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.3.6|^7.0" }, "conflict": { "doctrine/dbal": "<2.13.1", - "symfony/dependency-injection": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/var-dumper": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" }, "provide": { - "psr/cache-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0|2.0", - "symfony/cache-implementation": "1.0|2.0" + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/cache": "^1.6|^2.0", "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", - "psr/simple-cache": "^1.0|^2.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "classmap": [ + "Traits/ValueWrapper.php" + ], "exclude-from-classmap": [ "/Tests/" ] @@ -9356,7 +8801,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.46" + "source": "https://github.com/symfony/cache/tree/v6.4.24" }, "funding": [ { @@ -9367,33 +8812,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-04T11:43:55+00:00" + "time": "2025-07-30T09:32:03+00:00" }, { "name": "symfony/cache-contracts", - "version": "v2.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e" + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/517c3a3619dadfa6952c4651767fcadffb4df65e", - "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0" - }, - "suggest": { - "symfony/cache-implementation": "" + "php": ">=8.1", + "psr/cache": "^3.0" }, "type": "library", "extra": { @@ -9402,7 +8848,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -9435,7 +8881,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v2.5.4" + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" }, "funding": [ { @@ -9451,7 +8897,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-03-13T15:25:07+00:00" }, { "name": "symfony/config", @@ -9722,20 +9168,20 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.4", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { @@ -9744,7 +9190,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -9769,7 +9215,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -9785,7 +9231,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/doctrine-bridge", @@ -9905,16 +9351,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.4.45", + "version": "v5.4.48", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "89647a57db280f9f93c27271fea58babb77bb473" + "reference": "b57df76f4757a9a8dfbb57ba48d7780cc20776c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/89647a57db280f9f93c27271fea58babb77bb473", - "reference": "89647a57db280f9f93c27271fea58babb77bb473", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b57df76f4757a9a8dfbb57ba48d7780cc20776c6", + "reference": "b57df76f4757a9a8dfbb57ba48d7780cc20776c6", "shasum": "" }, "require": { @@ -9960,7 +9406,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.45" + "source": "https://github.com/symfony/dom-crawler/tree/v5.4.48" }, "funding": [ { @@ -9976,7 +9422,7 @@ "type": "tidelift" } ], - "time": "2024-10-22T13:05:35+00:00" + "time": "2024-11-13T14:36:38+00:00" }, { "name": "symfony/error-handler", @@ -10727,23 +10173,23 @@ }, { "name": "symfony/http-client", - "version": "v5.4.47", + "version": "v5.4.49", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde" + "reference": "d77d8e212cde7b5c4a64142bf431522f19487c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde", - "reference": "3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde", + "url": "https://api.github.com/repos/symfony/http-client/zipball/d77d8e212cde7b5c4a64142bf431522f19487c28", + "reference": "d77d8e212cde7b5c4a64142bf431522f19487c28", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-client-contracts": "^2.5.3", + "symfony/http-client-contracts": "^2.5.4", "symfony/polyfill-php73": "^1.11", "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.0|^2|^3" @@ -10798,7 +10244,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.47" + "source": "https://github.com/symfony/http-client/tree/v5.4.49" }, "funding": [ { @@ -10814,7 +10260,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T12:18:12+00:00" + "time": "2024-11-28T08:37:04+00:00" }, { "name": "symfony/http-client-contracts", @@ -11336,43 +10782,38 @@ }, { "name": "symfony/monolog-bridge", - "version": "v5.4.45", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "cf7d75d4d64a41fbb1c0e92301bec404134fa84b" + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/cf7d75d4d64a41fbb1c0e92301bec404134fa84b", - "reference": "cf7d75d4d64a41fbb1c0e92301bec404134fa84b", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/9d14621e59f22c2b6d030d92d37ffe5ae1e60452", + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452", "shasum": "" }, "require": { - "monolog/monolog": "^1.25.1|^2", - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^5.3|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/console": "<4.4", - "symfony/http-foundation": "<5.3" + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<5.4" }, "require-dev": { - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/mailer": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/security-core": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", - "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", - "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "symfony-bridge", "autoload": { @@ -11400,7 +10841,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v5.4.45" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.13" }, "funding": [ { @@ -11416,7 +10857,7 @@ "type": "tidelift" } ], - "time": "2024-10-10T06:37:45+00:00" + "time": "2024-10-14T08:49:08+00:00" }, { "name": "symfony/monolog-bundle", @@ -11570,29 +11011,27 @@ }, { "name": "symfony/password-hasher", - "version": "v5.4.45", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "6c5993b24505f98b90ca4896448012bbec54c7c8" + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/6c5993b24505f98b90ca4896448012bbec54c7c8", - "reference": "6c5993b24505f98b90ca4896448012bbec54c7c8", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/e97a1b31f60b8bdfc1fdedab4398538da9441d47", + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1" }, "conflict": { - "symfony/security-core": "<5.3" + "symfony/security-core": "<5.4" }, "require-dev": { - "symfony/console": "^5.3|^6.0", - "symfony/security-core": "^5.3|^6.0" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -11624,7 +11063,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v5.4.45" + "source": "https://github.com/symfony/password-hasher/tree/v6.4.13" }, "funding": [ { @@ -11640,11 +11079,11 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -11703,7 +11142,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -11714,6 +11153,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -11723,16 +11166,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -11781,7 +11224,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -11792,12 +11235,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-icu", @@ -11822,8 +11269,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -11907,8 +11354,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -11968,7 +11415,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -12029,7 +11476,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -12040,6 +11487,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -12049,19 +11500,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -12109,7 +11561,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -12121,68 +11573,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce", - "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "metapackage", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -12190,20 +11581,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { - "name": "symfony/polyfill-php73", + "name": "symfony/polyfill-php84", "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", "shasum": "" }, "require": { @@ -12221,7 +11612,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php84\\": "" }, "classmap": [ "Resources/stubs" @@ -12241,7 +11632,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -12250,7 +11641,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" }, "funding": [ { @@ -12266,163 +11657,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-09T12:04:04+00:00" }, { "name": "symfony/property-access", @@ -12507,42 +11742,37 @@ }, { "name": "symfony/property-info", - "version": "v5.4.48", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "a0396295ad585f95fccd690bc6a281e5bd303902" + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/a0396295ad585f95fccd690bc6a281e5bd303902", - "reference": "a0396295ad585f95fccd690bc6a281e5bd303902", + "url": "https://api.github.com/repos/symfony/property-info/zipball/94d18e5cc11a37fd92856d38b61d9cdf72536a1e", + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/string": "^5.1|^6.0" + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/dependency-injection": "<4.4" + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4|^2", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" - }, - "suggest": { - "phpdocumentor/reflection-docblock": "To use the PHPDoc", - "psr/cache-implementation": "To cache results", - "symfony/doctrine-bridge": "To use Doctrine metadata", - "symfony/serializer": "To use Serializer metadata" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0" }, "type": "library", "autoload": { @@ -12578,7 +11808,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v5.4.48" + "source": "https://github.com/symfony/property-info/tree/v6.4.18" }, "funding": [ { @@ -12594,7 +11824,7 @@ "type": "tidelift" } ], - "time": "2024-11-25T16:14:41+00:00" + "time": "2025-01-21T10:52:27+00:00" }, { "name": "symfony/proxy-manager-bridge", @@ -13041,32 +12271,27 @@ }, { "name": "symfony/security-csrf", - "version": "v5.4.45", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "28dcafc3220f12264bb2aabe2389a2163458c1f4" + "reference": "c34421b7d34efbaef5d611ab2e646a0ec464ffe3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/28dcafc3220f12264bb2aabe2389a2163458c1f4", - "reference": "28dcafc3220f12264bb2aabe2389a2163458c1f4", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/c34421b7d34efbaef5d611ab2e646a0ec464ffe3", + "reference": "c34421b7d34efbaef5d611ab2e646a0ec464ffe3", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/security-core": "^4.4|^5.0|^6.0" + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0|^7.0" }, "conflict": { - "symfony/http-foundation": "<5.3" + "symfony/http-foundation": "<5.4" }, "require-dev": { - "symfony/http-foundation": "^5.3|^6.0" - }, - "suggest": { - "symfony/http-foundation": "For using the class SessionTokenStorage." + "symfony/http-foundation": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -13094,7 +12319,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v5.4.45" + "source": "https://github.com/symfony/security-csrf/tree/v6.4.13" }, "funding": [ { @@ -13110,7 +12335,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/security-guard", @@ -13454,21 +12679,21 @@ }, { "name": "symfony/stopwatch", - "version": "v5.4.45", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004" + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fb2c199cf302eb207f8c23e7ee174c1c31a5c004", - "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1|^2|^3" + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -13496,7 +12721,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.45" + "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" }, "funding": [ { @@ -13512,38 +12737,38 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-02-24T10:49:57+00:00" }, { "name": "symfony/string", - "version": "v5.4.47", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" + "reference": "f0ce0bd36a3accb4a225435be077b4b4875587f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", - "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", + "url": "https://api.github.com/repos/symfony/string/zipball/f0ce0bd36a3accb4a225435be077b4b4875587f4", + "reference": "f0ce0bd36a3accb4a225435be077b4b4875587f4", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": ">=3.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -13582,7 +12807,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.47" + "source": "https://github.com/symfony/string/tree/v6.4.24" }, "funding": [ { @@ -13593,12 +12818,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-10T20:33:58+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/templating", @@ -13670,53 +12899,51 @@ }, { "name": "symfony/translation", - "version": "v5.4.45", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "98f26acc99341ca4bab345fb14d7b1d7cb825bed" + "reference": "3b9bf9f33997c064885a7bfc126c14b9daa0e00e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/98f26acc99341ca4bab345fb14d7b1d7cb825bed", - "reference": "98f26acc99341ca4bab345fb14d7b1d7cb825bed", + "url": "https://api.github.com/repos/symfony/translation/zipball/3b9bf9f33997c064885a7bfc126c14b9daa0e00e", + "reference": "3b9bf9f33997c064885a7bfc126c14b9daa0e00e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^2.3" + "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<4.4", - "symfony/console": "<5.3", - "symfony/dependency-injection": "<5.0", - "symfony/http-kernel": "<5.0", - "symfony/twig-bundle": "<5.0", - "symfony/yaml": "<4.4" + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" }, "provide": { - "symfony/translation-implementation": "2.3" + "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { + "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2.0|^3.0", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/service-contracts": "^1.1.2|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log-implementation": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -13747,7 +12974,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.45" + "source": "https://github.com/symfony/translation/tree/v6.4.19" }, "funding": [ { @@ -13763,7 +12990,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-02-13T10:18:43+00:00" }, { "name": "symfony/translation-contracts", @@ -13845,16 +13072,16 @@ }, { "name": "symfony/twig-bridge", - "version": "v5.4.45", + "version": "v5.4.48", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "b3d3738b4be14bf1a4544a6faeed89463fe8b60e" + "reference": "853a0c9aa40123a9feeb335c865b659d94e49e5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/b3d3738b4be14bf1a4544a6faeed89463fe8b60e", - "reference": "b3d3738b4be14bf1a4544a6faeed89463fe8b60e", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/853a0c9aa40123a9feeb335c865b659d94e49e5d", + "reference": "853a0c9aa40123a9feeb335c865b659d94e49e5d", "shasum": "" }, "require": { @@ -13946,7 +13173,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v5.4.45" + "source": "https://github.com/symfony/twig-bridge/tree/v5.4.48" }, "funding": [ { @@ -13962,7 +13189,7 @@ "type": "tidelift" } ], - "time": "2024-10-24T15:46:29+00:00" + "time": "2024-11-22T08:19:51+00:00" }, { "name": "symfony/twig-bundle", @@ -14056,16 +13283,16 @@ }, { "name": "symfony/validator", - "version": "v5.4.47", + "version": "v5.4.48", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "7caeb2a5f5d5a8a82f243e4dd0b1e7b89e54dc33" + "reference": "883667679d93d6c30f1b7490d669801712d3be2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/7caeb2a5f5d5a8a82f243e4dd0b1e7b89e54dc33", - "reference": "7caeb2a5f5d5a8a82f243e4dd0b1e7b89e54dc33", + "url": "https://api.github.com/repos/symfony/validator/zipball/883667679d93d6c30f1b7490d669801712d3be2f", + "reference": "883667679d93d6c30f1b7490d669801712d3be2f", "shasum": "" }, "require": { @@ -14149,7 +13376,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v5.4.47" + "source": "https://github.com/symfony/validator/tree/v5.4.48" }, "funding": [ { @@ -14165,7 +13392,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T08:12:23+00:00" + "time": "2024-11-27T08:58:20+00:00" }, { "name": "symfony/var-dumper", @@ -14258,24 +13485,26 @@ }, { "name": "symfony/var-exporter", - "version": "v5.4.45", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "862700068db0ddfd8c5b850671e029a90246ec75" + "reference": "05b3e90654c097817325d6abd284f7938b05f467" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/862700068db0ddfd8c5b850671e029a90246ec75", - "reference": "862700068db0ddfd8c5b850671e029a90246ec75", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/05b3e90654c097817325d6abd284f7938b05f467", + "reference": "05b3e90654c097817325d6abd284f7938b05f467", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -14308,10 +13537,89 @@ "export", "hydrate", "instantiate", + "lazy-loading", + "proxy", "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.4.45" + "source": "https://github.com/symfony/var-exporter/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:47:49+00:00" + }, + { + "name": "symfony/webpack-encore-bundle", + "version": "v1.17.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/webpack-encore-bundle.git", + "reference": "471ebbc03072dad6e31840dc317bc634a32785f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/471ebbc03072dad6e31840dc317bc634a32785f5", + "reference": "471ebbc03072dad6e31840dc317bc634a32785f5", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/asset": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25.0", + "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.3 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/web-link": "^4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/webpack-encore", + "name": "symfony/webpack-encore" + } + }, + "autoload": { + "psr-4": { + "Symfony\\WebpackEncoreBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Webpack Encore!", + "support": { + "issues": "https://github.com/symfony/webpack-encore-bundle/issues", + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v1.17.2" }, "funding": [ { @@ -14327,35 +13635,32 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2023-09-26T14:36:28+00:00" }, { "name": "symfony/yaml", - "version": "v5.4.45", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e" + "reference": "742a8efc94027624b36b10ba58e23d402f961f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/742a8efc94027624b36b10ba58e23d402f961f51", + "reference": "742a8efc94027624b36b10ba58e23d402f961f51", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.3" + "symfony/console": "<5.4" }, "require-dev": { - "symfony/console": "^5.3|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "symfony/console": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -14386,7 +13691,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.45" + "source": "https://github.com/symfony/yaml/tree/v6.4.24" }, "funding": [ { @@ -14397,25 +13702,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "tecnickcom/tcpdf", - "version": "6.8.0", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "14ffa0e308f5634aa2489568b4b90b24073b6731" + "reference": "ca5b6de294512145db96bcbc94e61696599c391d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/14ffa0e308f5634aa2489568b4b90b24073b6731", - "reference": "14ffa0e308f5634aa2489568b4b90b24073b6731", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", + "reference": "ca5b6de294512145db96bcbc94e61696599c391d", "shasum": "" }, "require": { @@ -14428,8 +13737,6 @@ "config", "include", "tcpdf.php", - "tcpdf_parser.php", - "tcpdf_import.php", "tcpdf_barcodes_1d.php", "tcpdf_barcodes_2d.php", "include/tcpdf_colors.php", @@ -14467,51 +13774,58 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.8.0" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", "type": "custom" } ], - "time": "2024-12-23T13:34:57+00:00" + "time": "2025-05-27T18:02:28+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.3", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" + "thecodingmachine/phpstan-strict-rules": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.1-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { "files": [ "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", "deprecated/mssql.php", "deprecated/stats.php", + "deprecated/strings.php", "lib/special_cases.php", + "deprecated/mysqli.php", "generated/apache.php", "generated/apcu.php", "generated/array.php", @@ -14532,6 +13846,7 @@ "generated/fpm.php", "generated/ftp.php", "generated/funchand.php", + "generated/gettext.php", "generated/gmp.php", "generated/gnupg.php", "generated/hash.php", @@ -14541,7 +13856,6 @@ "generated/image.php", "generated/imap.php", "generated/info.php", - "generated/ingres-ii.php", "generated/inotify.php", "generated/json.php", "generated/ldap.php", @@ -14550,20 +13864,14 @@ "generated/mailparse.php", "generated/mbstring.php", "generated/misc.php", - "generated/msql.php", "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", "generated/network.php", "generated/oci8.php", "generated/opcache.php", "generated/openssl.php", "generated/outcontrol.php", - "generated/password.php", "generated/pcntl.php", "generated/pcre.php", - "generated/pdf.php", "generated/pgsql.php", "generated/posix.php", "generated/ps.php", @@ -14574,7 +13882,6 @@ "generated/sem.php", "generated/session.php", "generated/shmop.php", - "generated/simplexml.php", "generated/sockets.php", "generated/sodium.php", "generated/solr.php", @@ -14597,13 +13904,13 @@ "generated/zip.php", "generated/zlib.php" ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -14612,80 +13919,29 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" }, - "time": "2020-10-28T17:51:34+00:00" - }, - { - "name": "true/punycode", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/true/php-punycode.git", - "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", - "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "symfony/polyfill-mbstring": "^1.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.7", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "TrueBV\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Renan Gonçalves", - "email": "renan.saddam@gmail.com" - } - ], - "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", - "homepage": "https://github.com/true/php-punycode", - "keywords": [ - "idna", - "punycode" - ], - "support": { - "issues": "https://github.com/true/php-punycode/issues", - "source": "https://github.com/true/php-punycode/tree/master" - }, - "abandoned": true, - "time": "2016-11-16T10:37:54+00:00" + "time": "2023-04-05T11:54:14+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.11.0", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "bf8a304eac15838d7724fdf64c345bdefbb75f03" + "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/bf8a304eac15838d7724fdf64c345bdefbb75f03", - "reference": "bf8a304eac15838d7724fdf64c345bdefbb75f03", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/62d1cf47a1aa009cbd07b21045b97d3d5cb79896", + "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", "symfony/framework-bundle": "^5.4|^6.4|^7.0", "symfony/twig-bundle": "^5.4|^6.4|^7.0", - "twig/twig": "^3.0" + "twig/twig": "^3.2|^4.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", @@ -14727,7 +13983,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.11.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.21.0" }, "funding": [ { @@ -14739,27 +13995,27 @@ "type": "tidelift" } ], - "time": "2024-06-21T06:25:01+00:00" + "time": "2025-02-19T14:29:33+00:00" }, { "name": "twig/string-extra", - "version": "v3.11.0", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "d25c61baf38705a72ebb5a92d2e9ecb7c473b8ac" + "reference": "4b3337544ac8f76c280def94e32b53acfaec0589" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/d25c61baf38705a72ebb5a92d2e9ecb7c473b8ac", - "reference": "d25c61baf38705a72ebb5a92d2e9ecb7c473b8ac", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/4b3337544ac8f76c280def94e32b53acfaec0589", + "reference": "4b3337544ac8f76c280def94e32b53acfaec0589", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", - "twig/twig": "^3.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" @@ -14794,7 +14050,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.11.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.21.0" }, "funding": [ { @@ -14806,31 +14062,30 @@ "type": "tidelift" } ], - "time": "2024-08-07T17:34:09+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/twig", - "version": "v3.11.2", + "version": "v3.21.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "5b580ec1882b54c98cbd8c0f8a3ca5d1904db6b1" + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/5b580ec1882b54c98cbd8c0f8a3ca5d1904db6b1", - "reference": "5b580ec1882b54c98cbd8c0f8a3ca5d1904db6b1", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -14874,7 +14129,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.11.2" + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" }, "funding": [ { @@ -14886,7 +14141,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T18:50:16+00:00" + "time": "2025-05-03T07:21:55+00:00" }, { "name": "wallabag/phpepub", @@ -15222,35 +14477,35 @@ }, { "name": "willdurand/hateoas", - "version": "3.11.0", + "version": "3.12.0", "source": { "type": "git", "url": "https://github.com/willdurand/Hateoas.git", - "reference": "73fcb546b22df5d8e3483ba9a48820f499009acd" + "reference": "4cb096e52603de2e3c7f6dc3128fe01bb944634e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/willdurand/Hateoas/zipball/73fcb546b22df5d8e3483ba9a48820f499009acd", - "reference": "73fcb546b22df5d8e3483ba9a48820f499009acd", + "url": "https://api.github.com/repos/willdurand/Hateoas/zipball/4cb096e52603de2e3c7f6dc3128fe01bb944634e", + "reference": "4cb096e52603de2e3c7f6dc3128fe01bb944634e", "shasum": "" }, "require": { - "doctrine/annotations": "^1.13.2 || ^2.0", "jms/metadata": "^2.0", "jms/serializer": "^3.18.2", - "php": "^7.2 | ^8.0", - "symfony/expression-language": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0" + "php": "^8.1", + "symfony/expression-language": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0" }, "require-dev": { + "doctrine/annotations": "^1.13.2 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/persistence": "^1.3.4 | ^2.0 | ^3.0", - "pagerfanta/core": "^2.4 || ^3.0", + "pagerfanta/core": "^2.4 || ^3.0 || ^4.0", "phpdocumentor/type-resolver": "^1.5.1", "phpspec/prophecy": "^1.16", "phpspec/prophecy-phpunit": "^2.0.1", - "phpunit/phpunit": "^7 | ^9.5.10", - "symfony/routing": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", - "symfony/yaml": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", + "phpunit/phpunit": "^10.5.38", + "symfony/routing": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0", + "symfony/yaml": "^3.4.47 || ~4.0 || ~5.0 || ~6.0 || ~7.0", "twig/twig": "^1.43 || ^2.13 || ^3.0" }, "suggest": { @@ -15290,38 +14545,38 @@ "description": "A PHP library to support implementing representations for HATEOAS REST web services", "support": { "issues": "https://github.com/willdurand/Hateoas/issues", - "source": "https://github.com/willdurand/Hateoas/tree/3.11.0" + "source": "https://github.com/willdurand/Hateoas/tree/3.12.0" }, - "time": "2024-10-15T19:28:23+00:00" + "time": "2024-11-09T06:05:24+00:00" }, { "name": "willdurand/hateoas-bundle", - "version": "2.6.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/willdurand/BazingaHateoasBundle.git", - "reference": "ccbdde8356ef02cbc72ac7ec40c7f1db3382b4ef" + "reference": "a797892b0a5e79f92e34c8fefe0e27e31114ca65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/ccbdde8356ef02cbc72ac7ec40c7f1db3382b4ef", - "reference": "ccbdde8356ef02cbc72ac7ec40c7f1db3382b4ef", + "url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/a797892b0a5e79f92e34c8fefe0e27e31114ca65", + "reference": "a797892b0a5e79f92e34c8fefe0e27e31114ca65", "shasum": "" }, "require": { - "jms/serializer-bundle": "^3.1 || ^4.2 || ^5.0", - "php": "^7.2 || ^8.0", + "jms/serializer-bundle": "^3.10 || ^4.2 || ^5.4", + "php": "^8.1", "symfony/expression-language": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", "symfony/framework-bundle": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", - "willdurand/hateoas": "^3.5" + "willdurand/hateoas": "^3.12@beta" }, "conflict": { "jms/serializer": "<3.18" }, "require-dev": { "doctrine/annotations": "^1.13.2 || ^2.0", - "doctrine/coding-standard": "^8.0", - "phpunit/phpunit": "^8.5.32 || ^9.5.10", + "doctrine/coding-standard": "^12.0", + "phpunit/phpunit": "^10.5.38", "symfony/stopwatch": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0", "twig/twig": "~1.12|~2.0|~3.0" }, @@ -15353,29 +14608,29 @@ ], "support": { "issues": "https://github.com/willdurand/BazingaHateoasBundle/issues", - "source": "https://github.com/willdurand/BazingaHateoasBundle/tree/2.6.0" + "source": "https://github.com/willdurand/BazingaHateoasBundle/tree/2.7.0" }, - "time": "2023-12-13T11:29:05+00:00" + "time": "2024-11-07T21:08:08+00:00" }, { "name": "willdurand/jsonp-callback-validator", - "version": "v1.1.0", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/willdurand/JsonpCallbackValidator.git", - "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909" + "reference": "738c36e91d4d7e0ff0cac145f77057e0fb88526e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/willdurand/JsonpCallbackValidator/zipball/1a7d388bb521959e612ef50c5c7b1691b097e909", - "reference": "1a7d388bb521959e612ef50c5c7b1691b097e909", + "url": "https://api.github.com/repos/willdurand/JsonpCallbackValidator/zipball/738c36e91d4d7e0ff0cac145f77057e0fb88526e", + "reference": "738c36e91d4d7e0ff0cac145f77057e0fb88526e", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~3.7" + "symfony/phpunit-bridge": "^5.0" }, "type": "library", "autoload": { @@ -15390,16 +14645,15 @@ "authors": [ { "name": "William Durand", - "email": "william.durand1@gmail.com", - "homepage": "http://www.willdurand.fr" + "email": "will+git@drnd.me" } ], "description": "JSONP callback validator.", "support": { "issues": "https://github.com/willdurand/JsonpCallbackValidator/issues", - "source": "https://github.com/willdurand/JsonpCallbackValidator/tree/master" + "source": "https://github.com/willdurand/JsonpCallbackValidator/tree/v2.0.0" }, - "time": "2014-01-20T22:35:06+00:00" + "time": "2022-01-30T20:33:09+00:00" }, { "name": "willdurand/negotiation", @@ -15459,36 +14713,41 @@ }, { "name": "zircote/swagger-php", - "version": "4.11.1", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1" + "reference": "ea60f1439aa4fefba230a4386a403eeb1ee52f08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/7df10e8ec47db07c031db317a25bef962b4e5de1", - "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/ea60f1439aa4fefba230a4386a403eeb1ee52f08", + "reference": "ea60f1439aa4fefba230a4386a403eeb1ee52f08", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.2", + "nikic/php-parser": "^4.19 || ^5.0", + "php": ">=7.4", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", - "symfony/finder": ">=2.2", - "symfony/yaml": ">=3.3" + "symfony/finder": "^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^5.0 || ^6.0 || ^7.0" + }, + "conflict": { + "symfony/process": ">=6, <6.4.14" }, "require-dev": { "composer/package-versions-deprecated": "^1.11", - "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0", - "phpstan/phpstan": "^1.6", - "phpunit/phpunit": ">=8", - "vimeo/psalm": "^4.23" + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.62.0", + "phpstan/phpstan": "^1.6 || ^2.0", + "phpunit/phpunit": "^9.0", + "rector/rector": "^1.0 || ^2.0", + "vimeo/psalm": "^4.30 || ^5.0" }, "suggest": { - "doctrine/annotations": "^1.7 || ^2.0" + "doctrine/annotations": "^2.0" }, "bin": [ "bin/openapi" @@ -15496,7 +14755,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -15534,9 +14793,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.11.1" + "source": "https://github.com/zircote/swagger-php/tree/5.0.6" }, - "time": "2024-10-15T19:20:02+00:00" + "time": "2025-03-05T19:40:05+00:00" } ], "packages-dev": [ @@ -15832,16 +15091,16 @@ }, { "name": "dama/doctrine-test-bundle", - "version": "v8.2.0", + "version": "v8.2.2", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f" + "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", - "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/eefe54fdf00d910f808efea9cfce9cc261064a0a", + "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a", "shasum": "" }, "require": { @@ -15855,9 +15114,9 @@ "require-dev": { "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", - "phpstan/phpstan": "^1.2", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", - "symfony/phpunit-bridge": "^6.3", + "symfony/phpunit-bridge": "^7.2", "symfony/process": "^5.4 || ^6.3 || ^7.0", "symfony/yaml": "^5.4 || ^6.3 || ^7.0" }, @@ -15893,29 +15152,28 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.0" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.2" }, - "time": "2024-05-28T15:41:06+00:00" + "time": "2025-02-04T14:37:36+00:00" }, { "name": "doctrine/data-fixtures", - "version": "1.8.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "d2ff5046b263868baf6e9b06cf4918f60096c0d0" + "reference": "f161e20f04ba5440a09330e156b40f04dd70d47f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/d2ff5046b263868baf6e9b06cf4918f60096c0d0", - "reference": "d2ff5046b263868baf6e9b06cf4918f60096c0d0", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f161e20f04ba5440a09330e156b40f04dd70d47f", + "reference": "f161e20f04ba5440a09330e156b40f04dd70d47f", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3 || ^1.0", - "doctrine/persistence": "^2.0 || ^3.0", - "php": "^7.4 || ^8.0", - "symfony/polyfill-php80": "^1" + "doctrine/persistence": "^3.1 || ^4.0", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" }, "conflict": { "doctrine/dbal": "<3.5 || >=5", @@ -15923,18 +15181,16 @@ "doctrine/phpcr-odm": "<1.3.0" }, "require-dev": { - "doctrine/annotations": "^1.12 || ^2", - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^13", "doctrine/dbal": "^3.5 || ^4", "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", "doctrine/orm": "^2.14 || ^3", "ext-sqlite3": "*", "fig/log-test": "^1", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6.13 || ^10.4.2", - "psr/log": "^1.1 || ^2 || ^3", - "symfony/cache": "^5.4 || ^6.3 || ^7", - "symfony/var-exporter": "^5.4 || ^6.3 || ^7" + "phpstan/phpstan": "2.1.17", + "phpunit/phpunit": "10.5.45", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" }, "suggest": { "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", @@ -15965,7 +15221,7 @@ ], "support": { "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.8.0" + "source": "https://github.com/doctrine/data-fixtures/tree/2.1.0" }, "funding": [ { @@ -15981,44 +15237,44 @@ "type": "tidelift" } ], - "time": "2024-11-04T22:36:12+00:00" + "time": "2025-07-08T17:48:20+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.7.0", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "3004c9b3ac3d2a8f484597416ae1882040b3ef3b" + "reference": "bd59519a7532b9e1a41cef4049d5326dfac7def9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/3004c9b3ac3d2a8f484597416ae1882040b3ef3b", - "reference": "3004c9b3ac3d2a8f484597416ae1882040b3ef3b", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/bd59519a7532b9e1a41cef4049d5326dfac7def9", + "reference": "bd59519a7532b9e1a41cef4049d5326dfac7def9", "shasum": "" }, "require": { - "doctrine/data-fixtures": "^1.5|^2.0", + "doctrine/data-fixtures": "^1.5 || ^2.0", "doctrine/doctrine-bundle": "^2.2", "doctrine/orm": "^2.14.0 || ^3.0", - "doctrine/persistence": "^2.4|^3.0", + "doctrine/persistence": "^2.4 || ^3.0", "php": "^7.4 || ^8.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/doctrine-bridge": "^5.4.48|^6.4.16|^7.1.9", - "symfony/http-kernel": "^5.4|^6.0|^7.0" + "psr/log": "^1 || ^2 || ^3", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^5.4.48 || ^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "doctrine/dbal": "< 3" }, "require-dev": { "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.10.39", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9.6.13", - "symfony/phpunit-bridge": "^6.3.6", - "vimeo/psalm": "^5.15" + "symfony/phpunit-bridge": "^6.3.6" }, "type": "symfony-bundle", "autoload": { @@ -16052,7 +15308,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.7.0" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.7.1" }, "funding": [ { @@ -16068,26 +15324,26 @@ "type": "tidelift" } ], - "time": "2024-11-28T07:19:21+00:00" + "time": "2024-12-03T17:07:51+00:00" }, { "name": "ergebnis/composer-normalize", - "version": "2.45.0", + "version": "2.47.0", "source": { "type": "git", "url": "https://github.com/ergebnis/composer-normalize.git", - "reference": "bb82b484bed2556da6311b9eff779fa7e73ce937" + "reference": "ed24b9f8901f8fbafeca98f662eaca39427f0544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/bb82b484bed2556da6311b9eff779fa7e73ce937", - "reference": "bb82b484bed2556da6311b9eff779fa7e73ce937", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/ed24b9f8901f8fbafeca98f662eaca39427f0544", + "reference": "ed24b9f8901f8fbafeca98f662eaca39427f0544", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "ergebnis/json": "^1.4.0", - "ergebnis/json-normalizer": "^4.8.0", + "ergebnis/json-normalizer": "^4.9.0", "ergebnis/json-printer": "^3.7.0", "ext-json": "*", "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", @@ -16097,17 +15353,17 @@ "require-dev": { "composer/composer": "^2.8.3", "ergebnis/license": "^2.6.0", - "ergebnis/php-cs-fixer-config": "^6.39.0", - "ergebnis/phpunit-slow-test-detector": "^2.17.0", + "ergebnis/php-cs-fixer-config": "^6.46.0", + "ergebnis/phpunit-slow-test-detector": "^2.19.1", "fakerphp/faker": "^1.24.1", "infection/infection": "~0.26.6", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.12", - "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-phpunit": "^1.4.1", - "phpstan/phpstan-strict-rules": "^1.6.1", + "phpstan/phpstan": "^2.1.11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.6", + "phpstan/phpstan-strict-rules": "^2.0.4", "phpunit/phpunit": "^9.6.20", - "rector/rector": "^1.2.10", + "rector/rector": "^2.0.11", "symfony/filesystem": "^5.4.41" }, "type": "composer-plugin", @@ -16151,7 +15407,7 @@ "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/composer-normalize" }, - "time": "2024-12-04T18:36:37+00:00" + "time": "2025-04-15T11:09:27+00:00" }, { "name": "ergebnis/json", @@ -16223,16 +15479,16 @@ }, { "name": "ergebnis/json-normalizer", - "version": "4.8.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ergebnis/json-normalizer.git", - "reference": "e3a477b62808f377f4fc69a50f9eb66ec102747b" + "reference": "cc4dcf3890448572a2d9bea97133c4d860e59fb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/e3a477b62808f377f4fc69a50f9eb66ec102747b", - "reference": "e3a477b62808f377f4fc69a50f9eb66ec102747b", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/cc4dcf3890448572a2d9bea97133c4d860e59fb1", + "reference": "cc4dcf3890448572a2d9bea97133c4d860e59fb1", "shasum": "" }, "require": { @@ -16301,7 +15557,7 @@ "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/json-normalizer" }, - "time": "2024-12-04T16:48:55+00:00" + "time": "2025-04-10T13:13:04+00:00" }, { "name": "ergebnis/json-pointer", @@ -16567,16 +15823,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -16586,10 +15842,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -16616,7 +15872,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -16624,61 +15880,63 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.68.0", + "version": "v3.86.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c" + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", - "reference": "73f78d8b2b34a0dd65fedb434a602ee4c2c8ad4c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4a952bd19dc97879b0620f495552ef09b55f7d36", + "reference": "4a952bd19dc97879b0620f495552ef09b55f7d36", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", + "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", + "composer/xdebug-handler": "^3.0.5", "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", "fidry/cpu-core-counter": "^1.2", "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/promise": "^3.2", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.13 || ^7.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", + "symfony/polyfill-mbstring": "^1.32", + "symfony/polyfill-php80": "^1.32", + "symfony/polyfill-php81": "^1.32", + "symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", + "symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.4", - "infection/infection": "^0.29.8", - "justinrainbow/json-schema": "^5.3 || ^6.0", - "keradus/cli-executor": "^2.1", + "facile-it/paraunit": "^1.3.1 || ^2.6", + "infection/infection": "^0.29.14", + "justinrainbow/json-schema": "^5.3 || ^6.4", + "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", + "php-coveralls/php-coveralls": "^2.8", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", - "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", - "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", + "symfony/polyfill-php84": "^1.32", + "symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", + "symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -16719,7 +15977,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.68.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.86.0" }, "funding": [ { @@ -16727,20 +15985,20 @@ "type": "github" } ], - "time": "2025-01-13T17:01:01+00:00" + "time": "2025-08-13T22:36:21+00:00" }, { "name": "friendsoftwig/twigcs", - "version": "v6.1.0", + "version": "6.5.0", "source": { "type": "git", "url": "https://github.com/friendsoftwig/twigcs.git", - "reference": "3c36d606c4f19db0dd2a01b735ec7a8151b7f182" + "reference": "aaa3ba112bf4fcee7b51a00d9b45b13bc2cc23bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/friendsoftwig/twigcs/zipball/3c36d606c4f19db0dd2a01b735ec7a8151b7f182", - "reference": "3c36d606c4f19db0dd2a01b735ec7a8151b7f182", + "url": "https://api.github.com/repos/friendsoftwig/twigcs/zipball/aaa3ba112bf4fcee7b51a00d9b45b13bc2cc23bc", + "reference": "aaa3ba112bf4fcee7b51a00d9b45b13bc2cc23bc", "shasum": "" }, "require": { @@ -16749,14 +16007,14 @@ "ext-json": "*", "ext-mbstring": "*", "ext-simplexml": "*", - "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0", - "symfony/console": "^4.4 || ^5.3 || ^6.0", - "symfony/filesystem": "^4.4 || ^5.3 || ^6.0", - "symfony/finder": "^4.4 || ^5.3 || ^6.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "symfony/console": "^4.4 || ^5.3 || ^6.0 || ^7.0", + "symfony/filesystem": "^4.4 || ^5.3 || ^6.0 || ^7.0", + "symfony/finder": "^4.4 || ^5.3 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.20", - "symfony/phpunit-bridge": "^6.2.3" + "phpunit/phpunit": "^9.6.19", + "symfony/phpunit-bridge": "^7.1.4" }, "bin": [ "bin/twigcs" @@ -16780,148 +16038,36 @@ "description": "Checkstyle automation for Twig", "support": { "issues": "https://github.com/friendsoftwig/twigcs/issues", - "source": "https://github.com/friendsoftwig/twigcs/tree/v6.1.0" + "source": "https://github.com/friendsoftwig/twigcs/tree/6.5.0" }, - "time": "2023-01-04T16:01:24+00:00" - }, - { - "name": "icecave/parity", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/icecave/parity.git", - "reference": "0109fef58b3230d23b20b2ac52ecdf477218d300" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/icecave/parity/zipball/0109fef58b3230d23b20b2ac52ecdf477218d300", - "reference": "0109fef58b3230d23b20b2ac52ecdf477218d300", - "shasum": "" - }, - "require": { - "icecave/repr": "~1", - "php": ">=5.3" - }, - "require-dev": { - "eloquent/liberator": "~1", - "icecave/archer": "~1" - }, - "suggest": { - "eloquent/asplode": "Drop-in exception-based error handling." - }, - "type": "library", - "autoload": { - "psr-0": { - "Icecave\\Parity": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Harris", - "email": "james.harris@icecave.com.au", - "homepage": "https://github.com/jmalloc" - } - ], - "description": "A customizable deep comparison library.", - "homepage": "https://github.com/IcecaveStudios/parity", - "keywords": [ - "compare", - "comparison", - "equal", - "equality", - "greater", - "less", - "sort", - "sorting" - ], - "support": { - "issues": "https://github.com/icecave/parity/issues", - "source": "https://github.com/icecave/parity/tree/1.0.0" - }, - "time": "2014-01-17T05:56:27+00:00" - }, - { - "name": "icecave/repr", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/icecave/repr.git", - "reference": "8a3d2953adf5f464a06e3e2587aeacc97e2bed07" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/icecave/repr/zipball/8a3d2953adf5f464a06e3e2587aeacc97e2bed07", - "reference": "8a3d2953adf5f464a06e3e2587aeacc97e2bed07", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "icecave/archer": "~1" - }, - "suggest": { - "eloquent/asplode": "Drop-in exception-based error handling." - }, - "type": "library", - "autoload": { - "psr-4": { - "Icecave\\Repr\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Harris", - "email": "james.harris@icecave.com.au", - "homepage": "https://github.com/jmalloc" - } - ], - "description": "A library for generating string representations of any value, inspired by Python's reprlib library.", - "homepage": "https://github.com/IcecaveStudios/repr", - "keywords": [ - "human", - "readable", - "repr", - "representation", - "string" - ], - "support": { - "issues": "https://github.com/icecave/repr/issues", - "source": "https://github.com/icecave/repr/tree/1.0.1" - }, - "time": "2014-07-25T05:44:41+00:00" + "time": "2024-11-27T21:59:24+00:00" }, { "name": "justinrainbow/json-schema", - "version": "6.0.0", + "version": "6.4.1", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "a38c6198d53b09c0702f440585a4f4a5d9137bd9" + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/a38c6198d53b09c0702f440585a4f4a5d9137bd9", - "reference": "a38c6198d53b09c0702f440585a4f4a5d9137bd9", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/35d262c94959571e8736db1e5c9bc36ab94ae900", + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900", "shasum": "" }, "require": { - "icecave/parity": "1.0.0", - "marc-mabe/php-enum": "^2.0 || ^3.0 || ^4.0", - "php": ">=5.3.3" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20 || ~2.19.0", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" @@ -16967,9 +16113,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.0.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.1" }, - "time": "2024-07-30T17:49:21+00:00" + "time": "2025-04-04T13:08:07+00:00" }, { "name": "localheinz/diff", @@ -17150,16 +16296,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -17198,7 +16344,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -17206,63 +16352,7 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.19.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.1" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" - }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "phar-io/manifest", @@ -17494,16 +16584,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.9", + "version": "1.12.20", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "ceb937fb39a92deabc02d20709cf14b2c452502c" + "reference": "3240b1972042c7f73cf1045e879ea5bd5f761bb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ceb937fb39a92deabc02d20709cf14b2c452502c", - "reference": "ceb937fb39a92deabc02d20709cf14b2c452502c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3240b1972042c7f73cf1045e879ea5bd5f761bb7", + "reference": "3240b1972042c7f73cf1045e879ea5bd5f761bb7", "shasum": "" }, "require": { @@ -17548,25 +16638,25 @@ "type": "github" } ], - "time": "2024-11-10T17:10:04+00:00" + "time": "2025-03-05T13:37:43+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.5.6", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "8ba022846e79238872e315fff61e19b42ba2f139" + "reference": "231d3f795ed5ef54c98961fd3958868cbe091207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/8ba022846e79238872e315fff61e19b42ba2f139", - "reference": "8ba022846e79238872e315fff61e19b42ba2f139", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/231d3f795ed5ef54c98961fd3958868cbe091207", + "reference": "231d3f795ed5ef54c98961fd3958868cbe091207", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.6" + "phpstan/phpstan": "^1.12.12" }, "conflict": { "doctrine/collections": "<1.0", @@ -17618,27 +16708,27 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.6" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.7" }, - "time": "2024-11-09T17:34:01+00:00" + "time": "2024-12-02T16:47:26+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" + "reference": "72a6721c9b64b3e4c9db55abbc38f790b318267e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f3ea021866f4263f07ca3636bf22c64be9610c11", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/72a6721c9b64b3e4c9db55abbc38f790b318267e", + "reference": "72a6721c9b64b3e4c9db55abbc38f790b318267e", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.12" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -17670,22 +16760,22 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.2" }, - "time": "2024-04-20T06:39:00+00:00" + "time": "2024-12-17T17:20:49+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.4.12", + "version": "1.4.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d" + "reference": "dd1aaa7f85f9916222a2ce7e4d21072fe03958f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/c7b7e7f520893621558bfbfdb2694d4364565c1d", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/dd1aaa7f85f9916222a2ce7e4d21072fe03958f4", + "reference": "dd1aaa7f85f9916222a2ce7e4d21072fe03958f4", "shasum": "" }, "require": { @@ -17742,41 +16832,41 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.12" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.13" }, - "time": "2024-11-06T10:13:18+00:00" + "time": "2025-01-04T13:55:31+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -17785,7 +16875,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -17814,7 +16904,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -17822,7 +16912,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -18067,16 +17157,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -18087,11 +17177,11 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -18150,7 +17240,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -18166,7 +17256,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "react/cache", @@ -18463,6 +17553,79 @@ ], "time": "2023-11-13T13:48:05+00:00" }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, { "name": "react/socket", "version": "v1.16.0", @@ -18621,6 +17784,65 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "rector/rector", + "version": "1.2.10", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "40f9cf38c05296bd32f444121336a521a293fa61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61", + "reference": "40f9cf38c05296bd32f444121336a521a293fa61", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.12.5" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/1.2.10" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-11-08T13:59:10+00:00" + }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -19586,16 +18808,16 @@ }, { "name": "shipmonk/composer-dependency-analyser", - "version": "1.7.0", + "version": "1.8.3", "source": { "type": "git", "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", - "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" + "reference": "ca6b2725cd4854d97c1ce08e6954a74fbdd25372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", - "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/ca6b2725cd4854d97c1ce08e6954a74fbdd25372", + "reference": "ca6b2725cd4854d97c1ce08e6954a74fbdd25372", "shasum": "" }, "require": { @@ -19604,17 +18826,17 @@ "php": "^7.2 || ^8.0" }, "require-dev": { - "editorconfig-checker/editorconfig-checker": "^10.3.0", - "ergebnis/composer-normalize": "^2.19", + "editorconfig-checker/editorconfig-checker": "^10.6.0", + "ergebnis/composer-normalize": "^2.19.0", "ext-dom": "*", "ext-libxml": "*", - "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.10.63", - "phpstan/phpstan-phpunit": "^1.1.1", - "phpstan/phpstan-strict-rules": "^1.2.3", - "phpunit/phpunit": "^8.5.28 || ^9.5.20", - "shipmonk/name-collision-detector": "^2.0.0", - "slevomat/coding-standard": "^8.0.1" + "phpcompatibility/php-compatibility": "^9.3.5", + "phpstan/phpstan": "^1.12.3", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "shipmonk/name-collision-detector": "^2.1.1", + "slevomat/coding-standard": "^8.15.0" }, "bin": [ "bin/composer-dependency-analyser" @@ -19646,81 +18868,9 @@ ], "support": { "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", - "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.8.3" }, - "time": "2024-08-08T08:12:32+00:00" - }, - { - "name": "symfony/browser-kit", - "version": "v5.4.45", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "03cce39764429e07fbab9b989a1182a24578341d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/03cce39764429e07fbab9b989a1182a24578341d", - "reference": "03cce39764429e07fbab9b989a1182a24578341d", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.45" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-10-22T13:05:35+00:00" + "time": "2025-02-10T13:31:57+00:00" }, { "name": "symfony/css-selector", @@ -19869,22 +19019,22 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.43.0", + "version": "v1.50.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "e3f9a1d9e0f4968f68454403e820dffc7db38a59" + "reference": "a1733f849b999460c308e66f6392fb09b621fa86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/e3f9a1d9e0f4968f68454403e820dffc7db38a59", - "reference": "e3f9a1d9e0f4968f68454403e820dffc7db38a59", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/a1733f849b999460c308e66f6392fb09b621fa86", + "reference": "a1733f849b999460c308e66f6392fb09b621fa86", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", "nikic/php-parser": "^4.11", - "php": ">=7.2.5", + "php": ">=8.0", "symfony/config": "^5.4.7|^6.0", "symfony/console": "^5.4.7|^6.0", "symfony/dependency-injection": "^5.4.7|^6.0", @@ -19892,19 +19042,21 @@ "symfony/filesystem": "^5.4.7|^6.0", "symfony/finder": "^5.4.3|^6.0", "symfony/framework-bundle": "^5.4.7|^6.0", - "symfony/http-kernel": "^5.4.7|^6.0" + "symfony/http-kernel": "^5.4.7|^6.0", + "symfony/process": "^5.4.7|^6.0" }, "conflict": { - "doctrine/orm": "<2.10" + "doctrine/doctrine-bundle": "<2.4", + "doctrine/orm": "<2.10", + "symfony/doctrine-bridge": "<5.4" }, "require-dev": { "composer/semver": "^3.0", "doctrine/doctrine-bundle": "^2.4", "doctrine/orm": "^2.10.0", "symfony/http-client": "^5.4.7|^6.0", - "symfony/phpunit-bridge": "^5.4.7|^6.0", + "symfony/phpunit-bridge": "^5.4.17|^6.0", "symfony/polyfill-php80": "^1.16.0", - "symfony/process": "^5.4.7|^6.0", "symfony/security-core": "^5.4.7|^6.0", "symfony/yaml": "^5.4.3|^6.0", "twig/twig": "^2.0|^3.0" @@ -19934,13 +19086,14 @@ "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", "keywords": [ "code generator", + "dev", "generator", "scaffold", "scaffolding" ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.43.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.50.0" }, "funding": [ { @@ -19956,20 +19109,20 @@ "type": "tidelift" } ], - "time": "2022-05-17T15:46:50+00:00" + "time": "2023-07-10T18:21:57+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v7.1.6", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "c6b9d8f52d3e276bedb49612aa4a2a046171287f" + "reference": "71624984d8bcad6acf7a790d4e3ceafe04bc2485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c6b9d8f52d3e276bedb49612aa4a2a046171287f", - "reference": "c6b9d8f52d3e276bedb49612aa4a2a046171287f", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/71624984d8bcad6acf7a790d4e3ceafe04bc2485", + "reference": "71624984d8bcad6acf7a790d4e3ceafe04bc2485", "shasum": "" }, "require": { @@ -19989,8 +19142,8 @@ "type": "symfony-bridge", "extra": { "thanks": { - "name": "phpunit/phpunit", - "url": "https://github.com/sebastianbergmann/phpunit" + "url": "https://github.com/sebastianbergmann/phpunit", + "name": "phpunit/phpunit" } }, "autoload": { @@ -20021,8 +19174,11 @@ ], "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", + "keywords": [ + "testing" + ], "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.6" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.3.1" }, "funding": [ { @@ -20038,7 +19194,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-06-04T10:09:06+00:00" }, { "name": "symfony/process", @@ -20104,16 +19260,16 @@ }, { "name": "symfony/web-profiler-bundle", - "version": "v5.4.46", + "version": "v5.4.48", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "8ce2ad95670ad50b92eb6456836d15d0cc0eeff8" + "reference": "4afb0399456b966be92410d2bbd6146cc3ce2174" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/8ce2ad95670ad50b92eb6456836d15d0cc0eeff8", - "reference": "8ce2ad95670ad50b92eb6456836d15d0cc0eeff8", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/4afb0399456b966be92410d2bbd6146cc3ce2174", + "reference": "4afb0399456b966be92410d2bbd6146cc3ce2174", "shasum": "" }, "require": { @@ -20164,7 +19320,7 @@ "description": "Provides a development tool that gives detailed information about the execution of any request", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v5.4.46" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v5.4.48" }, "funding": [ { @@ -20180,7 +19336,7 @@ "type": "tidelift" } ], - "time": "2024-11-04T11:02:15+00:00" + "time": "2024-11-19T09:26:40+00:00" }, { "name": "symfony/web-server-bundle", @@ -20310,14 +19466,13 @@ "minimum-stability": "dev", "stability-flags": { "friendsofsymfony/oauth-server-bundle": 20, - "rulerz-php/doctrine-orm": 20, "wallabag/rulerz": 20, "wallabag/rulerz-bundle": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.4", + "php": ">=8.2", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -20337,9 +19492,6 @@ "ext-tokenizer": "*", "ext-xml": "*" }, - "platform-dev": [], - "platform-overrides": { - "php": "7.4.29" - }, - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 44c923e21..3aef31c2b 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,4 +1,13 @@ -FROM php:8.1-fpm AS rootless +FROM golang as envsubst + +ARG ENVSUBST_VERSION=v1.3.0 + +# envsubst from gettext can not replace env vars with default values +# this package is not available for ARM32 and we have to build it from source code +# flag -ldflags "-s -w" produces a smaller executable +RUN go install -ldflags "-s -w" -v github.com/a8m/envsubst/cmd/envsubst@${ENVSUBST_VERSION} + +FROM php:8.2-fpm AS rootless ARG DEBIAN_FRONTEND=noninteractive ARG NODE_VERSION=20 @@ -12,8 +21,7 @@ RUN apt-get update \ openssl \ software-properties-common -RUN curl 'https://deb.nodesource.com/gpgkey/nodesource.gpg.key' | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_${NODE_VERSION}.x $(lsb_release -cs) main" > /etc/apt/sources.list.d/nodesource.list +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - RUN apt-get update && apt-get install -y \ libmcrypt-dev \ @@ -51,14 +59,13 @@ RUN docker-php-ext-install -j "$(nproc)" \ tidy \ zip -RUN pecl install redis; \ - pecl install imagick; \ - pecl install xdebug-3.1.6; \ - docker-php-ext-enable \ +RUN pecl install redis-6.1.0 \ + && pecl install imagick-3.7.0 \ + && pecl install xdebug-3.4.1 \ + && docker-php-ext-enable \ redis \ imagick \ - xdebug \ - ; + xdebug RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ && architecture=$(uname -m) \ @@ -77,10 +84,8 @@ RUN mkdir -p /tmp/blackfire \ RUN npm install -g yarn -RUN curl -L -o /usr/local/bin/envsubst https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m`; \ - chmod +x /usr/local/bin/envsubst - COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer +COPY --from=envsubst /go/bin/envsubst /usr/local/bin/envsubst COPY entrypoint.sh /entrypoint.sh COPY config/ /opt/wallabag/config/ diff --git a/docker/php/config/parameters.yml b/docker/php/config/parameters.yml index fa7573f9f..747d6f419 100644 --- a/docker/php/config/parameters.yml +++ b/docker/php/config/parameters.yml @@ -5,9 +5,9 @@ parameters: database_name: ${DATABASE_NAME:-symfony} database_user: ${DATABASE_USER:-root} database_password: ${DATABASE_PASSWORD:-~} - database_path: '${DATABASE_PATH:-"%kernel.project_dir%/data/db/wallabag.sqlite"}' + database_path: ${DATABASE_PATH:-"%kernel.project_dir%/data/db/wallabag.sqlite"} database_table_prefix: ${DATABASE_TABLE_PREFIX:-wallabag_} - database_socket: null + database_socket: ${DATABASE_SOCKET:-~} database_charset: ${DATABASE_CHARSET:-utf8} domain_name: ${DOMAIN_NAME:-https://www.example.com} @@ -27,22 +27,20 @@ parameters: fosuser_registration: ${FOSUSER_REGISTRATION:-false} fosuser_confirmation: ${FOSUSER_CONFIRMATION:-true} - fos_oauth_server_access_token_lifetime: 3600 - fos_oauth_server_refresh_token_lifetime: 1209600 + fos_oauth_server_access_token_lifetime: ${FOS_OAUTH_SERVER_ACCESS_TOKEN_LIFETIME:-3600} + fos_oauth_server_refresh_token_lifetime: ${FOS_OAUTH_SERVER_REFRESH_TOKEN_LIFETIME:-1209600} from_email: ${FROM_EMAIL:-wallabag@example.com} - rss_limit: 50 - # RabbitMQ processing rabbitmq_host: ${RABBITMQ_HOST:-rabbitmq} rabbitmq_port: ${RABBITMQ_PORT:-5672} rabbitmq_user: ${RABBITMQ_USER:-guest} rabbitmq_password: ${RABBITMQ_PASSWORD:-guest} - rabbitmq_prefetch_count: 10 + rabbitmq_prefetch_count: ${RABBITMQ_PREFETCH_COUNT:-10} # Redis processing - redis_scheme: ${REDIS_SCHEME:-tcp} + redis_scheme: ${REDIS_SCHEME:-redis} redis_host: ${REDIS_HOST:-redis} redis_port: ${REDIS_PORT:-6379} redis_path: ${REDIS_PATH:-~} diff --git a/docker/php/env.example b/docker/php/env.example index 9a57daade..ed40d0957 100644 --- a/docker/php/env.example +++ b/docker/php/env.example @@ -4,9 +4,9 @@ DATABASE_PORT=~ DATABASE_NAME=symfony DATABASE_USER=root DATABASE_PASSWORD=~ -DATABASE_PATH="%kernel.project_dir%/data/db/wallabag.sqlite" -DOMAIN_NAME=http://localhost:8000 +DATABASE_PATH='"%kernel.project_dir%/data/db/wallabag.sqlite"' +DOMAIN_NAME=http://127.0.0.1:8000 SECRET=ch4n63m31fy0uc4n -PHP_SESSION_SAVE_PATH=tcp://redis:6379?database=2 +PHP_SESSION_SAVE_PATH=redis://redis:6379?database=2 PHP_SESSION_HANDLER=redis TRUSTED_PROXIES=0.0.0.0/0 diff --git a/fixtures/AnnotationFixtures.php b/fixtures/AnnotationFixtures.php index c514f6854..5a54303a5 100644 --- a/fixtures/AnnotationFixtures.php +++ b/fixtures/AnnotationFixtures.php @@ -11,7 +11,7 @@ use Wallabag\Entity\User; class AnnotationFixtures extends Fixture implements DependentFixtureInterface { - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): void { $annotation1 = new Annotation($this->getReference('admin-user', User::class)); $annotation1->setEntry($this->getReference('entry1', Entry::class)); @@ -43,7 +43,7 @@ class AnnotationFixtures extends Fixture implements DependentFixtureInterface $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ EntryFixtures::class, diff --git a/fixtures/ConfigFixtures.php b/fixtures/ConfigFixtures.php index 27f47a3da..cb6359fb5 100644 --- a/fixtures/ConfigFixtures.php +++ b/fixtures/ConfigFixtures.php @@ -55,7 +55,7 @@ class ConfigFixtures extends Fixture implements DependentFixtureInterface $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ UserFixtures::class, diff --git a/fixtures/EntryFixtures.php b/fixtures/EntryFixtures.php index 22b797122..b408efa61 100644 --- a/fixtures/EntryFixtures.php +++ b/fixtures/EntryFixtures.php @@ -155,7 +155,7 @@ class EntryFixtures extends Fixture implements DependentFixtureInterface $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ UserFixtures::class, diff --git a/fixtures/IgnoreOriginInstanceRuleFixtures.php b/fixtures/IgnoreOriginInstanceRuleFixtures.php index 31833e31a..f425a0f1f 100644 --- a/fixtures/IgnoreOriginInstanceRuleFixtures.php +++ b/fixtures/IgnoreOriginInstanceRuleFixtures.php @@ -4,27 +4,20 @@ namespace Wallabag\DataFixtures; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Wallabag\Entity\IgnoreOriginInstanceRule; -class IgnoreOriginInstanceRuleFixtures extends Fixture implements ContainerAwareInterface +class IgnoreOriginInstanceRuleFixtures extends Fixture { - /** - * @var ContainerInterface - */ - private $container; - - public function setContainer(?ContainerInterface $container = null) - { - $this->container = $container; + public function __construct( + private readonly array $defaultIgnoreOriginInstanceRules, + ) { } public function load(ObjectManager $manager): void { - foreach ($this->container->getParameter('wallabag.default_ignore_origin_instance_rules') as $ignore_origin_instance_rule) { + foreach ($this->defaultIgnoreOriginInstanceRules as $ignoreOriginInstanceRule) { $newIgnoreOriginInstanceRule = new IgnoreOriginInstanceRule(); - $newIgnoreOriginInstanceRule->setRule($ignore_origin_instance_rule['rule']); + $newIgnoreOriginInstanceRule->setRule($ignoreOriginInstanceRule['rule']); $manager->persist($newIgnoreOriginInstanceRule); } diff --git a/fixtures/IgnoreOriginUserRuleFixtures.php b/fixtures/IgnoreOriginUserRuleFixtures.php index 6825a46d2..92b0652c4 100644 --- a/fixtures/IgnoreOriginUserRuleFixtures.php +++ b/fixtures/IgnoreOriginUserRuleFixtures.php @@ -21,7 +21,7 @@ class IgnoreOriginUserRuleFixtures extends Fixture implements DependentFixtureIn $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ UserFixtures::class, diff --git a/fixtures/InternalSettingFixtures.php b/fixtures/InternalSettingFixtures.php index f9d6c9385..1ca6d79cd 100644 --- a/fixtures/InternalSettingFixtures.php +++ b/fixtures/InternalSettingFixtures.php @@ -4,25 +4,18 @@ namespace Wallabag\DataFixtures; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Wallabag\Entity\InternalSetting; -class InternalSettingFixtures extends Fixture implements ContainerAwareInterface +class InternalSettingFixtures extends Fixture { - /** - * @var ContainerInterface - */ - private $container; - - public function setContainer(?ContainerInterface $container = null) - { - $this->container = $container; + public function __construct( + private readonly array $defaultInternalSettings, + ) { } public function load(ObjectManager $manager): void { - foreach ($this->container->getParameter('wallabag.default_internal_settings') as $setting) { + foreach ($this->defaultInternalSettings as $setting) { $newSetting = new InternalSetting(); $newSetting->setName($setting['name']); $newSetting->setValue($setting['value']); diff --git a/fixtures/SiteCredentialFixtures.php b/fixtures/SiteCredentialFixtures.php index dab7a1714..2657d9cec 100644 --- a/fixtures/SiteCredentialFixtures.php +++ b/fixtures/SiteCredentialFixtures.php @@ -5,44 +5,37 @@ namespace Wallabag\DataFixtures; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Persistence\ObjectManager; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Wallabag\Entity\SiteCredential; use Wallabag\Entity\User; use Wallabag\Helper\CryptoProxy; -class SiteCredentialFixtures extends Fixture implements DependentFixtureInterface, ContainerAwareInterface +class SiteCredentialFixtures extends Fixture implements DependentFixtureInterface { - /** - * @var ContainerInterface - */ - private $container; - - public function setContainer(?ContainerInterface $container = null) - { - $this->container = $container; + public function __construct( + private readonly CryptoProxy $cryptoProxy, + ) { } public function load(ObjectManager $manager): void { $credential = new SiteCredential($this->getReference('admin-user', User::class)); $credential->setHost('.super.com'); - $credential->setUsername($this->container->get(CryptoProxy::class)->crypt('.super')); - $credential->setPassword($this->container->get(CryptoProxy::class)->crypt('bar')); + $credential->setUsername($this->cryptoProxy->crypt('.super')); + $credential->setPassword($this->cryptoProxy->crypt('bar')); $manager->persist($credential); $credential = new SiteCredential($this->getReference('admin-user', User::class)); $credential->setHost('paywall.example.com'); - $credential->setUsername($this->container->get(CryptoProxy::class)->crypt('paywall.example')); - $credential->setPassword($this->container->get(CryptoProxy::class)->crypt('bar')); + $credential->setUsername($this->cryptoProxy->crypt('paywall.example')); + $credential->setPassword($this->cryptoProxy->crypt('bar')); $manager->persist($credential); $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ UserFixtures::class, diff --git a/fixtures/TaggingRuleFixtures.php b/fixtures/TaggingRuleFixtures.php index 2ab748b85..b66e5d3a0 100644 --- a/fixtures/TaggingRuleFixtures.php +++ b/fixtures/TaggingRuleFixtures.php @@ -58,7 +58,7 @@ class TaggingRuleFixtures extends Fixture implements DependentFixtureInterface $manager->flush(); } - public function getDependencies() + public function getDependencies(): array { return [ ConfigFixtures::class, diff --git a/fixtures/UserFixtures.php b/fixtures/UserFixtures.php index 4f3c26931..79424b848 100644 --- a/fixtures/UserFixtures.php +++ b/fixtures/UserFixtures.php @@ -8,7 +8,7 @@ use Wallabag\Entity\User; class UserFixtures extends Fixture { - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): void { $userAdmin = new User(); $userAdmin->setName('Big boss'); diff --git a/migrations/Version20160410190541.php b/migrations/Version20160410190541.php index e6374ae5f..c99b9f666 100644 --- a/migrations/Version20160410190541.php +++ b/migrations/Version20160410190541.php @@ -21,9 +21,7 @@ class Version20160410190541 extends WallabagMigration 'length' => 23, ]); - $sharePublic = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $sharePublic = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'share_public'"); if (false === $sharePublic) { diff --git a/migrations/Version20160812120952.php b/migrations/Version20160812120952.php index c5cd03f63..727f022f9 100644 --- a/migrations/Version20160812120952.php +++ b/migrations/Version20160812120952.php @@ -20,13 +20,12 @@ class Version20160812120952 extends WallabagMigration // Can't use $clientsTable->addColumn('name', 'blob'); // because of the error: // SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL - $databaseTablePrefix = $this->container->getParameter('database_table_prefix'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $databaseTablePrefix . 'oauth2_clients AS SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM ' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('DROP TABLE ' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('CREATE TABLE ' . $databaseTablePrefix . 'oauth2_clients (id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, random_id VARCHAR(255) NOT NULL COLLATE BINARY, secret VARCHAR(255) NOT NULL COLLATE BINARY, redirect_uris CLOB NOT NULL, allowed_grant_types CLOB NOT NULL, name CLOB NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "' . $databaseTablePrefix . 'user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); - $this->addSql('INSERT INTO ' . $databaseTablePrefix . 'oauth2_clients (id, random_id, redirect_uris, secret, allowed_grant_types) SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM __temp__' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('DROP TABLE __temp__' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('CREATE INDEX IDX_635D765EA76ED395 ON ' . $databaseTablePrefix . 'oauth2_clients (user_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_clients', true) . ' AS SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM ' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_clients', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_clients', true) . ' (id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL, random_id VARCHAR(255) NOT NULL COLLATE BINARY, secret VARCHAR(255) NOT NULL COLLATE BINARY, redirect_uris CLOB NOT NULL, allowed_grant_types CLOB NOT NULL, name CLOB NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_635D765EA76ED395 FOREIGN KEY (user_id) REFERENCES "' . $this->getTable('user', true) . '" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_clients', true) . ' (id, random_id, redirect_uris, secret, allowed_grant_types) SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM __temp__' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_clients', true)); + $this->addSql('CREATE INDEX IDX_635D765EA76ED395 ON ' . $this->getTable('oauth2_clients', true) . ' (user_id)'); } else { $clientsTable->addColumn('name', 'blob'); } @@ -37,13 +36,12 @@ class Version20160812120952 extends WallabagMigration $clientsTable = $schema->getTable($this->getTable('oauth2_clients')); if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { - $databaseTablePrefix = $this->container->getParameter('database_table_prefix'); $this->addSql('DROP INDEX IDX_635D765EA76ED395'); - $this->addSql('CREATE TEMPORARY TABLE __temp__' . $databaseTablePrefix . 'oauth2_clients AS SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM ' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('DROP TABLE ' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('CREATE TABLE ' . $databaseTablePrefix . 'oauth2_clients (id INTEGER NOT NULL, random_id VARCHAR(255) NOT NULL, secret VARCHAR(255) NOT NULL, redirect_uris CLOB NOT NULL COLLATE BINARY, allowed_grant_types CLOB NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); - $this->addSql('INSERT INTO ' . $databaseTablePrefix . 'oauth2_clients (id, random_id, redirect_uris, secret, allowed_grant_types) SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM __temp__' . $databaseTablePrefix . 'oauth2_clients'); - $this->addSql('DROP TABLE __temp__' . $databaseTablePrefix . 'oauth2_clients'); + $this->addSql('CREATE TEMPORARY TABLE __temp__' . $this->getTable('oauth2_clients', true) . ' AS SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM ' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE ' . $this->getTable('oauth2_clients', true)); + $this->addSql('CREATE TABLE ' . $this->getTable('oauth2_clients', true) . ' (id INTEGER NOT NULL, random_id VARCHAR(255) NOT NULL, secret VARCHAR(255) NOT NULL, redirect_uris CLOB NOT NULL COLLATE BINARY, allowed_grant_types CLOB NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO ' . $this->getTable('oauth2_clients', true) . ' (id, random_id, redirect_uris, secret, allowed_grant_types) SELECT id, random_id, redirect_uris, secret, allowed_grant_types FROM __temp__' . $this->getTable('oauth2_clients', true)); + $this->addSql('DROP TABLE __temp__' . $this->getTable('oauth2_clients', true)); } else { $clientsTable->dropColumn('name'); } diff --git a/migrations/Version20160911214952.php b/migrations/Version20160911214952.php index 2029a2445..078d95f0c 100644 --- a/migrations/Version20160911214952.php +++ b/migrations/Version20160911214952.php @@ -12,18 +12,14 @@ class Version20160911214952 extends WallabagMigration { public function up(Schema $schema): void { - $redis = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $redis = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'import_with_redis'"); if (false === $redis) { $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('import_with_redis', 0, 'import')"); } - $rabbitmq = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $rabbitmq = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'import_with_rabbitmq'"); if (false === $rabbitmq) { diff --git a/migrations/Version20161001072726.php b/migrations/Version20161001072726.php index c90604bb3..65e196146 100644 --- a/migrations/Version20161001072726.php +++ b/migrations/Version20161001072726.php @@ -27,7 +27,7 @@ class Version20161001072726 extends WallabagMigration // remove all FK from entry_tag switch (true) { case $platform instanceof MySQLPlatform: - $query = $this->connection->query(" + $query = $this->connection->executeQuery(" SELECT CONSTRAINT_NAME FROM information_schema.key_column_usage WHERE TABLE_NAME = '" . $this->getTable('entry_tag', WallabagMigration::UN_ESCAPED_TABLE) . "' AND CONSTRAINT_NAME LIKE 'FK_%' @@ -40,7 +40,7 @@ class Version20161001072726 extends WallabagMigration break; case $platform instanceof PostgreSQLPlatform: // http://dba.stackexchange.com/questions/36979/retrieving-all-pk-and-fk - $query = $this->connection->query(" + $query = $this->connection->executeQuery(" SELECT conrelid::regclass AS table_from ,conname ,pg_get_constraintdef(c.oid) @@ -64,7 +64,7 @@ class Version20161001072726 extends WallabagMigration switch (true) { case $platform instanceof MySQLPlatform: - $query = $this->connection->query(" + $query = $this->connection->executeQuery(" SELECT CONSTRAINT_NAME FROM information_schema.key_column_usage WHERE TABLE_NAME = '" . $this->getTable('annotation', WallabagMigration::UN_ESCAPED_TABLE) . "' @@ -79,7 +79,7 @@ class Version20161001072726 extends WallabagMigration break; case $platform instanceof PostgreSQLPlatform: // http://dba.stackexchange.com/questions/36979/retrieving-all-pk-and-fk - $query = $this->connection->query(" + $query = $this->connection->executeQuery(" SELECT conrelid::regclass AS table_from ,conname ,pg_get_constraintdef(c.oid) diff --git a/migrations/Version20161031132655.php b/migrations/Version20161031132655.php index e5a3567ab..00f67004c 100644 --- a/migrations/Version20161031132655.php +++ b/migrations/Version20161031132655.php @@ -12,9 +12,7 @@ class Version20161031132655 extends WallabagMigration { public function up(Schema $schema): void { - $images = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $images = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'download_images_enabled'"); $this->skipIf(false !== $images, 'It seems that you already played this migration.'); diff --git a/migrations/Version20161117071626.php b/migrations/Version20161117071626.php index 954fd4aa5..ba690099c 100644 --- a/migrations/Version20161117071626.php +++ b/migrations/Version20161117071626.php @@ -12,18 +12,14 @@ class Version20161117071626 extends WallabagMigration { public function up(Schema $schema): void { - $share = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $share = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'share_unmark'"); if (false === $share) { $this->addSql('INSERT INTO ' . $this->getTable('craue_config_setting') . " (name, value, section) VALUES ('share_unmark', 0, 'entry')"); } - $unmark = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $unmark = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'unmark_url'"); if (false === $unmark) { diff --git a/migrations/Version20161122144743.php b/migrations/Version20161122144743.php index 5082ecf3f..bdea4934c 100644 --- a/migrations/Version20161122144743.php +++ b/migrations/Version20161122144743.php @@ -12,9 +12,7 @@ class Version20161122144743 extends WallabagMigration { public function up(Schema $schema): void { - $access = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $access = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'restricted_access'"); $this->skipIf(false !== $access, 'It seems that you already played this migration.'); diff --git a/migrations/Version20170327194233.php b/migrations/Version20170327194233.php index a8d39ce9b..d617cfd99 100644 --- a/migrations/Version20170327194233.php +++ b/migrations/Version20170327194233.php @@ -12,9 +12,7 @@ class Version20170327194233 extends WallabagMigration { public function up(Schema $schema): void { - $scuttle = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $scuttle = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'share_scuttle'"); $this->skipIf(false !== $scuttle, 'It seems that you already played this migration.'); diff --git a/migrations/Version20170420134133.php b/migrations/Version20170420134133.php index de0e59f42..dc4059f14 100644 --- a/migrations/Version20170420134133.php +++ b/migrations/Version20170420134133.php @@ -17,9 +17,7 @@ class Version20170420134133 extends WallabagMigration public function down(Schema $schema): void { - $downloadPictures = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $downloadPictures = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'download_pictures'"); $this->skipIf(false !== $downloadPictures, 'It seems that you already played this migration.'); diff --git a/migrations/Version20170602075214.php b/migrations/Version20170602075214.php index 31267efab..d402c1eb2 100644 --- a/migrations/Version20170602075214.php +++ b/migrations/Version20170602075214.php @@ -12,9 +12,7 @@ class Version20170602075214 extends WallabagMigration { public function up(Schema $schema): void { - $apiUserRegistration = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $apiUserRegistration = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'api_user_registration'"); $this->skipIf(false !== $apiUserRegistration, 'It seems that you already played this migration.'); diff --git a/migrations/Version20170606155640.php b/migrations/Version20170606155640.php index 584dee95b..75721712e 100644 --- a/migrations/Version20170606155640.php +++ b/migrations/Version20170606155640.php @@ -19,9 +19,7 @@ class Version20170606155640 extends WallabagMigration return; } - $apiUserRegistration = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $apiUserRegistration = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'wallabag_url'"); if (false === $apiUserRegistration) { diff --git a/migrations/Version20170719231144.php b/migrations/Version20170719231144.php index 3e8afcf09..91db5d1ea 100644 --- a/migrations/Version20170719231144.php +++ b/migrations/Version20170719231144.php @@ -20,7 +20,7 @@ class Version20170719231144 extends WallabagMigration } // Find tags which need to be merged - $dupTags = $this->connection->query(' + $dupTags = $this->connection->executeQuery(' SELECT LOWER(label) AS lower_label FROM ' . $this->getTable('tag') . ' GROUP BY LOWER(label) @@ -31,7 +31,7 @@ class Version20170719231144 extends WallabagMigration $label = $duplicates['lower_label']; // Retrieve all duplicate tags for a given tag - $tags = $this->connection->query(' + $tags = $this->connection->executeQuery(' SELECT id FROM ' . $this->getTable('tag') . ' WHERE LOWER(label) = :label diff --git a/migrations/Version20171120163128.php b/migrations/Version20171120163128.php index c3bb7ac95..d6f019fba 100644 --- a/migrations/Version20171120163128.php +++ b/migrations/Version20171120163128.php @@ -12,9 +12,7 @@ class Version20171120163128 extends WallabagMigration { public function up(Schema $schema): void { - $storeArticleHeaders = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $storeArticleHeaders = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'store_article_headers'"); $this->skipIf(false !== $storeArticleHeaders, 'It seems that you already played this migration.'); diff --git a/migrations/Version20171125164500.php b/migrations/Version20171125164500.php index 17b4fb3ce..3211a4b65 100644 --- a/migrations/Version20171125164500.php +++ b/migrations/Version20171125164500.php @@ -12,9 +12,7 @@ class Version20171125164500 extends WallabagMigration { public function up(Schema $schema): void { - $shaarliShareOriginUrl = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $shaarliShareOriginUrl = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = 'shaarli_share_origin_url'"); $this->skipIf(false !== $shaarliShareOriginUrl, 'It seems that you already played this migration.'); diff --git a/migrations/Version20190129120000.php b/migrations/Version20190129120000.php index 176f41d96..6007aa649 100644 --- a/migrations/Version20190129120000.php +++ b/migrations/Version20190129120000.php @@ -121,9 +121,7 @@ final class Version20190129120000 extends WallabagMigration public function up(Schema $schema): void { foreach ($this->settings as $setting) { - $settingEnabled = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $settingEnabled = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('craue_config_setting') . " WHERE name = '" . $setting['name'] . "'"); if (false !== $settingEnabled) { diff --git a/migrations/Version20190826204730.php b/migrations/Version20190826204730.php index 59d4e4008..f86c3849b 100644 --- a/migrations/Version20190826204730.php +++ b/migrations/Version20190826204730.php @@ -45,14 +45,12 @@ final class Version20190826204730 extends WallabagMigration public function postUp(Schema $schema): void { - foreach ($this->container->getParameter('wallabag.default_ignore_origin_instance_rules') as $entity) { - $previous_rule = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + foreach ($this->defaultIgnoreOriginInstanceRules as $entity) { + $previous_rule = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('ignore_origin_instance_rule') . " WHERE rule = '" . $entity['rule'] . "'"); if (false === $previous_rule) { - $this->addSql('INSERT INTO ' . $this->getTable('ignore_origin_instance_rule') . " (rule) VALUES ('" . $entity['rule'] . "');"); + $this->connection->executeQuery('INSERT INTO ' . $this->getTable('ignore_origin_instance_rule') . " (rule) VALUES ('" . $entity['rule'] . "');"); } } } diff --git a/migrations/Version20230728093912.php b/migrations/Version20230728093912.php index 74394ecce..e6b11c09a 100644 --- a/migrations/Version20230728093912.php +++ b/migrations/Version20230728093912.php @@ -37,7 +37,7 @@ final class Version20230728093912 extends WallabagMigration 'UPDATE ' . $this->getTable('entry') . ' SET is_not_parsed = :isNotParsed WHERE content LIKE :content', [ 'isNotParsed' => true, - 'content' => str_replace("\n", '', addslashes($this->container->getParameter('wallabag.fetching_error_message'))) . '%', + 'content' => str_replace("\n", '', addslashes($this->fetchingErrorMessage)) . '%', ] ); } diff --git a/migrations/Version20240310150000.php b/migrations/Version20240310150000.php index 370674349..73591df3c 100644 --- a/migrations/Version20240310150000.php +++ b/migrations/Version20240310150000.php @@ -301,7 +301,7 @@ final class Version20240310150000 extends WallabagMigration $this->addSql('ALTER TABLE ' . $this->getTable('oauth2_clients') . ' ALTER name TYPE BYTEA USING name::bytea;'); - $this->addSql('ALTER TABLE ' . $this->getTable('ignore_origin_instance_rule') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTablePrefix() . 'ignore_origin_instance_rule_id_seq\');'); + $this->addSql('ALTER TABLE ' . $this->getTable('ignore_origin_instance_rule') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTable('ignore_origin_instance_rule_id_seq', true) . '\');'); $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ALTER salt SET NOT NULL;'); $this->addSql('ALTER TABLE ' . $this->getTable('user') . ' ALTER confirmation_token TYPE VARCHAR(255);'); @@ -315,9 +315,9 @@ final class Version20240310150000 extends WallabagMigration $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' ALTER hashed_given_url TYPE TEXT;'); $this->addSql('ALTER TABLE ' . $this->getTable('entry') . ' ALTER is_not_parsed DROP NOT NULL;'); - $this->addSql('ALTER TABLE ' . $this->getTable('site_credential') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTablePrefix() . 'site_credential_id_seq\');'); + $this->addSql('ALTER TABLE ' . $this->getTable('site_credential') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTable('site_credential_id_seq', true) . '\');'); - $this->addSql('ALTER TABLE ' . $this->getTable('ignore_origin_user_rule') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTablePrefix() . 'ignore_origin_user_rule_id_seq\');'); + $this->addSql('ALTER TABLE ' . $this->getTable('ignore_origin_user_rule') . ' ALTER id SET DEFAULT nextval(\'' . $this->getTable('ignore_origin_user_rule_id_seq', true) . '\');'); break; } diff --git a/migrations/Version20240521152037.php b/migrations/Version20240521152037.php index 5f4eda065..44cf660eb 100644 --- a/migrations/Version20240521152037.php +++ b/migrations/Version20240521152037.php @@ -12,18 +12,14 @@ final class Version20240521152037 extends WallabagMigration { public function up(Schema $schema): void { - $share = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $share = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('internal_setting') . " WHERE name = 'share_linkding'"); if (false === $share) { $this->addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('share_linkding', 0, 'entry')"); } - $linkding = $this->container - ->get('doctrine.orm.default_entity_manager') - ->getConnection() + $linkding = $this->connection ->fetchOne('SELECT * FROM ' . $this->getTable('internal_setting') . " WHERE name = 'linkding_url'"); if (false === $linkding) { diff --git a/migrations/Version20250413133131.php b/migrations/Version20250413133131.php new file mode 100644 index 000000000..db1fff2cf --- /dev/null +++ b/migrations/Version20250413133131.php @@ -0,0 +1,47 @@ +getTable($this->getTable('user')); + + $this->skipIf($userTable->hasColumn('google_authenticator'), 'It seems that you already played this migration.'); + + $userTable->addColumn('google_authenticator', 'boolean', [ + 'default' => false, + 'notnull' => true, + ]); + } + + /** + * Query to update data in user table, as it's not possible to perform this in the `up` method. + */ + public function postUp(Schema $schema): void + { + $this->skipIf(!$schema->getTable($this->getTable('user'))->hasColumn('google_authenticator'), 'Unable to update google_authenticator column'); + $this->connection->executeQuery( + 'UPDATE ' . $this->getTable('user') . ' SET google_authenticator = :googleAuthenticator WHERE googleAuthenticatorSecret IS NOT NULL AND googleAuthenticatorSecret <> :emptyString', + [ + 'googleAuthenticator' => true, + 'emptyString' => '', + ] + ); + } + + public function down(Schema $schema): void + { + $userTable = $schema->getTable($this->getTable('user')); + $userTable->dropColumn('google_authenticator'); + } +} diff --git a/package.json b/package.json index 1a56bd79d..4fcba37a4 100644 --- a/package.json +++ b/package.json @@ -41,53 +41,55 @@ "url": "https://github.com/wallabag/wallabag/issues" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/eslint-parser": "^7.26.5", - "@babel/preset-env": "^7.26.0", - "autoprefixer": "^10.4.20", - "babel-loader": "^9.2.1", + "@babel/core": "^7.28.3", + "@babel/eslint-parser": "^7.28.0", + "@babel/preset-env": "^7.28.3", + "@symfony/webpack-encore": "^5.1.0", + "autoprefixer": "^10.4.21", + "babel-loader": "^10.0.0", + "core-js": "^3.45.0", "css-loader": "^7.1.2", "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", - "eslint-plugin-import": "^2.31.0", - "eslint-webpack-plugin": "^4.2.0", + "eslint-plugin-import": "^2.32.0", + "eslint-webpack-plugin": "^5.0.2", "file-loader": "^6.2.0", "lato-font": "^3.0.0", - "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.4.49", + "mini-css-extract-plugin": "^2.9.4", + "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "postcss-scss": "^4.0.9", - "sass-embedded": "^1.83.1", - "sass-loader": "^16.0.4", + "regenerator-runtime": "^0.14.1", + "sass-embedded": "^1.90.0", + "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "stylelint": "^15.11.0", "stylelint-config-standard": "^34.0.0", "stylelint-config-standard-scss": "^11.1.0", "stylelint-scss": "^5.3.2", - "stylelint-webpack-plugin": "^5.0.1", - "terser-webpack-plugin": "^5.3.11", + "terser-webpack-plugin": "^5.3.14", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.101.2", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.2.0", - "webpack-manifest-plugin": "^5.0.0", - "webpack-merge": "^6.0.1" + "webpack-manifest-plugin": "^5.0.1", + "webpack-merge": "^6.0.1", + "webpack-notifier": "^1.15.0" }, "dependencies": { - "@fontsource/atkinson-hyperlegible": "^5.1.1", - "@fontsource/eb-garamond": "^5.1.2", - "@fontsource/montserrat": "^5.1.1", - "@fontsource/oswald": "^5.1.1", + "@fontsource/atkinson-hyperlegible": "^5.2.6", + "@fontsource/eb-garamond": "^5.2.6", + "@fontsource/montserrat": "^5.2.6", + "@fontsource/oswald": "^5.2.6", + "@hotwired/stimulus": "^3.2.2", + "@materializecss/materialize": "^1.2.2", + "@symfony/stimulus-bridge": "^4.0.1", "annotator": "wallabag/annotator#master", "clipboard": "^2.0.11", "hammerjs": "^2.0.8", "highlight.js": "^11.11.1", "icomoon-free-npm": "^0.0.0", - "jquery": "^3.7.1", - "jquery.cookie": "^1.4.1", "jr-qrcode": "^1.2.1", "material-design-icons-iconfont": "^6.7.0", - "materialize-css": "^0.100.2", "mathjax": "^3.2.2", "mousetrap": "^1.6.0", "open-dyslexic": "^1.0.3", @@ -95,8 +97,10 @@ "waypoints": "^4.0.1" }, "scripts": { - "watch": "webpack-dev-server --env=dev", - "build:dev": "webpack --env=dev", - "build:prod": "webpack --env=prod" + "build:dev": "encore dev", + "watch": "encore dev --watch", + "build:prod": "encore production --progress", + "lint:js": "eslint assets/*.js assets/controllers/*.js", + "lint:scss": "stylelint assets/scss/*.scss assets/scss/**/*.scss" } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a3a1e281f..595c34207 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,76 +1,6 @@ parameters: ignoreErrors: - - message: "#^Method Wallabag\\\\Controller\\\\AnnotationController\\:\\:postAnnotationAction\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\JsonResponse but returns Symfony\\\\Component\\\\Form\\\\FormInterface\\\\.$#" + message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#" count: 1 - path: src/Controller/AnnotationController.php - - - - message: "#^Method Wallabag\\\\Controller\\\\AnnotationController\\:\\:putAnnotationAction\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\JsonResponse but returns Symfony\\\\Component\\\\Form\\\\FormInterface\\\\.$#" - count: 1 - path: src/Controller/AnnotationController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Entity\\\\RuleInterface\\:\\:getConfig\\(\\)\\.$#" - count: 1 - path: src/Controller/ConfigController.php - - - - message: "#^Method FOS\\\\UserBundle\\\\Model\\\\UserManagerInterface\\:\\:updateUser\\(\\) invoked with 2 parameters, 1 required\\.$#" - count: 6 - path: src/Controller/ConfigController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setFilepath\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/BrowserController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setUser\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/BrowserController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setFilepath\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/HtmlController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setUser\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/HtmlController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setFilepath\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/WallabagController.php - - - - message: "#^Call to an undefined method Wallabag\\\\Import\\\\ImportInterface\\:\\:setUser\\(\\)\\.$#" - count: 1 - path: src/Controller/Import/WallabagController.php - - - - message: "#^Call to an undefined method Spiriit\\\\Bundle\\\\FormFilterBundle\\\\Filter\\\\Query\\\\QueryInterface\\:\\:getExpressionBuilder\\(\\)\\.$#" - count: 1 - path: src/Event/Subscriber/CustomDoctrineORMSubscriber.php - - - - message: "#^Call to an undefined method Spiriit\\\\Bundle\\\\FormFilterBundle\\\\Filter\\\\Query\\\\QueryInterface\\:\\:getExpr\\(\\)\\.$#" - count: 10 - path: src/Form/Type/EntryFilterType.php - - - - message: "#^Call to an undefined method Scheb\\\\TwoFactorBundle\\\\Model\\\\Email\\\\TwoFactorInterface\\:\\:getName\\(\\)\\.$#" - count: 2 - path: src/Mailer/AuthCodeMailer.php - - - - message: "#^PHPDoc type Symfony\\\\Component\\\\Mailer\\\\MailerInterface of property Wallabag\\\\Mailer\\\\UserMailer\\:\\:\\$mailer is not covariant with PHPDoc type Swift_Mailer of overridden property FOS\\\\UserBundle\\\\Mailer\\\\TwigSwiftMailer\\:\\:\\$mailer\\.$#" - count: 1 - path: src/Mailer/UserMailer.php - - - - message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#" - count: 1 - path: tests/Controller/FeedControllerTest.php + path: src/ParamConverter/UsernameFeedTokenConverter.php diff --git a/phpstan.neon b/phpstan.neon index ae2046019..f45cd1016 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 3 + level: 5 paths: - src - tests diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..a47ef4f95 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {}, + }, +}; diff --git a/rector.php b/rector.php new file mode 100644 index 000000000..1304330e0 --- /dev/null +++ b/rector.php @@ -0,0 +1,28 @@ +withPaths([ + __DIR__ . '/app', + __DIR__ . '/fixtures', + __DIR__ . '/src', + __DIR__ . '/tests', + __DIR__ . '/web', + ]) + ->withRootFiles() + ->withImportNames(importShortClasses: false) + ->withAttributesSets(symfony: true, doctrine: true, gedmo: true, jms: true, sensiolabs: true) + ->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [ + 'inline_public' => true, + ]) + ->withSkip([ + ClassPropertyAssignToConstructorPromotionRector::class => [ + __DIR__ . '/src/Entity/*', + ], + ]) + ->withPhpSets() + ->withTypeCoverageLevel(0); diff --git a/src/Command/CleanDownloadedImagesCommand.php b/src/Command/CleanDownloadedImagesCommand.php index ee1415c5c..b35825828 100644 --- a/src/Command/CleanDownloadedImagesCommand.php +++ b/src/Command/CleanDownloadedImagesCommand.php @@ -16,14 +16,10 @@ class CleanDownloadedImagesCommand extends Command protected static $defaultName = 'wallabag:clean-downloaded-images'; protected static $defaultDescription = 'Cleans downloaded images which are no more associated to an entry'; - private EntryRepository $entryRepository; - private DownloadImages $downloadImages; - - public function __construct(EntryRepository $entryRepository, DownloadImages $downloadImages) - { - $this->entryRepository = $entryRepository; - $this->downloadImages = $downloadImages; - + public function __construct( + private readonly EntryRepository $entryRepository, + private readonly DownloadImages $downloadImages, + ) { parent::__construct(); } @@ -38,7 +34,7 @@ class CleanDownloadedImagesCommand extends Command ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Command/CleanDuplicatesCommand.php b/src/Command/CleanDuplicatesCommand.php index 2ee306cd3..f3c476338 100644 --- a/src/Command/CleanDuplicatesCommand.php +++ b/src/Command/CleanDuplicatesCommand.php @@ -9,8 +9,9 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Wallabag\Entity\Entry; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Wallabag\Entity\User; +use Wallabag\Event\EntryDeletedEvent; use Wallabag\Repository\EntryRepository; use Wallabag\Repository\UserRepository; @@ -21,16 +22,13 @@ class CleanDuplicatesCommand extends Command protected SymfonyStyle $io; protected int $duplicates = 0; - private EntityManagerInterface $entityManager; - private EntryRepository $entryRepository; - private UserRepository $userRepository; - - public function __construct(EntityManagerInterface $entityManager, EntryRepository $entryRepository, UserRepository $userRepository) - { - $this->entityManager = $entityManager; - $this->entryRepository = $entryRepository; - $this->userRepository = $userRepository; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly EntryRepository $entryRepository, + private readonly UserRepository $userRepository, + private readonly EventDispatcherInterface $eventDispatcher, + ) { parent::__construct(); } @@ -45,7 +43,7 @@ class CleanDuplicatesCommand extends Command ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); @@ -55,7 +53,7 @@ class CleanDuplicatesCommand extends Command try { $user = $this->getUser($username); $this->cleanDuplicates($user); - } catch (NoResultException $e) { + } catch (NoResultException) { $this->io->error(\sprintf('User "%s" not found.', $username)); return 1; @@ -90,7 +88,12 @@ class CleanDuplicatesCommand extends Command if (\in_array($url, $urls, true)) { ++$duplicatesCount; - $this->entityManager->remove($this->entryRepository->find($entry['id'])); + $entryToDelete = $this->entryRepository->find($entry['id']); + + // entry deleted, dispatch event about it! + $this->eventDispatcher->dispatch(new EntryDeletedEvent($entryToDelete), EntryDeletedEvent::NAME); + + $this->entityManager->remove($entryToDelete); $this->entityManager->flush(); // Flushing at the end of the loop would require the instance not being online } else { $urls[] = $entry['url']; @@ -104,8 +107,8 @@ class CleanDuplicatesCommand extends Command private function similarUrl($url) { - if (\in_array(substr($url, -1), ['/', '#'], true)) { // get rid of "/" and "#" and the end of urls - return substr($url, 0, \strlen($url)); + if (\in_array(substr((string) $url, -1), ['/', '#'], true)) { // get rid of "/" and "#" and the end of urls + return substr((string) $url, 0, \strlen((string) $url)); } return $url; diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index 8db47c4e8..85fc85991 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -17,18 +17,12 @@ class ExportCommand extends Command protected static $defaultName = 'wallabag:export'; protected static $defaultDescription = 'Export all entries for an user'; - private EntryRepository $entryRepository; - private UserRepository $userRepository; - private EntriesExport $entriesExport; - private string $projectDir; - - public function __construct(EntryRepository $entryRepository, UserRepository $userRepository, EntriesExport $entriesExport, string $projectDir) - { - $this->entryRepository = $entryRepository; - $this->userRepository = $userRepository; - $this->entriesExport = $entriesExport; - $this->projectDir = $projectDir; - + public function __construct( + private readonly EntryRepository $entryRepository, + private readonly UserRepository $userRepository, + private readonly EntriesExport $entriesExport, + private readonly string $projectDir, + ) { parent::__construct(); } @@ -49,7 +43,7 @@ class ExportCommand extends Command ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Command/GenerateUrlHashesCommand.php b/src/Command/GenerateUrlHashesCommand.php index 9ac7de0f2..df50fd164 100644 --- a/src/Command/GenerateUrlHashesCommand.php +++ b/src/Command/GenerateUrlHashesCommand.php @@ -19,16 +19,12 @@ class GenerateUrlHashesCommand extends Command protected static $defaultDescription = 'Generates hashed urls for each entry'; protected OutputInterface $output; - private EntityManagerInterface $entityManager; - private EntryRepository $entryRepository; - private UserRepository $userRepository; - - public function __construct(EntityManagerInterface $entityManager, EntryRepository $entryRepository, UserRepository $userRepository) - { - $this->entityManager = $entityManager; - $this->entryRepository = $entryRepository; - $this->userRepository = $userRepository; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly EntryRepository $entryRepository, + private readonly UserRepository $userRepository, + ) { parent::__construct(); } @@ -39,7 +35,7 @@ class GenerateUrlHashesCommand extends Command ->addArgument('username', InputArgument::OPTIONAL, 'User to process entries'); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; @@ -49,7 +45,7 @@ class GenerateUrlHashesCommand extends Command try { $user = $this->getUser($username); $this->generateHashedUrls($user); - } catch (NoResultException $e) { + } catch (NoResultException) { $output->writeln(\sprintf('User "%s" not found.', $username)); return 1; diff --git a/src/Command/Import/ImportCommand.php b/src/Command/Import/ImportCommand.php index 119843a20..aa225b03c 100644 --- a/src/Command/Import/ImportCommand.php +++ b/src/Command/Import/ImportCommand.php @@ -21,6 +21,7 @@ use Wallabag\Import\FirefoxImport; use Wallabag\Import\InstapaperImport; use Wallabag\Import\OmnivoreImport; use Wallabag\Import\PinboardImport; +use Wallabag\Import\PocketCsvImport; use Wallabag\Import\PocketHtmlImport; use Wallabag\Import\ReadabilityImport; use Wallabag\Import\ShaarliImport; @@ -33,55 +34,24 @@ class ImportCommand extends Command protected static $defaultName = 'wallabag:import'; protected static $defaultDescription = 'Import entries from a JSON export'; - private EntityManagerInterface $entityManager; - private TokenStorageInterface $tokenStorage; - private UserRepository $userRepository; - private WallabagV2Import $wallabagV2Import; - private FirefoxImport $firefoxImport; - private ChromeImport $chromeImport; - private ReadabilityImport $readabilityImport; - private InstapaperImport $instapaperImport; - private PinboardImport $pinboardImport; - private DeliciousImport $deliciousImport; - private OmnivoreImport $omnivoreImport; - private WallabagV1Import $wallabagV1Import; - private ElcuratorImport $elcuratorImport; - private ShaarliImport $shaarliImport; - private PocketHtmlImport $pocketHtmlImport; - public function __construct( - EntityManagerInterface $entityManager, - TokenStorageInterface $tokenStorage, - UserRepository $userRepository, - WallabagV2Import $wallabagV2Import, - FirefoxImport $firefoxImport, - ChromeImport $chromeImport, - ReadabilityImport $readabilityImport, - InstapaperImport $instapaperImport, - PinboardImport $pinboardImport, - DeliciousImport $deliciousImport, - WallabagV1Import $wallabagV1Import, - ElcuratorImport $elcuratorImport, - ShaarliImport $shaarliImport, - PocketHtmlImport $pocketHtmlImport, - OmnivoreImport $omnivoreImport + private readonly EntityManagerInterface $entityManager, + private readonly TokenStorageInterface $tokenStorage, + private readonly UserRepository $userRepository, + private readonly WallabagV2Import $wallabagV2Import, + private readonly FirefoxImport $firefoxImport, + private readonly ChromeImport $chromeImport, + private readonly ReadabilityImport $readabilityImport, + private readonly InstapaperImport $instapaperImport, + private readonly PinboardImport $pinboardImport, + private readonly DeliciousImport $deliciousImport, + private readonly WallabagV1Import $wallabagV1Import, + private readonly ElcuratorImport $elcuratorImport, + private readonly ShaarliImport $shaarliImport, + private readonly PocketHtmlImport $pocketHtmlImport, + private readonly PocketCsvImport $pocketCsvImport, + private readonly OmnivoreImport $omnivoreImport, ) { - $this->entityManager = $entityManager; - $this->tokenStorage = $tokenStorage; - $this->userRepository = $userRepository; - $this->wallabagV2Import = $wallabagV2Import; - $this->firefoxImport = $firefoxImport; - $this->chromeImport = $chromeImport; - $this->readabilityImport = $readabilityImport; - $this->instapaperImport = $instapaperImport; - $this->pinboardImport = $pinboardImport; - $this->deliciousImport = $deliciousImport; - $this->omnivoreImport = $omnivoreImport; - $this->wallabagV1Import = $wallabagV1Import; - $this->elcuratorImport = $elcuratorImport; - $this->shaarliImport = $shaarliImport; - $this->pocketHtmlImport = $pocketHtmlImport; - parent::__construct(); } @@ -90,14 +60,14 @@ class ImportCommand extends Command $this ->addArgument('username', InputArgument::REQUIRED, 'User to populate') ->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file') - ->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox, chrome, elcurator, shaarli or pocket', 'v1') + ->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox, chrome, elcurator, shaarli, pocket or pocket_csv', 'v1') ->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark all entries as read', false) ->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account') ->addOption('disableContentUpdate', null, InputOption::VALUE_NONE, 'Disable fetching updated content from URL') ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Start : ' . (new \DateTime())->format('d-m-Y G:i:s') . ' ---'); @@ -107,9 +77,7 @@ class ImportCommand extends Command // Turning off doctrine default logs queries for saving memory $middlewares = $this->entityManager->getConnection()->getConfiguration()->getMiddlewares(); - $middlewaresWithoutLogging = array_filter($middlewares, function (Middleware $middleware) { - return !$middleware instanceof LoggingMiddleware; - }); + $middlewaresWithoutLogging = array_filter($middlewares, fn (Middleware $middleware) => !$middleware instanceof LoggingMiddleware); $this->entityManager->getConnection()->getConfiguration()->setMiddlewares($middlewaresWithoutLogging); if ($input->getOption('useUserId')) { @@ -125,50 +93,28 @@ class ImportCommand extends Command // Authenticate user for paywalled websites $token = new UsernamePasswordToken( $entityUser, - null, 'main', $entityUser->getRoles()); $this->tokenStorage->setToken($token); $user = $this->tokenStorage->getToken()->getUser(); + \assert($user instanceof User); - switch ($input->getOption('importer')) { - case 'v2': - $import = $this->wallabagV2Import; - break; - case 'firefox': - $import = $this->firefoxImport; - break; - case 'chrome': - $import = $this->chromeImport; - break; - case 'readability': - $import = $this->readabilityImport; - break; - case 'instapaper': - $import = $this->instapaperImport; - break; - case 'pinboard': - $import = $this->pinboardImport; - break; - case 'delicious': - $import = $this->deliciousImport; - break; - case 'elcurator': - $import = $this->elcuratorImport; - break; - case 'shaarli': - $import = $this->shaarliImport; - break; - case 'pocket': - $import = $this->pocketHtmlImport; - break; - case 'omnivore': - $import = $this->omnivoreImport; - break; - default: - $import = $this->wallabagV1Import; - } + $import = match ($input->getOption('importer')) { + 'v2' => $this->wallabagV2Import, + 'firefox' => $this->firefoxImport, + 'chrome' => $this->chromeImport, + 'readability' => $this->readabilityImport, + 'instapaper' => $this->instapaperImport, + 'pinboard' => $this->pinboardImport, + 'delicious' => $this->deliciousImport, + 'elcurator' => $this->elcuratorImport, + 'shaarli' => $this->shaarliImport, + 'pocket' => $this->pocketHtmlImport, + 'pocket_csv' => $this->pocketCsvImport, + 'omnivore' => $this->omnivoreImport, + default => $this->wallabagV1Import, + }; $import->setMarkAsRead($input->getOption('markAsRead')); $import->setDisableContentUpdate($input->getOption('disableContentUpdate')); diff --git a/src/Command/Import/RedisWorkerCommand.php b/src/Command/Import/RedisWorkerCommand.php index f5ec875aa..266863be2 100644 --- a/src/Command/Import/RedisWorkerCommand.php +++ b/src/Command/Import/RedisWorkerCommand.php @@ -16,12 +16,9 @@ class RedisWorkerCommand extends Command protected static $defaultName = 'wallabag:import:redis-worker'; protected static $defaultDescription = 'Launch Redis worker'; - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - + public function __construct( + private readonly ContainerInterface $container, + ) { parent::__construct(); } @@ -33,7 +30,7 @@ class RedisWorkerCommand extends Command ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Worker started at: ' . (new \DateTime())->format('d-m-Y G:i:s')); $output->writeln('Waiting for message ...'); diff --git a/src/Command/Import/UrlCommand.php b/src/Command/Import/UrlCommand.php new file mode 100644 index 000000000..9f40d7cde --- /dev/null +++ b/src/Command/Import/UrlCommand.php @@ -0,0 +1,118 @@ +setName('wallabag:import:url') + ->setDescription('Import a single URL') + ->addArgument('username', InputArgument::REQUIRED, 'User to add the URL to (value: username or id)') + ->addArgument('url', InputArgument::REQUIRED, 'URL to import') + ->addArgument('tags', InputArgument::OPTIONAL, 'Comma-separated list of tags to add') + ->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark entry as read', false) + ->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // Turning off doctrine default logs queries for saving memory + $middlewares = $this->entityManager->getConnection()->getConfiguration()->getMiddlewares(); + $middlewaresWithoutLogging = array_filter($middlewares, fn (Middleware $middleware) => !$middleware instanceof LoggingMiddleware); + $this->entityManager->getConnection()->getConfiguration()->setMiddlewares($middlewaresWithoutLogging); + + if ($input->getOption('useUserId')) { + $entityUser = $this->userRepository->findOneById($input->getArgument('username')); + } else { + $entityUser = $this->userRepository->findOneByUsername($input->getArgument('username')); + } + + if (!\is_object($entityUser)) { + throw new Exception(\sprintf('User "%s" not found', $input->getArgument('username'))); + } + + // Authenticate user for paywalled websites + $token = new UsernamePasswordToken( + $entityUser, + 'main', + $entityUser->getRoles() + ); + + $this->tokenStorage->setToken($token); + $user = $this->tokenStorage->getToken()->getUser(); + \assert($user instanceof User); + + $url = $input->getArgument('url'); + + $existingEntry = $this->entityManager + ->getRepository(Entry::class) + ->findByUrlAndUserId($url, $user->getId()); + + if (false !== $existingEntry) { + $output->writeln(\sprintf('The URL %s is already in user’s entries.', $url)); + + return 1; + } + + $entry = new Entry($user); + + try { + $this->contentProxy->updateEntry($entry, $url); + } catch (\Exception $e) { + $output->writeln(\sprintf('Error trying to import the URL %s: %s.', $url, $e->getMessage())); + + return 1; + } + + if ($input->getOption('markAsRead')) { + $entry->updateArchived(true); + } + + $this->entityManager->persist($entry); + + $tags = explode(',', $input->getArgument('tags')); + if (\count($tags) > 1) { + $this->tagsAssigner->assignTagsToEntry( + $entry, + $tags, + $this->entityManager->getUnitOfWork()->getScheduledEntityInsertions() + ); + } + + $this->entityManager->flush(); + + $output->writeln(\sprintf('URL %s successfully imported.', $url)); + + return 0; + } +} diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 9ef7a3ec0..026bb4e8e 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -37,24 +37,15 @@ class InstallCommand extends Command 'curl_multi_init', ]; - private EntityManagerInterface $entityManager; - private EventDispatcherInterface $dispatcher; - private UserManagerInterface $userManager; - private TableMetadataStorageConfiguration $tableMetadataStorageConfiguration; - private string $databaseDriver; - private array $defaultSettings; - private array $defaultIgnoreOriginInstanceRules; - - public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $dispatcher, UserManagerInterface $userManager, TableMetadataStorageConfiguration $tableMetadataStorageConfiguration, string $databaseDriver, array $defaultSettings, array $defaultIgnoreOriginInstanceRules) - { - $this->entityManager = $entityManager; - $this->dispatcher = $dispatcher; - $this->userManager = $userManager; - $this->tableMetadataStorageConfiguration = $tableMetadataStorageConfiguration; - $this->databaseDriver = $databaseDriver; - $this->defaultSettings = $defaultSettings; - $this->defaultIgnoreOriginInstanceRules = $defaultIgnoreOriginInstanceRules; - + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly EventDispatcherInterface $dispatcher, + private readonly UserManagerInterface $userManager, + private readonly TableMetadataStorageConfiguration $tableMetadataStorageConfiguration, + private readonly string $databaseDriver, + private readonly array $defaultSettings, + private readonly array $defaultIgnoreOriginInstanceRules, + ) { parent::__construct(); } @@ -70,7 +61,7 @@ class InstallCommand extends Command ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->defaultInput = $input; @@ -138,7 +129,7 @@ class InstallCommand extends Command // now check if MySQL isn't too old to handle utf8mb4 if ($conn->isConnected() && $conn->getDatabasePlatform() instanceof MySQLPlatform) { - $version = $conn->query('select version()')->fetchOne(); + $version = $conn->executeQuery('select version()')->fetchOne(); $minimalVersion = '5.5.4'; if (false === version_compare($version, $minimalVersion, '>')) { @@ -151,9 +142,9 @@ class InstallCommand extends Command // testing if PostgreSQL > 9.1 if ($conn->isConnected() && $conn->getDatabasePlatform() instanceof PostgreSQLPlatform) { // return version should be like "PostgreSQL 9.5.4 on x86_64-apple-darwin15.6.0, compiled by Apple LLVM version 8.0.0 (clang-800.0.38), 64-bit" - $version = $conn->query('SELECT version();')->fetchOne(); + $version = $conn->executeQuery('SELECT version();')->fetchOne(); - preg_match('/PostgreSQL ([0-9\.]+)/i', $version, $matches); + preg_match('/PostgreSQL ([0-9\.]+)/i', (string) $version, $matches); if (isset($matches[1]) & version_compare($matches[1], '9.2.0', '<')) { $fulfilled = false; @@ -411,7 +402,7 @@ class InstallCommand extends Command try { return \in_array($databaseName, $schemaManager->listDatabases(), true); - } catch (DriverException $e) { + } catch (DriverException) { // it means we weren't able to get database list, assume the database doesn't exist return false; diff --git a/src/Command/ListUserCommand.php b/src/Command/ListUserCommand.php index e31f0ef47..b76e059d3 100644 --- a/src/Command/ListUserCommand.php +++ b/src/Command/ListUserCommand.php @@ -15,12 +15,9 @@ class ListUserCommand extends Command protected static $defaultName = 'wallabag:user:list'; protected static $defaultDescription = 'List all users'; - private UserRepository $userRepository; - - public function __construct(UserRepository $userRepository) - { - $this->userRepository = $userRepository; - + public function __construct( + private readonly UserRepository $userRepository, + ) { parent::__construct(); } @@ -33,7 +30,7 @@ class ListUserCommand extends Command ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Command/ReloadEntryCommand.php b/src/Command/ReloadEntryCommand.php index 1aebd3a7d..2ba48d2fe 100644 --- a/src/Command/ReloadEntryCommand.php +++ b/src/Command/ReloadEntryCommand.php @@ -21,20 +21,13 @@ class ReloadEntryCommand extends Command protected static $defaultName = 'wallabag:entry:reload'; protected static $defaultDescription = 'Reload entries'; - private EntryRepository $entryRepository; - private UserRepository $userRepository; - private EntityManagerInterface $entityManager; - private ContentProxy $contentProxy; - private EventDispatcherInterface $dispatcher; - - public function __construct(EntryRepository $entryRepository, UserRepository $userRepository, EntityManagerInterface $entityManager, ContentProxy $contentProxy, EventDispatcherInterface $dispatcher) - { - $this->entryRepository = $entryRepository; - $this->userRepository = $userRepository; - $this->entityManager = $entityManager; - $this->contentProxy = $contentProxy; - $this->dispatcher = $dispatcher; - + public function __construct( + private readonly EntryRepository $entryRepository, + private readonly UserRepository $userRepository, + private readonly EntityManagerInterface $entityManager, + private readonly ContentProxy $contentProxy, + private readonly EventDispatcherInterface $dispatcher, + ) { parent::__construct(); } @@ -51,7 +44,7 @@ class ReloadEntryCommand extends Command ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -62,7 +55,7 @@ class ReloadEntryCommand extends Command $userId = $this->userRepository ->findOneByUserName($username) ->getId(); - } catch (NoResultException $e) { + } catch (NoResultException) { $io->error(\sprintf('User "%s" not found.', $username)); return 1; diff --git a/src/Command/ShowUserCommand.php b/src/Command/ShowUserCommand.php index 6a341b6c8..ad03c63bc 100644 --- a/src/Command/ShowUserCommand.php +++ b/src/Command/ShowUserCommand.php @@ -17,12 +17,10 @@ class ShowUserCommand extends Command protected static $defaultDescription = 'Show user details'; protected SymfonyStyle $io; - private UserRepository $userRepository; - - public function __construct(UserRepository $userRepository) - { - $this->userRepository = $userRepository; + public function __construct( + private readonly UserRepository $userRepository, + ) { parent::__construct(); } @@ -37,7 +35,7 @@ class ShowUserCommand extends Command ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); @@ -46,7 +44,7 @@ class ShowUserCommand extends Command try { $user = $this->getUser($username); $this->showUser($user); - } catch (NoResultException $e) { + } catch (NoResultException) { $this->io->error(\sprintf('User "%s" not found.', $username)); return 1; diff --git a/src/Command/TagAllCommand.php b/src/Command/TagAllCommand.php index 628858ec5..4a0fa0905 100644 --- a/src/Command/TagAllCommand.php +++ b/src/Command/TagAllCommand.php @@ -18,16 +18,11 @@ class TagAllCommand extends Command protected static $defaultName = 'wallabag:tag:all'; protected static $defaultDescription = 'Tag all entries using the tagging rules.'; - private EntityManagerInterface $entityManager; - private RuleBasedTagger $ruleBasedTagger; - private UserRepository $userRepository; - - public function __construct(EntityManagerInterface $entityManager, RuleBasedTagger $ruleBasedTagger, UserRepository $userRepository) - { - $this->entityManager = $entityManager; - $this->ruleBasedTagger = $ruleBasedTagger; - $this->userRepository = $userRepository; - + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly RuleBasedTagger $ruleBasedTagger, + private readonly UserRepository $userRepository, + ) { parent::__construct(); } @@ -42,13 +37,13 @@ class TagAllCommand extends Command ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); try { $user = $this->getUser($input->getArgument('username')); - } catch (NoResultException $e) { + } catch (NoResultException) { $io->error(\sprintf('User "%s" not found.', $input->getArgument('username'))); return 1; diff --git a/src/Command/UpdatePicturesPathCommand.php b/src/Command/UpdatePicturesPathCommand.php index 0a3eecdec..40f144369 100644 --- a/src/Command/UpdatePicturesPathCommand.php +++ b/src/Command/UpdatePicturesPathCommand.php @@ -15,15 +15,11 @@ class UpdatePicturesPathCommand extends Command protected static $defaultName = 'wallabag:update-pictures-path'; protected static $defaultDescription = 'Update the path of the pictures for each entry when you changed your wallabag instance URL.'; - private EntityManagerInterface $entityManager; - private EntryRepository $entryRepository; - private string $wallabagUrl; - - public function __construct(EntityManagerInterface $entityManager, EntryRepository $entryRepository, $wallabagUrl) - { - $this->entityManager = $entityManager; - $this->entryRepository = $entryRepository; - $this->wallabagUrl = $wallabagUrl; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly EntryRepository $entryRepository, + private readonly string $wallabagUrl, + ) { parent::__construct(); } @@ -37,7 +33,7 @@ class UpdatePicturesPathCommand extends Command ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); diff --git a/src/Consumer/AMQPEntryConsumer.php b/src/Consumer/AMQPEntryConsumer.php index cd51b42f8..659245167 100644 --- a/src/Consumer/AMQPEntryConsumer.php +++ b/src/Consumer/AMQPEntryConsumer.php @@ -7,8 +7,8 @@ use PhpAmqpLib\Message\AMQPMessage; class AMQPEntryConsumer extends AbstractConsumer implements ConsumerInterface { - public function execute(AMQPMessage $msg) + public function execute(AMQPMessage $msg): int|bool { - return $this->handleMessage($msg->body); + return $this->handleMessage($msg->getBody()); } } diff --git a/src/Consumer/AbstractConsumer.php b/src/Consumer/AbstractConsumer.php index 3d635b0f3..7a0736114 100644 --- a/src/Consumer/AbstractConsumer.php +++ b/src/Consumer/AbstractConsumer.php @@ -7,25 +7,19 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Wallabag\Entity\Entry; -use Wallabag\Entity\Tag; use Wallabag\Event\EntrySavedEvent; use Wallabag\Import\AbstractImport; use Wallabag\Repository\UserRepository; abstract class AbstractConsumer { - protected $em; - protected $userRepository; - protected $import; - protected $eventDispatcher; - protected $logger; - - public function __construct(EntityManagerInterface $em, UserRepository $userRepository, AbstractImport $import, EventDispatcherInterface $eventDispatcher, ?LoggerInterface $logger = null) - { - $this->em = $em; - $this->userRepository = $userRepository; - $this->import = $import; - $this->eventDispatcher = $eventDispatcher; + public function __construct( + protected EntityManagerInterface $em, + protected UserRepository $userRepository, + protected AbstractImport $import, + protected EventDispatcherInterface $eventDispatcher, + protected ?LoggerInterface $logger = null, + ) { $this->logger = $logger ?: new NullLogger(); } @@ -74,9 +68,7 @@ abstract class AbstractConsumer // entry saved, dispatch event about it! $this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME); - // clear only affected entities - $this->em->clear(Entry::class); - $this->em->clear(Tag::class); + $this->em->clear(); } catch (\Exception $e) { $this->logger->warning('Unable to save entry', ['entry' => $storedEntry, 'exception' => $e]); diff --git a/src/Consumer/RabbitMQConsumerTotalProxy.php b/src/Consumer/RabbitMQConsumerTotalProxy.php index bb2f45acd..45125a3ac 100644 --- a/src/Consumer/RabbitMQConsumerTotalProxy.php +++ b/src/Consumer/RabbitMQConsumerTotalProxy.php @@ -10,48 +10,22 @@ use OldSound\RabbitMqBundle\RabbitMq\Consumer; */ class RabbitMQConsumerTotalProxy { - private Consumer $pocketConsumer; - private Consumer $readabilityConsumer; - private Consumer $wallabagV1Consumer; - private Consumer $wallabagV2Consumer; - private Consumer $firefoxConsumer; - private Consumer $chromeConsumer; - private Consumer $instapaperConsumer; - private Consumer $pinboardConsumer; - private Consumer $deliciousConsumer; - private Consumer $elcuratorConsumer; - private Consumer $shaarliConsumer; - private Consumer $pocketHtmlConsumer; - private Consumer $omnivoreConsumer; - public function __construct( - Consumer $pocketConsumer, - Consumer $readabilityConsumer, - Consumer $wallabagV1Consumer, - Consumer $wallabagV2Consumer, - Consumer $firefoxConsumer, - Consumer $chromeConsumer, - Consumer $instapaperConsumer, - Consumer $pinboardConsumer, - Consumer $deliciousConsumer, - Consumer $elcuratorConsumer, - Consumer $shaarliConsumer, - Consumer $pocketHtmlConsumer, - Consumer $omnivoreConsumer + private readonly Consumer $pocketConsumer, + private readonly Consumer $readabilityConsumer, + private readonly Consumer $wallabagV1Consumer, + private readonly Consumer $wallabagV2Consumer, + private readonly Consumer $firefoxConsumer, + private readonly Consumer $chromeConsumer, + private readonly Consumer $instapaperConsumer, + private readonly Consumer $pinboardConsumer, + private readonly Consumer $deliciousConsumer, + private readonly Consumer $elcuratorConsumer, + private readonly Consumer $shaarliConsumer, + private readonly Consumer $pocketHtmlConsumer, + private readonly Consumer $pocketCsvConsumer, + private readonly Consumer $omnivoreConsumer, ) { - $this->pocketConsumer = $pocketConsumer; - $this->readabilityConsumer = $readabilityConsumer; - $this->wallabagV1Consumer = $wallabagV1Consumer; - $this->wallabagV2Consumer = $wallabagV2Consumer; - $this->firefoxConsumer = $firefoxConsumer; - $this->chromeConsumer = $chromeConsumer; - $this->instapaperConsumer = $instapaperConsumer; - $this->pinboardConsumer = $pinboardConsumer; - $this->deliciousConsumer = $deliciousConsumer; - $this->elcuratorConsumer = $elcuratorConsumer; - $this->shaarliConsumer = $shaarliConsumer; - $this->pocketHtmlConsumer = $pocketHtmlConsumer; - $this->omnivoreConsumer = $omnivoreConsumer; } /** @@ -102,6 +76,9 @@ class RabbitMQConsumerTotalProxy case 'pocket_html': $consumer = $this->pocketHtmlConsumer; break; + case 'pocket_csv': + $consumer = $this->pocketCsvConsumer; + break; case 'omnivore': $consumer = $this->omnivoreConsumer; break; @@ -115,6 +92,6 @@ class RabbitMQConsumerTotalProxy return 0; } - return $message->delivery_info['message_count'] + 1; + return $message->getMessageCount() + 1; } } diff --git a/src/Controller/AnnotationController.php b/src/Controller/AnnotationController.php index 6dca89a18..5c88969d7 100644 --- a/src/Controller/AnnotationController.php +++ b/src/Controller/AnnotationController.php @@ -5,6 +5,7 @@ namespace Wallabag\Controller; use Doctrine\ORM\EntityManagerInterface; use FOS\RestBundle\Controller\AbstractFOSRestController; use JMS\Serializer\SerializerInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,15 +20,11 @@ use Wallabag\Repository\AnnotationRepository; class AnnotationController extends AbstractFOSRestController { - protected EntityManagerInterface $entityManager; - protected SerializerInterface $serializer; - protected FormFactoryInterface $formFactory; - - public function __construct(EntityManagerInterface $entityManager, SerializerInterface $serializer, FormFactoryInterface $formFactory) - { - $this->entityManager = $entityManager; - $this->serializer = $serializer; - $this->formFactory = $formFactory; + public function __construct( + protected EntityManagerInterface $entityManager, + protected SerializerInterface $serializer, + protected FormFactoryInterface $formFactory, + ) { } /** @@ -35,10 +32,10 @@ class AnnotationController extends AbstractFOSRestController * * @see Api\WallabagRestController * - * @Route("/annotations/{entry}.{_format}", methods={"GET"}, name="annotations_get_annotations", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/annotations/{entry}.{_format}', name: 'annotations_get_annotations', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_ANNOTATIONS', subject: 'entry')] public function getAnnotationsAction(Entry $entry, AnnotationRepository $annotationRepository) { $annotationRows = $annotationRepository->findByEntryIdAndUserId($entry->getId(), $this->getUser()->getId()); @@ -56,10 +53,10 @@ class AnnotationController extends AbstractFOSRestController * * @see Api\WallabagRestController * - * @Route("/annotations/{entry}.{_format}", methods={"POST"}, name="annotations_post_annotation", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/annotations/{entry}.{_format}', name: 'annotations_post_annotation', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('CREATE_ANNOTATIONS', subject: 'entry')] public function postAnnotationAction(Request $request, Entry $entry) { $data = json_decode($request->getContent(), true); @@ -82,7 +79,7 @@ class AnnotationController extends AbstractFOSRestController return JsonResponse::fromJsonString($json); } - return $form; + return new JsonResponse(status: 400); } /** @@ -90,15 +87,13 @@ class AnnotationController extends AbstractFOSRestController * * @see Api\WallabagRestController * - * @Route("/annotations/{annotation}.{_format}", methods={"PUT"}, name="annotations_put_annotation", defaults={"_format": "json"}) - * * @return JsonResponse */ - public function putAnnotationAction(Request $request, AnnotationRepository $annotationRepository, int $annotation) + #[Route(path: '/annotations/{annotation}.{_format}', name: 'annotations_put_annotation', methods: ['PUT'], defaults: ['_format' => 'json'])] + #[IsGranted('EDIT', subject: 'annotation')] + public function putAnnotationAction(Request $request, Annotation $annotation) { try { - $annotation = $this->validateAnnotation($annotationRepository, $annotation, $this->getUser()->getId()); - $data = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR); $form = $this->formFactory->createNamed('', EditAnnotationType::class, $annotation, [ @@ -116,7 +111,7 @@ class AnnotationController extends AbstractFOSRestController return JsonResponse::fromJsonString($json); } - return $form; + return new JsonResponse(status: 400); } catch (\InvalidArgumentException $e) { throw new NotFoundHttpException($e); } @@ -127,15 +122,13 @@ class AnnotationController extends AbstractFOSRestController * * @see Api\WallabagRestController * - * @Route("/annotations/{annotation}.{_format}", methods={"DELETE"}, name="annotations_delete_annotation", defaults={"_format": "json"}) - * * @return JsonResponse */ - public function deleteAnnotationAction(AnnotationRepository $annotationRepository, int $annotation) + #[Route(path: '/annotations/{annotation}.{_format}', name: 'annotations_delete_annotation', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('DELETE', subject: 'annotation')] + public function deleteAnnotationAction(Annotation $annotation) { try { - $annotation = $this->validateAnnotation($annotationRepository, $annotation, $this->getUser()->getId()); - $this->entityManager->remove($annotation); $this->entityManager->flush(); @@ -157,15 +150,4 @@ class AnnotationController extends AbstractFOSRestController return $user; } - - private function validateAnnotation(AnnotationRepository $annotationRepository, int $annotationId, int $userId) - { - $annotation = $annotationRepository->findOneByIdAndUserId($annotationId, $userId); - - if (null === $annotation) { - throw new NotFoundHttpException(); - } - - return $annotation; - } } diff --git a/src/Controller/Api/AnnotationRestController.php b/src/Controller/Api/AnnotationRestController.php index d9c588cf4..ee3285b03 100644 --- a/src/Controller/Api/AnnotationRestController.php +++ b/src/Controller/Api/AnnotationRestController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Api; use Nelmio\ApiDocBundle\Annotation\Operation; use OpenApi\Annotations as OA; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -34,14 +35,12 @@ class AnnotationRestController extends WallabagRestController * ) * ) * - * @Route("/api/annotations/{entry}.{_format}", methods={"GET"}, name="api_get_annotations", defaults={"_format": "json"}) - * * @return Response */ + #[Route(path: '/api/annotations/{entry}.{_format}', name: 'api_get_annotations', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_ANNOTATIONS', subject: 'entry')] public function getAnnotationsAction(Entry $entry) { - $this->validateAuthentication(); - return $this->forward('Wallabag\Controller\AnnotationController::getAnnotationsAction', [ 'entry' => $entry, ]); @@ -100,14 +99,12 @@ class AnnotationRestController extends WallabagRestController * ) * ) * - * @Route("/api/annotations/{entry}.{_format}", methods={"POST"}, name="api_post_annotation", defaults={"_format": "json"}) - * * @return Response */ + #[Route(path: '/api/annotations/{entry}.{_format}', name: 'api_post_annotation', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('CREATE_ANNOTATIONS', subject: 'entry')] public function postAnnotationAction(Request $request, Entry $entry) { - $this->validateAuthentication(); - return $this->forward('Wallabag\Controller\AnnotationController::postAnnotationAction', [ 'request' => $request, 'entry' => $entry, @@ -136,14 +133,12 @@ class AnnotationRestController extends WallabagRestController * ) * ) * - * @Route("/api/annotations/{annotation}.{_format}", methods={"PUT"}, name="api_put_annotation", defaults={"_format": "json"}) - * * @return Response */ - public function putAnnotationAction(int $annotation, Request $request) + #[Route(path: '/api/annotations/{annotation}.{_format}', name: 'api_put_annotation', methods: ['PUT'], defaults: ['_format' => 'json'])] + #[IsGranted('EDIT', subject: 'annotation')] + public function putAnnotationAction(Annotation $annotation, Request $request) { - $this->validateAuthentication(); - return $this->forward('Wallabag\Controller\AnnotationController::putAnnotationAction', [ 'annotation' => $annotation, 'request' => $request, @@ -172,14 +167,12 @@ class AnnotationRestController extends WallabagRestController * ) * ) * - * @Route("/api/annotations/{annotation}.{_format}", methods={"DELETE"}, name="api_delete_annotation", defaults={"_format": "json"}) - * * @return Response */ - public function deleteAnnotationAction(int $annotation) + #[Route(path: '/api/annotations/{annotation}.{_format}', name: 'api_delete_annotation', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('DELETE', subject: 'annotation')] + public function deleteAnnotationAction(Annotation $annotation) { - $this->validateAuthentication(); - return $this->forward('Wallabag\Controller\AnnotationController::deleteAnnotationAction', [ 'annotation' => $annotation, ]); diff --git a/src/Controller/Api/ConfigRestController.php b/src/Controller/Api/ConfigRestController.php index 272d5cd75..bdc23843f 100644 --- a/src/Controller/Api/ConfigRestController.php +++ b/src/Controller/Api/ConfigRestController.php @@ -23,10 +23,9 @@ class ConfigRestController extends WallabagRestController * ) * ) * - * @Route("/api/config.{_format}", methods={"GET"}, name="api_get_config", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/config.{_format}', name: 'api_get_config', methods: ['GET'], defaults: ['_format' => 'json'])] public function getConfigAction(SerializerInterface $serializer) { $this->validateAuthentication(); diff --git a/src/Controller/Api/DeveloperController.php b/src/Controller/Api/DeveloperController.php index 6e41279b5..2eb774f3e 100644 --- a/src/Controller/Api/DeveloperController.php +++ b/src/Controller/Api/DeveloperController.php @@ -6,6 +6,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; use Wallabag\Controller\AbstractController; @@ -18,10 +19,9 @@ class DeveloperController extends AbstractController /** * List all clients and link to create a new one. * - * @Route("/developer", name="developer") - * * @return Response */ + #[Route(path: '/developer', name: 'developer', methods: ['GET'])] public function indexAction(ClientRepository $repo) { $clients = $repo->findByUser($this->getUser()->getId()); @@ -34,10 +34,9 @@ class DeveloperController extends AbstractController /** * Create a client (an app). * - * @Route("/developer/client/create", name="developer_create_client") - * * @return Response */ + #[Route(path: '/developer/client/create', name: 'developer_create_client', methods: ['GET', 'POST'])] public function createClientAction(Request $request, EntityManagerInterface $entityManager, TranslatorInterface $translator) { $client = new Client($this->getUser()); @@ -69,14 +68,13 @@ class DeveloperController extends AbstractController /** * Remove a client. * - * @Route("/developer/client/delete/{id}", requirements={"id" = "\d+"}, name="developer_delete_client", methods={"POST"}) - * * @return RedirectResponse */ + #[Route(path: '/developer/client/delete/{id}', name: 'developer_delete_client', methods: ['POST'], requirements: ['id' => '\d+'])] public function deleteClientAction(Request $request, Client $client, EntityManagerInterface $entityManager, TranslatorInterface $translator) { if (!$this->isCsrfTokenValid('delete-client', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } if (null === $this->getUser() || $client->getUser()->getId() !== $this->getUser()->getId()) { @@ -97,14 +95,11 @@ class DeveloperController extends AbstractController /** * Display developer how to use an existing app. * - * @Route("/developer/howto/first-app", name="developer_howto_firstapp") - * * @return Response */ + #[Route(path: '/developer/howto/first-app', name: 'developer_howto_firstapp', methods: ['GET'])] public function howtoFirstAppAction() { - return $this->render('Developer/howto_app.html.twig', [ - 'wallabag_url' => $this->getParameter('domain_name'), - ]); + return $this->render('Developer/howto_app.html.twig'); } } diff --git a/src/Controller/Api/EntryRestController.php b/src/Controller/Api/EntryRestController.php index eeb4a75c1..848ce1aff 100644 --- a/src/Controller/Api/EntryRestController.php +++ b/src/Controller/Api/EntryRestController.php @@ -8,6 +8,7 @@ use Nelmio\ApiDocBundle\Annotation\Operation; use OpenApi\Annotations as OA; use Pagerfanta\Pagerfanta; use Psr\Log\LoggerInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -15,6 +16,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Wallabag\Entity\Entry; use Wallabag\Entity\Tag; use Wallabag\Event\EntryDeletedEvent; @@ -83,14 +85,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/exists.{_format}", methods={"GET"}, name="api_get_entries_exists", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/exists.{_format}', name: 'api_get_entries_exists', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_ENTRIES')] public function getEntriesExistsAction(Request $request, EntryRepository $entryRepository) { - $this->validateAuthentication(); - $returnId = (null === $request->query->get('return_id')) ? false : (bool) $request->query->get('return_id'); $hashedUrls = $request->query->all('hashed_urls'); @@ -131,9 +131,7 @@ class EntryRestController extends WallabagRestController } if (false === $returnId) { - $results = array_map(function ($v) { - return null !== $v; - }, $results); + $results = array_map(fn ($v) => null !== $v, $results); } $results = $this->replaceUrlHashes($results, $urlHashMap); @@ -292,33 +290,43 @@ class EntryRestController extends WallabagRestController * example="200", * ) * ), + * @OA\Parameter( + * name="annotations", + * in="query", + * description="filter entries with annotations. all entries by default", + * required=false, + * @OA\Schema( + * type="integer", + * enum={"1", "0"}, + * default="0" + * ) + * ), * @OA\Response( * response="200", * description="Returned when successful" * ) * ) * - * @Route("/api/entries.{_format}", methods={"GET"}, name="api_get_entries", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries.{_format}', name: 'api_get_entries', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_ENTRIES')] public function getEntriesAction(Request $request, EntryRepository $entryRepository) { - $this->validateAuthentication(); - $isArchived = (null === $request->query->get('archive')) ? null : (bool) $request->query->get('archive'); $isStarred = (null === $request->query->get('starred')) ? null : (bool) $request->query->get('starred'); $isPublic = (null === $request->query->get('public')) ? null : (bool) $request->query->get('public'); $isNotParsed = (null === $request->query->get('notParsed')) ? null : (bool) $request->query->get('notParsed'); $sort = strtolower($request->query->get('sort', 'created')); $order = strtolower($request->query->get('order', 'desc')); - $page = (int) $request->query->get('page', 1); - $perPage = (int) $request->query->get('perPage', 30); - $tags = \is_array($request->query->get('tags')) ? '' : (string) $request->query->get('tags', ''); - $since = $request->query->get('since', 0); + $page = $request->query->getInt('page', 1); + $perPage = $request->query->getInt('perPage', 30); + $tags = \is_array($request->query->all()['tags'] ?? '') ? '' : (string) $request->query->get('tags', ''); + $since = $request->query->getInt('since'); $detail = strtolower($request->query->get('detail', 'full')); $domainName = (null === $request->query->get('domain_name')) ? '' : (string) $request->query->get('domain_name'); $httpStatus = (!\array_key_exists((int) $request->query->get('http_status'), Response::$statusTexts)) ? null : (int) $request->query->get('http_status'); + $hasAnnotations = (null === $request->query->get('annotations')) ? null : (bool) $request->query->get('annotations'); try { /** @var Pagerfanta $pager */ @@ -334,7 +342,8 @@ class EntryRestController extends WallabagRestController $detail, $domainName, $isNotParsed, - $httpStatus + $httpStatus, + $hasAnnotations ); } catch (\Exception $e) { throw new BadRequestHttpException($e->getMessage()); @@ -360,6 +369,7 @@ class EntryRestController extends WallabagRestController 'tags' => $tags, 'since' => $since, 'detail' => $detail, + 'annotations' => $hasAnnotations, ], true ) @@ -390,15 +400,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}.{_format}", methods={"GET"}, name="api_get_entry", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}.{_format}', name: 'api_get_entry', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('VIEW', subject: 'entry')] public function getEntryAction(Entry $entry) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - return $this->sendResponse($entry); } @@ -434,15 +441,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}/export.{_format}", methods={"GET"}, name="api_get_entry_export", defaults={"_format": "json"}) - * * @return Response */ + #[Route(path: '/api/entries/{entry}/export.{_format}', name: 'api_get_entry_export', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('VIEW', subject: 'entry')] public function getEntryExportAction(Entry $entry, Request $request, EntriesExport $entriesExport) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - return $entriesExport ->setEntries($entry) ->updateTitle('entry') @@ -469,15 +473,13 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/list.{_format}", methods={"DELETE"}, name="api_delete_entries_list", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/list.{_format}', name: 'api_delete_entries_list', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('DELETE_ENTRIES')] public function deleteEntriesListAction(Request $request, EntryRepository $entryRepository, EventDispatcherInterface $eventDispatcher) { - $this->validateAuthentication(); - - $urls = json_decode($request->query->get('urls', [])); + $urls = json_decode($request->query->get('urls', '[]')); if (empty($urls)) { return $this->sendResponse([]); @@ -494,7 +496,7 @@ class EntryRestController extends WallabagRestController $results[$key]['url'] = $url; - if (false !== $entry) { + if (false !== $entry && $this->authorizationChecker->isGranted('DELETE', $entry)) { // entry deleted, dispatch event about it! $eventDispatcher->dispatch(new EntryDeletedEvent($entry), EntryDeletedEvent::NAME); @@ -527,17 +529,15 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/lists.{_format}", methods={"POST"}, name="api_post_entries_list", defaults={"_format": "json"}) - * * @throws HttpException When limit is reached * * @return JsonResponse */ + #[Route(path: '/api/entries/lists.{_format}', name: 'api_post_entries_list', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('CREATE_ENTRIES')] public function postEntriesListAction(Request $request, EntryRepository $entryRepository, EventDispatcherInterface $eventDispatcher, ContentProxy $contentProxy) { - $this->validateAuthentication(); - - $urls = json_decode($request->query->get('urls', [])); + $urls = json_decode($request->query->get('urls', '[]')); $limit = $this->getParameter('wallabag.api_limit_mass_actions'); @@ -568,7 +568,7 @@ class EntryRestController extends WallabagRestController $this->entityManager->persist($entry); $this->entityManager->flush(); - $results[$key]['entry'] = $entry instanceof Entry ? $entry->getId() : false; + $results[$key]['entry'] = $entry->getId(); // entry saved, dispatch event about it! $eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME); @@ -712,14 +712,19 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries.{_format}", methods={"POST"}, name="api_post_entries", defaults={"_format": "json"}) - * * @return JsonResponse */ - public function postEntriesAction(Request $request, EntryRepository $entryRepository, ContentProxy $contentProxy, LoggerInterface $logger, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher) - { - $this->validateAuthentication(); - + #[Route(path: '/api/entries.{_format}', name: 'api_post_entries', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('CREATE_ENTRIES')] + public function postEntriesAction( + Request $request, + EntryRepository $entryRepository, + ContentProxy $contentProxy, + LoggerInterface $logger, + TagsAssigner $tagsAssigner, + EventDispatcherInterface $eventDispatcher, + ValidatorInterface $validator, + ) { $url = $request->request->get('url'); $entry = $entryRepository->findByUrlAndUserId( @@ -743,7 +748,7 @@ class EntryRestController extends WallabagRestController 'html' => !empty($data['content']) ? $data['content'] : $entry->getContent(), 'url' => $entry->getUrl(), 'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(), - 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(), + 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt()?->format('Y-m-d H:i:s') ?? '', // faking the open graph preview picture 'image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(), 'authors' => \is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(), @@ -788,6 +793,16 @@ class EntryRestController extends WallabagRestController $contentProxy->setDefaultEntryTitle($entry); } + $errors = $validator->validate($entry); + if (\count($errors) > 0) { + $errorsString = ''; + foreach ($errors as $error) { + $errorsString .= $error->getMessage() . "\n"; + } + + throw new BadRequestHttpException($errorsString); + } + $this->entityManager->persist($entry); $this->entityManager->flush(); @@ -920,15 +935,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}.{_format}", methods={"PATCH"}, name="api_patch_entries", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}.{_format}', name: 'api_patch_entries', methods: ['PATCH'], defaults: ['_format' => 'json'])] + #[IsGranted('EDIT', subject: 'entry')] public function patchEntriesAction(Entry $entry, Request $request, ContentProxy $contentProxy, LoggerInterface $logger, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - $data = $this->retrieveValueFromRequest($request); // this is a special case where user want to manually update the entry content @@ -1037,15 +1049,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}/reload.{_format}", methods={"PATCH"}, name="api_patch_entries_reload", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}/reload.{_format}', name: 'api_patch_entries_reload', methods: ['PATCH'], defaults: ['_format' => 'json'])] + #[IsGranted('RELOAD', subject: 'entry')] public function patchEntriesReloadAction(Entry $entry, ContentProxy $contentProxy, LoggerInterface $logger, EventDispatcherInterface $eventDispatcher) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - try { $contentProxy->updateEntry($entry, $entry->getUrl()); } catch (\Exception $e) { @@ -1094,18 +1103,16 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}.{_format}", methods={"DELETE"}, name="api_delete_entries", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}.{_format}', name: 'api_delete_entries', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('DELETE', subject: 'entry')] public function deleteEntriesAction(Entry $entry, Request $request, EventDispatcherInterface $eventDispatcher) { $expect = $request->query->get('expect', 'entry'); if (!\in_array($expect, ['id', 'entry'], true)) { throw new BadRequestHttpException(\sprintf("expect: 'id' or 'entry' expected, %s given", $expect)); } - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); $response = $this->sendResponse([ 'id' => $entry->getId(), @@ -1147,15 +1154,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}/tags.{_format}", methods={"GET"}, name="api_get_entries_tags", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}/tags.{_format}', name: 'api_get_entries_tags', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_TAGS', subject: 'entry')] public function getEntriesTagsAction(Entry $entry) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - return $this->sendResponse($entry->getTags()); } @@ -1191,15 +1195,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}/tags.{_format}", methods={"POST"}, name="api_post_entries_tags", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}/tags.{_format}', name: 'api_post_entries_tags', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('TAG', subject: 'entry')] public function postEntriesTagsAction(Request $request, Entry $entry, TagsAssigner $tagsAssigner) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - $tags = $request->request->get('tags', ''); if (!empty($tags)) { $tagsAssigner->assignTagsToEntry($entry, $tags); @@ -1243,15 +1244,12 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/{entry}/tags/{tag}.{_format}", methods={"DELETE"}, name="api_delete_entries_tags", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/{entry}/tags/{tag}.{_format}', name: 'api_delete_entries_tags', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('UNTAG', subject: 'entry')] public function deleteEntriesTagsAction(Entry $entry, Tag $tag) { - $this->validateAuthentication(); - $this->validateUserAccess($entry->getUser()->getId()); - $entry->removeTag($tag); $this->entityManager->persist($entry); @@ -1279,15 +1277,13 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/tags/list.{_format}", methods={"DELETE"}, name="api_delete_entries_tags_list", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/tags/list.{_format}', name: 'api_delete_entries_tags_list', methods: ['DELETE'], defaults: ['_format' => 'json'])] + #[IsGranted('DELETE_TAGS')] public function deleteEntriesTagsListAction(Request $request, TagRepository $tagRepository, EntryRepository $entryRepository) { - $this->validateAuthentication(); - - $list = json_decode($request->query->get('list', [])); + $list = json_decode($request->query->get('list', '[]')); if (empty($list)) { return $this->sendResponse([]); @@ -1307,7 +1303,7 @@ class EntryRestController extends WallabagRestController $tags = $element->tags; - if (false !== $entry && !(empty($tags))) { + if (false !== $entry && !(empty($tags)) && $this->authorizationChecker->isGranted('UNTAG', $entry)) { $tags = explode(',', $tags); foreach ($tags as $label) { $label = trim($label); @@ -1346,15 +1342,13 @@ class EntryRestController extends WallabagRestController * ) * ) * - * @Route("/api/entries/tags/lists.{_format}", methods={"POST"}, name="api_post_entries_tags_list", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/entries/tags/lists.{_format}', name: 'api_post_entries_tags_list', methods: ['POST'], defaults: ['_format' => 'json'])] + #[IsGranted('CREATE_TAGS')] public function postEntriesTagsListAction(Request $request, EntryRepository $entryRepository, TagsAssigner $tagsAssigner) { - $this->validateAuthentication(); - - $list = json_decode($request->query->get('list', [])); + $list = json_decode($request->query->get('list', '[]')); if (empty($list)) { return $this->sendResponse([]); @@ -1374,7 +1368,7 @@ class EntryRestController extends WallabagRestController $tags = $element->tags; - if (false !== $entry && !(empty($tags))) { + if (false !== $entry && !(empty($tags)) && $this->authorizationChecker->isGranted('TAG', $entry)) { $tagsAssigner->assignTagsToEntry($entry, $tags); $this->entityManager->persist($entry); @@ -1413,7 +1407,7 @@ class EntryRestController extends WallabagRestController { return [ 'title' => $request->request->get('title'), - 'tags' => $request->request->get('tags', []), + 'tags' => $request->request->get('tags', ''), 'isArchived' => $request->request->get('archive'), 'isStarred' => $request->request->get('starred'), 'isPublic' => $request->request->get('public'), @@ -1421,7 +1415,7 @@ class EntryRestController extends WallabagRestController 'language' => $request->request->get('language'), 'picture' => $request->request->get('preview_picture'), 'publishedAt' => $request->request->get('published_at'), - 'authors' => $request->request->get('authors', ''), + 'authors' => $request->request->all()['authors'] ?? '', 'origin_url' => $request->request->get('origin_url', ''), ]; } diff --git a/src/Controller/Api/SearchRestController.php b/src/Controller/Api/SearchRestController.php index 2bb0a9c86..93ee3f7ad 100644 --- a/src/Controller/Api/SearchRestController.php +++ b/src/Controller/Api/SearchRestController.php @@ -8,6 +8,7 @@ use Nelmio\ApiDocBundle\Annotation\Operation; use OpenApi\Annotations as OA; use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter; use Pagerfanta\Pagerfanta; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; @@ -54,17 +55,15 @@ class SearchRestController extends WallabagRestController * ) * ) * - * @Route("/api/search.{_format}", methods={"GET"}, name="api_get_search", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/search.{_format}', name: 'api_get_search', methods: ['GET'], defaults: ['_format' => 'json'])] + #[IsGranted('LIST_ENTRIES')] public function getSearchAction(Request $request, EntryRepository $entryRepository) { - $this->validateAuthentication(); - $term = $request->query->get('term'); - $page = (int) $request->query->get('page', 1); - $perPage = (int) $request->query->get('perPage', 30); + $page = $request->query->getInt('page', 1); + $perPage = $request->query->getInt('perPage', 30); $qb = $entryRepository->getBuilderForSearchByUser( $this->getUser()->getId(), diff --git a/src/Controller/Api/TagRestController.php b/src/Controller/Api/TagRestController.php index a7053165d..1fac9f13f 100644 --- a/src/Controller/Api/TagRestController.php +++ b/src/Controller/Api/TagRestController.php @@ -26,10 +26,9 @@ class TagRestController extends WallabagRestController * ) * ) * - * @Route("/api/tags.{_format}", methods={"GET"}, name="api_get_tags", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/tags.{_format}', name: 'api_get_tags', methods: ['GET'], defaults: ['_format' => 'json'])] public function getTagsAction(TagRepository $tagRepository) { $this->validateAuthentication(); @@ -63,14 +62,13 @@ class TagRestController extends WallabagRestController * ) * ) * - * @Route("/api/tag/label.{_format}", methods={"DELETE"}, name="api_delete_tag_label", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/tag/label.{_format}', name: 'api_delete_tag_label', methods: ['DELETE'], defaults: ['_format' => 'json'])] public function deleteTagLabelAction(Request $request, TagRepository $tagRepository, EntryRepository $entryRepository) { $this->validateAuthentication(); - $label = $request->get('tag', ''); + $label = $request->request->get('tag', $request->query->get('tag', '')); $tags = $tagRepository->findByLabelsAndUser([$label], $this->getUser()->getId()); @@ -111,15 +109,14 @@ class TagRestController extends WallabagRestController * ) * ) * - * @Route("/api/tags/label.{_format}", methods={"DELETE"}, name="api_delete_tags_label", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/tags/label.{_format}', name: 'api_delete_tags_label', methods: ['DELETE'], defaults: ['_format' => 'json'])] public function deleteTagsLabelAction(Request $request, TagRepository $tagRepository, EntryRepository $entryRepository) { $this->validateAuthentication(); - $tagsLabels = $request->get('tags', ''); + $tagsLabels = $request->request->get('tags', $request->query->get('tags', '')); $tags = $tagRepository->findByLabelsAndUser(explode(',', $tagsLabels), $this->getUser()->getId()); @@ -158,10 +155,9 @@ class TagRestController extends WallabagRestController * ) * ) * - * @Route("/api/tags/{tag}.{_format}", methods={"DELETE"}, name="api_delete_tag", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/tags/{tag}.{_format}', name: 'api_delete_tag', methods: ['DELETE'], defaults: ['_format' => 'json'])] public function deleteTagAction(Tag $tag, TagRepository $tagRepository, EntryRepository $entryRepository) { $this->validateAuthentication(); diff --git a/src/Controller/Api/TaggingRuleRestController.php b/src/Controller/Api/TaggingRuleRestController.php index 19f879132..7f9f11d5f 100644 --- a/src/Controller/Api/TaggingRuleRestController.php +++ b/src/Controller/Api/TaggingRuleRestController.php @@ -23,10 +23,9 @@ class TaggingRuleRestController extends WallabagRestController * ) * ) * - * @Route("/api/taggingrule/export.{_format}", methods={"GET"}, name="api_get_taggingrule_export", defaults={"_format": "json"}) - * * @return Response */ + #[Route(path: '/api/taggingrule/export.{_format}', name: 'api_get_taggingrule_export', methods: ['GET'], defaults: ['_format' => 'json'])] public function getTaggingruleExportAction() { $this->validateAuthentication(); @@ -37,7 +36,7 @@ class TaggingRuleRestController extends WallabagRestController SerializationContext::create()->setGroups(['export_tagging_rule']) ); - return Response::create( + return new Response( $data, 200, [ diff --git a/src/Controller/Api/UserRestController.php b/src/Controller/Api/UserRestController.php index a9319bea2..79c73d4bd 100644 --- a/src/Controller/Api/UserRestController.php +++ b/src/Controller/Api/UserRestController.php @@ -34,7 +34,7 @@ class UserRestController extends WallabagRestController * ) * ) * - * @Route("/api/user.{_format}", methods={"GET"}, name="api_get_user", defaults={"_format": "json"}) + * @Route("/api/user.{_format}", name="api_get_user", methods={"GET"}, defaults={"_format": "json"}) * * @return JsonResponse */ @@ -98,13 +98,13 @@ class UserRestController extends WallabagRestController * * @todo Make this method (or the whole API) accessible only through https * - * @Route("/api/user.{_format}", methods={"PUT"}, name="api_put_user", defaults={"_format": "json"}) + * @Route("/api/user.{_format}", name="api_put_user", methods={"PUT"}, defaults={"_format": "json"}) * * @return JsonResponse */ public function putUserAction(Request $request, Config $craueConfig, UserManagerInterface $userManager, EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher) { - if (!$this->getParameter('fosuser_registration') || !$craueConfig->get('api_user_registration')) { + if (!$this->registrationEnabled || !$craueConfig->get('api_user_registration')) { $json = $this->serializer->serialize(['error' => "Server doesn't allow registrations"], 'json'); return (new JsonResponse()) diff --git a/src/Controller/Api/WallabagRestController.php b/src/Controller/Api/WallabagRestController.php index 80f0eeb4c..fad611536 100644 --- a/src/Controller/Api/WallabagRestController.php +++ b/src/Controller/Api/WallabagRestController.php @@ -21,19 +21,14 @@ use Wallabag\Entity\User; class WallabagRestController extends AbstractFOSRestController { - protected EntityManagerInterface $entityManager; - protected SerializerInterface $serializer; - protected AuthorizationCheckerInterface $authorizationChecker; - protected TokenStorageInterface $tokenStorage; - protected TranslatorInterface $translator; - - public function __construct(EntityManagerInterface $entityManager, SerializerInterface $serializer, AuthorizationCheckerInterface $authorizationChecker, TokenStorageInterface $tokenStorage, TranslatorInterface $translator) - { - $this->entityManager = $entityManager; - $this->serializer = $serializer; - $this->authorizationChecker = $authorizationChecker; - $this->tokenStorage = $tokenStorage; - $this->translator = $translator; + public function __construct( + protected EntityManagerInterface $entityManager, + protected SerializerInterface $serializer, + protected AuthorizationCheckerInterface $authorizationChecker, + protected TokenStorageInterface $tokenStorage, + protected TranslatorInterface $translator, + protected bool $registrationEnabled, + ) { } /** @@ -55,10 +50,9 @@ class WallabagRestController extends AbstractFOSRestController * * @deprecated Should use info endpoint instead * - * @Route("/api/version.{_format}", methods={"GET"}, name="api_get_version", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/version.{_format}', name: 'api_get_version', methods: ['GET'], defaults: ['_format' => 'json'])] public function getVersionAction() { $version = $this->getParameter('wallabag.version'); @@ -78,15 +72,14 @@ class WallabagRestController extends AbstractFOSRestController * ) * ) * - * @Route("/api/info.{_format}", methods={"GET"}, name="api_get_info", defaults={"_format": "json"}) - * * @return JsonResponse */ + #[Route(path: '/api/info.{_format}', name: 'api_get_info', methods: ['GET'], defaults: ['_format' => 'json'])] public function getInfoAction(Config $craueConfig) { $info = new ApplicationInfo( $this->getParameter('wallabag.version'), - $this->getParameter('fosuser_registration') && $craueConfig->get('api_user_registration'), + $this->registrationEnabled && $craueConfig->get('api_user_registration'), ); return (new JsonResponse())->setJson($this->serializer->serialize($info, 'json')); @@ -99,22 +92,6 @@ class WallabagRestController extends AbstractFOSRestController } } - /** - * Validate that the first id is equal to the second one. - * If not, throw exception. It means a user try to access information from an other user. - * - * @param int $requestUserId User id from the requested source - */ - protected function validateUserAccess($requestUserId) - { - $user = $this->tokenStorage->getToken()->getUser(); - \assert($user instanceof User); - - if ($requestUserId !== $user->getId()) { - throw $this->createAccessDeniedException('Access forbidden. Entry user id: ' . $requestUserId . ', logged user id: ' . $user->getId()); - } - } - /** * Shortcut to send data serialized in json. * diff --git a/src/Controller/ConfigController.php b/src/Controller/ConfigController.php index 80ab75095..fde92ddaa 100644 --- a/src/Controller/ConfigController.php +++ b/src/Controller/ConfigController.php @@ -10,19 +10,20 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerBuilder; use PragmaRX\Recovery\Recovery as BackupCodes; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; use Symfony\Component\Validator\Validator\ValidatorInterface; use Wallabag\Entity\Config as ConfigEntity; use Wallabag\Entity\IgnoreOriginUserRule; -use Wallabag\Entity\RuleInterface; use Wallabag\Entity\TaggingRule; use Wallabag\Event\ConfigUpdatedEvent; use Wallabag\Form\Type\ChangePasswordType; @@ -44,38 +45,20 @@ use Wallabag\Tools\Utils; class ConfigController extends AbstractController { - private EntityManagerInterface $entityManager; - private UserManagerInterface $userManager; - private EntryRepository $entryRepository; - private TagRepository $tagRepository; - private AnnotationRepository $annotationRepository; - private ConfigRepository $configRepository; - private EventDispatcherInterface $eventDispatcher; - private Redirect $redirectHelper; - public function __construct( - EntityManagerInterface $entityManager, - UserManagerInterface $userManager, - EntryRepository $entryRepository, - TagRepository $tagRepository, - AnnotationRepository $annotationRepository, - ConfigRepository $configRepository, - EventDispatcherInterface $eventDispatcher, - Redirect $redirectHelper + private readonly EntityManagerInterface $entityManager, + private readonly UserManagerInterface $userManager, + private readonly EntryRepository $entryRepository, + private readonly TagRepository $tagRepository, + private readonly AnnotationRepository $annotationRepository, + private readonly ConfigRepository $configRepository, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly Redirect $redirectHelper, ) { - $this->entityManager = $entityManager; - $this->userManager = $userManager; - $this->entryRepository = $entryRepository; - $this->tagRepository = $tagRepository; - $this->annotationRepository = $annotationRepository; - $this->configRepository = $configRepository; - $this->eventDispatcher = $eventDispatcher; - $this->redirectHelper = $redirectHelper; } - /** - * @Route("/config", name="config") - */ + #[Route(path: '/config', name: 'config', methods: ['GET', 'POST'])] + #[IsGranted('EDIT_CONFIG')] public function indexAction(Request $request, Config $craueConfig, TaggingRuleRepository $taggingRuleRepository, IgnoreOriginUserRuleRepository $ignoreOriginUserRuleRepository, UserRepository $userRepository) { $config = $this->getConfig(); @@ -108,7 +91,8 @@ class ConfigController extends AbstractController $message = 'flashes.config.notice.password_updated'; $user->setPlainPassword($pwdForm->get('new_password')->getData()); - $this->userManager->updateUser($user, true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); $this->addFlash('notice', $message); @@ -123,7 +107,8 @@ class ConfigController extends AbstractController $userForm->handleRequest($request); if ($userForm->isSubmitted() && $userForm->isValid()) { - $this->userManager->updateUser($user, true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); $this->addFlash( 'notice', @@ -257,26 +242,26 @@ class ConfigController extends AbstractController 'username' => $user->getUsername(), 'token' => $config->getFeedToken(), ], - 'wallabag_url' => $this->getParameter('domain_name'), 'enabled_users' => $userRepository->getSumEnabledUsers(), ]); } /** * Disable 2FA using email. - * - * @Route("/config/otp/email/disable", name="disable_otp_email", methods={"POST"}) */ + #[Route(path: '/config/otp/email/disable', name: 'disable_otp_email', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function disableOtpEmailAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); $user->setEmailTwoFactor(false); - $this->userManager->updateUser($user, true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); $this->addFlash( 'notice', @@ -288,13 +273,13 @@ class ConfigController extends AbstractController /** * Enable 2FA using email. - * - * @Route("/config/otp/email", name="config_otp_email", methods={"POST"}) */ + #[Route(path: '/config/otp/email', name: 'config_otp_email', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function otpEmailAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -303,7 +288,8 @@ class ConfigController extends AbstractController $user->setBackupCodes(null); $user->setEmailTwoFactor(true); - $this->userManager->updateUser($user, true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); $this->addFlash( 'notice', @@ -315,21 +301,23 @@ class ConfigController extends AbstractController /** * Disable 2FA using OTP app. - * - * @Route("/config/otp/app/disable", name="disable_otp_app", methods={"POST"}) */ + #[Route(path: '/config/otp/app/disable', name: 'disable_otp_app', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function disableOtpAppAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); $user->setGoogleAuthenticatorSecret(''); + $user->setGoogleAuthenticator(false); $user->setBackupCodes(null); - $this->userManager->updateUser($user, true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); $this->addFlash( 'notice', @@ -341,13 +329,13 @@ class ConfigController extends AbstractController /** * Enable 2FA using OTP app, user will need to confirm the generated code from the app. - * - * @Route("/config/otp/app", name="config_otp_app", methods={"POST"}) */ + #[Route(path: '/config/otp/app', name: 'config_otp_app', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -358,20 +346,14 @@ class ConfigController extends AbstractController $backupCodes = (new BackupCodes())->toArray(); $backupCodesHashed = array_map( - function ($backupCode) { - return password_hash($backupCode, \PASSWORD_DEFAULT); - }, + fn ($backupCode) => password_hash((string) $backupCode, \PASSWORD_DEFAULT), $backupCodes ); $user->setBackupCodes($backupCodesHashed); - $this->userManager->updateUser($user, true); - - $this->addFlash( - 'notice', - 'flashes.config.notice.otp_enabled' - ); + $this->userManager->updateUser($user); + $this->entityManager->flush(); return $this->render('Config/otp_app.html.twig', [ 'backupCodes' => $backupCodes, @@ -384,6 +366,7 @@ class ConfigController extends AbstractController * Cancelling 2FA using OTP app. * * @Route("/config/otp/app/cancel", name="config_otp_app_cancel") + * @IsGranted("EDIT_CONFIG") * * XXX: commented until we rewrite 2fa with a real two-steps activation */ @@ -400,59 +383,65 @@ class ConfigController extends AbstractController /** * Validate OTP code. - * - * @Route("/config/otp/app/check", name="config_otp_app_check", methods={"POST"}) */ + #[Route(path: '/config/otp/app/check', name: 'config_otp_app_check', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } + $user = $this->getUser(); + $isValid = $googleAuthenticator->checkCode( - $this->getUser(), - $request->get('_auth_code') + $user, + $request->request->get('_auth_code') ); - if (true === $isValid) { + if ($isValid) { $this->addFlash( 'notice', 'flashes.config.notice.otp_enabled' ); + $user->setGoogleAuthenticator(true); + $this->userManager->updateUser($user); + $this->entityManager->flush(); return $this->redirect($this->generateUrl('config') . '#set3'); } - $this->addFlash( - 'two_factor', - 'scheb_two_factor.code_invalid' - ); - $this->addFlash( 'notice', - 'scheb_two_factor.code_invalid' + 'flashes.config.notice.otp_code_invalid' ); - return $this->redirect($this->generateUrl('config') . '#set3'); + $user->setGoogleAuthenticatorSecret(null); + $user->setBackupCodes(null); + + $this->userManager->updateUser($user); + $this->entityManager->flush(); + + return $this->redirect($this->generateUrl('config_otp_app'), 307); } /** - * @Route("/generate-token", name="generate_token") - * * @return RedirectResponse|JsonResponse */ + #[Route(path: '/generate-token', name: 'generate_token', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function generateTokenAction(Request $request) { + if (!$this->isCsrfTokenValid('generate-token', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $config = $this->getConfig(); $config->setFeedToken(Utils::generateToken()); $this->entityManager->persist($config); $this->entityManager->flush(); - if ($request->isXmlHttpRequest()) { - return new JsonResponse(['token' => $config->getFeedToken()]); - } - $this->addFlash( 'notice', 'flashes.config.notice.feed_token_updated' @@ -462,22 +451,22 @@ class ConfigController extends AbstractController } /** - * @Route("/revoke-token", name="revoke_token") - * * @return RedirectResponse|JsonResponse */ + #[Route(path: '/revoke-token', name: 'revoke_token', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function revokeTokenAction(Request $request) { + if (!$this->isCsrfTokenValid('revoke-token', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $config = $this->getConfig(); $config->setFeedToken(null); $this->entityManager->persist($config); $this->entityManager->flush(); - if ($request->isXmlHttpRequest()) { - return new JsonResponse(); - } - $this->addFlash( 'notice', 'flashes.config.notice.feed_token_revoked' @@ -489,15 +478,17 @@ class ConfigController extends AbstractController /** * Deletes a tagging rule and redirect to the config homepage. * - * @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule") - * * @return RedirectResponse */ - public function deleteTaggingRuleAction(TaggingRule $rule) + #[Route(path: '/tagging-rule/delete/{taggingRule}', name: 'delete_tagging_rule', methods: ['POST'], requirements: ['taggingRule' => '\d+'])] + #[IsGranted('DELETE', subject: 'taggingRule')] + public function deleteTaggingRuleAction(Request $request, TaggingRule $taggingRule) { - $this->validateRuleAction($rule); + if (!$this->isCsrfTokenValid('delete-tagging-rule', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } - $this->entityManager->remove($rule); + $this->entityManager->remove($taggingRule); $this->entityManager->flush(); $this->addFlash( @@ -511,29 +502,29 @@ class ConfigController extends AbstractController /** * Edit a tagging rule. * - * @Route("/tagging-rule/edit/{id}", requirements={"id" = "\d+"}, name="edit_tagging_rule") - * * @return RedirectResponse */ - public function editTaggingRuleAction(TaggingRule $rule) + #[Route(path: '/tagging-rule/edit/{taggingRule}', name: 'edit_tagging_rule', methods: ['GET'], requirements: ['taggingRule' => '\d+'])] + #[IsGranted('EDIT', subject: 'taggingRule')] + public function editTaggingRuleAction(TaggingRule $taggingRule) { - $this->validateRuleAction($rule); - - return $this->redirect($this->generateUrl('config') . '?tagging-rule=' . $rule->getId() . '#set5'); + return $this->redirect($this->generateUrl('config') . '?tagging-rule=' . $taggingRule->getId() . '#set5'); } /** * Deletes an ignore origin rule and redirect to the config homepage. * - * @Route("/ignore-origin-user-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_ignore_origin_rule") - * * @return RedirectResponse */ - public function deleteIgnoreOriginRuleAction(IgnoreOriginUserRule $rule) + #[Route(path: '/ignore-origin-user-rule/delete/{ignoreOriginUserRule}', name: 'delete_ignore_origin_rule', methods: ['POST'], requirements: ['ignoreOriginUserRule' => '\d+'])] + #[IsGranted('DELETE', subject: 'ignoreOriginUserRule')] + public function deleteIgnoreOriginRuleAction(Request $request, IgnoreOriginUserRule $ignoreOriginUserRule) { - $this->validateRuleAction($rule); + if (!$this->isCsrfTokenValid('delete-ignore-origin-rule', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } - $this->entityManager->remove($rule); + $this->entityManager->remove($ignoreOriginUserRule); $this->entityManager->flush(); $this->addFlash( @@ -547,28 +538,26 @@ class ConfigController extends AbstractController /** * Edit an ignore origin rule. * - * @Route("/ignore-origin-user-rule/edit/{id}", requirements={"id" = "\d+"}, name="edit_ignore_origin_rule") - * * @return RedirectResponse */ - public function editIgnoreOriginRuleAction(IgnoreOriginUserRule $rule) + #[Route(path: '/ignore-origin-user-rule/edit/{ignoreOriginUserRule}', name: 'edit_ignore_origin_rule', methods: ['GET'], requirements: ['ignoreOriginUserRule' => '\d+'])] + #[IsGranted('EDIT', subject: 'ignoreOriginUserRule')] + public function editIgnoreOriginRuleAction(IgnoreOriginUserRule $ignoreOriginUserRule) { - $this->validateRuleAction($rule); - - return $this->redirect($this->generateUrl('config') . '?ignore-origin-user-rule=' . $rule->getId() . '#set6'); + return $this->redirect($this->generateUrl('config') . '?ignore-origin-user-rule=' . $ignoreOriginUserRule->getId() . '#set6'); } /** * Remove all annotations OR tags OR entries for the current user. * - * @Route("/reset/{type}", requirements={"id" = "annotations|tags|entries|tagging_rules"}, name="config_reset", methods={"POST"}) - * * @return RedirectResponse */ + #[Route(path: '/reset/{type}', name: 'config_reset', methods: ['POST'], requirements: ['id' => 'annotations|tags|entries|tagging_rules'])] + #[IsGranted('EDIT_CONFIG')] public function resetAction(Request $request, string $type, AnnotationRepository $annotationRepository, EntryRepository $entryRepository, TaggingRuleRepository $taggingRuleRepository) { if (!$this->isCsrfTokenValid('reset-area', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } switch ($type) { @@ -616,16 +605,15 @@ class ConfigController extends AbstractController /** * Delete account for current user. * - * @Route("/account/delete", name="delete_account", methods={"POST"}) - * * @throws AccessDeniedHttpException - * * @return RedirectResponse */ + #[Route(path: '/account/delete', name: 'delete_account', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function deleteAccountAction(Request $request, UserRepository $userRepository, TokenStorageInterface $tokenStorage) { if (!$this->isCsrfTokenValid('delete-account', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $enabledUsers = $userRepository->getSumEnabledUsers(); @@ -648,14 +636,18 @@ class ConfigController extends AbstractController /** * Switch view mode for current user. * - * @Route("/config/view-mode", name="switch_view_mode") - * * @return RedirectResponse */ + #[Route(path: '/config/view-mode', name: 'switch_view_mode', methods: ['POST'])] + #[IsGranted('EDIT_CONFIG')] public function changeViewModeAction(Request $request) { + if (!$this->isCsrfTokenValid('switch-view-mode', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $user = $this->getUser(); - $user->getConfig()->setListMode(!$user->getConfig()->getListMode()); + $user->getConfig()->setListMode((int) !$user->getConfig()->getListMode()); $this->entityManager->persist($user); $this->entityManager->flush(); @@ -670,12 +662,16 @@ class ConfigController extends AbstractController * * @param string $language * - * @Route("/locale/{language}", name="changeLocale") - * * @return RedirectResponse */ + #[Route(path: '/locale/{language}', name: 'changeLocale', methods: ['POST'])] + #[IsGranted('PUBLIC_ACCESS')] public function setLocaleAction(Request $request, ValidatorInterface $validator, $language = null) { + if (!$this->isCsrfTokenValid('change-locale', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $errors = $validator->validate($language, new LocaleConstraint(['canonicalize' => true])); if (0 === \count($errors)) { @@ -688,10 +684,10 @@ class ConfigController extends AbstractController /** * Export tagging rules for the logged in user. * - * @Route("/tagging-rule/export", name="export_tagging_rule") - * * @return Response */ + #[Route(path: '/tagging-rule/export', name: 'export_tagging_rule', methods: ['GET'])] + #[IsGranted('EDIT_CONFIG')] public function exportTaggingRulesAction() { $data = SerializerBuilder::create()->build()->serialize( @@ -700,7 +696,7 @@ class ConfigController extends AbstractController SerializationContext::create()->setGroups(['export_tagging_rule']) ); - return Response::create( + return new Response( $data, 200, [ @@ -769,16 +765,6 @@ class ConfigController extends AbstractController $this->entityManager->flush(); } - /** - * Validate that a rule can be edited/deleted by the current user. - */ - private function validateRuleAction(RuleInterface $rule) - { - if ($this->getUser()->getId() !== $rule->getConfig()->getUser()->getId()) { - throw $this->createAccessDeniedException('You can not access this rule.'); - } - } - /** * Retrieve config for the current user. * If no config were found, create a new one. diff --git a/src/Controller/EntryController.php b/src/Controller/EntryController.php index bd5fb4bad..ca34e0dd1 100644 --- a/src/Controller/EntryController.php +++ b/src/Controller/EntryController.php @@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; @@ -33,35 +34,29 @@ use Wallabag\Repository\TagRepository; class EntryController extends AbstractController { - private EntityManagerInterface $entityManager; - private EventDispatcherInterface $eventDispatcher; - private EntryRepository $entryRepository; - private Redirect $redirectHelper; - private PreparePagerForEntries $preparePagerForEntriesHelper; - private FilterBuilderUpdaterInterface $filterBuilderUpdater; - private ContentProxy $contentProxy; - private Security $security; - - public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, EntryRepository $entryRepository, Redirect $redirectHelper, PreparePagerForEntries $preparePagerForEntriesHelper, FilterBuilderUpdaterInterface $filterBuilderUpdater, ContentProxy $contentProxy, Security $security) - { - $this->entityManager = $entityManager; - $this->eventDispatcher = $eventDispatcher; - $this->entryRepository = $entryRepository; - $this->redirectHelper = $redirectHelper; - $this->preparePagerForEntriesHelper = $preparePagerForEntriesHelper; - $this->filterBuilderUpdater = $filterBuilderUpdater; - $this->contentProxy = $contentProxy; - $this->security = $security; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly EntryRepository $entryRepository, + private readonly Redirect $redirectHelper, + private readonly PreparePagerForEntries $preparePagerForEntriesHelper, + private readonly FilterBuilderUpdaterInterface $filterBuilderUpdater, + private readonly ContentProxy $contentProxy, + private readonly Security $security, + ) { } /** - * @Route("/mass", name="mass_action") - * @IsGranted("EDIT_ENTRIES") - * * @return Response */ + #[Route(path: '/mass', name: 'mass_action', methods: ['POST'])] + #[IsGranted('EDIT_ENTRIES')] public function massAction(Request $request, TagRepository $tagRepository) { + if (!$this->isCsrfTokenValid('mass-action', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $values = $request->request->all(); $tagsToAdd = []; @@ -76,7 +71,7 @@ class EntryController extends AbstractController $action = 'tag'; if (isset($values['tags'])) { - $labels = array_filter(explode(',', $values['tags']), + $labels = array_filter(explode(',', (string) $values['tags']), function ($v) { $v = trim($v); @@ -107,7 +102,7 @@ class EntryController extends AbstractController if (isset($values['entry-checkbox'])) { foreach ($values['entry-checkbox'] as $id) { /** @var Entry * */ - $entry = $this->entryRepository->findById((int) $id)[0]; + $entry = $this->entryRepository->findById([(int) $id])[0]; if (!$this->security->isGranted('EDIT', $entry)) { throw $this->createAccessDeniedException('You can not access this entry.'); @@ -141,14 +136,12 @@ class EntryController extends AbstractController /** * @param int $page * - * @Route("/search/{page}", name="search", defaults={"page" = 1}) - * @IsGranted("LIST_ENTRIES") - * * Default parameter for page is hardcoded (in duplication of the defaults from the Route) * because this controller is also called inside the layout template without any page as argument - * * @return Response */ + #[Route(path: '/search/{page}', name: 'search', methods: ['GET', 'POST'], defaults: ['page' => 1])] + #[IsGranted('LIST_ENTRIES')] public function searchFormAction(Request $request, $page = 1, $currentRoute = null) { // fallback to retrieve currentRoute from query parameter instead of injected one (when using inside a template) @@ -171,11 +164,10 @@ class EntryController extends AbstractController } /** - * @Route("/new-entry", name="new_entry") - * @IsGranted("CREATE_ENTRIES") - * * @return Response */ + #[Route(path: '/new-entry', name: 'new_entry', methods: ['GET', 'POST'])] + #[IsGranted('CREATE_ENTRIES')] public function addEntryFormAction(Request $request, TranslatorInterface $translator) { $entry = new Entry($this->getUser()); @@ -204,6 +196,8 @@ class EntryController extends AbstractController // entry saved, dispatch event about it! $this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME); + return $this->redirect($this->generateUrl('homepage')); + } elseif ($form->isSubmitted() && !$form->isValid()) { return $this->redirect($this->generateUrl('homepage')); } @@ -213,15 +207,14 @@ class EntryController extends AbstractController } /** - * @Route("/bookmarklet", name="bookmarklet") - * @IsGranted("CREATE_ENTRIES") - * * @return Response */ + #[Route(path: '/bookmarklet', name: 'bookmarklet', methods: ['GET'])] + #[IsGranted('CREATE_ENTRIES')] public function addEntryViaBookmarkletAction(Request $request) { $entry = new Entry($this->getUser()); - $entry->setUrl($request->get('url')); + $entry->setUrl($request->query->get('url')); if (false === $this->checkIfEntryAlreadyExists($entry)) { $this->updateEntry($entry); @@ -237,11 +230,10 @@ class EntryController extends AbstractController } /** - * @Route("/new", name="new") - * @IsGranted("CREATE_ENTRIES") - * * @return Response */ + #[Route(path: '/new', name: 'new', methods: ['GET'])] + #[IsGranted('CREATE_ENTRIES')] public function addEntryAction() { return $this->render('Entry/new.html.twig'); @@ -250,11 +242,10 @@ class EntryController extends AbstractController /** * Edit an entry content. * - * @Route("/edit/{id}", requirements={"id" = "\d+"}, name="edit") - * @IsGranted("EDIT", subject="entry") - * * @return Response */ + #[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'], requirements: ['id' => '\d+'])] + #[IsGranted('EDIT', subject: 'entry')] public function editEntryAction(Request $request, Entry $entry) { $form = $this->createForm(EditEntryType::class, $entry); @@ -283,11 +274,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/all/list/{page}", name="all", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/all/list/{page}', name: 'all', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showAllAction(Request $request, $page) { return $this->showEntries('all', $request, $page); @@ -298,11 +288,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/unread/list/{page}", name="unread", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/unread/list/{page}', name: 'unread', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showUnreadAction(Request $request, $page) { // load the quickstart if no entry in database @@ -318,11 +307,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/archive/list/{page}", name="archive", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/archive/list/{page}', name: 'archive', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showArchiveAction(Request $request, $page) { return $this->showEntries('archive', $request, $page); @@ -333,11 +321,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/starred/list/{page}", name="starred", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/starred/list/{page}', name: 'starred', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showStarredAction(Request $request, $page) { return $this->showEntries('starred', $request, $page); @@ -348,11 +335,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/untagged/list/{page}", name="untagged", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/untagged/list/{page}', name: 'untagged', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showUntaggedEntriesAction(Request $request, $page) { return $this->showEntries('untagged', $request, $page); @@ -363,11 +349,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/annotated/list/{page}", name="annotated", defaults={"page" = "1"}) - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/annotated/list/{page}', name: 'annotated', methods: ['GET'], defaults: ['page' => '1'])] + #[IsGranted('LIST_ENTRIES')] public function showWithAnnotationsEntriesAction(Request $request, $page) { return $this->showEntries('annotated', $request, $page); @@ -376,17 +361,16 @@ class EntryController extends AbstractController /** * Shows random entry depending on the given type. * - * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|annotated|all"}) - * @IsGranted("LIST_ENTRIES") - * * @return RedirectResponse */ + #[Route(path: '/{type}/random', name: 'random_entry', methods: ['GET'], requirements: ['type' => 'unread|starred|archive|untagged|annotated|all'])] + #[IsGranted('LIST_ENTRIES')] public function redirectRandomEntryAction(string $type = 'all') { try { $entry = $this->entryRepository ->getRandomEntry($this->getUser()->getId(), $type); - } catch (NoResultException $e) { + } catch (NoResultException) { $this->addFlash('notice', 'flashes.entry.notice.no_random_entry'); return $this->redirect($this->generateUrl($type)); @@ -398,11 +382,10 @@ class EntryController extends AbstractController /** * Shows entry content. * - * @Route("/view/{id}", requirements={"id" = "\d+"}, name="view") - * @IsGranted("VIEW", subject="entry") - * * @return Response */ + #[Route(path: '/view/{id}', name: 'view', methods: ['GET'], requirements: ['id' => '\d+'])] + #[IsGranted('VIEW', subject: 'entry')] public function viewAction(Entry $entry) { return $this->render( @@ -415,13 +398,16 @@ class EntryController extends AbstractController * Reload an entry. * Refetch content from the website and make it readable again. * - * @Route("/reload/{id}", requirements={"id" = "\d+"}, name="reload_entry") - * @IsGranted("RELOAD", subject="entry") - * * @return RedirectResponse */ - public function reloadAction(Entry $entry) + #[Route(path: '/reload/{id}', name: 'reload_entry', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('RELOAD', subject: 'entry')] + public function reloadAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('reload-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $this->updateEntry($entry, 'entry_reloaded'); // if refreshing entry failed, don't save it @@ -443,13 +429,16 @@ class EntryController extends AbstractController /** * Changes read status for an entry. * - * @Route("/archive/{id}", requirements={"id" = "\d+"}, name="archive_entry") - * @IsGranted("ARCHIVE", subject="entry") - * * @return RedirectResponse */ + #[Route(path: '/archive/{id}', name: 'archive_entry', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('ARCHIVE', subject: 'entry')] public function toggleArchiveAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('archive-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->toggleArchive(); $this->entityManager->flush(); @@ -471,13 +460,16 @@ class EntryController extends AbstractController /** * Changes starred status for an entry. * - * @Route("/star/{id}", requirements={"id" = "\d+"}, name="star_entry") - * @IsGranted("STAR", subject="entry") - * * @return RedirectResponse */ + #[Route(path: '/star/{id}', name: 'star_entry', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('STAR', subject: 'entry')] public function toggleStarAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('star-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->toggleStar(); $entry->updateStar($entry->isStarred()); $this->entityManager->flush(); @@ -500,13 +492,16 @@ class EntryController extends AbstractController /** * Deletes entry and redirect to the homepage or the last viewed page. * - * @Route("/delete/{id}", requirements={"id" = "\d+"}, name="delete_entry") - * @IsGranted("DELETE", subject="entry") - * * @return RedirectResponse */ + #[Route(path: '/delete/{id}', name: 'delete_entry', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('DELETE', subject: 'entry')] public function deleteEntryAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('delete-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + // generates the view url for this entry to check for redirection later // to avoid redirecting to the deleted entry. Ugh. $url = $this->generateUrl( @@ -537,13 +532,16 @@ class EntryController extends AbstractController /** * Get public URL for entry (and generate it if necessary). * - * @Route("/share/{id}", requirements={"id" = "\d+"}, name="share") - * @IsGranted("SHARE", subject="entry") - * * @return Response */ - public function shareAction(Entry $entry) + #[Route(path: '/share/{id}', name: 'share', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('SHARE', subject: 'entry')] + public function shareAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('share-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + if (null === $entry->getUid()) { $entry->generateUid(); @@ -559,13 +557,16 @@ class EntryController extends AbstractController /** * Disable public sharing for an entry. * - * @Route("/share/delete/{id}", requirements={"id" = "\d+"}, name="delete_share") - * @IsGranted("UNSHARE", subject="entry") - * * @return Response */ - public function deleteShareAction(Entry $entry) + #[Route(path: '/share/delete/{id}', name: 'delete_share', methods: ['POST'], requirements: ['id' => '\d+'])] + #[IsGranted('UNSHARE', subject: 'entry')] + public function deleteShareAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('delete-share', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->cleanUid(); $this->entityManager->persist($entry); @@ -579,12 +580,11 @@ class EntryController extends AbstractController /** * Ability to view a content publicly. * - * @Route("/share/{uid}", requirements={"uid" = ".+"}, name="share_entry") - * @Cache(maxage="25200", smaxage="25200", public=true) - * @IsGranted("PUBLIC_ACCESS") - * * @return Response */ + #[Route(path: '/share/{uid}', name: 'share_entry', methods: ['GET'], requirements: ['uid' => '.+'])] + #[Cache(maxage: 25200, smaxage: 25200, public: true)] + #[IsGranted('PUBLIC_ACCESS')] public function shareEntryAction(Entry $entry, Config $craueConfig) { if (!$craueConfig->get('share_public')) { @@ -602,11 +602,10 @@ class EntryController extends AbstractController * * @param int $page * - * @Route("/domain/{id}/{page}", requirements={"id" = "\d+"}, defaults={"page" = 1}, name="same_domain") - * @IsGranted("LIST_ENTRIES") - * * @return Response */ + #[Route(path: '/domain/{id}/{page}', name: 'same_domain', methods: ['GET'], requirements: ['id' => '\d+'], defaults: ['page' => 1])] + #[IsGranted('LIST_ENTRIES')] public function getSameDomainEntries(Request $request, $page = 1) { return $this->showEntries('same-domain', $request, $page); @@ -623,8 +622,9 @@ class EntryController extends AbstractController */ private function showEntries($type, Request $request, $page) { - $searchTerm = (isset($request->get('search_entry')['term']) ? trim($request->get('search_entry')['term']) : ''); - $currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : ''); + $searchTerm = (isset($request->query->all('search_entry')['term']) ? trim((string) $request->query->all('search_entry')['term']) : ''); + $currentRoute = $request->query->get('currentRoute') ?? ''; + $currentEntryId = $request->attributes->getInt('id'); $formOptions = []; @@ -651,7 +651,7 @@ class EntryController extends AbstractController $formOptions['filter_unread'] = true; break; case 'same-domain': - $qb = $this->entryRepository->getBuilderForSameDomainByUser($this->getUser()->getId(), $request->get('id')); + $qb = $this->entryRepository->getBuilderForSameDomainByUser($this->getUser()->getId(), $currentEntryId); break; case 'all': $qb = $this->entryRepository->getBuilderForAllByUser($this->getUser()->getId()); @@ -664,7 +664,7 @@ class EntryController extends AbstractController if ($request->query->has($form->getName())) { // manually bind values from the request - $form->submit($request->query->get($form->getName())); + $form->submit($request->query->all($form->getName())); // build the query from the given form object $this->filterBuilderUpdater->addFilterConditions($form, $qb); @@ -676,7 +676,7 @@ class EntryController extends AbstractController try { $entries->setCurrentPage($page); - } catch (OutOfRangeCurrentPageException $e) { + } catch (OutOfRangeCurrentPageException) { if ($page > 1) { return $this->redirect($this->generateUrl($type, ['page' => $entries->getNbPages()]), 302); } @@ -705,7 +705,7 @@ class EntryController extends AbstractController try { $this->contentProxy->updateEntry($entry, $entry->getUrl()); - } catch (\Exception $e) { + } catch (\Exception) { // $this->logger->error('Error while saving an entry', [ // 'exception' => $e, // 'entry' => $entry, diff --git a/src/Controller/ExportController.php b/src/Controller/ExportController.php index c49a3d2c1..7998d54a8 100644 --- a/src/Controller/ExportController.php +++ b/src/Controller/ExportController.php @@ -2,10 +2,12 @@ namespace Wallabag\Controller; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; +use Wallabag\Entity\Entry; use Wallabag\Helper\EntriesExport; use Wallabag\Repository\EntryRepository; use Wallabag\Repository\TagRepository; @@ -19,27 +21,13 @@ class ExportController extends AbstractController /** * Gets one entry content. * - * @Route("/export/{id}.{format}", name="export_entry", requirements={ - * "format": "epub|pdf|json|xml|txt|csv|md", - * "id": "\d+" - * }) - * * @return Response */ - public function downloadEntryAction(Request $request, EntryRepository $entryRepository, EntriesExport $entriesExport, string $format, int $id) + #[Route(path: '/export/{entry}.{format}', name: 'export_entry', methods: ['GET'], requirements: ['format' => 'epub|pdf|json|xml|txt|csv|md', 'entry' => '\d+'])] + #[IsGranted('EXPORT', subject: 'entry')] + public function downloadEntryAction(Request $request, EntryRepository $entryRepository, EntriesExport $entriesExport, string $format, Entry $entry) { try { - $entry = $entryRepository->find($id); - - /* - * We duplicate EntryController::checkUserAction here as a quick fix for an improper authorization vulnerability - * - * This should be eventually rewritten - */ - if (null === $entry || null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) { - throw new NotFoundHttpException(); - } - return $entriesExport ->setEntries($entry) ->updateTitle('entry') @@ -53,13 +41,10 @@ class ExportController extends AbstractController /** * Export all entries for current user. * - * @Route("/export/{category}.{format}", name="export_entries", requirements={ - * "format": "epub|pdf|json|xml|txt|csv|md", - * "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain" - * }) - * * @return Response */ + #[Route(path: '/export/{category}.{format}', name: 'export_entries', methods: ['GET'], requirements: ['format' => 'epub|pdf|json|xml|txt|csv|md', 'category' => 'all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain'])] + #[IsGranted('EXPORT_ENTRIES')] public function downloadEntriesAction(Request $request, EntryRepository $entryRepository, TagRepository $tagRepository, EntriesExport $entriesExport, string $format, string $category, int $entry = 0) { $method = ucfirst($category); @@ -69,7 +54,7 @@ class ExportController extends AbstractController if ('same_domain' === $category) { $entries = $entryRepository->getBuilderForSameDomainByUser( $this->getUser()->getId(), - $request->get('entry') + $request->query->getInt('entry') )->getQuery() ->getResult(); @@ -84,8 +69,8 @@ class ExportController extends AbstractController $title = 'Tag ' . $tag->getLabel(); } elseif ('search' === $category) { - $searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : ''); - $currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : ''); + $searchTerm = $request->query->all('search_entry')['term'] ?? ''; + $currentRoute = $request->query->get('currentRoute') ?? ''; $entries = $entryRepository->getBuilderForSearchByUser( $this->getUser()->getId(), diff --git a/src/Controller/FeedController.php b/src/Controller/FeedController.php index 70b9de359..3b69d6c47 100644 --- a/src/Controller/FeedController.php +++ b/src/Controller/FeedController.php @@ -6,6 +6,7 @@ use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; use Pagerfanta\Pagerfanta; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -19,22 +20,19 @@ use Wallabag\Repository\EntryRepository; class FeedController extends AbstractController { - private EntryRepository $entryRepository; - - public function __construct(EntryRepository $entryRepository) - { - $this->entryRepository = $entryRepository; + public function __construct( + private readonly EntryRepository $entryRepository, + ) { } /** * Shows unread entries for current user. * - * @Route("/feed/{username}/{token}/unread/{page}", name="unread_feed", defaults={"page"=1, "_format"="xml"}) - * - * @ParamConverter("user", class="Wallabag\Entity\User", converter="username_feed_token_converter") - * * @return Response */ + #[Route(path: '/feed/{username}/{token}/unread/{page}', name: 'unread_feed', methods: ['GET'], defaults: ['page' => 1, '_format' => 'xml'])] + #[IsGranted('PUBLIC_ACCESS')] + #[ParamConverter('user', class: User::class, converter: 'username_feed_token_converter')] public function showUnreadFeedAction(User $user, $page) { return $this->showEntries('unread', $user, $page); @@ -43,12 +41,11 @@ class FeedController extends AbstractController /** * Shows read entries for current user. * - * @Route("/feed/{username}/{token}/archive/{page}", name="archive_feed", defaults={"page"=1, "_format"="xml"}) - * - * @ParamConverter("user", class="Wallabag\Entity\User", converter="username_feed_token_converter") - * * @return Response */ + #[Route(path: '/feed/{username}/{token}/archive/{page}', name: 'archive_feed', methods: ['GET'], defaults: ['page' => 1, '_format' => 'xml'])] + #[IsGranted('PUBLIC_ACCESS')] + #[ParamConverter('user', class: User::class, converter: 'username_feed_token_converter')] public function showArchiveFeedAction(User $user, $page) { return $this->showEntries('archive', $user, $page); @@ -57,12 +54,11 @@ class FeedController extends AbstractController /** * Shows starred entries for current user. * - * @Route("/feed/{username}/{token}/starred/{page}", name="starred_feed", defaults={"page"=1, "_format"="xml"}) - * - * @ParamConverter("user", class="Wallabag\Entity\User", converter="username_feed_token_converter") - * * @return Response */ + #[Route(path: '/feed/{username}/{token}/starred/{page}', name: 'starred_feed', methods: ['GET'], defaults: ['page' => 1, '_format' => 'xml'])] + #[IsGranted('PUBLIC_ACCESS')] + #[ParamConverter('user', class: User::class, converter: 'username_feed_token_converter')] public function showStarredFeedAction(User $user, $page) { return $this->showEntries('starred', $user, $page); @@ -71,12 +67,11 @@ class FeedController extends AbstractController /** * Shows all entries for current user. * - * @Route("/feed/{username}/{token}/all/{page}", name="all_feed", defaults={"page"=1, "_format"="xml"}) - * - * @ParamConverter("user", class="Wallabag\Entity\User", converter="username_feed_token_converter") - * * @return Response */ + #[Route(path: '/feed/{username}/{token}/all/{page}', name: 'all_feed', methods: ['GET'], defaults: ['page' => 1, '_format' => 'xml'])] + #[IsGranted('PUBLIC_ACCESS')] + #[ParamConverter('user', class: User::class, converter: 'username_feed_token_converter')] public function showAllFeedAction(User $user, $page) { return $this->showEntries('all', $user, $page); @@ -85,13 +80,12 @@ class FeedController extends AbstractController /** * Shows entries associated to a tag for current user. * - * @Route("/feed/{username}/{token}/tags/{slug}/{page}", name="tag_feed", defaults={"page"=1, "_format"="xml"}) - * - * @ParamConverter("user", class="Wallabag\Entity\User", converter="username_feed_token_converter") - * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) - * * @return Response */ + #[Route(path: '/feed/{username}/{token}/tags/{slug}/{page}', name: 'tag_feed', methods: ['GET'], defaults: ['page' => 1, '_format' => 'xml'])] + #[IsGranted('PUBLIC_ACCESS')] + #[ParamConverter('user', class: User::class, converter: 'username_feed_token_converter')] + #[ParamConverter('tag', options: ['mapping' => ['slug' => 'slug']])] public function showTagsFeedAction(Request $request, User $user, Tag $tag, PreparePagerForEntries $preparePagerForEntries, $page) { $sort = $request->query->get('sort', 'created'); @@ -131,13 +125,9 @@ class FeedController extends AbstractController $perPage = $user->getConfig()->getFeedLimit() ?: $this->getParameter('wallabag.feed_limit'); $entries->setMaxPerPage($perPage); - if (null === $entries) { - throw $this->createNotFoundException('No entries found?'); - } - try { $entries->setCurrentPage($page); - } catch (OutOfRangeCurrentPageException $e) { + } catch (OutOfRangeCurrentPageException) { if ($page > 1) { return $this->redirect($url . '?page=' . $entries->getNbPages(), 302); } @@ -150,7 +140,6 @@ class FeedController extends AbstractController 'url' => $url, 'entries' => $entries, 'user' => $user->getUsername(), - 'domainName' => $this->getParameter('domain_name'), 'version' => $this->getParameter('wallabag.version'), 'tag' => $tag->getSlug(), 'updated' => $this->prepareFeedUpdatedDate($entries, $sort), @@ -186,22 +175,13 @@ class FeedController extends AbstractController */ private function showEntries(string $type, User $user, $page = 1) { - switch ($type) { - case 'starred': - $qb = $this->entryRepository->getBuilderForStarredByUser($user->getId()); - break; - case 'archive': - $qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId()); - break; - case 'unread': - $qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId()); - break; - case 'all': - $qb = $this->entryRepository->getBuilderForAllByUser($user->getId()); - break; - default: - throw new \InvalidArgumentException(\sprintf('Type "%s" is not implemented.', $type)); - } + $qb = match ($type) { + 'starred' => $this->entryRepository->getBuilderForStarredByUser($user->getId()), + 'archive' => $this->entryRepository->getBuilderForArchiveByUser($user->getId()), + 'unread' => $this->entryRepository->getBuilderForUnreadByUser($user->getId()), + 'all' => $this->entryRepository->getBuilderForAllByUser($user->getId()), + default => throw new \InvalidArgumentException(\sprintf('Type "%s" is not implemented.', $type)), + }; $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); $entries = new Pagerfanta($pagerAdapter); @@ -220,7 +200,7 @@ class FeedController extends AbstractController try { $entries->setCurrentPage((int) $page); - } catch (OutOfRangeCurrentPageException $e) { + } catch (OutOfRangeCurrentPageException) { if ($page > 1) { return $this->redirect($url . '/' . $entries->getNbPages()); } @@ -231,7 +211,6 @@ class FeedController extends AbstractController 'url' => $url, 'entries' => $entries, 'user' => $user->getUsername(), - 'domainName' => $this->getParameter('domain_name'), 'version' => $this->getParameter('wallabag.version'), 'updated' => $this->prepareFeedUpdatedDate($entries), ], new Response('', 200, ['Content-Type' => 'application/atom+xml'])); diff --git a/src/Controller/IgnoreOriginInstanceRuleController.php b/src/Controller/IgnoreOriginInstanceRuleController.php index 5d8652c28..996d7c49f 100644 --- a/src/Controller/IgnoreOriginInstanceRuleController.php +++ b/src/Controller/IgnoreOriginInstanceRuleController.php @@ -17,26 +17,20 @@ use Wallabag\Repository\IgnoreOriginInstanceRuleRepository; /** * IgnoreOriginInstanceRuleController controller. - * - * @Route("/ignore-origin-instance-rules") */ class IgnoreOriginInstanceRuleController extends AbstractController { - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; - - public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator) - { - $this->entityManager = $entityManager; - $this->translator = $translator; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + ) { } /** * Lists all IgnoreOriginInstanceRule entities. - * - * @Route("/", name="ignore_origin_instance_rules_index", methods={"GET"}) - * @IsGranted("LIST_IGNORE_ORIGIN_INSTANCE_RULES") */ + #[Route(path: '/ignore-origin-instance-rules', name: 'ignore_origin_instance_rules_index', methods: ['GET'])] + #[IsGranted('LIST_IGNORE_ORIGIN_INSTANCE_RULES')] public function indexAction(IgnoreOriginInstanceRuleRepository $repository) { $rules = $repository->findAll(); @@ -49,11 +43,10 @@ class IgnoreOriginInstanceRuleController extends AbstractController /** * Creates a new ignore origin instance rule entity. * - * @Route("/new", name="ignore_origin_instance_rules_new", methods={"GET", "POST"}) - * @IsGranted("CREATE_IGNORE_ORIGIN_INSTANCE_RULES") - * * @return Response */ + #[Route(path: '/ignore-origin-instance-rules/new', name: 'ignore_origin_instance_rules_new', methods: ['GET', 'POST'])] + #[IsGranted('CREATE_IGNORE_ORIGIN_INSTANCE_RULES')] public function newAction(Request $request) { $ignoreOriginInstanceRule = new IgnoreOriginInstanceRule(); @@ -82,11 +75,10 @@ class IgnoreOriginInstanceRuleController extends AbstractController /** * Displays a form to edit an existing ignore origin instance rule entity. * - * @Route("/{id}/edit", name="ignore_origin_instance_rules_edit", methods={"GET", "POST"}) - * @IsGranted("EDIT", subject="ignoreOriginInstanceRule") - * * @return Response */ + #[Route(path: '/ignore-origin-instance-rules/{id}/edit', name: 'ignore_origin_instance_rules_edit', methods: ['GET', 'POST'])] + #[IsGranted('EDIT', subject: 'ignoreOriginInstanceRule')] public function editAction(Request $request, IgnoreOriginInstanceRule $ignoreOriginInstanceRule) { $deleteForm = $this->createDeleteForm($ignoreOriginInstanceRule); @@ -115,11 +107,10 @@ class IgnoreOriginInstanceRuleController extends AbstractController /** * Deletes a site credential entity. * - * @Route("/{id}", name="ignore_origin_instance_rules_delete", methods={"DELETE"}) - * @IsGranted("DELETE", subject="ignoreOriginInstanceRule") - * * @return RedirectResponse */ + #[Route(path: '/ignore-origin-instance-rules/{id}', name: 'ignore_origin_instance_rules_delete', methods: ['DELETE'])] + #[IsGranted('DELETE', subject: 'ignoreOriginInstanceRule')] public function deleteAction(Request $request, IgnoreOriginInstanceRule $ignoreOriginInstanceRule) { $form = $this->createDeleteForm($ignoreOriginInstanceRule); diff --git a/src/Controller/Import/BrowserController.php b/src/Controller/Import/BrowserController.php index e4b5307b1..3b897248b 100644 --- a/src/Controller/Import/BrowserController.php +++ b/src/Controller/Import/BrowserController.php @@ -2,6 +2,7 @@ namespace Wallabag\Controller\Import; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -13,10 +14,10 @@ use Wallabag\Import\ImportInterface; abstract class BrowserController extends AbstractController { /** - * @Route("/import/browser", name="import_browser") - * * @return Response */ + #[Route(path: '/import/browser', name: 'import_browser', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/ChromeController.php b/src/Controller/Import/ChromeController.php index 2287bc17e..0283cdee4 100644 --- a/src/Controller/Import/ChromeController.php +++ b/src/Controller/Import/ChromeController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class ChromeController extends BrowserController { - private ChromeImport $chromeImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(ChromeImport $chromeImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->chromeImport = $chromeImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly ChromeImport $chromeImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/chrome", name="import_chrome") - */ + #[Route(path: '/import/chrome', name: 'import_chrome', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/DeliciousController.php b/src/Controller/Import/DeliciousController.php index da19b8795..2caa6bdc1 100644 --- a/src/Controller/Import/DeliciousController.php +++ b/src/Controller/Import/DeliciousController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -14,18 +15,14 @@ use Wallabag\Redis\Producer as RedisProducer; class DeliciousController extends AbstractController { - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/delicious", name="import_delicious") - */ + #[Route(path: '/import/delicious', name: 'import_delicious', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, DeliciousImport $delicious, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/ElcuratorController.php b/src/Controller/Import/ElcuratorController.php index daa7a1f8e..4132f01d4 100644 --- a/src/Controller/Import/ElcuratorController.php +++ b/src/Controller/Import/ElcuratorController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class ElcuratorController extends WallabagController { - private ElcuratorImport $elcuratorImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(ElcuratorImport $elcuratorImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->elcuratorImport = $elcuratorImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly ElcuratorImport $elcuratorImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/elcurator", name="import_elcurator") - */ + #[Route(path: '/import/elcurator', name: 'import_elcurator', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/FirefoxController.php b/src/Controller/Import/FirefoxController.php index e059e4e4d..d3c6ad75d 100644 --- a/src/Controller/Import/FirefoxController.php +++ b/src/Controller/Import/FirefoxController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class FirefoxController extends BrowserController { - private FirefoxImport $firefoxImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(FirefoxImport $firefoxImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->firefoxImport = $firefoxImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly FirefoxImport $firefoxImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/firefox", name="import_firefox") - */ + #[Route(path: '/import/firefox', name: 'import_firefox', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/HtmlController.php b/src/Controller/Import/HtmlController.php index e53cd8a46..c55e114e1 100644 --- a/src/Controller/Import/HtmlController.php +++ b/src/Controller/Import/HtmlController.php @@ -2,6 +2,7 @@ namespace Wallabag\Controller\Import; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -13,10 +14,10 @@ use Wallabag\Import\ImportInterface; abstract class HtmlController extends AbstractController { /** - * @Route("/import/html", name="import_html") - * * @return Response */ + #[Route(path: '/import/html', name: 'import_html', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/ImportController.php b/src/Controller/Import/ImportController.php index bc506d264..8ce60437e 100644 --- a/src/Controller/Import/ImportController.php +++ b/src/Controller/Import/ImportController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use Predis\Client; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Wallabag\Consumer\RabbitMQConsumerTotalProxy; @@ -12,16 +13,14 @@ use Wallabag\Import\ImportChain; class ImportController extends AbstractController { - private RabbitMQConsumerTotalProxy $rabbitMQConsumerTotalProxy; - - public function __construct(RabbitMQConsumerTotalProxy $rabbitMQConsumerTotalProxy) - { - $this->rabbitMQConsumerTotalProxy = $rabbitMQConsumerTotalProxy; + public function __construct( + private readonly RabbitMQConsumerTotalProxy $rabbitMQConsumerTotalProxy, + private readonly Client $redisClient, + ) { } - /** - * @Route("/import/", name="import") - */ + #[Route(path: '/import/', name: 'import', methods: ['GET'])] + #[IsGranted('IMPORT_ENTRIES')] public function importAction(ImportChain $importChain) { return $this->render('Import/index.html.twig', [ @@ -59,30 +58,30 @@ class ImportController extends AbstractController + $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('shaarli') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_html') + + $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_csv') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('omnivore') ; - } catch (\Exception $e) { + } catch (\Exception) { $rabbitNotInstalled = true; } } elseif ($craueConfig->get('import_with_redis')) { - $redis = $this->get(Client::class); - try { - $nbRedisMessages = $redis->llen('wallabag.import.pocket') - + $redis->llen('wallabag.import.readability') - + $redis->llen('wallabag.import.wallabag_v1') - + $redis->llen('wallabag.import.wallabag_v2') - + $redis->llen('wallabag.import.firefox') - + $redis->llen('wallabag.import.chrome') - + $redis->llen('wallabag.import.instapaper') - + $redis->llen('wallabag.import.pinboard') - + $redis->llen('wallabag.import.delicious') - + $redis->llen('wallabag.import.elcurator') - + $redis->llen('wallabag.import.shaarli') - + $redis->llen('wallabag.import.pocket_html') - + $redis->llen('wallabag.import.omnivore') + $nbRedisMessages = $this->redisClient->llen('wallabag.import.pocket') + + $this->redisClient->llen('wallabag.import.readability') + + $this->redisClient->llen('wallabag.import.wallabag_v1') + + $this->redisClient->llen('wallabag.import.wallabag_v2') + + $this->redisClient->llen('wallabag.import.firefox') + + $this->redisClient->llen('wallabag.import.chrome') + + $this->redisClient->llen('wallabag.import.instapaper') + + $this->redisClient->llen('wallabag.import.pinboard') + + $this->redisClient->llen('wallabag.import.delicious') + + $this->redisClient->llen('wallabag.import.elcurator') + + $this->redisClient->llen('wallabag.import.shaarli') + + $this->redisClient->llen('wallabag.import.pocket_html') + + $this->redisClient->llen('wallabag.import.pocket_csv') + + $this->redisClient->llen('wallabag.import.omnivore') ; - } catch (\Exception $e) { + } catch (\Exception) { $redisNotInstalled = true; } } diff --git a/src/Controller/Import/InstapaperController.php b/src/Controller/Import/InstapaperController.php index daf7f6111..7edb4a9e7 100644 --- a/src/Controller/Import/InstapaperController.php +++ b/src/Controller/Import/InstapaperController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -14,18 +15,14 @@ use Wallabag\Redis\Producer as RedisProducer; class InstapaperController extends AbstractController { - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/instapaper", name="import_instapaper") - */ + #[Route(path: '/import/instapaper', name: 'import_instapaper', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, InstapaperImport $instapaper, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/OmnivoreController.php b/src/Controller/Import/OmnivoreController.php index 2482c93fc..1796cb4ab 100644 --- a/src/Controller/Import/OmnivoreController.php +++ b/src/Controller/Import/OmnivoreController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -14,18 +15,14 @@ use Wallabag\Redis\Producer as RedisProducer; class OmnivoreController extends AbstractController { - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/omnivore", name="import_omnivore") - */ + #[Route(path: '/import/omnivore', name: 'import_omnivore', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, OmnivoreImport $omnivore, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/PinboardController.php b/src/Controller/Import/PinboardController.php index 791f0e0f7..437faac83 100644 --- a/src/Controller/Import/PinboardController.php +++ b/src/Controller/Import/PinboardController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -14,18 +15,14 @@ use Wallabag\Redis\Producer as RedisProducer; class PinboardController extends AbstractController { - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/pinboard", name="import_pinboard") - */ + #[Route(path: '/import/pinboard', name: 'import_pinboard', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, PinboardImport $pinboard, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/PocketController.php b/src/Controller/Import/PocketController.php index 829ec69e4..543f867aa 100644 --- a/src/Controller/Import/PocketController.php +++ b/src/Controller/Import/PocketController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -16,22 +17,16 @@ use Wallabag\Redis\Producer as RedisProducer; class PocketController extends AbstractController { - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - private SessionInterface $session; - - public function __construct(Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer, SessionInterface $session) - { - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; - $this->session = $session; + public function __construct( + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + private readonly SessionInterface $session, + ) { } - /** - * @Route("/import/pocket", name="import_pocket") - */ + #[Route(path: '/import/pocket', name: 'import_pocket', methods: ['GET'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(PocketImport $pocketImport) { $pocket = $this->getPocketImportService($pocketImport); @@ -50,9 +45,8 @@ class PocketController extends AbstractController ]); } - /** - * @Route("/import/pocket/auth", name="import_pocket_auth") - */ + #[Route(path: '/import/pocket/auth', name: 'import_pocket_auth', methods: ['POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function authAction(Request $request, PocketImport $pocketImport) { $requestToken = $this->getPocketImportService($pocketImport) @@ -67,10 +61,10 @@ class PocketController extends AbstractController return $this->redirect($this->generateUrl('import_pocket')); } - $form = $request->request->get('form'); + $form = $request->request->all('form'); $this->session->set('import.pocket.code', $requestToken); - if (null !== $form && \array_key_exists('mark_as_read', $form)) { + if (\array_key_exists('mark_as_read', $form)) { $this->session->set('mark_as_read', $form['mark_as_read']); } @@ -80,9 +74,8 @@ class PocketController extends AbstractController ); } - /** - * @Route("/import/pocket/callback", name="import_pocket_callback") - */ + #[Route(path: '/import/pocket/callback', name: 'import_pocket_callback', methods: ['GET'])] + #[IsGranted('IMPORT_ENTRIES')] public function callbackAction(PocketImport $pocketImport, TranslatorInterface $translator) { $message = 'flashes.import.notice.failed'; diff --git a/src/Controller/Import/PocketCsvController.php b/src/Controller/Import/PocketCsvController.php new file mode 100644 index 000000000..8630e5b4c --- /dev/null +++ b/src/Controller/Import/PocketCsvController.php @@ -0,0 +1,99 @@ +createForm(UploadImportType::class); + $form->handleRequest($request); + + $this->pocketCsvImport->setUser($this->getUser()); + + if ($this->craueConfig->get('import_with_rabbitmq')) { + $this->pocketCsvImport->setProducer($this->rabbitMqProducer); + } elseif ($this->craueConfig->get('import_with_redis')) { + $this->pocketCsvImport->setProducer($this->redisProducer); + } + + if ($form->isSubmitted() && $form->isValid()) { + $file = $form->get('file')->getData(); + $markAsRead = $form->get('mark_as_read')->getData(); + $name = 'pocket_' . $this->getUser()->getId() . '.csv'; + + if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag.resource_dir'), $name)) { + $res = $this->pocketCsvImport + ->setFilepath($this->getParameter('wallabag.resource_dir') . '/' . $name) + ->setMarkAsRead($markAsRead) + ->import(); + + $message = 'flashes.import.notice.failed'; + + if (true === $res) { + $summary = $this->pocketCsvImport->getSummary(); + $message = $translator->trans('flashes.import.notice.summary', [ + '%imported%' => $summary['imported'], + '%skipped%' => $summary['skipped'], + ]); + + if (0 < $summary['queued']) { + $message = $translator->trans('flashes.import.notice.summary_with_queue', [ + '%queued%' => $summary['queued'], + ]); + } + + unlink($this->getParameter('wallabag.resource_dir') . '/' . $name); + } + + $this->addFlash('notice', $message); + + return $this->redirect($this->generateUrl('homepage')); + } + + $this->addFlash('notice', 'flashes.import.notice.failed_on_file'); + } + + return $this->render('Import/PocketCsv/index.html.twig', [ + 'form' => $form->createView(), + 'import' => $this->pocketCsvImport, + ]); + } + + protected function getImportService() + { + if ($this->craueConfig->get('import_with_rabbitmq')) { + $this->pocketCsvImport->setProducer($this->rabbitMqProducer); + } elseif ($this->craueConfig->get('import_with_redis')) { + $this->pocketCsvImport->setProducer($this->redisProducer); + } + + return $this->pocketCsvImport; + } + + protected function getImportTemplate() + { + return 'Import/PocketCsv/index.html.twig'; + } +} diff --git a/src/Controller/Import/PocketHtmlController.php b/src/Controller/Import/PocketHtmlController.php index bf4d379d9..00f6b7b7d 100644 --- a/src/Controller/Import/PocketHtmlController.php +++ b/src/Controller/Import/PocketHtmlController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class PocketHtmlController extends HtmlController { - private PocketHtmlImport $pocketHtmlImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(PocketHtmlImport $pocketHtmlImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->pocketHtmlImport = $pocketHtmlImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly PocketHtmlImport $pocketHtmlImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/pocket_html", name="import_pocket_html") - */ + #[Route(path: '/import/pocket_html', name: 'import_pocket_html', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/ReadabilityController.php b/src/Controller/Import/ReadabilityController.php index 6943d60e7..6409f5d05 100644 --- a/src/Controller/Import/ReadabilityController.php +++ b/src/Controller/Import/ReadabilityController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -14,18 +15,14 @@ use Wallabag\Redis\Producer as RedisProducer; class ReadabilityController extends AbstractController { - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/readability", name="import_readability") - */ + #[Route(path: '/import/readability', name: 'import_readability', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, ReadabilityImport $readability, Config $craueConfig, TranslatorInterface $translator) { $form = $this->createForm(UploadImportType::class); diff --git a/src/Controller/Import/ShaarliController.php b/src/Controller/Import/ShaarliController.php index 092b6e695..8be50a180 100644 --- a/src/Controller/Import/ShaarliController.php +++ b/src/Controller/Import/ShaarliController.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class ShaarliController extends HtmlController { - private ShaarliImport $shaarliImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(ShaarliImport $shaarliImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->shaarliImport = $shaarliImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly ShaarliImport $shaarliImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/shaarli", name="import_shaarli") - */ + #[Route(path: '/import/shaarli', name: 'import_shaarli', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/WallabagV1Controller.php b/src/Controller/Import/WallabagV1Controller.php index 123cde2e3..7537db124 100644 --- a/src/Controller/Import/WallabagV1Controller.php +++ b/src/Controller/Import/WallabagV1Controller.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class WallabagV1Controller extends WallabagController { - private WallabagV1Import $wallabagImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(WallabagV1Import $wallabagImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->wallabagImport = $wallabagImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly WallabagV1Import $wallabagImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/wallabag-v1", name="import_wallabag_v1") - */ + #[Route(path: '/import/wallabag-v1', name: 'import_wallabag_v1', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/Import/WallabagV2Controller.php b/src/Controller/Import/WallabagV2Controller.php index 1b864e96b..dee455336 100644 --- a/src/Controller/Import/WallabagV2Controller.php +++ b/src/Controller/Import/WallabagV2Controller.php @@ -4,6 +4,7 @@ namespace Wallabag\Controller\Import; use Craue\ConfigBundle\Util\Config; use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -12,22 +13,16 @@ use Wallabag\Redis\Producer as RedisProducer; class WallabagV2Controller extends WallabagController { - private WallabagV2Import $wallabagImport; - private Config $craueConfig; - private RabbitMqProducer $rabbitMqProducer; - private RedisProducer $redisProducer; - - public function __construct(WallabagV2Import $wallabagImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer) - { - $this->wallabagImport = $wallabagImport; - $this->craueConfig = $craueConfig; - $this->rabbitMqProducer = $rabbitMqProducer; - $this->redisProducer = $redisProducer; + public function __construct( + private readonly WallabagV2Import $wallabagImport, + private readonly Config $craueConfig, + private readonly RabbitMqProducer $rabbitMqProducer, + private readonly RedisProducer $redisProducer, + ) { } - /** - * @Route("/import/wallabag-v2", name="import_wallabag_v2") - */ + #[Route(path: '/import/wallabag-v2', name: 'import_wallabag_v2', methods: ['GET', 'POST'])] + #[IsGranted('IMPORT_ENTRIES')] public function indexAction(Request $request, TranslatorInterface $translator) { return parent::indexAction($request, $translator); diff --git a/src/Controller/SiteCredentialController.php b/src/Controller/SiteCredentialController.php index e9cfb203f..70f9af98e 100644 --- a/src/Controller/SiteCredentialController.php +++ b/src/Controller/SiteCredentialController.php @@ -20,30 +20,22 @@ use Wallabag\Repository\SiteCredentialRepository; /** * SiteCredential controller. - * - * @Route("/site-credentials") */ class SiteCredentialController extends AbstractController { - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; - private CryptoProxy $cryptoProxy; - private Config $craueConfig; - - public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator, CryptoProxy $cryptoProxy, Config $craueConfig) - { - $this->entityManager = $entityManager; - $this->translator = $translator; - $this->cryptoProxy = $cryptoProxy; - $this->craueConfig = $craueConfig; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + private readonly CryptoProxy $cryptoProxy, + private readonly Config $craueConfig, + ) { } /** * Lists all User entities. - * - * @Route("/", name="site_credentials_index", methods={"GET"}) - * @IsGranted("LIST_SITE_CREDENTIALS") */ + #[Route(path: '/site-credentials', name: 'site_credentials_index', methods: ['GET'])] + #[IsGranted('LIST_SITE_CREDENTIALS')] public function indexAction(SiteCredentialRepository $repository) { $this->isSiteCredentialsEnabled(); @@ -58,11 +50,10 @@ class SiteCredentialController extends AbstractController /** * Creates a new site credential entity. * - * @Route("/new", name="site_credentials_new", methods={"GET", "POST"}) - * @IsGranted("CREATE_SITE_CREDENTIALS") - * * @return Response */ + #[Route(path: '/site-credentials/new', name: 'site_credentials_new', methods: ['GET', 'POST'])] + #[IsGranted('CREATE_SITE_CREDENTIALS')] public function newAction(Request $request) { $this->isSiteCredentialsEnabled(); @@ -96,11 +87,10 @@ class SiteCredentialController extends AbstractController /** * Displays a form to edit an existing site credential entity. * - * @Route("/{id}/edit", name="site_credentials_edit", methods={"GET", "POST"}) - * @IsGranted("EDIT", subject="siteCredential") - * * @return Response */ + #[Route(path: '/site-credentials/{id}/edit', name: 'site_credentials_edit', methods: ['GET', 'POST'])] + #[IsGranted('EDIT', subject: 'siteCredential')] public function editAction(Request $request, SiteCredential $siteCredential) { $this->isSiteCredentialsEnabled(); @@ -134,11 +124,10 @@ class SiteCredentialController extends AbstractController /** * Deletes a site credential entity. * - * @Route("/{id}", name="site_credentials_delete", methods={"DELETE"}) - * @IsGranted("DELETE", subject="siteCredential") - * * @return RedirectResponse */ + #[Route(path: '/site-credentials/{id}', name: 'site_credentials_delete', methods: ['DELETE'])] + #[IsGranted('DELETE', subject: 'siteCredential')] public function deleteAction(Request $request, SiteCredential $siteCredential) { $this->isSiteCredentialsEnabled(); diff --git a/src/Controller/StaticController.php b/src/Controller/StaticController.php index fda356851..5c7bb06e3 100644 --- a/src/Controller/StaticController.php +++ b/src/Controller/StaticController.php @@ -2,13 +2,13 @@ namespace Wallabag\Controller; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Routing\Annotation\Route; class StaticController extends AbstractController { - /** - * @Route("/howto", name="howto") - */ + #[Route(path: '/howto', name: 'howto', methods: ['GET'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function howtoAction() { $addonsUrl = $this->getParameter('addons_url'); @@ -21,9 +21,8 @@ class StaticController extends AbstractController ); } - /** - * @Route("/about", name="about") - */ + #[Route(path: '/about', name: 'about', methods: ['GET'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function aboutAction() { return $this->render( @@ -35,9 +34,8 @@ class StaticController extends AbstractController ); } - /** - * @Route("/quickstart", name="quickstart") - */ + #[Route(path: '/quickstart', name: 'quickstart', methods: ['GET'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function quickstartAction() { return $this->render( diff --git a/src/Controller/TagController.php b/src/Controller/TagController.php index 4c8a69281..d921ddb4c 100644 --- a/src/Controller/TagController.php +++ b/src/Controller/TagController.php @@ -6,10 +6,13 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Pagerfanta\Adapter\ArrayAdapter; use Pagerfanta\Exception\OutOfRangeCurrentPageException; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; use Wallabag\Entity\Entry; use Wallabag\Entity\Tag; @@ -23,32 +26,29 @@ use Wallabag\Repository\TagRepository; class TagController extends AbstractController { - private EntityManagerInterface $entityManager; - private TagsAssigner $tagsAssigner; - private Redirect $redirectHelper; - - public function __construct(EntityManagerInterface $entityManager, TagsAssigner $tagsAssigner, Redirect $redirectHelper) - { - $this->entityManager = $entityManager; - $this->tagsAssigner = $tagsAssigner; - $this->redirectHelper = $redirectHelper; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly TagsAssigner $tagsAssigner, + private readonly Redirect $redirectHelper, + private readonly Security $security, + ) { } /** - * @Route("/new-tag/{entry}", requirements={"entry" = "\d+"}, name="new_tag", methods={"POST"}) - * * @return Response */ + #[Route(path: '/new-tag/{entry}', name: 'new_tag', methods: ['POST'], requirements: ['entry' => '\d+'])] + #[IsGranted('TAG', subject: 'entry')] public function addTagFormAction(Request $request, Entry $entry, TranslatorInterface $translator) { $form = $this->createForm(NewTagType::class, new Tag()); $form->handleRequest($request); $tags = $form->get('label')->getData() ?? ''; - $tagsExploded = explode(',', $tags); + $tagsExploded = explode(',', (string) $tags); // avoid too much tag to be added - if (\count($tagsExploded) >= NewTagType::MAX_TAGS || \strlen($tags) >= NewTagType::MAX_LENGTH) { + if (\count($tagsExploded) >= NewTagType::MAX_TAGS || \strlen((string) $tags) >= NewTagType::MAX_LENGTH) { $message = $translator->trans('flashes.tag.notice.too_much_tags', [ '%tags%' => NewTagType::MAX_TAGS, '%characters%' => NewTagType::MAX_LENGTH, @@ -59,8 +59,6 @@ class TagController extends AbstractController } if ($form->isSubmitted() && $form->isValid()) { - $this->checkUserAction($entry); - $this->tagsAssigner->assignTagsToEntry( $entry, $form->get('label')->getData() @@ -86,19 +84,21 @@ class TagController extends AbstractController /** * Removes tag from entry. * - * @Route("/remove-tag/{entry}/{tag}", requirements={"entry" = "\d+", "tag" = "\d+"}, name="remove_tag") - * * @return Response */ + #[Route(path: '/remove-tag/{entry}/{tag}', name: 'remove_tag', methods: ['POST'], requirements: ['entry' => '\d+', 'tag' => '\d+'])] + #[IsGranted('UNTAG', subject: 'entry')] public function removeTagFromEntry(Request $request, Entry $entry, Tag $tag) { - $this->checkUserAction($entry); + if (!$this->isCsrfTokenValid('remove-tag', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } $entry->removeTag($tag); $this->entityManager->flush(); // remove orphan tag in case no entries are associated to it - if (0 === \count($tag->getEntries())) { + if (0 === \count($tag->getEntries()) && $this->security->isGranted('DELETE', $tag)) { $this->entityManager->remove($tag); $this->entityManager->flush(); } @@ -111,22 +111,22 @@ class TagController extends AbstractController /** * Shows tags for current user. * - * @Route("/tag/list", name="tag") - * * @return Response */ + #[Route(path: '/tag/list', name: 'tag', methods: ['GET'])] + #[IsGranted('LIST_TAGS')] public function showTagAction(TagRepository $tagRepository, EntryRepository $entryRepository) { - $tags = $tagRepository->findAllFlatTagsWithNbEntries($this->getUser()->getId()); + $allTagsWithNbEntries = $tagRepository->findAllTagsWithNbEntries($this->getUser()->getId()); $nbEntriesUntagged = $entryRepository->countUntaggedEntriesByUser($this->getUser()->getId()); $renameForms = []; - foreach ($tags as $tag) { - $renameForms[$tag['id']] = $this->createForm(RenameTagType::class, new Tag())->createView(); + foreach ($allTagsWithNbEntries as $tagWithNbEntries) { + $renameForms[$tagWithNbEntries['tag']->getId()] = $this->createForm(RenameTagType::class, new Tag())->createView(); } return $this->render('Tag/tags.html.twig', [ - 'tags' => $tags, + 'allTagsWithNbEntries' => $allTagsWithNbEntries, 'renameForms' => $renameForms, 'nbEntriesUntagged' => $nbEntriesUntagged, ]); @@ -135,11 +135,12 @@ class TagController extends AbstractController /** * @param int $page * - * @Route("/tag/list/{slug}/{page}", name="tag_entries", defaults={"page" = "1"}) - * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) - * * @return Response */ + #[Route(path: '/tag/list/{slug}/{page}', name: 'tag_entries', methods: ['GET'], defaults: ['page' => '1'])] + #[ParamConverter('tag', options: ['mapping' => ['slug' => 'slug']])] + #[IsGranted('LIST_ENTRIES')] + #[IsGranted('VIEW', subject: 'tag')] public function showEntriesForTagAction(Tag $tag, EntryRepository $entryRepository, PreparePagerForEntries $preparePagerForEntries, $page, Request $request) { $entriesByTag = $entryRepository->findAllByTagId( @@ -153,9 +154,9 @@ class TagController extends AbstractController try { $entries->setCurrentPage($page); - } catch (OutOfRangeCurrentPageException $e) { + } catch (OutOfRangeCurrentPageException) { if ($page > 1) { - return $this->redirect($this->generateUrl($request->get('_route'), [ + return $this->redirect($this->generateUrl($request->attributes->get('_route'), [ 'slug' => $tag->getSlug(), 'page' => $entries->getNbPages(), ]), 302); @@ -174,11 +175,11 @@ class TagController extends AbstractController * Rename a given tag with a new label * Create a new tag with the new name and drop the old one. * - * @Route("/tag/rename/{slug}", name="tag_rename") - * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) - * * @return Response */ + #[Route(path: '/tag/rename/{slug}', name: 'tag_rename', methods: ['POST'])] + #[ParamConverter('tag', options: ['mapping' => ['slug' => 'slug']])] + #[IsGranted('EDIT', subject: 'tag')] public function renameTagAction(Tag $tag, Request $request, TagRepository $tagRepository, EntryRepository $entryRepository) { $form = $this->createForm(RenameTagType::class, new Tag()); @@ -227,12 +228,16 @@ class TagController extends AbstractController /** * Tag search results with the current search term. * - * @Route("/tag/search/{filter}", name="tag_this_search") - * * @return Response */ + #[Route(path: '/tag/search/{filter}', name: 'tag_this_search', methods: ['POST'])] + #[IsGranted('CREATE_TAGS')] public function tagThisSearchAction($filter, Request $request, EntryRepository $entryRepository) { + if (!$this->isCsrfTokenValid('tag-this-search', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $currentRoute = $request->query->has('currentRoute') ? $request->query->get('currentRoute') : ''; /** @var QueryBuilder $qb */ @@ -248,7 +253,7 @@ class TagController extends AbstractController // check to avoid duplicate tags creation foreach ($this->entityManager->getUnitOfWork()->getScheduledEntityInsertions() as $entity) { - if ($entity instanceof Tag && strtolower($entity->getLabel()) === strtolower($filter)) { + if ($entity instanceof Tag && strtolower($entity->getLabel()) === strtolower((string) $filter)) { continue 2; } $this->entityManager->persist($entry); @@ -262,13 +267,17 @@ class TagController extends AbstractController /** * Delete a given tag for the current user. * - * @Route("/tag/delete/{slug}", name="tag_delete") - * @ParamConverter("tag", options={"mapping": {"slug": "slug"}}) - * * @return Response */ + #[Route(path: '/tag/delete/{slug}', name: 'tag_delete', methods: ['POST'])] + #[ParamConverter('tag', options: ['mapping' => ['slug' => 'slug']])] + #[IsGranted('DELETE', subject: 'tag')] public function removeTagAction(Tag $tag, Request $request, EntryRepository $entryRepository) { + if (!$this->isCsrfTokenValid('tag-delete', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + foreach ($tag->getEntriesByUserId($this->getUser()->getId()) as $entry) { $entryRepository->removeTag($this->getUser()->getId(), $tag); } @@ -282,14 +291,4 @@ class TagController extends AbstractController return $this->redirect($redirectUrl); } - - /** - * Check if the logged user can manage the given entry. - */ - private function checkUserAction(Entry $entry) - { - if (null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) { - throw $this->createAccessDeniedException('You can not access this entry.'); - } - } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index f9fba7b07..9f576b7dc 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -29,21 +29,17 @@ use Wallabag\Repository\UserRepository; */ class UserController extends AbstractController { - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; - - public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator) - { - $this->entityManager = $entityManager; - $this->translator = $translator; + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + ) { } /** * Creates a new User entity. - * - * @Route("/users/new", name="user_new", methods={"GET", "POST"}) - * @IsGranted("CREATE_USERS") */ + #[Route(path: '/users/new', name: 'user_new', methods: ['GET', 'POST'])] + #[IsGranted('CREATE_USERS')] public function newAction(Request $request, UserManagerInterface $userManager, EventDispatcherInterface $eventDispatcher) { $user = $userManager->createUser(); @@ -77,10 +73,9 @@ class UserController extends AbstractController /** * Displays a form to edit an existing User entity. - * - * @Route("/users/{id}/edit", name="user_edit", methods={"GET", "POST"}) - * @IsGranted("EDIT", subject="user") */ + #[Route(path: '/users/{id}/edit', name: 'user_edit', methods: ['GET', 'POST'])] + #[IsGranted('EDIT', subject: 'user')] public function editAction(Request $request, User $user, UserManagerInterface $userManager, GoogleAuthenticatorInterface $googleAuthenticator) { $deleteForm = $this->createDeleteForm($user); @@ -120,10 +115,9 @@ class UserController extends AbstractController /** * Deletes a User entity. - * - * @Route("/users/{id}", name="user_delete", methods={"DELETE"}) - * @IsGranted("DELETE", subject="user") */ + #[Route(path: '/users/{id}', name: 'user_delete', methods: ['DELETE'])] + #[IsGranted('DELETE', subject: 'user')] public function deleteAction(Request $request, User $user) { $form = $this->createDeleteForm($user); @@ -145,14 +139,10 @@ class UserController extends AbstractController /** * @param int $page * - * @Route("/users/list/{page}", name="user_index", defaults={"page" = 1}) - * @IsGranted("LIST_USERS") - * - * Default parameter for page is hardcoded (in duplication of the defaults from the Route) - * because this controller is also called inside the layout template without any page as argument - * * @return Response */ + #[Route(path: '/users/list/{page}', name: 'user_index', methods: ['GET'], defaults: ['page' => 1])] + #[IsGranted('LIST_USERS')] // Default parameter for page is hardcoded (in duplication of the defaults from the Route) public function searchFormAction(Request $request, UserRepository $userRepository, $page = 1) { $qb = $userRepository->createQueryBuilder('u'); @@ -161,7 +151,7 @@ class UserController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : ''); + $searchTerm = $request->query->all('search_user')['term'] ?? ''; $qb = $userRepository->getQueryBuilderForSearch($searchTerm); } @@ -172,7 +162,7 @@ class UserController extends AbstractController try { $pagerFanta->setCurrentPage($page); - } catch (OutOfRangeCurrentPageException $e) { + } catch (OutOfRangeCurrentPageException) { if ($page > 1) { return $this->redirect($this->generateUrl('user_index', ['page' => $pagerFanta->getNbPages()]), 302); } diff --git a/src/Doctrine/JsonArrayType.php b/src/Doctrine/JsonArrayType.php index 6e57157ae..0f2a80e58 100644 --- a/src/Doctrine/JsonArrayType.php +++ b/src/Doctrine/JsonArrayType.php @@ -14,7 +14,7 @@ use Doctrine\DBAL\Types\JsonType; */ class JsonArrayType extends JsonType { - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue($value, AbstractPlatform $platform): mixed { if (null === $value || '' === $value) { return []; @@ -22,15 +22,15 @@ class JsonArrayType extends JsonType $value = \is_resource($value) ? stream_get_contents($value) : $value; - return json_decode($value, true); + return json_decode((string) $value, true); } - public function getName() + public function getName(): string { return 'json_array'; } - public function requiresSQLCommentHint(AbstractPlatform $platform) + public function requiresSQLCommentHint(AbstractPlatform $platform): bool { return true; } diff --git a/src/Doctrine/MigrationFactoryDecorator.php b/src/Doctrine/MigrationFactoryDecorator.php new file mode 100644 index 000000000..b99f31af0 --- /dev/null +++ b/src/Doctrine/MigrationFactoryDecorator.php @@ -0,0 +1,33 @@ +migrationFactory->createVersion($migrationClassName); + + if ($instance instanceof WallabagMigration) { + $instance->setTablePrefix($this->tablePrefix); + $instance->setDefaultIgnoreOriginInstanceRules($this->defaultIgnoreOriginInstanceRules); + $instance->setFetchingErrorMessage($this->fetchingErrorMessage); + } + + return $instance; + } +} diff --git a/src/Doctrine/WallabagMigration.php b/src/Doctrine/WallabagMigration.php index 4679b9852..4fcb445e6 100644 --- a/src/Doctrine/WallabagMigration.php +++ b/src/Doctrine/WallabagMigration.php @@ -5,17 +5,14 @@ namespace Wallabag\Doctrine; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -abstract class WallabagMigration extends AbstractMigration implements ContainerAwareInterface +abstract class WallabagMigration extends AbstractMigration { public const UN_ESCAPED_TABLE = true; - /** - * @var ContainerInterface - */ - protected $container; + protected string $tablePrefix; + protected array $defaultIgnoreOriginInstanceRules; + protected string $fetchingErrorMessage; // because there are declared as abstract in `AbstractMigration` we need to delarer here too public function up(Schema $schema): void @@ -26,11 +23,6 @@ abstract class WallabagMigration extends AbstractMigration implements ContainerA { } - public function setContainer(?ContainerInterface $container = null) - { - $this->container = $container; - } - /** * @todo remove when upgrading DoctrineMigration (only needed for PHP 8) * @@ -41,14 +33,24 @@ abstract class WallabagMigration extends AbstractMigration implements ContainerA return false; } - protected function getTablePrefix(): string + public function setTablePrefix(string $tablePrefix): void { - return (string) $this->container->getParameter('database_table_prefix'); + $this->tablePrefix = $tablePrefix; + } + + public function setDefaultIgnoreOriginInstanceRules(array $defaultIgnoreOriginInstanceRules): void + { + $this->defaultIgnoreOriginInstanceRules = $defaultIgnoreOriginInstanceRules; + } + + public function setFetchingErrorMessage(string $fetchingErrorMessage): void + { + $this->fetchingErrorMessage = $fetchingErrorMessage; } protected function getTable($tableName, $unEscaped = false) { - $table = $this->container->getParameter('database_table_prefix') . $tableName; + $table = $this->tablePrefix . $tableName; if (self::UN_ESCAPED_TABLE === $unEscaped) { return $table; @@ -87,9 +89,7 @@ abstract class WallabagMigration extends AbstractMigration implements ContainerA */ protected function generateIdentifierName(array $columnNames, string $prefix = ''): string { - $hash = implode('', array_map(static function ($column): string { - return dechex(crc32($column)); - }, $columnNames)); + $hash = implode('', array_map(static fn ($column): string => dechex(crc32($column)), $columnNames)); return strtoupper(substr($prefix . '_' . $hash, 0, $this->platform->getMaxIdentifierLength())); } diff --git a/src/Entity/Annotation.php b/src/Entity/Annotation.php index 4469a9224..9c265c4d6 100644 --- a/src/Entity/Annotation.php +++ b/src/Entity/Annotation.php @@ -10,86 +10,68 @@ use JMS\Serializer\Annotation\SerializedName; use JMS\Serializer\Annotation\VirtualProperty; use Symfony\Component\Validator\Constraints as Assert; use Wallabag\Helper\EntityTimestampsTrait; +use Wallabag\Repository\AnnotationRepository; /** * Annotation. - * - * @ORM\Table(name="annotation") - * @ORM\Entity(repositoryClass="Wallabag\Repository\AnnotationRepository") - * @ORM\HasLifecycleCallbacks() - * @ExclusionPolicy("none") */ +#[ORM\Table(name: 'annotation')] +#[ORM\Entity(repositoryClass: AnnotationRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[ExclusionPolicy('none')] class Annotation { use EntityTimestampsTrait; /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] private $id; /** * @var string - * - * @ORM\Column(name="text", type="text") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'text', type: 'text')] + #[Groups(['entries_for_user', 'export_all'])] private $text; /** * @var \DateTime - * - * @ORM\Column(name="created_at", type="datetime") */ + #[ORM\Column(name: 'created_at', type: 'datetime')] private $createdAt; /** * @var \DateTime - * - * @ORM\Column(name="updated_at", type="datetime") */ + #[ORM\Column(name: 'updated_at', type: 'datetime')] private $updatedAt; /** * @var string - * - * @Assert\Length( - * max = 10000, - * maxMessage = "validator.quote_length_too_high" - * ) - * @ORM\Column(name="quote", type="text") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'quote', type: 'text')] + #[Assert\Length(max: 10000, maxMessage: 'validator.quote_length_too_high')] + #[Groups(['entries_for_user', 'export_all'])] private $quote; /** * @var array - * - * @ORM\Column(name="ranges", type="array") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'ranges', type: 'array')] + #[Groups(['entries_for_user', 'export_all'])] private $ranges; - /** - * @Exclude - * - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User") - */ + #[ORM\ManyToOne(targetEntity: User::class)] + #[Exclude] private $user; - /** - * @Exclude - * - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Entry", inversedBy="annotations") - * @ORM\JoinColumn(name="entry_id", referencedColumnName="id", onDelete="cascade") - */ + #[ORM\JoinColumn(name: 'entry_id', referencedColumnName: 'id', onDelete: 'cascade')] + #[ORM\ManyToOne(targetEntity: Entry::class, inversedBy: 'annotations')] + #[Exclude] private $entry; /* @@ -226,10 +208,8 @@ class Annotation return $this->user; } - /** - * @VirtualProperty - * @SerializedName("user") - */ + #[VirtualProperty] + #[SerializedName('user')] public function getUserName() { return $this->user->getName(); @@ -260,10 +240,8 @@ class Annotation return $this->entry; } - /** - * @VirtualProperty - * @SerializedName("annotator_schema_version") - */ + #[VirtualProperty] + #[SerializedName('annotator_schema_version')] public function getVersion() { return 'v1.0'; diff --git a/src/Entity/Api/AccessToken.php b/src/Entity/Api/AccessToken.php index e332207a1..b1dcd2056 100644 --- a/src/Entity/Api/AccessToken.php +++ b/src/Entity/Api/AccessToken.php @@ -4,29 +4,22 @@ namespace Wallabag\Entity\Api; use Doctrine\ORM\Mapping as ORM; use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; +use Wallabag\Entity\User; -/** - * @ORM\Table("oauth2_access_tokens") - * @ORM\Entity - */ +#[ORM\Table('oauth2_access_tokens')] +#[ORM\Entity] class AccessToken extends BaseAccessToken { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Api\Client", inversedBy="accessTokens") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'accessTokens')] protected $client; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User") - * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") - */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: User::class)] protected $user; } diff --git a/src/Entity/Api/AuthCode.php b/src/Entity/Api/AuthCode.php index 85c61c5e2..3f8fe8b0b 100644 --- a/src/Entity/Api/AuthCode.php +++ b/src/Entity/Api/AuthCode.php @@ -4,29 +4,22 @@ namespace Wallabag\Entity\Api; use Doctrine\ORM\Mapping as ORM; use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; +use Wallabag\Entity\User; -/** - * @ORM\Table("oauth2_auth_codes") - * @ORM\Entity - */ +#[ORM\Table('oauth2_auth_codes')] +#[ORM\Entity] class AuthCode extends BaseAuthCode { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Api\Client") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Client::class)] protected $client; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User") - * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") - */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: User::class)] protected $user; } diff --git a/src/Entity/Api/Client.php b/src/Entity/Api/Client.php index ce5675958..af71be3fd 100644 --- a/src/Entity/Api/Client.php +++ b/src/Entity/Api/Client.php @@ -9,43 +9,34 @@ use JMS\Serializer\Annotation\SerializedName; use JMS\Serializer\Annotation\VirtualProperty; use OpenApi\Annotations as OA; use Wallabag\Entity\User; +use Wallabag\Repository\Api\ClientRepository; -/** - * @ORM\Table("oauth2_clients") - * @ORM\Entity(repositoryClass="Wallabag\Repository\Api\ClientRepository") - */ +#[ORM\Table('oauth2_clients')] +#[ORM\Entity(repositoryClass: ClientRepository::class)] class Client extends BaseClient { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; /** * @var string * - * @ORM\Column(name="name", type="text", nullable=false) - * * @OA\Property( * description="Name of the API client", * type="string", * example="Default Client", * ) - * - * @Groups({"user_api_with_client"}) */ + #[ORM\Column(name: 'name', type: 'text', nullable: false)] + #[Groups(['user_api_with_client'])] protected $name; - /** - * @ORM\OneToMany(targetEntity="Wallabag\Entity\Api\RefreshToken", mappedBy="client", cascade={"remove"}) - */ + #[ORM\OneToMany(targetEntity: RefreshToken::class, mappedBy: 'client', cascade: ['remove'])] protected $refreshTokens; - /** - * @ORM\OneToMany(targetEntity="Wallabag\Entity\Api\AccessToken", mappedBy="client", cascade={"remove"}) - */ + #[ORM\OneToMany(targetEntity: AccessToken::class, mappedBy: 'client', cascade: ['remove'])] protected $accessTokens; /** @@ -56,15 +47,12 @@ class Client extends BaseClient * type="string", * example="2lmubx2m9vy80ss8c4wwcsg8ok44s88ocwcc8wo0w884oc8440", * ) - * - * @SerializedName("client_secret") - * @Groups({"user_api_with_client"}) */ + #[SerializedName('client_secret')] + #[Groups(['user_api_with_client'])] protected $secret; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User", inversedBy="clients") - */ + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'clients')] private $user; public function __construct(User $user) @@ -106,17 +94,15 @@ class Client extends BaseClient } /** - * @VirtualProperty - * * @OA\Property( * description="Client secret used for authorization", * type="string", * example="3_1lpybsn0od40css4w4ko8gsc8cwwskggs8kgg448ko0owo4c84", * ) - * - * @SerializedName("client_id") - * @Groups({"user_api_with_client"}) */ + #[VirtualProperty] + #[SerializedName('client_id')] + #[Groups(['user_api_with_client'])] public function getClientId() { return $this->getId() . '_' . $this->getRandomId(); diff --git a/src/Entity/Api/RefreshToken.php b/src/Entity/Api/RefreshToken.php index 9c6e9b9b8..0b90b6b4e 100644 --- a/src/Entity/Api/RefreshToken.php +++ b/src/Entity/Api/RefreshToken.php @@ -4,29 +4,22 @@ namespace Wallabag\Entity\Api; use Doctrine\ORM\Mapping as ORM; use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; +use Wallabag\Entity\User; -/** - * @ORM\Table("oauth2_refresh_tokens") - * @ORM\Entity - */ +#[ORM\Table('oauth2_refresh_tokens')] +#[ORM\Entity] class RefreshToken extends BaseRefreshToken { - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Api\Client", inversedBy="refreshTokens") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'refreshTokens')] protected $client; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User") - * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") - */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: User::class)] protected $user; } diff --git a/src/Entity/Config.php b/src/Entity/Config.php index 12ba65eec..780f14dd8 100644 --- a/src/Entity/Config.php +++ b/src/Entity/Config.php @@ -6,18 +6,14 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Wallabag\Repository\ConfigRepository; /** * Config. - * - * @ORM\Entity(repositoryClass="Wallabag\Repository\ConfigRepository") - * @ORM\Table( - * name="`config`", - * indexes={ - * @ORM\Index(columns={"feed_token"}), - * } - * ) */ +#[ORM\Table(name: '`config`')] +#[ORM\Index(columns: ['feed_token'])] +#[ORM\Entity(repositoryClass: ConfigRepository::class)] class Config { public const REDIRECT_TO_HOMEPAGE = 0; @@ -25,168 +21,128 @@ class Config /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Groups(['config_api'])] private $id; /** * @var int - * - * @Assert\NotBlank() - * @Assert\Range( - * min = 1, - * max = 100000, - * maxMessage = "validator.item_per_page_too_high" - * ) - * @ORM\Column(name="items_per_page", type="integer", nullable=false) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'items_per_page', type: 'integer', nullable: false)] + #[Assert\NotBlank] + #[Assert\Range(min: 1, max: 100000, maxMessage: 'validator.item_per_page_too_high')] + #[Groups(['config_api'])] private $itemsPerPage; /** * @var string - * - * @Assert\NotBlank() - * @ORM\Column(name="language", type="string", nullable=false) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'language', type: 'string', nullable: false)] + #[Assert\NotBlank] + #[Groups(['config_api'])] private $language; /** * @var string|null - * - * @ORM\Column(name="feed_token", type="string", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'feed_token', type: 'string', nullable: true)] + #[Groups(['config_api'])] private $feedToken; /** * @var int|null - * - * @ORM\Column(name="feed_limit", type="integer", nullable=true) - * @Assert\Range( - * min = 1, - * max = 100000, - * maxMessage = "validator.feed_limit_too_high" - * ) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'feed_limit', type: 'integer', nullable: true)] + #[Assert\Range(min: 1, max: 100000, maxMessage: 'validator.feed_limit_too_high')] + #[Groups(['config_api'])] private $feedLimit; /** * @var float|null - * - * @ORM\Column(name="reading_speed", type="float", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'reading_speed', type: 'float', nullable: true)] + #[Groups(['config_api'])] private $readingSpeed; /** * @var string|null - * - * @ORM\Column(name="pocket_consumer_key", type="string", nullable=true) */ + #[ORM\Column(name: 'pocket_consumer_key', type: 'string', nullable: true)] private $pocketConsumerKey; /** * @var int|null - * - * @ORM\Column(name="action_mark_as_read", type="integer", nullable=true, options={"default" = 0}) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'action_mark_as_read', type: 'integer', nullable: true, options: ['default' => 0])] + #[Groups(['config_api'])] private $actionMarkAsRead; /** * @var int|null - * - * @ORM\Column(name="list_mode", type="integer", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'list_mode', type: 'integer', nullable: true)] + #[Groups(['config_api'])] private $listMode; /** * @var int|null - * - * @ORM\Column(name="display_thumbnails", type="integer", nullable=true, options={"default" = 1}) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'display_thumbnails', type: 'integer', nullable: true, options: ['default' => 1])] + #[Groups(['config_api'])] private $displayThumbnails; /** * @var string|null - * - * @ORM\Column(name="font", type="text", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'font', type: 'text', nullable: true)] + #[Groups(['config_api'])] private $font; /** * @var float|null - * - * @ORM\Column(name="fontsize", type="float", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'fontsize', type: 'float', nullable: true)] + #[Groups(['config_api'])] private $fontsize; /** * @var float|null - * - * @ORM\Column(name="line_height", type="float", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'line_height', type: 'float', nullable: true)] + #[Groups(['config_api'])] private $lineHeight; /** * @var float|null - * - * @ORM\Column(name="max_width", type="float", nullable=true) - * - * @Groups({"config_api"}) */ + #[ORM\Column(name: 'max_width', type: 'float', nullable: true)] + #[Groups(['config_api'])] private $maxWidth; /** * @var string|null - * - * @ORM\Column(name="custom_css", type="text", nullable=true) */ + #[ORM\Column(name: 'custom_css', type: 'text', nullable: true)] private $customCSS; - /** - * @ORM\OneToOne(targetEntity="Wallabag\Entity\User", inversedBy="config") - */ + #[ORM\OneToOne(targetEntity: User::class, inversedBy: 'config')] private $user; /** * @var ArrayCollection - * - * @ORM\OneToMany(targetEntity="Wallabag\Entity\TaggingRule", mappedBy="config", cascade={"remove"}) - * @ORM\OrderBy({"id" = "ASC"}) */ + #[ORM\OneToMany(targetEntity: TaggingRule::class, mappedBy: 'config', cascade: ['remove'])] + #[ORM\OrderBy(['id' => 'ASC'])] private $taggingRules; /** * @var ArrayCollection - * - * @ORM\OneToMany(targetEntity="Wallabag\Entity\IgnoreOriginUserRule", mappedBy="config", cascade={"remove"}) - * @ORM\OrderBy({"id" = "ASC"}) */ + #[ORM\OneToMany(targetEntity: IgnoreOriginUserRule::class, mappedBy: 'config', cascade: ['remove'])] + #[ORM\OrderBy(['id' => 'ASC'])] private $ignoreOriginRules; /* @@ -282,7 +238,7 @@ class Config /** * Set feed Token. * - * @param string $feedToken + * @param string|null $feedToken * * @return Config */ @@ -296,7 +252,7 @@ class Config /** * Get feedToken. * - * @return string + * @return string|null */ public function getFeedToken() { diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php index 6f109bac2..0824b1c4f 100644 --- a/src/Entity/Entry.php +++ b/src/Entity/Entry.php @@ -3,6 +3,7 @@ namespace Wallabag\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Hateoas\Configuration\Annotation as Hateoas; use JMS\Serializer\Annotation\Exclude; @@ -13,28 +14,25 @@ use JMS\Serializer\Annotation\XmlRoot; use Symfony\Component\Validator\Constraints as Assert; use Wallabag\Helper\EntityTimestampsTrait; use Wallabag\Helper\UrlHasher; +use Wallabag\Repository\EntryRepository; /** * Entry. * - * @XmlRoot("entry") - * @ORM\Entity(repositoryClass="Wallabag\Repository\EntryRepository") - * @ORM\Table( - * name="`entry`", - * indexes={ - * @ORM\Index(columns={"created_at"}), - * @ORM\Index(columns={"uid"}), - * @ORM\Index(columns={"user_id", "hashed_url"}), - * @ORM\Index(columns={"user_id", "hashed_given_url"}), - * @ORM\Index(columns={"language", "user_id"}), - * @ORM\Index(columns={"user_id", "is_archived", "archived_at"}), - * @ORM\Index(columns={"user_id", "created_at"}), - * @ORM\Index(columns={"user_id", "is_starred", "starred_at"}) - * } - * ) - * @ORM\HasLifecycleCallbacks() * @Hateoas\Relation("self", href = "expr('/api/entries/' ~ object.getId())") */ +#[ORM\Table(name: '`entry`')] +#[ORM\Index(columns: ['created_at'])] +#[ORM\Index(columns: ['uid'])] +#[ORM\Index(columns: ['user_id', 'hashed_url'])] +#[ORM\Index(columns: ['user_id', 'hashed_given_url'])] +#[ORM\Index(columns: ['language', 'user_id'])] +#[ORM\Index(columns: ['user_id', 'is_archived', 'archived_at'])] +#[ORM\Index(columns: ['user_id', 'created_at'])] +#[ORM\Index(columns: ['user_id', 'is_starred', 'starred_at'])] +#[ORM\Entity(repositoryClass: EntryRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[XmlRoot('entry')] class Entry { use EntityTimestampsTrait; @@ -42,270 +40,208 @@ class Entry /** @Serializer\XmlAttribute */ /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Groups(['entries_for_user', 'export_all'])] private $id; /** * @var string|null - * - * @ORM\Column(name="uid", type="string", length=23, nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'uid', type: 'string', length: 23, nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $uid; /** * @var string|null - * - * @ORM\Column(name="title", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'title', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $title; /** * Define the url fetched by wallabag (the final url after potential redirections). * * @var string|null - * - * @Assert\NotBlank() - * @ORM\Column(name="url", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'url', type: 'text', nullable: true)] + #[Assert\NotBlank] + #[Assert\Url(message: "The url '{{ value }}' is not a valid url")] + #[Groups(['entries_for_user', 'export_all'])] private $url; /** * @var string|null - * - * @ORM\Column(name="hashed_url", type="string", length=40, nullable=true) */ + #[ORM\Column(name: 'hashed_url', type: 'string', length: 40, nullable: true)] private $hashedUrl; /** * From where user retrieved/found the url (an other article, a twitter, or the given_url if non are provided). * * @var string|null - * - * @ORM\Column(name="origin_url", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'origin_url', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $originUrl; /** * Define the url entered by the user (without redirections). * * @var string|null - * - * @ORM\Column(name="given_url", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'given_url', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $givenUrl; /** * @var string|null - * - * @ORM\Column(name="hashed_given_url", type="string", length=40, nullable=true) */ + #[ORM\Column(name: 'hashed_given_url', type: 'string', length: 40, nullable: true)] private $hashedGivenUrl; /** * @var bool - * - * @Exclude - * - * @ORM\Column(name="is_archived", type="boolean") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'is_archived', type: 'boolean')] + #[Exclude] + #[Groups(['entries_for_user', 'export_all'])] private $isArchived = false; /** * @var \DateTimeInterface|null - * - * @ORM\Column(name="archived_at", type="datetime", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'archived_at', type: 'datetime', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $archivedAt; /** * @var bool - * - * @Exclude - * - * @ORM\Column(name="is_starred", type="boolean") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'is_starred', type: 'boolean')] + #[Exclude] + #[Groups(['entries_for_user', 'export_all'])] private $isStarred = false; /** * @var string|null - * - * @ORM\Column(name="content", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'content', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $content; /** * @var \DateTimeInterface - * - * @ORM\Column(name="created_at", type="datetime") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'created_at', type: 'datetime')] + #[Groups(['entries_for_user', 'export_all'])] private $createdAt; /** * @var \DateTimeInterface - * - * @ORM\Column(name="updated_at", type="datetime") - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'updated_at', type: 'datetime')] + #[Groups(['entries_for_user', 'export_all'])] private $updatedAt; /** * @var \DateTimeInterface|null - * - * @ORM\Column(name="published_at", type="datetime", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'published_at', type: 'datetime', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $publishedAt; /** * @var array|null - * - * @ORM\Column(name="published_by", type="array", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'published_by', type: 'array', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $publishedBy; /** * @var \DateTimeInterface|null - * - * @ORM\Column(name="starred_at", type="datetime", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'starred_at', type: 'datetime', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $starredAt; - /** - * @ORM\OneToMany(targetEntity="Wallabag\Entity\Annotation", mappedBy="entry", cascade={"persist", "remove"}) - * @ORM\JoinTable - * - * @Groups({"entries_for_user", "export_all"}) - */ + #[ORM\JoinTable] + #[ORM\OneToMany(targetEntity: Annotation::class, mappedBy: 'entry', cascade: ['persist', 'remove'])] + #[Groups(['entries_for_user', 'export_all'])] private $annotations; /** * @var string|null - * - * @ORM\Column(name="mimetype", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'mimetype', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $mimetype; /** * @var string|null - * - * @ORM\Column(name="language", type="string", length=20, nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'language', type: 'string', length: 20, nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $language; /** * @var int - * - * @ORM\Column(name="reading_time", type="integer", nullable=false) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'reading_time', type: 'integer', nullable: false)] + #[Groups(['entries_for_user', 'export_all'])] private $readingTime = 0; /** * @var string|null - * - * @ORM\Column(name="domain_name", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'domain_name', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $domainName; /** * @var string|null - * - * @ORM\Column(name="preview_picture", type="text", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'preview_picture', type: 'text', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $previewPicture; /** * @var string|null - * - * @ORM\Column(name="http_status", type="string", length=3, nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'http_status', type: 'string', length: 3, nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $httpStatus; /** * @var array|null - * - * @ORM\Column(name="headers", type="array", nullable=true) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'headers', type: 'array', nullable: true)] + #[Groups(['entries_for_user', 'export_all'])] private $headers; /** * @var bool - * - * @Exclude - * - * @ORM\Column(name="is_not_parsed", type="boolean", options={"default": false}) - * - * @Groups({"entries_for_user", "export_all"}) */ + #[ORM\Column(name: 'is_not_parsed', type: 'boolean', options: ['default' => false])] + #[Exclude] + #[Groups(['entries_for_user', 'export_all'])] private $isNotParsed = false; - /** - * @Exclude - * - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User", inversedBy="entries") - * - * @Groups({"export_all"}) - */ + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'entries')] + #[Exclude] + #[Groups(['export_all'])] private $user; /** - * @ORM\ManyToMany(targetEntity="Wallabag\Entity\Tag", inversedBy="entries", cascade={"persist"}) - * @ORM\JoinTable( - * name="entry_tag", - * joinColumns={ - * @ORM\JoinColumn(name="entry_id", referencedColumnName="id", onDelete="cascade") - * }, - * inverseJoinColumns={ - * @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="cascade") - * } - * ) + * @var Collection */ - private $tags; + #[ORM\JoinTable(name: 'entry_tag')] + #[ORM\JoinColumn(name: 'entry_id', referencedColumnName: 'id', onDelete: 'cascade')] + #[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'cascade')] + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'entries', cascade: ['persist'])] + private Collection $tags; /* * @param User $user @@ -437,11 +373,9 @@ class Entry return $this->isArchived; } - /** - * @VirtualProperty - * @SerializedName("is_archived") - * @Groups({"entries_for_user", "export_all"}) - */ + #[VirtualProperty] + #[SerializedName('is_archived')] + #[Groups(['entries_for_user', 'export_all'])] public function is_Archived() { return (int) $this->isArchived(); @@ -449,7 +383,7 @@ class Entry public function toggleArchive() { - $this->updateArchived($this->isArchived() ^ 1); + $this->updateArchived((bool) ($this->isArchived() ^ 1)); return $this; } @@ -478,11 +412,9 @@ class Entry return $this->isStarred; } - /** - * @VirtualProperty - * @SerializedName("is_starred") - * @Groups({"entries_for_user", "export_all"}) - */ + #[VirtualProperty] + #[SerializedName('is_starred')] + #[Groups(['entries_for_user', 'export_all'])] public function is_Starred() { return (int) $this->isStarred(); @@ -527,28 +459,22 @@ class Entry return $this->user; } - /** - * @VirtualProperty - * @SerializedName("user_name") - */ + #[VirtualProperty] + #[SerializedName('user_name')] public function getUserName() { return $this->user->getUserName(); } - /** - * @VirtualProperty - * @SerializedName("user_email") - */ + #[VirtualProperty] + #[SerializedName('user_email')] public function getUserEmail() { return $this->user->getEmail(); } - /** - * @VirtualProperty - * @SerializedName("user_id") - */ + #[VirtualProperty] + #[SerializedName('user_id')] public function getUserId() { return $this->user->getId(); @@ -667,7 +593,7 @@ class Entry } /** - * @return string + * @return string|null */ public function getDomainName() { @@ -675,7 +601,7 @@ class Entry } /** - * @param string $domainName + * @param string|null $domainName */ public function setDomainName($domainName) { @@ -683,7 +609,7 @@ class Entry } /** - * @return ArrayCollection + * @return Collection */ public function getTags() { @@ -703,11 +629,9 @@ class Entry return $tags; } - /** - * @VirtualProperty - * @SerializedName("tags") - * @Groups({"entries_for_user", "export_all"}) - */ + #[VirtualProperty] + #[SerializedName('tags')] + #[Groups(['entries_for_user', 'export_all'])] public function getSerializedTags() { $data = []; @@ -776,7 +700,7 @@ class Entry /** * Get previewPicture. * - * @return string + * @return string|null */ public function getPreviewPicture() { @@ -786,7 +710,7 @@ class Entry /** * Set language. * - * @param string $language + * @param string|null $language * * @return Entry */ @@ -800,7 +724,7 @@ class Entry /** * Get language. * - * @return string + * @return string|null */ public function getLanguage() { @@ -857,12 +781,11 @@ class Entry * Used in the entries filter so it's more explicit for the end user than the uid. * Also used in the API. * - * @VirtualProperty - * @SerializedName("is_public") - * @Groups({"entries_for_user"}) - * * @return bool */ + #[VirtualProperty] + #[SerializedName('is_public')] + #[Groups(['entries_for_user'])] public function isPublic() { return null !== $this->uid; @@ -889,7 +812,7 @@ class Entry } /** - * @return \DateTimeInterface + * @return \DateTimeInterface|null */ public function getPublishedAt() { diff --git a/src/Entity/IgnoreOriginInstanceRule.php b/src/Entity/IgnoreOriginInstanceRule.php index 17279b43b..d0c1cce31 100644 --- a/src/Entity/IgnoreOriginInstanceRule.php +++ b/src/Entity/IgnoreOriginInstanceRule.php @@ -5,35 +5,34 @@ namespace Wallabag\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\RulerZ\Validator\Constraints as RulerZAssert; use Symfony\Component\Validator\Constraints as Assert; +use Wallabag\Repository\IgnoreOriginInstanceRuleRepository; /** * Ignore Origin rule. - * - * @ORM\Entity(repositoryClass="Wallabag\Repository\IgnoreOriginInstanceRuleRepository") - * @ORM\Table(name="`ignore_origin_instance_rule`") */ +#[ORM\Table(name: '`ignore_origin_instance_rule`')] +#[ORM\Entity(repositoryClass: IgnoreOriginInstanceRuleRepository::class)] class IgnoreOriginInstanceRule implements IgnoreOriginRuleInterface, RuleInterface { /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] private $id; /** * @var string * - * @Assert\NotBlank() - * @Assert\Length(max=255) * @RulerZAssert\ValidRule( * allowed_variables={"host","_all"}, * allowed_operators={"=","~"} * ) - * @ORM\Column(name="rule", type="string", nullable=false) */ + #[ORM\Column(name: 'rule', type: 'string', nullable: false)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] private $rule; /** diff --git a/src/Entity/IgnoreOriginUserRule.php b/src/Entity/IgnoreOriginUserRule.php index 7b41182d2..58af49d82 100644 --- a/src/Entity/IgnoreOriginUserRule.php +++ b/src/Entity/IgnoreOriginUserRule.php @@ -5,41 +5,38 @@ namespace Wallabag\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\RulerZ\Validator\Constraints as RulerZAssert; use Symfony\Component\Validator\Constraints as Assert; +use Wallabag\Repository\IgnoreOriginUserRuleRepository; /** * Ignore Origin rule. - * - * @ORM\Entity(repositoryClass="Wallabag\Repository\IgnoreOriginUserRuleRepository") - * @ORM\Table(name="`ignore_origin_user_rule`") */ +#[ORM\Table(name: '`ignore_origin_user_rule`')] +#[ORM\Entity(repositoryClass: IgnoreOriginUserRuleRepository::class)] class IgnoreOriginUserRule implements IgnoreOriginRuleInterface, RuleInterface { /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] private $id; /** * @var string * - * @Assert\NotBlank() - * @Assert\Length(max=255) * @RulerZAssert\ValidRule( * allowed_variables={"host","_all"}, * allowed_operators={"=","~"} * ) - * @ORM\Column(name="rule", type="string", nullable=false) */ + #[ORM\Column(name: 'rule', type: 'string', nullable: false)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] private $rule; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Config", inversedBy="ignoreOriginRules") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Config::class, inversedBy: 'ignoreOriginRules')] private $config; /** diff --git a/src/Entity/InternalSetting.php b/src/Entity/InternalSetting.php index 705fa7121..9eaba6ca2 100644 --- a/src/Entity/InternalSetting.php +++ b/src/Entity/InternalSetting.php @@ -3,22 +3,21 @@ namespace Wallabag\Entity; use Craue\ConfigBundle\Entity\BaseSetting; +use Craue\ConfigBundle\Repository\SettingRepository; use Doctrine\ORM\Mapping as ORM; /** * InternalSetting. * * Re-define setting so we can override length attribute to fix utf8mb4 issue. - * - * @ORM\Entity(repositoryClass="Craue\ConfigBundle\Repository\SettingRepository") - * @ORM\Table(name="`internal_setting`") */ +#[ORM\Table(name: '`internal_setting`')] +#[ORM\Entity(repositoryClass: SettingRepository::class)] class InternalSetting extends BaseSetting { /** * @var string|null - * - * @ORM\Column(name="value", type="string", nullable=true) */ + #[ORM\Column(name: 'value', type: 'string', nullable: true)] protected $value; } diff --git a/src/Entity/SiteCredential.php b/src/Entity/SiteCredential.php index 5c8da7672..4ea007844 100644 --- a/src/Entity/SiteCredential.php +++ b/src/Entity/SiteCredential.php @@ -5,70 +5,62 @@ namespace Wallabag\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Wallabag\Helper\EntityTimestampsTrait; +use Wallabag\Repository\SiteCredentialRepository; /** * SiteCredential. - * - * @ORM\Entity(repositoryClass="Wallabag\Repository\SiteCredentialRepository") - * @ORM\Table(name="`site_credential`") - * @ORM\HasLifecycleCallbacks() */ +#[ORM\Table(name: '`site_credential`')] +#[ORM\Entity(repositoryClass: SiteCredentialRepository::class)] +#[ORM\HasLifecycleCallbacks] class SiteCredential { use EntityTimestampsTrait; /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] private $id; /** * @var string - * - * @Assert\NotBlank() - * @Assert\Length(max=255) - * @ORM\Column(name="host", type="string", length=255) */ + #[ORM\Column(name: 'host', type: 'string', length: 255)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] private $host; /** * @var string - * - * @Assert\NotBlank() - * @ORM\Column(name="username", type="text") */ + #[ORM\Column(name: 'username', type: 'text')] + #[Assert\NotBlank] private $username; /** * @var string - * - * @Assert\NotBlank() - * @ORM\Column(name="password", type="text") */ + #[ORM\Column(name: 'password', type: 'text')] + #[Assert\NotBlank] private $password; /** * @var \DateTime - * - * @ORM\Column(name="createdAt", type="datetime") */ + #[ORM\Column(name: 'createdAt', type: 'datetime')] private $createdAt; /** * @var \DateTime|null - * - * @ORM\Column(name="updated_at", type="datetime", nullable=true) */ + #[ORM\Column(name: 'updated_at', type: 'datetime', nullable: true)] private $updatedAt; - /** - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\User", inversedBy="siteCredentials") - * @ORM\JoinColumn(nullable=false) - */ + #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'siteCredentials')] private $user; /* diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index ef8233eb2..94737d759 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -3,63 +3,57 @@ namespace Wallabag\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use JMS\Serializer\Annotation\ExclusionPolicy; use JMS\Serializer\Annotation\Expose; use JMS\Serializer\Annotation\XmlRoot; +use Wallabag\Repository\TagRepository; /** * Tag. - * - * @XmlRoot("tag") - * @ORM\Table( - * name="`tag`", - * indexes={ - * @ORM\Index(columns={"label"}), - * } - * ) - * @ORM\Entity(repositoryClass="Wallabag\Repository\TagRepository") - * @ExclusionPolicy("all") */ -class Tag +#[ORM\Table(name: '`tag`')] +#[ORM\Index(columns: ['label'])] +#[ORM\Entity(repositoryClass: TagRepository::class)] +#[XmlRoot('tag')] +#[ExclusionPolicy('all')] +class Tag implements \Stringable { /** * @var int - * - * @Expose - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Expose] private $id; /** * @var string - * - * @Expose - * @ORM\Column(name="label", type="text") */ + #[ORM\Column(name: 'label', type: 'text')] + #[Expose] private $label; - /** - * @Expose - * @Gedmo\Slug(fields={"label"}, prefix="t:") - * @ORM\Column(length=128, unique=true) - */ + #[ORM\Column(length: 128, unique: true)] + #[Gedmo\Slug(fields: ['label'], prefix: 't:')] + #[Expose] private $slug; /** - * @ORM\ManyToMany(targetEntity="Entry", mappedBy="tags", cascade={"persist"}) + * @var Collection */ - private $entries; + #[ORM\ManyToMany(targetEntity: Entry::class, mappedBy: 'tags', cascade: ['persist'])] + private Collection $entries; public function __construct() { $this->entries = new ArrayCollection(); } - public function __toString() + public function __toString(): string { return $this->label; } @@ -131,7 +125,7 @@ class Tag /** * Get entries for this tag. * - * @return ArrayCollection + * @return Collection */ public function getEntries() { diff --git a/src/Entity/TaggingRule.php b/src/Entity/TaggingRule.php index c7834a801..c7729398b 100644 --- a/src/Entity/TaggingRule.php +++ b/src/Entity/TaggingRule.php @@ -8,56 +8,48 @@ use JMS\Serializer\Annotation\Groups; use JMS\Serializer\Annotation\XmlRoot; use Symfony\Bridge\RulerZ\Validator\Constraints as RulerZAssert; use Symfony\Component\Validator\Constraints as Assert; +use Wallabag\Repository\TaggingRuleRepository; /** * Tagging rule. - * - * @XmlRoot("tagging_rule") - * @ORM\Entity(repositoryClass="Wallabag\Repository\TaggingRuleRepository") - * @ORM\Table(name="`tagging_rule`") - * @ORM\Entity */ +#[ORM\Table(name: '`tagging_rule`')] +#[ORM\Entity(repositoryClass: TaggingRuleRepository::class)] +#[XmlRoot('tagging_rule')] class TaggingRule implements RuleInterface { /** * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] private $id; /** * @var string * - * @Assert\NotBlank() - * @Assert\Length(max=255) * @RulerZAssert\ValidRule( * allowed_variables={"title", "url", "isArchived", "isStarred", "content", "language", "mimetype", "readingTime", "domainName"}, * allowed_operators={">", "<", ">=", "<=", "=", "is", "!=", "and", "not", "or", "matches", "notmatches"} * ) - * @ORM\Column(name="rule", type="string", nullable=false) - * - * @Groups({"export_tagging_rule"}) */ + #[ORM\Column(name: 'rule', type: 'string', nullable: false)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + #[Groups(['export_tagging_rule'])] private $rule; /** * @var array - * - * @Assert\NotBlank() - * @ORM\Column(name="tags", type="simple_array", nullable=false) - * - * @Groups({"export_tagging_rule"}) */ + #[ORM\Column(name: 'tags', type: 'simple_array', nullable: false)] + #[Assert\NotBlank] + #[Groups(['export_tagging_rule'])] private $tags = []; - /** - * @Exclude - * - * @ORM\ManyToOne(targetEntity="Wallabag\Entity\Config", inversedBy="taggingRules") - */ + #[ORM\ManyToOne(targetEntity: Config::class, inversedBy: 'taggingRules')] + #[Exclude] private $config; /** diff --git a/src/Entity/User.php b/src/Entity/User.php index 61face2ad..f3668e787 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,7 @@ namespace Wallabag\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use FOS\UserBundle\Model\User as BaseUser; use JMS\Serializer\Annotation\Accessor; @@ -16,18 +17,17 @@ use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface as GoogleTwoFactorInte use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Wallabag\Entity\Api\Client; use Wallabag\Helper\EntityTimestampsTrait; +use Wallabag\Repository\UserRepository; /** * User. - * - * @XmlRoot("user") - * @ORM\Entity(repositoryClass="Wallabag\Repository\UserRepository") - * @ORM\Table(name="`user`") - * @ORM\HasLifecycleCallbacks() - * - * @UniqueEntity("email") - * @UniqueEntity("username") */ +#[ORM\Table(name: '`user`')] +#[ORM\Entity(repositoryClass: UserRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[UniqueEntity('email')] +#[UniqueEntity('username')] +#[XmlRoot('user')] class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface, BackupCodeInterface { use EntityTimestampsTrait; @@ -36,33 +36,29 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI /** * @var int * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * * @OA\Property( * description="The unique numeric id of the user", * type="int", * example=12, * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Groups(['user_api', 'user_api_with_client'])] protected $id; /** * @var string|null * - * @ORM\Column(name="name", type="text", nullable=true) - * * @OA\Property( * description="The personal Name of the user", * type="string", * example="Walla Baggger", * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[ORM\Column(name: 'name', type: 'text', nullable: true)] + #[Groups(['user_api', 'user_api_with_client'])] protected $name; /** @@ -73,9 +69,8 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI * type="string", * example="wallabag", * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[Groups(['user_api', 'user_api_with_client'])] protected $username; /** @@ -86,64 +81,53 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI * type="string", * example="wallabag@wallabag.io", * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[Groups(['user_api', 'user_api_with_client'])] protected $email; /** * @var \DateTime * - * @ORM\Column(name="created_at", type="datetime") - * * @OA\Property( * description="Creation date of the user account. (In ISO 8601 format)", * type="string", * example="2023-06-27T19:25:44+0000", * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[ORM\Column(name: 'created_at', type: 'datetime')] + #[Groups(['user_api', 'user_api_with_client'])] protected $createdAt; /** * @var \DateTime * - * @ORM\Column(name="updated_at", type="datetime") - * * @OA\Property( * description="Update date of the user account. (In ISO 8601 format)", * type="string", * example="2023-06-27T19:37:30+0000", * ) - * - * @Groups({"user_api", "user_api_with_client"}) */ + #[ORM\Column(name: 'updated_at', type: 'datetime')] + #[Groups(['user_api', 'user_api_with_client'])] protected $updatedAt; - /** - * @ORM\OneToMany(targetEntity="Wallabag\Entity\Entry", mappedBy="user", cascade={"remove"}) - */ + #[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'user', cascade: ['remove'])] protected $entries; - /** - * @ORM\OneToOne(targetEntity="Wallabag\Entity\Config", mappedBy="user", cascade={"remove"}) - */ + #[ORM\OneToOne(targetEntity: Config::class, mappedBy: 'user', cascade: ['remove'])] protected $config; /** - * @var ArrayCollection&iterable - * - * @ORM\OneToMany(targetEntity="Wallabag\Entity\SiteCredential", mappedBy="user", cascade={"remove"}) + * @var Collection */ - protected $siteCredentials; + #[ORM\OneToMany(targetEntity: SiteCredential::class, mappedBy: 'user', cascade: ['remove'])] + protected Collection $siteCredentials; /** - * @var ArrayCollection&iterable - * - * @ORM\OneToMany(targetEntity="Wallabag\Entity\Api\Client", mappedBy="user", cascade={"remove"}) + * @var Collection */ - protected $clients; + #[ORM\OneToMany(targetEntity: Client::class, mappedBy: 'user', cascade: ['remove'])] + protected Collection $clients; /** * @see getFirstClient() below @@ -152,40 +136,40 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI * description="Default client created during user registration. Used for further authorization", * ref=@Model(type=Client::class, groups={"user_api_with_client"}) * ) - * - * @Groups({"user_api_with_client"}) - * @Accessor(getter="getFirstClient") */ + #[Groups(['user_api_with_client'])] + #[Accessor(getter: 'getFirstClient')] protected $default_client; - /** - * @ORM\Column(type="integer", nullable=true) - */ + #[ORM\Column(type: 'integer', nullable: true)] private $authCode; - /** - * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true) - */ + #[ORM\Column(name: 'googleAuthenticatorSecret', type: 'string', nullable: true)] private $googleAuthenticatorSecret; + // default value is explicitly set to false here to ensure that Doctrine + // does not complain about schema mapping mismatch + #[ORM\Column(name: 'google_authenticator', type: 'boolean', options: ['default' => false])] + private $googleAuthenticator = false; + /** * @var array - * - * @ORM\Column(type="json", nullable=true) */ + #[ORM\Column(type: 'json', nullable: true)] private $backupCodes; /** * @var bool - * - * @ORM\Column(type="boolean") */ + #[ORM\Column(type: 'boolean')] private $emailTwoFactor = false; public function __construct() { parent::__construct(); $this->entries = new ArrayCollection(); + $this->siteCredentials = new ArrayCollection(); + $this->clients = new ArrayCollection(); $this->roles = ['ROLE_USER']; } @@ -240,7 +224,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI } /** - * @return ArrayCollection + * @return Collection */ public function getEntries() { @@ -285,6 +269,11 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI $this->emailTwoFactor = $emailTwoFactor; } + public function setGoogleAuthenticator(bool $googleAuthenticator): void + { + $this->googleAuthenticator = $googleAuthenticator; + } + /** * Used in the user config form to be "like" the email option. */ @@ -315,7 +304,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI public function isGoogleAuthenticatorEnabled(): bool { - return $this->googleAuthenticatorSecret ? true : false; + return $this->googleAuthenticator; } public function getGoogleAuthenticatorUsername(): string @@ -362,13 +351,15 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI */ public function addClient(Client $client) { - $this->clients[] = $client; + if (!$this->clients->contains($client)) { + $this->clients->add($client); + } return $this; } /** - * @return ArrayCollection + * @return Collection */ public function getClients() { @@ -382,7 +373,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI */ public function getFirstClient() { - if (!empty($this->clients)) { + if (!$this->clients->isEmpty()) { return $this->clients->first(); } @@ -401,7 +392,7 @@ class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorI foreach ($this->backupCodes as $key => $backupCode) { // backup code are hashed using `password_hash` // see ConfigController->otpAppAction - if (password_verify($code, $backupCode)) { + if (password_verify($code, (string) $backupCode)) { return $key; } } diff --git a/src/Event/ConfigUpdatedEvent.php b/src/Event/ConfigUpdatedEvent.php index d1806a57f..455789137 100644 --- a/src/Event/ConfigUpdatedEvent.php +++ b/src/Event/ConfigUpdatedEvent.php @@ -12,11 +12,9 @@ class ConfigUpdatedEvent extends Event { public const NAME = 'config.updated'; - protected $config; - - public function __construct(Config $entry) - { - $this->config = $entry; + public function __construct( + protected Config $config, + ) { } public function getConfig(): Config diff --git a/src/Event/EntryDeletedEvent.php b/src/Event/EntryDeletedEvent.php index c81bf711e..b95806c80 100644 --- a/src/Event/EntryDeletedEvent.php +++ b/src/Event/EntryDeletedEvent.php @@ -12,11 +12,9 @@ class EntryDeletedEvent extends Event { public const NAME = 'entry.deleted'; - protected $entry; - - public function __construct(Entry $entry) - { - $this->entry = $entry; + public function __construct( + protected Entry $entry, + ) { } public function getEntry(): Entry diff --git a/src/Event/EntrySavedEvent.php b/src/Event/EntrySavedEvent.php index ec3bdfa93..c1deaedbe 100644 --- a/src/Event/EntrySavedEvent.php +++ b/src/Event/EntrySavedEvent.php @@ -12,11 +12,9 @@ class EntrySavedEvent extends Event { public const NAME = 'entry.saved'; - protected $entry; - - public function __construct(Entry $entry) - { - $this->entry = $entry; + public function __construct( + protected Entry $entry, + ) { } public function getEntry(): Entry diff --git a/src/Event/Listener/AuthenticationFailureListener.php b/src/Event/Listener/AuthenticationFailureListener.php index 0c36856cd..077eeec80 100644 --- a/src/Event/Listener/AuthenticationFailureListener.php +++ b/src/Event/Listener/AuthenticationFailureListener.php @@ -5,23 +5,20 @@ namespace Wallabag\Event\Listener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; class AuthenticationFailureListener implements EventSubscriberInterface { - private $requestStack; - private $logger; - - public function __construct(RequestStack $requestStack, LoggerInterface $logger) - { - $this->requestStack = $requestStack; - $this->logger = $logger; + public function __construct( + private readonly RequestStack $requestStack, + private readonly LoggerInterface $logger, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ - AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure', + LoginFailureEvent::class => 'onAuthenticationFailure', ]; } @@ -30,7 +27,7 @@ class AuthenticationFailureListener implements EventSubscriberInterface */ public function onAuthenticationFailure() { - $request = $this->requestStack->getMasterRequest(); + $request = $this->requestStack->getMainRequest(); $this->logger->error('Authentication failure for user "' . $request->request->get('_username') . '", from IP "' . $request->getClientIp() . '", with UA: "' . $request->server->get('HTTP_USER_AGENT') . '".'); } diff --git a/src/Event/Listener/CreateConfigListener.php b/src/Event/Listener/CreateConfigListener.php index 383589aad..b4df73042 100644 --- a/src/Event/Listener/CreateConfigListener.php +++ b/src/Event/Listener/CreateConfigListener.php @@ -8,6 +8,7 @@ use FOS\UserBundle\FOSUserEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; use Wallabag\Entity\Config; +use Wallabag\Entity\User; /** * This listener will create the associated configuration when a user register. @@ -15,30 +16,20 @@ use Wallabag\Entity\Config; */ class CreateConfigListener implements EventSubscriberInterface { - private $em; - private $itemsOnPage; - private $feedLimit; - private $language; - private $readingSpeed; - private $actionMarkAsRead; - private $listMode; - private $requestStack; - private $displayThumbnails; - - public function __construct(EntityManagerInterface $em, $itemsOnPage, $feedLimit, $language, $readingSpeed, $actionMarkAsRead, $listMode, $displayThumbnails, RequestStack $requestStack) - { - $this->em = $em; - $this->itemsOnPage = $itemsOnPage; - $this->feedLimit = $feedLimit; - $this->language = $language; - $this->readingSpeed = $readingSpeed; - $this->actionMarkAsRead = $actionMarkAsRead; - $this->listMode = $listMode; - $this->requestStack = $requestStack; - $this->displayThumbnails = $displayThumbnails; + public function __construct( + private readonly EntityManagerInterface $em, + private $itemsOnPage, + private $feedLimit, + private $language, + private $readingSpeed, + private $actionMarkAsRead, + private $listMode, + private $displayThumbnails, + private readonly RequestStack $requestStack, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // when a user register using the normal form @@ -53,12 +44,15 @@ class CreateConfigListener implements EventSubscriberInterface { $language = $this->language; - if ($this->requestStack->getMasterRequest()) { - $session = $this->requestStack->getMasterRequest()->getSession(); + if ($this->requestStack->getMainRequest()) { + $session = $this->requestStack->getMainRequest()->getSession(); $language = $session->get('_locale', $this->language); } - $config = new Config($event->getUser()); + $user = $event->getUser(); + \assert($user instanceof User); + + $config = new Config($user); $config->setItemsPerPage($this->itemsOnPage); $config->setFeedLimit($this->feedLimit); $config->setLanguage($language); diff --git a/src/Event/Listener/LocaleListener.php b/src/Event/Listener/LocaleListener.php index 1937cda85..cd48551ec 100644 --- a/src/Event/Listener/LocaleListener.php +++ b/src/Event/Listener/LocaleListener.php @@ -11,11 +11,9 @@ use Symfony\Component\HttpKernel\KernelEvents; */ class LocaleListener implements EventSubscriberInterface { - private $defaultLocale; - - public function __construct($defaultLocale = 'en') - { - $this->defaultLocale = $defaultLocale; + public function __construct( + private $defaultLocale = 'en', + ) { } public function onKernelRequest(RequestEvent $event) @@ -34,7 +32,7 @@ class LocaleListener implements EventSubscriberInterface } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ // must be registered before the default Locale listener diff --git a/src/Event/Listener/PasswordResettingListener.php b/src/Event/Listener/PasswordResettingListener.php index 611d1fead..b34522167 100644 --- a/src/Event/Listener/PasswordResettingListener.php +++ b/src/Event/Listener/PasswordResettingListener.php @@ -15,14 +15,12 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; */ class PasswordResettingListener implements EventSubscriberInterface { - private $router; - - public function __construct(UrlGeneratorInterface $router) - { - $this->router = $router; + public function __construct( + private readonly UrlGeneratorInterface $router, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ FOSUserEvents::RESETTING_RESET_SUCCESS => 'onPasswordResettingSuccess', diff --git a/src/Event/Listener/RegistrationListener.php b/src/Event/Listener/RegistrationListener.php index 8baea7918..eaeef75f5 100644 --- a/src/Event/Listener/RegistrationListener.php +++ b/src/Event/Listener/RegistrationListener.php @@ -11,22 +11,15 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class RegistrationListener implements EventSubscriberInterface { /** - * @var bool + * @param bool $registrationEnabled */ - private $registrationEnabled; - - /** - * @var UrlGeneratorInterface - */ - private $urlGenerator; - - public function __construct($registrationEnabled, UrlGeneratorInterface $urlGenerator) - { - $this->registrationEnabled = $registrationEnabled; - $this->urlGenerator = $urlGenerator; + public function __construct( + private $registrationEnabled, + private readonly UrlGeneratorInterface $urlGenerator, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInitialize', diff --git a/src/Event/Listener/UserLocaleListener.php b/src/Event/Listener/UserLocaleListener.php index df195a2b0..5c9403376 100644 --- a/src/Event/Listener/UserLocaleListener.php +++ b/src/Event/Listener/UserLocaleListener.php @@ -16,11 +16,9 @@ use Wallabag\Entity\User; */ class UserLocaleListener { - private SessionInterface $session; - - public function __construct(SessionInterface $session) - { - $this->session = $session; + public function __construct( + private readonly SessionInterface $session, + ) { } public function onInteractiveLogin(InteractiveLoginEvent $event) diff --git a/src/Event/Subscriber/AccessDeniedToNotFoundSubscriber.php b/src/Event/Subscriber/AccessDeniedToNotFoundSubscriber.php new file mode 100644 index 000000000..c0e3126e2 --- /dev/null +++ b/src/Event/Subscriber/AccessDeniedToNotFoundSubscriber.php @@ -0,0 +1,29 @@ + 'onKernelException', + ]; + } + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + + if ($exception instanceof AccessDeniedHttpException) { + $notFoundException = new NotFoundHttpException('', $exception); + $event->setThrowable($notFoundException); + } + } +} diff --git a/src/Event/Subscriber/CustomDoctrineORMSubscriber.php b/src/Event/Subscriber/CustomDoctrineORMSubscriber.php index 6e35de28e..9784f7c02 100644 --- a/src/Event/Subscriber/CustomDoctrineORMSubscriber.php +++ b/src/Event/Subscriber/CustomDoctrineORMSubscriber.php @@ -4,6 +4,7 @@ namespace Wallabag\Event\Subscriber; use Spiriit\Bundle\FormFilterBundle\Event\GetFilterConditionEvent; use Spiriit\Bundle\FormFilterBundle\Event\Subscriber\DoctrineORMSubscriber; +use Spiriit\Bundle\FormFilterBundle\Filter\Doctrine\ORMQuery; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -14,7 +15,11 @@ class CustomDoctrineORMSubscriber extends DoctrineORMSubscriber implements Event { public function filterDateRange(GetFilterConditionEvent $event) { - $expr = $event->getFilterQuery()->getExpressionBuilder(); + $filterQuery = $event->getFilterQuery(); + + \assert($filterQuery instanceof ORMQuery); + + $expr = $filterQuery->getExpressionBuilder(); $values = $event->getValues(); $value = $values['value']; diff --git a/src/Event/Subscriber/DownloadImagesSubscriber.php b/src/Event/Subscriber/DownloadImagesSubscriber.php index 6c8eb4b05..2becf355f 100644 --- a/src/Event/Subscriber/DownloadImagesSubscriber.php +++ b/src/Event/Subscriber/DownloadImagesSubscriber.php @@ -12,20 +12,15 @@ use Wallabag\Helper\DownloadImages; class DownloadImagesSubscriber implements EventSubscriberInterface { - private $em; - private $downloadImages; - private $enabled; - private $logger; - - public function __construct(EntityManagerInterface $em, DownloadImages $downloadImages, $enabled, LoggerInterface $logger) - { - $this->em = $em; - $this->downloadImages = $downloadImages; - $this->enabled = $enabled; - $this->logger = $logger; + public function __construct( + private readonly EntityManagerInterface $em, + private readonly DownloadImages $downloadImages, + private $enabled, + private readonly LoggerInterface $logger, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ EntrySavedEvent::NAME => 'onEntrySaved', @@ -84,7 +79,7 @@ class DownloadImagesSubscriber implements EventSubscriberInterface * * @todo If we want to add async download, it should be done in that method * - * @return string|false False in case of async + * @return string */ private function downloadImages(Entry $entry) { diff --git a/src/Event/Subscriber/GenerateCustomCSSSubscriber.php b/src/Event/Subscriber/GenerateCustomCSSSubscriber.php index 9fc72bee9..06dfbdbd5 100644 --- a/src/Event/Subscriber/GenerateCustomCSSSubscriber.php +++ b/src/Event/Subscriber/GenerateCustomCSSSubscriber.php @@ -9,16 +9,13 @@ use Wallabag\Event\ConfigUpdatedEvent; class GenerateCustomCSSSubscriber implements EventSubscriberInterface { - private $em; - private $compiler; - - public function __construct(EntityManagerInterface $em, Compiler $compiler) - { - $this->em = $em; - $this->compiler = $compiler; + public function __construct( + private readonly EntityManagerInterface $em, + private readonly Compiler $compiler, + ) { } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ ConfigUpdatedEvent::NAME => 'onConfigUpdated', diff --git a/src/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php b/src/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php index 56f68792e..5e3e2ea68 100644 --- a/src/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php +++ b/src/Event/Subscriber/SQLiteCascadeDeleteSubscriber.php @@ -4,7 +4,7 @@ namespace Wallabag\Event\Subscriber; use Doctrine\Common\EventSubscriber; use Doctrine\DBAL\Platforms\SqlitePlatform; -use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Event\PreRemoveEventArgs; use Doctrine\Persistence\ManagerRegistry; use Wallabag\Entity\Entry; @@ -17,11 +17,9 @@ use Wallabag\Entity\Entry; */ class SQLiteCascadeDeleteSubscriber implements EventSubscriber { - private $doctrine; - - public function __construct(ManagerRegistry $doctrine) - { - $this->doctrine = $doctrine; + public function __construct( + private readonly ManagerRegistry $doctrine, + ) { } /** @@ -38,7 +36,7 @@ class SQLiteCascadeDeleteSubscriber implements EventSubscriber * We removed everything related to the upcoming removed entry because SQLite can't handle it on it own. * We do it in the preRemove, because we can't retrieve tags in the postRemove (because the entry id is gone). */ - public function preRemove(LifecycleEventArgs $args) + public function preRemove(PreRemoveEventArgs $args) { $entity = $args->getObject(); if (!$this->doctrine->getConnection()->getDatabasePlatform() instanceof SqlitePlatform diff --git a/src/Event/Subscriber/SchemaAdapterSubscriber.php b/src/Event/Subscriber/SchemaAdapterSubscriber.php index 018877e97..ae02181dd 100644 --- a/src/Event/Subscriber/SchemaAdapterSubscriber.php +++ b/src/Event/Subscriber/SchemaAdapterSubscriber.php @@ -8,14 +8,12 @@ use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; class SchemaAdapterSubscriber implements EventSubscriber { - private string $databaseTablePrefix; - - public function __construct(string $databaseTablePrefix) - { - $this->databaseTablePrefix = $databaseTablePrefix; + public function __construct( + private readonly string $databaseTablePrefix, + ) { } - public function getSubscribedEvents() + public function getSubscribedEvents(): array { return ['postGenerateSchema']; } diff --git a/src/Event/Subscriber/TablePrefixSubscriber.php b/src/Event/Subscriber/TablePrefixSubscriber.php index 9a6f12f8c..cf329f7d4 100644 --- a/src/Event/Subscriber/TablePrefixSubscriber.php +++ b/src/Event/Subscriber/TablePrefixSubscriber.php @@ -25,7 +25,7 @@ class TablePrefixSubscriber implements EventSubscriber $this->tablePrefix = (string) $tablePrefix; } - public function getSubscribedEvents() + public function getSubscribedEvents(): array { return ['loadClassMetadata']; } diff --git a/src/ExpressionLanguage/AuthenticatorProvider.php b/src/ExpressionLanguage/AuthenticatorProvider.php index 25c748d3e..c98db51fd 100644 --- a/src/ExpressionLanguage/AuthenticatorProvider.php +++ b/src/ExpressionLanguage/AuthenticatorProvider.php @@ -9,11 +9,9 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class AuthenticatorProvider implements ExpressionFunctionProviderInterface { - private HttpClientInterface $requestHtmlFunctionClient; - - public function __construct(HttpClientInterface $requestHtmlFunctionClient) - { - $this->requestHtmlFunctionClient = $requestHtmlFunctionClient; + public function __construct( + private readonly HttpClientInterface $requestHtmlFunctionClient, + ) { } public function getFunctions(): array @@ -31,12 +29,10 @@ class AuthenticatorProvider implements ExpressionFunctionProviderInterface { return new ExpressionFunction( 'request_html', - function () { + function (): void { throw new \Exception('Not supported'); }, - function (array $arguments, $uri) { - return $this->requestHtmlFunctionClient->request('GET', $uri)->getContent(); - } + fn (array $arguments, $uri) => $this->requestHtmlFunctionClient->request('GET', $uri)->getContent() ); } @@ -44,7 +40,7 @@ class AuthenticatorProvider implements ExpressionFunctionProviderInterface { return new ExpressionFunction( 'preg_match', - function () { + function (): void { throw new \Exception('Not supported'); }, function (array $arguments, $pattern, $html) { @@ -63,7 +59,7 @@ class AuthenticatorProvider implements ExpressionFunctionProviderInterface { return new ExpressionFunction( 'xpath', - function () { + function (): void { throw new \Exception('Not supported'); }, function (array $arguments, $xpathQuery, $html) { @@ -71,7 +67,7 @@ class AuthenticatorProvider implements ExpressionFunctionProviderInterface $crawler = new Crawler((string) $html); $crawler = $crawler->filterXPath($xpathQuery); - } catch (\Throwable $e) { + } catch (\Throwable) { return ''; } diff --git a/src/Form/DataTransformer/StringToListTransformer.php b/src/Form/DataTransformer/StringToListTransformer.php index 97e012a16..148203317 100644 --- a/src/Form/DataTransformer/StringToListTransformer.php +++ b/src/Form/DataTransformer/StringToListTransformer.php @@ -3,6 +3,7 @@ namespace Wallabag\Form\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; /** * Transforms a comma-separated list to a proper PHP array. @@ -10,24 +11,17 @@ use Symfony\Component\Form\DataTransformerInterface; */ class StringToListTransformer implements DataTransformerInterface { - /** - * @var string - */ - private $separator; - /** * @param string $separator The separator used in the list */ - public function __construct($separator = ',') - { - $this->separator = $separator; + public function __construct( + private $separator = ',', + ) { } /** * Transforms a list to a string. * - * @param array|null $list - * * @return string */ public function transform($list) @@ -36,14 +30,16 @@ class StringToListTransformer implements DataTransformerInterface return ''; } + if (!\is_array($list)) { + throw new UnexpectedTypeException($list, 'array'); + } + return implode($this->separator, $list); } /** * Transforms a string to a list. * - * @param string $string - * * @return array|null */ public function reverseTransform($string) @@ -52,6 +48,10 @@ class StringToListTransformer implements DataTransformerInterface return null; } + if (!\is_string($string)) { + throw new UnexpectedTypeException($string, 'string'); + } + return array_values(array_filter(array_map('trim', explode($this->separator, $string)))); } } diff --git a/src/Form/Type/Api/ClientType.php b/src/Form/Type/Api/ClientType.php index 66284fe58..accb8ee2d 100644 --- a/src/Form/Type/Api/ClientType.php +++ b/src/Form/Type/Api/ClientType.php @@ -28,12 +28,8 @@ class ClientType extends AbstractType $builder->get('redirect_uris') ->addModelTransformer(new CallbackTransformer( - function ($originalUri) { - return $originalUri; - }, - function ($submittedUri) { - return [$submittedUri]; - } + fn ($originalUri) => $originalUri, + fn ($submittedUri) => [$submittedUri] )) ; } @@ -45,7 +41,7 @@ class ClientType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'client'; } diff --git a/src/Form/Type/ChangePasswordType.php b/src/Form/Type/ChangePasswordType.php index 512a01750..e6a3ed7a9 100644 --- a/src/Form/Type/ChangePasswordType.php +++ b/src/Form/Type/ChangePasswordType.php @@ -41,7 +41,7 @@ class ChangePasswordType extends AbstractType ; } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'change_passwd'; } diff --git a/src/Form/Type/ConfigType.php b/src/Form/Type/ConfigType.php index 1aea4fc6f..2a4ee9a47 100644 --- a/src/Form/Type/ConfigType.php +++ b/src/Form/Type/ConfigType.php @@ -14,17 +14,14 @@ use Wallabag\Entity\Config; class ConfigType extends AbstractType { - private $languages = []; - private $fonts = []; - /** * @param array $languages Languages come from configuration, array just code language as key and label as value * @param array $fonts Fonts come from configuration, array just font name as key / value */ - public function __construct($languages, $fonts) - { - $this->languages = $languages; - $this->fonts = $fonts; + public function __construct( + private $languages, + private $fonts, + ) { } public function buildForm(FormBuilderInterface $builder, array $options) @@ -104,7 +101,7 @@ class ConfigType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'config'; } diff --git a/src/Form/Type/EditEntryType.php b/src/Form/Type/EditEntryType.php index 090e73e83..cb0d75144 100644 --- a/src/Form/Type/EditEntryType.php +++ b/src/Form/Type/EditEntryType.php @@ -44,7 +44,7 @@ class EditEntryType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'entry'; } diff --git a/src/Form/Type/EntryFilterType.php b/src/Form/Type/EntryFilterType.php index d4462f36d..f4e9b76f0 100644 --- a/src/Form/Type/EntryFilterType.php +++ b/src/Form/Type/EntryFilterType.php @@ -2,6 +2,7 @@ namespace Wallabag\Form\Type; +use Spiriit\Bundle\FormFilterBundle\Filter\Doctrine\ORMQuery; use Spiriit\Bundle\FormFilterBundle\Filter\FilterOperands; use Spiriit\Bundle\FormFilterBundle\Filter\Form\Type\CheckboxFilterType; use Spiriit\Bundle\FormFilterBundle\Filter\Form\Type\ChoiceFilterType; @@ -20,16 +21,13 @@ use Wallabag\Repository\EntryRepository; class EntryFilterType extends AbstractType { - private $repository; - private $tokenStorage; - /** * Repository & user are used to get a list of language entries for this user. */ - public function __construct(EntryRepository $entryRepository, TokenStorageInterface $tokenStorage) - { - $this->repository = $entryRepository; - $this->tokenStorage = $tokenStorage; + public function __construct( + private readonly EntryRepository $repository, + private readonly TokenStorageInterface $tokenStorage, + ) { } public function buildForm(FormBuilderInterface $builder, array $options) @@ -57,6 +55,8 @@ class EntryFilterType extends AbstractType return; } + \assert($filterQuery instanceof ORMQuery); + $min = (int) ($lower * $user->getConfig()->getReadingSpeed() / 200); $max = (int) ($upper * $user->getConfig()->getReadingSpeed() / 200); @@ -98,6 +98,9 @@ class EntryFilterType extends AbstractType if (empty($value) || \strlen($value) <= 2) { return false; } + + \assert($filterQuery instanceof ORMQuery); + $expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%'))); return $filterQuery->createCondition($expression); @@ -114,6 +117,8 @@ class EntryFilterType extends AbstractType return false; } + \assert($filterQuery instanceof ORMQuery); + $paramName = \sprintf('%s', str_replace('.', '_', $field)); $expression = $filterQuery->getExpr()->eq($field, ':' . $paramName); $parameters = [$paramName => $value]; @@ -143,6 +148,8 @@ class EntryFilterType extends AbstractType return false; } + \assert($filterQuery instanceof ORMQuery); + $expression = $filterQuery->getExpr()->eq('e.isArchived', 'false'); return $filterQuery->createCondition($expression); @@ -170,6 +177,8 @@ class EntryFilterType extends AbstractType return false; } + \assert($filterQuery instanceof ORMQuery); + $expression = $filterQuery->getExpr()->isNotNull($field); return $filterQuery->createCondition($expression); @@ -182,6 +191,8 @@ class EntryFilterType extends AbstractType return false; } + \assert($filterQuery instanceof ORMQuery); + // is_public isn't a real field // we should use the "uid" field to determine if the entry has been made public $expression = $filterQuery->getExpr()->isNotNull($values['alias'] . '.uid'); @@ -197,7 +208,7 @@ class EntryFilterType extends AbstractType ; } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'entry_filter'; } diff --git a/src/Form/Type/FeedType.php b/src/Form/Type/FeedType.php index 3739cb421..7b71670a6 100644 --- a/src/Form/Type/FeedType.php +++ b/src/Form/Type/FeedType.php @@ -30,7 +30,7 @@ class FeedType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'feed_config'; } diff --git a/src/Form/Type/IgnoreOriginInstanceRuleType.php b/src/Form/Type/IgnoreOriginInstanceRuleType.php index c4a4a29b5..440beef52 100644 --- a/src/Form/Type/IgnoreOriginInstanceRuleType.php +++ b/src/Form/Type/IgnoreOriginInstanceRuleType.php @@ -31,7 +31,7 @@ class IgnoreOriginInstanceRuleType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'ignore_origin_instance_rule'; } diff --git a/src/Form/Type/IgnoreOriginUserRuleType.php b/src/Form/Type/IgnoreOriginUserRuleType.php index e4da264c5..122c782d8 100644 --- a/src/Form/Type/IgnoreOriginUserRuleType.php +++ b/src/Form/Type/IgnoreOriginUserRuleType.php @@ -31,7 +31,7 @@ class IgnoreOriginUserRuleType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'ignore_origin_user_rule'; } diff --git a/src/Form/Type/NewEntryType.php b/src/Form/Type/NewEntryType.php index 9892adf74..6dd2eecdd 100644 --- a/src/Form/Type/NewEntryType.php +++ b/src/Form/Type/NewEntryType.php @@ -28,7 +28,7 @@ class NewEntryType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'entry'; } diff --git a/src/Form/Type/NewTagType.php b/src/Form/Type/NewTagType.php index 5f99d8aaf..4f0cbbb08 100644 --- a/src/Form/Type/NewTagType.php +++ b/src/Form/Type/NewTagType.php @@ -37,7 +37,7 @@ class NewTagType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'tag'; } diff --git a/src/Form/Type/NewUserType.php b/src/Form/Type/NewUserType.php index 5a3da7790..459a2f720 100644 --- a/src/Form/Type/NewUserType.php +++ b/src/Form/Type/NewUserType.php @@ -53,7 +53,7 @@ class NewUserType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'new_user'; } diff --git a/src/Form/Type/RenameTagType.php b/src/Form/Type/RenameTagType.php index 0cdabf401..2c27c6f13 100644 --- a/src/Form/Type/RenameTagType.php +++ b/src/Form/Type/RenameTagType.php @@ -29,7 +29,7 @@ class RenameTagType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'tag'; } diff --git a/src/Form/Type/SiteCredentialType.php b/src/Form/Type/SiteCredentialType.php index 294db8ff6..f8b8177a0 100644 --- a/src/Form/Type/SiteCredentialType.php +++ b/src/Form/Type/SiteCredentialType.php @@ -38,7 +38,7 @@ class SiteCredentialType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'site_credential'; } diff --git a/src/Form/Type/TaggingRuleImportType.php b/src/Form/Type/TaggingRuleImportType.php index cd9946042..b07a44a51 100644 --- a/src/Form/Type/TaggingRuleImportType.php +++ b/src/Form/Type/TaggingRuleImportType.php @@ -22,7 +22,7 @@ class TaggingRuleImportType extends AbstractType ; } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'upload_tagging_rule_file'; } diff --git a/src/Form/Type/TaggingRuleType.php b/src/Form/Type/TaggingRuleType.php index 1aa5da5cb..153a6aad4 100644 --- a/src/Form/Type/TaggingRuleType.php +++ b/src/Form/Type/TaggingRuleType.php @@ -40,7 +40,7 @@ class TaggingRuleType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'tagging_rule'; } diff --git a/src/Form/Type/UploadImportType.php b/src/Form/Type/UploadImportType.php index 301e6a23c..09b793f9e 100644 --- a/src/Form/Type/UploadImportType.php +++ b/src/Form/Type/UploadImportType.php @@ -27,7 +27,7 @@ class UploadImportType extends AbstractType ; } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'upload_import_file'; } diff --git a/src/Form/Type/UserInformationType.php b/src/Form/Type/UserInformationType.php index a5c633dcd..94020138b 100644 --- a/src/Form/Type/UserInformationType.php +++ b/src/Form/Type/UserInformationType.php @@ -30,7 +30,7 @@ class UserInformationType extends AbstractType ; } - public function getParent() + public function getParent(): ?string { return RegistrationFormType::class; } @@ -42,7 +42,7 @@ class UserInformationType extends AbstractType ]); } - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'update_user'; } diff --git a/src/Guzzle/AuthenticatorSubscriber.php b/src/Guzzle/AuthenticatorSubscriber.php deleted file mode 100644 index 4b565076e..000000000 --- a/src/Guzzle/AuthenticatorSubscriber.php +++ /dev/null @@ -1,121 +0,0 @@ -configBuilder = $configBuilder; - $this->authenticator = $authenticator; - $this->logger = new NullLogger(); - } - - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - - public function getEvents(): array - { - return [ - 'before' => ['loginIfRequired'], - 'complete' => ['loginIfRequested'], - ]; - } - - public function loginIfRequired(BeforeEvent $event) - { - $config = $this->buildSiteConfig($event->getRequest()); - if (false === $config || !$config->requiresLogin()) { - $this->logger->debug('loginIfRequired> will not require login'); - - return; - } - - $client = $event->getClient(); - - if (!$this->authenticator->isLoggedIn($config, $client)) { - $this->logger->debug('loginIfRequired> user is not logged in, attach authenticator'); - - $emitter = $client->getEmitter(); - $emitter->detach($this); - $this->authenticator->login($config, $client); - $emitter->attach($this); - } - } - - public function loginIfRequested(CompleteEvent $event) - { - $config = $this->buildSiteConfig($event->getRequest()); - if (false === $config || !$config->requiresLogin()) { - $this->logger->debug('loginIfRequested> will not require login'); - - return; - } - - $body = $event->getResponse()->getBody(); - - if ( - null === $body - || '' === $body->getContents() - ) { - $this->logger->debug('loginIfRequested> empty body, ignoring'); - - return; - } - - $isLoginRequired = $this->authenticator->isLoginRequired($config, $body); - - $this->logger->debug('loginIfRequested> retry #' . $this->retries . ' with login ' . ($isLoginRequired ? '' : 'not ') . 'required'); - - if ($isLoginRequired && $this->retries < self::MAX_RETRIES) { - $client = $event->getClient(); - - $emitter = $client->getEmitter(); - $emitter->detach($this); - $this->authenticator->login($config, $client); - $emitter->attach($this); - - $event->retry(); - - ++$this->retries; - } - } - - /** - * @return SiteConfig|false - */ - private function buildSiteConfig(RequestInterface $request) - { - return $this->configBuilder->buildForHost($request->getHost()); - } -} diff --git a/src/Helper/ContentProxy.php b/src/Helper/ContentProxy.php index bfdc9f163..0533b2cfe 100644 --- a/src/Helper/ContentProxy.php +++ b/src/Helper/ContentProxy.php @@ -17,26 +17,19 @@ use Wallabag\Tools\Utils; */ class ContentProxy { - protected $graby; - protected $tagger; - protected $ignoreOriginProcessor; - protected $validator; - protected $logger; protected $mimeTypes; - protected $fetchingErrorMessage; protected $eventDispatcher; - protected $storeArticleHeaders; - public function __construct(Graby $graby, RuleBasedTagger $tagger, RuleBasedIgnoreOriginProcessor $ignoreOriginProcessor, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage, $storeArticleHeaders = false) - { - $this->graby = $graby; - $this->tagger = $tagger; - $this->ignoreOriginProcessor = $ignoreOriginProcessor; - $this->validator = $validator; - $this->logger = $logger; + public function __construct( + protected Graby $graby, + protected RuleBasedTagger $tagger, + protected RuleBasedIgnoreOriginProcessor $ignoreOriginProcessor, + protected ValidatorInterface $validator, + protected LoggerInterface $logger, + protected $fetchingErrorMessage, + protected $storeArticleHeaders = false, + ) { $this->mimeTypes = new MimeTypes(); - $this->fetchingErrorMessage = $fetchingErrorMessage; - $this->storeArticleHeaders = $storeArticleHeaders; } /** @@ -59,7 +52,7 @@ class ContentProxy $fetchedContent['title'] = $this->sanitizeContentTitle( $fetchedContent['title'], - isset($fetchedContent['headers']['content-type']) ? $fetchedContent['headers']['content-type'] : '' + $fetchedContent['headers']['content-type'] ?? '' ); // when content is imported, we have information in $content @@ -107,7 +100,9 @@ class ContentProxy return; } - $this->logger->warning('Language validation failed. ' . (string) $errors); + foreach ($errors as $error) { + $this->logger->warning('Language validation failed. ' . $error->getMessage()); + } } /** @@ -128,7 +123,9 @@ class ContentProxy return; } - $this->logger->warning('PreviewPicture validation failed. ' . (string) $errors); + foreach ($errors as $error) { + $this->logger->warning('PreviewPicture validation failed. ' . $error->getMessage()); + } } /** @@ -146,11 +143,8 @@ class ContentProxy } try { - // is it already a DateTime? // (it's inside the try/catch in case of fail to be parse time string) - if (!$date instanceof \DateTime) { - $date = new \DateTime($date); - } + $date = new \DateTime($date); $entry->setPublishedAt($date); } catch (\Exception $e) { diff --git a/src/Helper/CryptoProxy.php b/src/Helper/CryptoProxy.php index cfc365eb3..fc1814109 100644 --- a/src/Helper/CryptoProxy.php +++ b/src/Helper/CryptoProxy.php @@ -13,13 +13,12 @@ use Psr\Log\LoggerInterface; */ class CryptoProxy { - private $logger; private $encryptionKey; - public function __construct($encryptionKeyPath, LoggerInterface $logger) - { - $this->logger = $logger; - + public function __construct( + $encryptionKeyPath, + private readonly LoggerInterface $logger, + ) { if (!file_exists($encryptionKeyPath)) { $key = Key::createNewRandomKey(); diff --git a/src/Helper/DownloadImages.php b/src/Helper/DownloadImages.php index d7423d364..d5a00b0f8 100644 --- a/src/Helper/DownloadImages.php +++ b/src/Helper/DownloadImages.php @@ -16,19 +16,16 @@ use Symfony\Contracts\HttpClient\ResponseInterface; class DownloadImages { public const REGENERATE_PICTURES_QUALITY = 80; - - private $client; - private $baseFolder; - private $logger; private $mimeTypes; private $wallabagUrl; - public function __construct(HttpClientInterface $downloadImagesClient, $baseFolder, $wallabagUrl, LoggerInterface $logger) - { - $this->client = $downloadImagesClient; - $this->baseFolder = $baseFolder; - $this->wallabagUrl = rtrim($wallabagUrl, '/'); - $this->logger = $logger; + public function __construct( + private readonly HttpClientInterface $client, + private $baseFolder, + $wallabagUrl, + private readonly LoggerInterface $logger, + ) { + $this->wallabagUrl = rtrim((string) $wallabagUrl, '/'); $this->mimeTypes = new MimeTypes(); $this->setFolder(); @@ -101,10 +98,10 @@ class DownloadImages * - re-saved it (for security reason) * - return the new local path. * - * @param int $entryId ID of the entry - * @param string $imagePath Path to the image to retrieve - * @param string $url Url from where the image were found - * @param string $relativePath Relative local path to saved the image + * @param int $entryId ID of the entry + * @param string|null $imagePath Path to the image to retrieve + * @param string $url Url from where the image were found + * @param string $relativePath Relative local path to saved the image * * @return string|false Relative url to access the image from the web */ @@ -139,7 +136,7 @@ class DownloadImages } $ext = $this->getExtensionFromResponse($res, $imagePath); - if (false === $res) { + if (false === $ext) { return false; } @@ -174,7 +171,7 @@ class DownloadImages try { $im = imagecreatefromstring($res->getContent()); - } catch (\Exception $e) { + } catch (\Exception) { $im = false; } @@ -193,7 +190,7 @@ class DownloadImages $imagick->readImageBlob($res->getContent()); $imagick->setImageFormat('gif'); $imagick->writeImages($localPath, true); - } catch (\Exception $e) { + } catch (\Exception) { // if Imagick fail, fallback to the default solution imagegif($im, $localPath); } @@ -211,7 +208,7 @@ class DownloadImages case 'png': imagealphablending($im, false); imagesavealpha($im, true); - imagepng($im, $localPath, ceil(self::REGENERATE_PICTURES_QUALITY / 100 * 9)); + imagepng($im, $localPath, (int) ceil(self::REGENERATE_PICTURES_QUALITY / 100 * 9)); $this->logger->debug('DownloadImages: Re-creating png'); break; case 'webp': @@ -257,7 +254,7 @@ class DownloadImages */ public function getRelativePath($entryId, $createFolder = true) { - $hashId = hash('crc32', $entryId); + $hashId = hash('crc32', (string) $entryId); $relativePath = $hashId[0] . '/' . $hashId[1] . '/' . $hashId; $folderPath = $this->baseFolder . '/' . $relativePath; @@ -294,9 +291,7 @@ class DownloadImages preg_match_all($pattern, $srcsetAttribute, $matches); $srcset = \call_user_func_array('array_merge', $matches); - $srcsetUrls = array_map(function ($src) { - return trim(explode(' ', $src, 2)[0]); - }, $srcset); + $srcsetUrls = array_map(fn ($src) => trim(explode(' ', (string) $src, 2)[0]), $srcset); $urls = array_merge($srcsetUrls, $urls); } diff --git a/src/Helper/EntityTimestampsTrait.php b/src/Helper/EntityTimestampsTrait.php index a9a87cad5..7cc8c5d81 100644 --- a/src/Helper/EntityTimestampsTrait.php +++ b/src/Helper/EntityTimestampsTrait.php @@ -9,10 +9,8 @@ use Doctrine\ORM\Mapping as ORM; */ trait EntityTimestampsTrait { - /** - * @ORM\PrePersist - * @ORM\PreUpdate - */ + #[ORM\PrePersist] + #[ORM\PreUpdate] public function timestamps() { if (null === $this->createdAt) { diff --git a/src/Helper/EntriesExport.php b/src/Helper/EntriesExport.php index 695d7b268..30ecc94fb 100644 --- a/src/Helper/EntriesExport.php +++ b/src/Helper/EntriesExport.php @@ -19,10 +19,6 @@ use Wallabag\Entity\User; */ class EntriesExport { - private $wallabagUrl; - private $logoPath; - private $translator; - private $tokenStorage; private $title = ''; private $entries = []; private $author = 'wallabag'; @@ -34,12 +30,12 @@ class EntriesExport * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE * @param TokenStorageInterface $tokenStorage Needed to retrieve the current user */ - public function __construct(TranslatorInterface $translator, $wallabagUrl, $logoPath, TokenStorageInterface $tokenStorage) - { - $this->translator = $translator; - $this->wallabagUrl = $wallabagUrl; - $this->logoPath = $logoPath; - $this->tokenStorage = $tokenStorage; + public function __construct( + private readonly TranslatorInterface $translator, + private $wallabagUrl, + private $logoPath, + private readonly TokenStorageInterface $tokenStorage, + ) { } /** @@ -258,7 +254,7 @@ class EntriesExport // Do not add "Guide" chapter for "CoverPage" $book->setisReferencesAddedToToc(false); - return Response::create( + return new Response( $book->getBook(), 200, [ @@ -313,13 +309,13 @@ class EntriesExport '
' . $this->translator->trans('entry.metadata.added_on') . '
' . $entry->getCreatedAt()->format('Y-m-d') . '
' . '
' . $this->translator->trans('entry.metadata.address') . '
' . $entry->getUrl() . '
' . ''; - $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->writeHTMLCell(0, 0, null, null, $html, 0, 1); $pdf->AddPage(); $html = '

' . $entry->getTitle() . '

'; $html .= $entry->getContent(); - $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->writeHTMLCell(0, 0, null, null, $html, 0, 1); } /* @@ -328,12 +324,12 @@ class EntriesExport $pdf->AddPage(); $html = $this->getExportInformation('tcpdf'); - $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->writeHTMLCell(0, 0, null, null, $html, 0, 1); // set image scale factor $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); - return Response::create( + return new Response( $pdf->Output('', 'S'), 200, [ @@ -378,7 +374,7 @@ class EntriesExport $output = stream_get_contents($handle); fclose($handle); - return Response::create( + return new Response( $output, 200, [ @@ -394,7 +390,7 @@ class EntriesExport */ private function produceJson(): Response { - return Response::create( + return new Response( $this->prepareSerializingContent('json'), 200, [ @@ -410,7 +406,7 @@ class EntriesExport */ private function produceXml(): Response { - return Response::create( + return new Response( $this->prepareSerializingContent('xml'), 200, [ @@ -434,7 +430,7 @@ class EntriesExport $content .= $html->getText(); } - return Response::create( + return new Response( $content, 200, [ @@ -457,7 +453,7 @@ class EntriesExport $content .= $converter->convert('

' . $entry->getTitle() . '

' . $entry->getContent()); } - return Response::create( + return new Response( $content, 200, [ diff --git a/src/Helper/HttpClientFactory.php b/src/Helper/HttpClientFactory.php deleted file mode 100644 index 7a5f6a5a6..000000000 --- a/src/Helper/HttpClientFactory.php +++ /dev/null @@ -1,74 +0,0 @@ -cookieJar = $cookieJar; - $this->restrictedAccess = $restrictedAccess; - $this->logger = $logger; - } - - /** - * Adds a subscriber to the HTTP client. - */ - public function addSubscriber(SubscriberInterface $subscriber) - { - $this->subscribers[] = $subscriber; - } - - /** - * Input an array of configuration to be able to create a HttpClient. - * - * @return HttpClient - */ - public function createClient(array $config = []) - { - $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); - - if (0 === (int) $this->restrictedAccess) { - return new GuzzleAdapter(new GuzzleClient($config)); - } - - // we clear the cookie to avoid websites who use cookies for analytics - $this->cookieJar->clear(); - if (!isset($config['defaults']['cookies'])) { - // need to set the (shared) cookie jar - $config['defaults']['cookies'] = $this->cookieJar; - } - - $guzzle = new GuzzleClient($config); - foreach ($this->subscribers as $subscriber) { - $guzzle->getEmitter()->attach($subscriber); - } - - return new GuzzleAdapter($guzzle); - } -} diff --git a/src/Helper/PreparePagerForEntries.php b/src/Helper/PreparePagerForEntries.php index 2a7564f0e..fe0446e6a 100644 --- a/src/Helper/PreparePagerForEntries.php +++ b/src/Helper/PreparePagerForEntries.php @@ -10,11 +10,9 @@ use Wallabag\Entity\User; class PreparePagerForEntries { - private $tokenStorage; - - public function __construct(TokenStorageInterface $tokenStorage) - { - $this->tokenStorage = $tokenStorage; + public function __construct( + private readonly TokenStorageInterface $tokenStorage, + ) { } /** diff --git a/src/Helper/Redirect.php b/src/Helper/Redirect.php index 777951ac9..7ac2f2fc8 100644 --- a/src/Helper/Redirect.php +++ b/src/Helper/Redirect.php @@ -13,18 +13,15 @@ use Wallabag\Entity\User; */ class Redirect { - private $router; - private $tokenStorage; - - public function __construct(UrlGeneratorInterface $router, TokenStorageInterface $tokenStorage) - { - $this->router = $router; - $this->tokenStorage = $tokenStorage; + public function __construct( + private readonly UrlGeneratorInterface $router, + private readonly TokenStorageInterface $tokenStorage, + ) { } /** - * @param string $url URL to redirect - * @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read + * @param string|null $url URL to redirect + * @param bool $ignoreActionMarkAsRead Ignore configured action when mark as read * * @return string */ diff --git a/src/Helper/RuleBasedIgnoreOriginProcessor.php b/src/Helper/RuleBasedIgnoreOriginProcessor.php index a44390406..dc6ac5654 100644 --- a/src/Helper/RuleBasedIgnoreOriginProcessor.php +++ b/src/Helper/RuleBasedIgnoreOriginProcessor.php @@ -9,15 +9,11 @@ use Wallabag\Repository\IgnoreOriginInstanceRuleRepository; class RuleBasedIgnoreOriginProcessor { - protected $rulerz; - protected $logger; - protected $ignoreOriginInstanceRuleRepository; - - public function __construct(RulerZ $rulerz, LoggerInterface $logger, IgnoreOriginInstanceRuleRepository $ignoreOriginInstanceRuleRepository) - { - $this->rulerz = $rulerz; - $this->logger = $logger; - $this->ignoreOriginInstanceRuleRepository = $ignoreOriginInstanceRuleRepository; + public function __construct( + protected RulerZ $rulerz, + protected LoggerInterface $logger, + protected IgnoreOriginInstanceRuleRepository $ignoreOriginInstanceRuleRepository, + ) { } /** diff --git a/src/Helper/RuleBasedTagger.php b/src/Helper/RuleBasedTagger.php index 355394579..de5dbea20 100644 --- a/src/Helper/RuleBasedTagger.php +++ b/src/Helper/RuleBasedTagger.php @@ -14,17 +14,12 @@ use Wallabag\Repository\TagRepository; class RuleBasedTagger { - private $rulerz; - private $tagRepository; - private $entryRepository; - private $logger; - - public function __construct(RulerZ $rulerz, TagRepository $tagRepository, EntryRepository $entryRepository, LoggerInterface $logger) - { - $this->rulerz = $rulerz; - $this->tagRepository = $tagRepository; - $this->entryRepository = $entryRepository; - $this->logger = $logger; + public function __construct( + private readonly RulerZ $rulerz, + private readonly TagRepository $tagRepository, + private readonly EntryRepository $entryRepository, + private readonly LoggerInterface $logger, + ) { } /** @@ -134,7 +129,8 @@ class RuleBasedTagger private function fixEntry(Entry $entry) { $clonedEntry = clone $entry; - $clonedEntry->setReadingTime($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200); + $newReadingTime = (int) ($entry->getReadingTime() / $entry->getUser()->getConfig()->getReadingSpeed() * 200); + $clonedEntry->setReadingTime($newReadingTime); return $clonedEntry; } diff --git a/src/Helper/TagsAssigner.php b/src/Helper/TagsAssigner.php index d39683e30..470c53b86 100644 --- a/src/Helper/TagsAssigner.php +++ b/src/Helper/TagsAssigner.php @@ -8,14 +8,9 @@ use Wallabag\Repository\TagRepository; class TagsAssigner { - /** - * @var TagRepository - */ - protected $tagRepository; - - public function __construct(TagRepository $tagRepository) - { - $this->tagRepository = $tagRepository; + public function __construct( + protected TagRepository $tagRepository, + ) { } /** @@ -44,7 +39,7 @@ class TagsAssigner } foreach ($tags as $label) { - $label = trim(mb_convert_case($label, \MB_CASE_LOWER)); + $label = trim(mb_convert_case((string) $label, \MB_CASE_LOWER)); // avoid empty tag if ('' === $label) { diff --git a/src/HttpClient/Authenticator.php b/src/HttpClient/Authenticator.php new file mode 100644 index 000000000..40028e5ec --- /dev/null +++ b/src/HttpClient/Authenticator.php @@ -0,0 +1,89 @@ +logger = new NullLogger(); + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function loginIfRequired(string $url): bool + { + $config = $this->buildSiteConfig(new Uri($url)); + if (false === $config || !$config->requiresLogin()) { + $this->logger->debug('loginIfRequired> will not require login'); + + return false; + } + + if ($this->authenticator->isLoggedIn($config)) { + return false; + } + + $this->logger->debug('loginIfRequired> user is not logged in, attach authenticator'); + + $this->authenticator->login($config); + + return true; + } + + public function loginIfRequested(ResponseInterface $response): bool + { + $config = $this->buildSiteConfig(new Uri($response->getInfo('url'))); + if (false === $config || !$config->requiresLogin()) { + $this->logger->debug('loginIfRequested> will not require login'); + + return false; + } + + $body = $response->getContent(); + + if ('' === $body) { + $this->logger->debug('loginIfRequested> empty body, ignoring'); + + return false; + } + + $isLoginRequired = $this->authenticator->isLoginRequired($config, $body); + + $this->logger->debug('loginIfRequested> retry with login ' . ($isLoginRequired ? '' : 'not ') . 'required'); + + if (!$isLoginRequired) { + return false; + } + + $this->authenticator->login($config); + + return true; + } + + /** + * @return SiteConfig|false + */ + private function buildSiteConfig(UriInterface $uri) + { + return $this->configBuilder->buildForHost($uri->getHost()); + } +} diff --git a/src/HttpClient/WallabagClient.php b/src/HttpClient/WallabagClient.php new file mode 100644 index 000000000..0bf9947ae --- /dev/null +++ b/src/HttpClient/WallabagClient.php @@ -0,0 +1,84 @@ +httpClient = HttpClient::create([ + 'timeout' => 10, + ]); + } + + public function request(string $method, string $url, array $options = []): ResponseInterface + { + $this->logger->log('debug', 'Restricted access config enabled?', ['enabled' => (int) $this->restrictedAccess]); + + if (0 === (int) $this->restrictedAccess) { + return $this->httpClient->request($method, $url, $options); + } + + $login = $this->authenticator->loginIfRequired($url); + + if (!$login) { + return $this->httpClient->request($method, $url, $options); + } + + if (null !== $cookieHeader = $this->getCookieHeader($url)) { + $options['headers']['cookie'] = $cookieHeader; + } + + $response = $this->httpClient->request($method, $url, $options); + + $login = $this->authenticator->loginIfRequested($response); + + if (!$login) { + return $response; + } + + if (null !== $cookieHeader = $this->getCookieHeader($url)) { + $options['headers']['cookie'] = $cookieHeader; + } + + return $this->httpClient->request($method, $url, $options); + } + + public function stream($responses, ?float $timeout = null): ResponseStreamInterface + { + return $this->httpClient->stream($responses, $timeout); + } + + public function withOptions(array $options): HttpClientInterface + { + return new self($this->restrictedAccess, $this->browser, $this->authenticator, $this->logger); + } + + private function getCookieHeader(string $url): ?string + { + $cookies = []; + + foreach ($this->browser->getCookieJar()->allRawValues($url) as $name => $value) { + $cookies[] = $name . '=' . $value; + } + + if ([] === $cookies) { + return null; + } + + return implode('; ', $cookies); + } +} diff --git a/src/Import/AbstractImport.php b/src/Import/AbstractImport.php index 9ebe307dd..09d551028 100644 --- a/src/Import/AbstractImport.php +++ b/src/Import/AbstractImport.php @@ -7,7 +7,6 @@ use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Wallabag\Entity\Entry; -use Wallabag\Entity\Tag; use Wallabag\Entity\User; use Wallabag\Event\EntrySavedEvent; use Wallabag\Helper\ContentProxy; @@ -15,11 +14,6 @@ use Wallabag\Helper\TagsAssigner; abstract class AbstractImport implements ImportInterface { - protected $em; - protected $logger; - protected $contentProxy; - protected $tagsAssigner; - protected $eventDispatcher; protected $producer; protected $user; protected $markAsRead; @@ -28,16 +22,16 @@ abstract class AbstractImport implements ImportInterface protected $importedEntries = 0; protected $queuedEntries = 0; - public function __construct(EntityManagerInterface $em, ContentProxy $contentProxy, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger) - { - $this->em = $em; - $this->logger = $logger; - $this->contentProxy = $contentProxy; - $this->tagsAssigner = $tagsAssigner; - $this->eventDispatcher = $eventDispatcher; + public function __construct( + protected EntityManagerInterface $em, + protected ContentProxy $contentProxy, + protected TagsAssigner $tagsAssigner, + protected EventDispatcherInterface $eventDispatcher, + protected LoggerInterface $logger, + ) { } - public function setLogger(LoggerInterface $logger) + public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } @@ -65,7 +59,7 @@ abstract class AbstractImport implements ImportInterface * * @param bool $markAsRead */ - public function setMarkAsRead($markAsRead) + public function setMarkAsRead($markAsRead): static { $this->markAsRead = $markAsRead; @@ -101,6 +95,11 @@ abstract class AbstractImport implements ImportInterface ]; } + public function setFilepath($filepath): static + { + return $this; + } + /** * Parse one entry. * @@ -173,9 +172,7 @@ abstract class AbstractImport implements ImportInterface $entryToBeFlushed = []; - // clear only affected entities - $this->em->clear(Entry::class); - $this->em->clear(Tag::class); + $this->em->clear(); } ++$i; } diff --git a/src/Import/BrowserImport.php b/src/Import/BrowserImport.php index 2a640f67d..11be4f686 100644 --- a/src/Import/BrowserImport.php +++ b/src/Import/BrowserImport.php @@ -53,7 +53,7 @@ abstract class BrowserImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/ChromeImport.php b/src/Import/ChromeImport.php index 27ae96221..20333c269 100644 --- a/src/Import/ChromeImport.php +++ b/src/Import/ChromeImport.php @@ -39,7 +39,7 @@ class ChromeImport extends BrowserImport 'is_archived' => (int) $this->markAsRead, 'is_starred' => false, 'tags' => '', - 'created_at' => substr($entry['date_added'], 0, 10), + 'created_at' => substr((string) $entry['date_added'], 0, 10), ]; if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { diff --git a/src/Import/DeliciousImport.php b/src/Import/DeliciousImport.php index 4cf45a906..ccd6a3068 100644 --- a/src/Import/DeliciousImport.php +++ b/src/Import/DeliciousImport.php @@ -28,7 +28,7 @@ class DeliciousImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/FirefoxImport.php b/src/Import/FirefoxImport.php index 2dba50d6a..efd9d2052 100644 --- a/src/Import/FirefoxImport.php +++ b/src/Import/FirefoxImport.php @@ -39,7 +39,7 @@ class FirefoxImport extends BrowserImport 'is_archived' => (int) $this->markAsRead, 'is_starred' => false, 'tags' => '', - 'created_at' => substr($entry['dateAdded'], 0, 10), + 'created_at' => substr((string) $entry['dateAdded'], 0, 10), ]; if (\array_key_exists('tags', $entry) && '' !== $entry['tags']) { diff --git a/src/Import/HtmlImport.php b/src/Import/HtmlImport.php index 909ff9bc8..607fd5da9 100644 --- a/src/Import/HtmlImport.php +++ b/src/Import/HtmlImport.php @@ -40,13 +40,11 @@ abstract class HtmlImport extends AbstractImport return false; } - $entries = $hrefs->each(function (Crawler $node) { - return [ - 'url' => $node->attr('href'), - 'tags' => $node->attr('tags'), - 'created_at' => $node->attr('add_date'), - ]; - }); + $entries = $hrefs->each(fn (Crawler $node) => [ + 'url' => $node->attr('href'), + 'tags' => $node->attr('tags'), + 'created_at' => $node->attr('add_date'), + ]); if ($this->producer) { $this->parseEntriesForProducer($entries); @@ -64,7 +62,7 @@ abstract class HtmlImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/ImportInterface.php b/src/Import/ImportInterface.php index b243f5245..e4e16dbb7 100644 --- a/src/Import/ImportInterface.php +++ b/src/Import/ImportInterface.php @@ -3,6 +3,7 @@ namespace Wallabag\Import; use Psr\Log\LoggerAwareInterface; +use Wallabag\Entity\User; interface ImportInterface extends LoggerAwareInterface { @@ -42,4 +43,24 @@ interface ImportInterface extends LoggerAwareInterface * @return array */ public function getSummary(); + + /** + * Set current user. + * Could the current *connected* user or one retrieve by the consumer. + */ + public function setUser(User $user); + + /** + * Set file path to the json file. + * + * @param string $filepath + */ + public function setFilepath($filepath): static; + + /** + * Set whether articles must be all marked as read. + * + * @param bool $markAsRead + */ + public function setMarkAsRead($markAsRead): static; } diff --git a/src/Import/InstapaperImport.php b/src/Import/InstapaperImport.php index c7b3c2a39..e2db92cc2 100644 --- a/src/Import/InstapaperImport.php +++ b/src/Import/InstapaperImport.php @@ -28,7 +28,7 @@ class InstapaperImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/OmnivoreImport.php b/src/Import/OmnivoreImport.php index 1ac2a39b6..947c011ce 100644 --- a/src/Import/OmnivoreImport.php +++ b/src/Import/OmnivoreImport.php @@ -28,7 +28,7 @@ class OmnivoreImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/PinboardImport.php b/src/Import/PinboardImport.php index ad5310f20..d0e78ef42 100644 --- a/src/Import/PinboardImport.php +++ b/src/Import/PinboardImport.php @@ -28,7 +28,7 @@ class PinboardImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; @@ -95,7 +95,7 @@ class PinboardImport extends AbstractImport 'is_archived' => ('no' === $importedEntry['toread']) || $this->markAsRead, 'is_starred' => false, 'created_at' => $importedEntry['time'], - 'tags' => explode(' ', $importedEntry['tags']), + 'tags' => array_filter(explode(' ', (string) $importedEntry['tags'])), ]; $entry = new Entry($this->user); diff --git a/src/Import/PocketCsvImport.php b/src/Import/PocketCsvImport.php new file mode 100644 index 000000000..eaa293fcb --- /dev/null +++ b/src/Import/PocketCsvImport.php @@ -0,0 +1,133 @@ +filepath = $filepath; + + return $this; + } + + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + + public function import() + { + if (!$this->user) { + $this->logger->error('Pocket CSV Import: user is not defined'); + + return false; + } + + if (!file_exists($this->filepath) || !is_readable($this->filepath)) { + $this->logger->error('Pocket CSV Import: unable to read file', ['filepath' => $this->filepath]); + + return false; + } + + $entries = []; + $handle = fopen($this->filepath, 'r'); + while (false !== ($data = fgetcsv($handle, 10240))) { + if ('title' === $data[0]) { + continue; + } + + $entries[] = [ + 'url' => $data[1], + 'title' => $data[0], + 'is_archived' => 'archive' === $data[4], + 'created_at' => $data[2], + 'tags' => $data[3], + ]; + } + fclose($handle); + + if (empty($entries)) { + $this->logger->error('PocketCsvImport: no entries in imported file'); + + return false; + } + + if ($this->producer) { + $this->parseEntriesForProducer($entries); + + return true; + } + + $this->parseEntries($entries); + + return true; + } + + public function parseEntry(array $importedEntry) + { + $existingEntry = $this->em + ->getRepository(Entry::class) + ->findByUrlAndUserId($importedEntry['url'], $this->user->getId()); + + if (false !== $existingEntry) { + ++$this->skippedEntries; + + return null; + } + + $entry = new Entry($this->user); + $entry->setUrl($importedEntry['url']); + $entry->setTitle($importedEntry['title']); + + // update entry with content (in case fetching failed, the given entry will be return) + $this->fetchContent($entry, $importedEntry['url'], $importedEntry); + + if (!empty($importedEntry['tags'])) { + $tags = str_replace('|', ',', $importedEntry['tags']); + $this->tagsAssigner->assignTagsToEntry( + $entry, + $tags, + $this->em->getUnitOfWork()->getScheduledEntityInsertions() + ); + } + + $entry->updateArchived($importedEntry['is_archived']); + $entry->setCreatedAt(\DateTime::createFromFormat('U', $importedEntry['created_at'])); + + $this->em->persist($entry); + ++$this->importedEntries; + + return $entry; + } + + protected function setEntryAsRead(array $importedEntry) + { + $importedEntry['is_archived'] = 'archive'; + + return $importedEntry; + } +} diff --git a/src/Import/PocketHtmlImport.php b/src/Import/PocketHtmlImport.php index b1c7e3edb..08c8da1e2 100644 --- a/src/Import/PocketHtmlImport.php +++ b/src/Import/PocketHtmlImport.php @@ -56,13 +56,11 @@ class PocketHtmlImport extends HtmlImport return false; } - $entries = $hrefs->each(function (Crawler $node) { - return [ - 'url' => $node->attr('href'), - 'tags' => $node->attr('tags'), - 'created_at' => $node->attr('time_added'), - ]; - }); + $entries = $hrefs->each(fn (Crawler $node) => [ + 'url' => $node->attr('href'), + 'tags' => $node->attr('tags'), + 'created_at' => $node->attr('time_added'), + ]); if ($this->producer) { $this->parseEntriesForProducer($entries); diff --git a/src/Import/PocketImport.php b/src/Import/PocketImport.php index 24da0d97f..61772236f 100644 --- a/src/Import/PocketImport.php +++ b/src/Import/PocketImport.php @@ -9,7 +9,7 @@ use Wallabag\Entity\Entry; class PocketImport extends AbstractImport { - public const NB_ELEMENTS = 5000; + public const NB_ELEMENTS = 30; /** * @var HttpClientInterface */ diff --git a/src/Import/ReadabilityImport.php b/src/Import/ReadabilityImport.php index fa939f239..52614922b 100644 --- a/src/Import/ReadabilityImport.php +++ b/src/Import/ReadabilityImport.php @@ -28,7 +28,7 @@ class ReadabilityImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/WallabagImport.php b/src/Import/WallabagImport.php index f6dfc0650..6f6cb590b 100644 --- a/src/Import/WallabagImport.php +++ b/src/Import/WallabagImport.php @@ -67,7 +67,7 @@ abstract class WallabagImport extends AbstractImport * * @param string $filepath */ - public function setFilepath($filepath) + public function setFilepath($filepath): static { $this->filepath = $filepath; diff --git a/src/Import/WallabagV1Import.php b/src/Import/WallabagV1Import.php index 9463d4b95..2dfd7f404 100644 --- a/src/Import/WallabagV1Import.php +++ b/src/Import/WallabagV1Import.php @@ -10,14 +10,15 @@ use Wallabag\Helper\TagsAssigner; class WallabagV1Import extends WallabagImport { - protected $fetchingErrorMessage; - protected $fetchingErrorMessageTitle; - - public function __construct(EntityManagerInterface $em, ContentProxy $contentProxy, TagsAssigner $tagsAssigner, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger, $fetchingErrorMessageTitle, $fetchingErrorMessage) - { - $this->fetchingErrorMessageTitle = $fetchingErrorMessageTitle; - $this->fetchingErrorMessage = $fetchingErrorMessage; - + public function __construct( + EntityManagerInterface $em, + ContentProxy $contentProxy, + TagsAssigner $tagsAssigner, + EventDispatcherInterface $eventDispatcher, + LoggerInterface $logger, + protected $fetchingErrorMessageTitle, + protected $fetchingErrorMessage, + ) { parent::__construct($em, $contentProxy, $tagsAssigner, $eventDispatcher, $logger); } diff --git a/src/Mailer/AuthCodeMailer.php b/src/Mailer/AuthCodeMailer.php index 2a12c4d50..2626fc369 100644 --- a/src/Mailer/AuthCodeMailer.php +++ b/src/Mailer/AuthCodeMailer.php @@ -8,6 +8,7 @@ use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Twig\Environment; +use Wallabag\Entity\User; /** * Custom mailer for TwoFactorBundle email. @@ -16,61 +17,17 @@ use Twig\Environment; class AuthCodeMailer implements AuthCodeMailerInterface { /** - * Mailer. - * - * @var MailerInterface + * @param string $senderEmail sender email address + * @param string $senderName sender name + * @param string $supportUrl support URL to report any bugs */ - private $mailer; - - /** - * Twig to render the html's email. - * - * @var Environment - */ - private $twig; - - /** - * Sender email address. - * - * @var string - */ - private $senderEmail; - - /** - * Sender name. - * - * @var string - */ - private $senderName; - - /** - * Support URL to report any bugs. - * - * @var string - */ - private $supportUrl; - - /** - * Url for the wallabag instance (only used for image in the HTML email template). - * - * @var string - */ - private $wallabagUrl; - - /** - * @param string $senderEmail - * @param string $senderName - * @param string $supportUrl wallabag support url - * @param string $wallabagUrl wallabag instance url - */ - public function __construct(MailerInterface $mailer, Environment $twig, $senderEmail, $senderName, $supportUrl, $wallabagUrl) - { - $this->mailer = $mailer; - $this->twig = $twig; - $this->senderEmail = $senderEmail; - $this->senderName = $senderName; - $this->supportUrl = $supportUrl; - $this->wallabagUrl = $wallabagUrl; + public function __construct( + private readonly MailerInterface $mailer, + private readonly Environment $twig, + private $senderEmail, + private $senderName, + private $supportUrl, + ) { } /** @@ -78,6 +35,8 @@ class AuthCodeMailer implements AuthCodeMailerInterface */ public function sendAuthCode(TwoFactorInterface $user): void { + \assert($user instanceof User); + $template = $this->twig->load('TwoFactor/email_auth_code.html.twig'); $subject = $template->renderBlock('subject', []); @@ -85,7 +44,6 @@ class AuthCodeMailer implements AuthCodeMailerInterface 'user' => $user->getName(), 'code' => $user->getEmailAuthCode(), 'support_url' => $this->supportUrl, - 'wallabag_url' => $this->wallabagUrl, ]); $bodyText = $template->renderBlock('body_text', [ 'user' => $user->getName(), @@ -94,7 +52,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface ]); $email = (new Email()) - ->from(new Address($this->senderEmail, $this->senderName ?? $this->senderEmail)) + ->from(new Address($this->senderEmail, $this->senderName ?: $this->senderEmail)) ->to($user->getEmailAuthRecipient()) ->subject($subject) ->text($bodyText) diff --git a/src/Mailer/UserMailer.php b/src/Mailer/UserMailer.php deleted file mode 100644 index fa108c3e4..000000000 --- a/src/Mailer/UserMailer.php +++ /dev/null @@ -1,78 +0,0 @@ -|string, resetting: array|string}} - */ - protected $parameters; - - public function __construct(MailerInterface $mailer, UrlGeneratorInterface $router, Environment $twig, array $parameters) - { - $this->mailer = $mailer; - $this->router = $router; - $this->twig = $twig; - $this->parameters = $parameters; - } - - /** - * @param string $templateName - * @param array $context - * @param array $fromEmail - * @param string $toEmail - */ - protected function sendMessage($templateName, $context, $fromEmail, $toEmail) - { - $template = $this->twig->load($templateName); - $subject = $template->renderBlock('subject', $context); - $textBody = $template->renderBlock('body_text', $context); - - $htmlBody = ''; - - if ($template->hasBlock('body_html', $context)) { - $htmlBody = $template->renderBlock('body_html', $context); - } - - $email = (new Email()) - ->from(new Address(key($fromEmail), current($fromEmail))) - ->to($toEmail) - ->subject($subject); - - if (!empty($htmlBody)) { - $email - ->text($textBody) - ->html($htmlBody); - } else { - $email->text($textBody); - } - - $this->mailer->send($email); - } -} diff --git a/src/Operator/Doctrine/Matches.php b/src/Operator/Doctrine/Matches.php deleted file mode 100644 index 00e4d0b96..000000000 --- a/src/Operator/Doctrine/Matches.php +++ /dev/null @@ -1,25 +0,0 @@ - 0; } diff --git a/src/ParamConverter/UsernameFeedTokenConverter.php b/src/ParamConverter/UsernameFeedTokenConverter.php index 8001436d6..8e54e874c 100644 --- a/src/ParamConverter/UsernameFeedTokenConverter.php +++ b/src/ParamConverter/UsernameFeedTokenConverter.php @@ -18,14 +18,12 @@ use Wallabag\Repository\UserRepository; */ class UsernameFeedTokenConverter implements ParamConverterInterface { - private $registry; - /** * @param ManagerRegistry $registry Manager registry */ - public function __construct(?ManagerRegistry $registry = null) - { - $this->registry = $registry; + public function __construct( + private readonly ?ManagerRegistry $registry = null, + ) { } /** @@ -33,7 +31,7 @@ class UsernameFeedTokenConverter implements ParamConverterInterface * * Check, if object supported by our converter */ - public function supports(ParamConverter $configuration) + public function supports(ParamConverter $configuration): bool { // If there is no manager, this means that only Doctrine DBAL is configured // In this case we can do nothing and just return @@ -50,7 +48,7 @@ class UsernameFeedTokenConverter implements ParamConverterInterface $em = $this->registry->getManagerForClass($configuration->getClass()); // Check, if class name is what we need - if (null !== $em && 'Wallabag\Entity\User' !== $em->getClassMetadata($configuration->getClass())->getName()) { + if (null !== $em && User::class !== $em->getClassMetadata($configuration->getClass())->getName()) { return false; } @@ -65,7 +63,7 @@ class UsernameFeedTokenConverter implements ParamConverterInterface * @throws \InvalidArgumentException When route attributes are missing * @throws NotFoundHttpException When object not found */ - public function apply(Request $request, ParamConverter $configuration) + public function apply(Request $request, ParamConverter $configuration): bool { $username = $request->attributes->get('username'); $feedToken = $request->attributes->get('token'); diff --git a/src/Redis/Producer.php b/src/Redis/Producer.php index 9cc64aae3..1e8c487d1 100644 --- a/src/Redis/Producer.php +++ b/src/Redis/Producer.php @@ -15,11 +15,9 @@ use Simpleue\Queue\RedisQueue; */ class Producer implements ProducerInterface { - private $queue; - - public function __construct(RedisQueue $queue) - { - $this->queue = $queue; + public function __construct( + private readonly RedisQueue $queue, + ) { } /** diff --git a/src/Repository/AnnotationRepository.php b/src/Repository/AnnotationRepository.php index 6a762484d..524f8ebe6 100644 --- a/src/Repository/AnnotationRepository.php +++ b/src/Repository/AnnotationRepository.php @@ -148,6 +148,12 @@ class AnnotationRepository extends ServiceEntityRepository ->getResult(); } + public function getCountBuilderByUser($userId = null) + { + return $this->createQueryBuilder('e') + ->andWhere('e.user = :userId')->setParameter('userId', $userId); + } + /** * Return a query builder to used by other getBuilderFor* method. * diff --git a/src/Repository/Api/ClientRepository.php b/src/Repository/Api/ClientRepository.php index 28ed7389c..926f013db 100644 --- a/src/Repository/Api/ClientRepository.php +++ b/src/Repository/Api/ClientRepository.php @@ -16,7 +16,7 @@ class ClientRepository extends ServiceEntityRepository parent::__construct($registry, Client::class); } - public function findOneBy(array $criteria, ?array $orderBy = null) + public function findOneBy(array $criteria, ?array $orderBy = null): ?object { if (!empty($criteria['id'])) { // cast client id to be an integer to avoid postgres error: diff --git a/src/Repository/ConfigRepository.php b/src/Repository/ConfigRepository.php index 7c86994bf..60ec164b7 100644 --- a/src/Repository/ConfigRepository.php +++ b/src/Repository/ConfigRepository.php @@ -5,9 +5,10 @@ namespace Wallabag\Repository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; use Wallabag\Entity\Config; +use Wallabag\Entity\User; /** - * @method Config|null findOneByUser(int $userId) + * @method Config|null findOneByUser(User $user) */ class ConfigRepository extends ServiceEntityRepository { diff --git a/src/Repository/EntryRepository.php b/src/Repository/EntryRepository.php index d2c7b06a6..b60bafe84 100644 --- a/src/Repository/EntryRepository.php +++ b/src/Repository/EntryRepository.php @@ -13,7 +13,7 @@ use Wallabag\Entity\Tag; use Wallabag\Helper\UrlHasher; /** - * @method Entry[] findById(int $id) + * @method Entry[] findById(int[] $id) * @method Entry|null findOneByUser(int $userId) */ class EntryRepository extends ServiceEntityRepository @@ -171,9 +171,9 @@ class EntryRepository extends ServiceEntityRepository /** * Retrieves entries filtered with a search term for a user. * - * @param int $userId - * @param string $term - * @param string $currentRoute + * @param int $userId + * @param string $term + * @param 'starred'|'unread'|'homepage'|'archive'|null $currentRoute * * @return QueryBuilder */ @@ -228,21 +228,6 @@ class EntryRepository extends ServiceEntityRepository ; } - /** - * Retrieve entries with annotations count for a user. - * - * @param int $userId - * - * @return QueryBuilder - */ - public function getCountBuilderForAnnotationsByUser($userId) - { - return $this - ->getQueryBuilderByUser($userId) - ->innerJoin('e.annotations', 'a') - ; - } - /** * Retrieve untagged entries for a user. * @@ -283,16 +268,17 @@ class EntryRepository extends ServiceEntityRepository * @param string $order * @param int $since * @param string $tags - * @param string $detail 'metadata' or 'full'. Include content field if 'full' + * @param string $detail 'metadata' or 'full'. Include content field if 'full' * @param string $domainName * @param int $httpStatus * @param bool $isNotParsed + * @param bool $hasAnnotations * * @todo Breaking change: replace default detail=full by detail=metadata in a future version * * @return Pagerfanta */ - public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '', $isNotParsed = null, $httpStatus = null) + public function findEntries($userId, $isArchived = null, $isStarred = null, $isPublic = null, $sort = 'created', $order = 'asc', $since = 0, $tags = '', $detail = 'full', $domainName = '', $isNotParsed = null, $httpStatus = null, $hasAnnotations = null) { if (!\in_array(strtolower($detail), ['full', 'metadata'], true)) { throw new \Exception('Detail "' . $detail . '" parameter is wrong, allowed: full or metadata'); @@ -304,9 +290,7 @@ class EntryRepository extends ServiceEntityRepository if ('metadata' === $detail) { $fieldNames = $this->getClassMetadata()->getFieldNames(); - $fields = array_filter($fieldNames, function ($k) { - return 'content' !== $k; - }); + $fields = array_filter($fieldNames, fn ($k) => 'content' !== $k); $qb->select(\sprintf('partial e.{%s}', implode(',', $fields))); } @@ -359,6 +343,16 @@ class EntryRepository extends ServiceEntityRepository $qb->andWhere('e.domainName = :domainName')->setParameter('domainName', $domainName); } + if (null !== $hasAnnotations) { + if ($hasAnnotations) { + $qb->leftJoin('e.annotations', 'a') + ->andWhere('a.id IS NOT NULL'); + } else { + $qb->leftJoin('e.annotations', 'a') + ->andWhere('a.id IS NULL'); + } + } + if (!\in_array(strtolower($order), ['asc', 'desc'], true)) { throw new \Exception('Order "' . $order . '" parameter is wrong, allowed: asc or desc'); } diff --git a/src/Repository/SiteCredentialRepository.php b/src/Repository/SiteCredentialRepository.php index 086c23b2d..3a2d982aa 100644 --- a/src/Repository/SiteCredentialRepository.php +++ b/src/Repository/SiteCredentialRepository.php @@ -5,22 +5,21 @@ namespace Wallabag\Repository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; use Wallabag\Entity\SiteCredential; +use Wallabag\Entity\User; use Wallabag\Helper\CryptoProxy; /** * SiteCredentialRepository. * - * @method SiteCredential[] findByUser(int $userId) + * @method SiteCredential[] findByUser(User $user) */ class SiteCredentialRepository extends ServiceEntityRepository { - private $cryptoProxy; - - public function __construct(ManagerRegistry $registry, CryptoProxy $cryptoProxy) - { + public function __construct( + ManagerRegistry $registry, + private readonly CryptoProxy $cryptoProxy, + ) { parent::__construct($registry, SiteCredential::class); - - $this->cryptoProxy = $cryptoProxy; } /** diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 1eefcf307..7440bf3c0 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -3,6 +3,7 @@ namespace Wallabag\Repository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Wallabag\Entity\Tag; @@ -13,8 +14,10 @@ use Wallabag\Entity\Tag; */ class TagRepository extends ServiceEntityRepository { - public function __construct(ManagerRegistry $registry) - { + public function __construct( + ManagerRegistry $registry, + private readonly string $tablePrefix, + ) { parent::__construct($registry, Tag::class); } @@ -85,6 +88,36 @@ class TagRepository extends ServiceEntityRepository ->getArrayResult(); } + /** + * Find all tags per user with nb entries. + * + * @param int $userId + * + * @return array + */ + public function findAllTagsWithNbEntries($userId) + { + $rsm = new ResultSetMappingBuilder($this->getEntityManager()); + $rsm->addRootEntityFromClassMetadata(Tag::class, 't'); + $rsm->addEntityResult(Tag::class, 't', 'tag'); + $rsm->addScalarResult('nbEntries', 'nbEntries', 'integer'); + + $sql = <<generateSelectClause()}, count(e.id) as nbEntries + FROM {$this->tablePrefix}tag t + LEFT JOIN {$this->tablePrefix}entry_tag et ON et.tag_id = t.id + JOIN {$this->tablePrefix}entry e ON e.id = et.entry_id + WHERE e.user_id = :userId + GROUP BY t.id + ORDER BY t.label + SQL; + + $query = $this->getEntityManager()->createNativeQuery($sql, $rsm); + $query->setParameter('userId', $userId); + + return $query->getResult(); + } + public function findByLabelsAndUser($labels, $userId) { $qb = $this->getQueryBuilderByUser($userId) diff --git a/src/Security/Voter/AdminVoter.php b/src/Security/Voter/AdminVoter.php index f4d6dda68..4b96b75e7 100644 --- a/src/Security/Voter/AdminVoter.php +++ b/src/Security/Voter/AdminVoter.php @@ -14,11 +14,9 @@ class AdminVoter extends Voter public const LIST_IGNORE_ORIGIN_INSTANCE_RULES = 'LIST_IGNORE_ORIGIN_INSTANCE_RULES'; public const CREATE_IGNORE_ORIGIN_INSTANCE_RULES = 'CREATE_IGNORE_ORIGIN_INSTANCE_RULES'; - private Security $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private readonly Security $security, + ) { } protected function supports(string $attribute, $subject): bool @@ -38,14 +36,9 @@ class AdminVoter extends Voter return false; } - switch ($attribute) { - case self::LIST_USERS: - case self::CREATE_USERS: - case self::LIST_IGNORE_ORIGIN_INSTANCE_RULES: - case self::CREATE_IGNORE_ORIGIN_INSTANCE_RULES: - return $this->security->isGranted('ROLE_SUPER_ADMIN'); - } - - return false; + return match ($attribute) { + self::LIST_USERS, self::CREATE_USERS, self::LIST_IGNORE_ORIGIN_INSTANCE_RULES, self::CREATE_IGNORE_ORIGIN_INSTANCE_RULES => $this->security->isGranted('ROLE_SUPER_ADMIN'), + default => false, + }; } } diff --git a/src/Security/Voter/AnnotationVoter.php b/src/Security/Voter/AnnotationVoter.php new file mode 100644 index 000000000..a4b021de6 --- /dev/null +++ b/src/Security/Voter/AnnotationVoter.php @@ -0,0 +1,43 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::EDIT, self::DELETE => $subject->getUser() === $user, + default => false, + }; + } +} diff --git a/src/Security/Voter/EntryVoter.php b/src/Security/Voter/EntryVoter.php index 5a8f8a709..202dacb8c 100644 --- a/src/Security/Voter/EntryVoter.php +++ b/src/Security/Voter/EntryVoter.php @@ -16,7 +16,13 @@ class EntryVoter extends Voter public const ARCHIVE = 'ARCHIVE'; public const SHARE = 'SHARE'; public const UNSHARE = 'UNSHARE'; + public const EXPORT = 'EXPORT'; public const DELETE = 'DELETE'; + public const LIST_ANNOTATIONS = 'LIST_ANNOTATIONS'; + public const CREATE_ANNOTATIONS = 'CREATE_ANNOTATIONS'; + public const LIST_TAGS = 'LIST_TAGS'; + public const TAG = 'TAG'; + public const UNTAG = 'UNTAG'; protected function supports(string $attribute, $subject): bool { @@ -24,7 +30,7 @@ class EntryVoter extends Voter return false; } - if (!\in_array($attribute, [self::VIEW, self::EDIT, self::RELOAD, self::STAR, self::ARCHIVE, self::SHARE, self::UNSHARE, self::DELETE], true)) { + if (!\in_array($attribute, [self::VIEW, self::EDIT, self::RELOAD, self::STAR, self::ARCHIVE, self::SHARE, self::UNSHARE, self::EXPORT, self::DELETE, self::LIST_ANNOTATIONS, self::CREATE_ANNOTATIONS, self::LIST_TAGS, self::TAG, self::UNTAG], true)) { return false; } @@ -41,18 +47,9 @@ class EntryVoter extends Voter return false; } - switch ($attribute) { - case self::VIEW: - case self::EDIT: - case self::RELOAD: - case self::STAR: - case self::ARCHIVE: - case self::SHARE: - case self::UNSHARE: - case self::DELETE: - return $user === $subject->getUser(); - } - - return false; + return match ($attribute) { + self::VIEW, self::EDIT, self::RELOAD, self::STAR, self::ARCHIVE, self::SHARE, self::UNSHARE, self::EXPORT, self::DELETE, self::LIST_ANNOTATIONS, self::CREATE_ANNOTATIONS, self::LIST_TAGS, self::TAG, self::UNTAG => $user === $subject->getUser(), + default => false, + }; } } diff --git a/src/Security/Voter/IgnoreOriginInstanceRuleVoter.php b/src/Security/Voter/IgnoreOriginInstanceRuleVoter.php index d8ccf88e6..8035aed28 100644 --- a/src/Security/Voter/IgnoreOriginInstanceRuleVoter.php +++ b/src/Security/Voter/IgnoreOriginInstanceRuleVoter.php @@ -12,11 +12,9 @@ class IgnoreOriginInstanceRuleVoter extends Voter public const EDIT = 'EDIT'; public const DELETE = 'DELETE'; - private Security $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private readonly Security $security, + ) { } protected function supports(string $attribute, $subject): bool @@ -34,12 +32,9 @@ class IgnoreOriginInstanceRuleVoter extends Voter protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - switch ($attribute) { - case self::EDIT: - case self::DELETE: - return $this->security->isGranted('ROLE_SUPER_ADMIN'); - } - - return false; + return match ($attribute) { + self::EDIT, self::DELETE => $this->security->isGranted('ROLE_SUPER_ADMIN'), + default => false, + }; } } diff --git a/src/Security/Voter/IgnoreOriginUserRuleVoter.php b/src/Security/Voter/IgnoreOriginUserRuleVoter.php new file mode 100644 index 000000000..d6f9b15b6 --- /dev/null +++ b/src/Security/Voter/IgnoreOriginUserRuleVoter.php @@ -0,0 +1,43 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::EDIT, self::DELETE => $subject->getConfig()->getUser() === $user, + default => false, + }; + } +} diff --git a/src/Security/Voter/MainVoter.php b/src/Security/Voter/MainVoter.php index b036cc0cb..478a08099 100644 --- a/src/Security/Voter/MainVoter.php +++ b/src/Security/Voter/MainVoter.php @@ -11,14 +11,19 @@ class MainVoter extends Voter public const LIST_ENTRIES = 'LIST_ENTRIES'; public const CREATE_ENTRIES = 'CREATE_ENTRIES'; public const EDIT_ENTRIES = 'EDIT_ENTRIES'; + public const EXPORT_ENTRIES = 'EXPORT_ENTRIES'; + public const IMPORT_ENTRIES = 'IMPORT_ENTRIES'; + public const DELETE_ENTRIES = 'DELETE_ENTRIES'; + public const LIST_TAGS = 'LIST_TAGS'; + public const CREATE_TAGS = 'CREATE_TAGS'; + public const DELETE_TAGS = 'DELETE_TAGS'; public const LIST_SITE_CREDENTIALS = 'LIST_SITE_CREDENTIALS'; public const CREATE_SITE_CREDENTIALS = 'CREATE_SITE_CREDENTIALS'; + public const EDIT_CONFIG = 'EDIT_CONFIG'; - private Security $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private readonly Security $security, + ) { } protected function supports(string $attribute, $subject): bool @@ -27,7 +32,7 @@ class MainVoter extends Voter return false; } - if (!\in_array($attribute, [self::LIST_ENTRIES, self::CREATE_ENTRIES, self::EDIT_ENTRIES, self::LIST_SITE_CREDENTIALS, self::CREATE_SITE_CREDENTIALS], true)) { + if (!\in_array($attribute, [self::LIST_ENTRIES, self::CREATE_ENTRIES, self::EDIT_ENTRIES, self::EXPORT_ENTRIES, self::IMPORT_ENTRIES, self::DELETE_ENTRIES, self::LIST_TAGS, self::CREATE_TAGS, self::DELETE_TAGS, self::LIST_SITE_CREDENTIALS, self::CREATE_SITE_CREDENTIALS, self::EDIT_CONFIG], true)) { return false; } @@ -36,15 +41,9 @@ class MainVoter extends Voter protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - switch ($attribute) { - case self::LIST_ENTRIES: - case self::CREATE_ENTRIES: - case self::EDIT_ENTRIES: - case self::LIST_SITE_CREDENTIALS: - case self::CREATE_SITE_CREDENTIALS: - return $this->security->isGranted('ROLE_USER'); - } - - return false; + return match ($attribute) { + self::LIST_ENTRIES, self::CREATE_ENTRIES, self::EDIT_ENTRIES, self::EXPORT_ENTRIES, self::IMPORT_ENTRIES, self::DELETE_ENTRIES, self::LIST_TAGS, self::CREATE_TAGS, self::DELETE_TAGS, self::LIST_SITE_CREDENTIALS, self::CREATE_SITE_CREDENTIALS, self::EDIT_CONFIG => $this->security->isGranted('ROLE_USER'), + default => false, + }; } } diff --git a/src/Security/Voter/SiteCredentialVoter.php b/src/Security/Voter/SiteCredentialVoter.php index 35ab28484..89c6982d4 100644 --- a/src/Security/Voter/SiteCredentialVoter.php +++ b/src/Security/Voter/SiteCredentialVoter.php @@ -35,12 +35,9 @@ class SiteCredentialVoter extends Voter return false; } - switch ($attribute) { - case self::EDIT: - case self::DELETE: - return $user === $subject->getUser(); - } - - return false; + return match ($attribute) { + self::EDIT, self::DELETE => $user === $subject->getUser(), + default => false, + }; } } diff --git a/src/Security/Voter/TagVoter.php b/src/Security/Voter/TagVoter.php new file mode 100644 index 000000000..dccdde979 --- /dev/null +++ b/src/Security/Voter/TagVoter.php @@ -0,0 +1,50 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::VIEW, self::EDIT, self::DELETE => $this->security->isGranted('ROLE_USER'), + default => false, + }; + } +} diff --git a/src/Security/Voter/TaggingRuleVoter.php b/src/Security/Voter/TaggingRuleVoter.php new file mode 100644 index 000000000..fc98ac66b --- /dev/null +++ b/src/Security/Voter/TaggingRuleVoter.php @@ -0,0 +1,43 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::EDIT, self::DELETE => $subject->getConfig()->getUser() === $user, + default => false, + }; + } +} diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index 876f29aff..8d111eeea 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -12,11 +12,9 @@ class UserVoter extends Voter public const EDIT = 'EDIT'; public const DELETE = 'DELETE'; - private Security $security; - - public function __construct(Security $security) - { - $this->security = $security; + public function __construct( + private readonly Security $security, + ) { } protected function supports(string $attribute, $subject): bool diff --git a/src/SiteConfig/ArraySiteConfigBuilder.php b/src/SiteConfig/ArraySiteConfigBuilder.php index d150650c4..c6d20a501 100644 --- a/src/SiteConfig/ArraySiteConfigBuilder.php +++ b/src/SiteConfig/ArraySiteConfigBuilder.php @@ -21,14 +21,10 @@ class ArraySiteConfigBuilder implements SiteConfigBuilder { $host = strtolower($host); - if ('www.' === substr($host, 0, 4)) { + if (str_starts_with($host, 'www.')) { $host = substr($host, 4); } - if (isset($this->configs[$host])) { - return $this->configs[$host]; - } - - return false; + return $this->configs[$host] ?? false; } } diff --git a/src/SiteConfig/GrabySiteConfigBuilder.php b/src/SiteConfig/GrabySiteConfigBuilder.php index 5c501f92c..91b143402 100644 --- a/src/SiteConfig/GrabySiteConfigBuilder.php +++ b/src/SiteConfig/GrabySiteConfigBuilder.php @@ -9,35 +9,15 @@ use Wallabag\Repository\SiteCredentialRepository; class GrabySiteConfigBuilder implements SiteConfigBuilder { - /** - * @var ConfigBuilder - */ - private $grabyConfigBuilder; - - /** - * @var SiteCredentialRepository - */ - private $credentialRepository; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var TokenStorageInterface - */ - private $token; - /** * GrabySiteConfigBuilder constructor. */ - public function __construct(ConfigBuilder $grabyConfigBuilder, TokenStorageInterface $token, SiteCredentialRepository $credentialRepository, LoggerInterface $logger) - { - $this->grabyConfigBuilder = $grabyConfigBuilder; - $this->credentialRepository = $credentialRepository; - $this->logger = $logger; - $this->token = $token; + public function __construct( + private readonly ConfigBuilder $grabyConfigBuilder, + private readonly TokenStorageInterface $token, + private readonly SiteCredentialRepository $credentialRepository, + private readonly LoggerInterface $logger, + ) { } public function buildForHost($host) @@ -46,7 +26,7 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder // required by credentials below $host = strtolower($host); - if ('www.' === substr($host, 0, 4)) { + if (str_starts_with($host, 'www.')) { $host = substr($host, 4); } @@ -85,6 +65,7 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder 'notLoggedInXpath' => $config->not_logged_in_xpath ?: null, 'username' => $credentials['username'], 'password' => $credentials['password'], + 'httpHeaders' => $config->http_header, ]; $config = new SiteConfig($parameters); @@ -114,11 +95,11 @@ class GrabySiteConfigBuilder implements SiteConfigBuilder $extraFields = []; foreach ($extraFieldsStrings as $extraField) { - if (!str_contains($extraField, '=')) { + if (!str_contains((string) $extraField, '=')) { continue; } - list($fieldName, $fieldValue) = explode('=', $extraField, 2); + [$fieldName, $fieldValue] = explode('=', (string) $extraField, 2); $extraFields[$fieldName] = $fieldValue; } diff --git a/src/SiteConfig/LoginFormAuthenticator.php b/src/SiteConfig/LoginFormAuthenticator.php index b3ebbd5d1..5afd89648 100644 --- a/src/SiteConfig/LoginFormAuthenticator.php +++ b/src/SiteConfig/LoginFormAuthenticator.php @@ -2,18 +2,19 @@ namespace Wallabag\SiteConfig; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Cookie\CookieJar; +use Symfony\Component\BrowserKit\HttpBrowser; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Wallabag\ExpressionLanguage\AuthenticatorProvider; class LoginFormAuthenticator { - private ExpressionLanguage $expressionLanguage; + private readonly ExpressionLanguage $expressionLanguage; - public function __construct(AuthenticatorProvider $authenticatorProvider) - { + public function __construct( + private readonly HttpBrowser $browser, + AuthenticatorProvider $authenticatorProvider, + ) { $this->expressionLanguage = new ExpressionLanguage(null, [$authenticatorProvider]); } @@ -22,17 +23,14 @@ class LoginFormAuthenticator * * @return self */ - public function login(SiteConfig $siteConfig, ClientInterface $guzzle) + public function login(SiteConfig $siteConfig) { $postFields = [ $siteConfig->getUsernameField() => $siteConfig->getUsername(), $siteConfig->getPasswordField() => $siteConfig->getPassword(), ] + $this->getExtraFields($siteConfig); - $guzzle->post( - $siteConfig->getLoginUri(), - ['body' => $postFields, 'allow_redirects' => true, 'verify' => false] - ); + $this->browser->request('POST', $siteConfig->getLoginUri(), $postFields, [], $this->getHttpHeaders($siteConfig)); return $this; } @@ -42,15 +40,12 @@ class LoginFormAuthenticator * * @return bool */ - public function isLoggedIn(SiteConfig $siteConfig, ClientInterface $guzzle) + public function isLoggedIn(SiteConfig $siteConfig) { - if (($cookieJar = $guzzle->getDefaultOption('cookies')) instanceof CookieJar) { - /** @var \GuzzleHttp\Cookie\SetCookie $cookie */ - foreach ($cookieJar as $cookie) { - // check required cookies - if ($cookie->getDomain() === $siteConfig->getHost()) { - return true; - } + foreach ($this->browser->getCookieJar()->all() as $cookie) { + // check required cookies + if ($cookie->getDomain() === $siteConfig->getHost()) { + return true; } } @@ -71,13 +66,27 @@ class LoginFormAuthenticator $crawler = new Crawler((string) $html); $loggedIn = $crawler->evaluate((string) $siteConfig->getNotLoggedInXpath()); - } catch (\Throwable $e) { + } catch (\Throwable) { return false; } return \count($loggedIn) > 0; } + /** + * Processes http_header(*) config, prepending HTTP_ string to the header's name. + * See : https://github.com/symfony/browser-kit/blob/5.4/AbstractBrowser.php#L349. + */ + protected function getHttpHeaders(SiteConfig $siteConfig): array + { + $headers = []; + foreach ($siteConfig->getHttpHeaders() as $headerName => $headerValue) { + $headers["HTTP_$headerName"] = $headerValue; + } + + return $headers; + } + /** * Returns extra fields from the configuration. * Evaluates any field value that is an expression language string. @@ -89,9 +98,9 @@ class LoginFormAuthenticator $extraFields = []; foreach ($siteConfig->getExtraFields() as $fieldName => $fieldValue) { - if ('@=' === substr($fieldValue, 0, 2)) { + if (str_starts_with((string) $fieldValue, '@=')) { $fieldValue = $this->expressionLanguage->evaluate( - substr($fieldValue, 2), + substr((string) $fieldValue, 2), [ 'config' => $siteConfig, ] diff --git a/src/SiteConfig/SiteConfig.php b/src/SiteConfig/SiteConfig.php index 4e6c8912e..f2254291e 100644 --- a/src/SiteConfig/SiteConfig.php +++ b/src/SiteConfig/SiteConfig.php @@ -70,6 +70,13 @@ class SiteConfig */ protected $password; + /** + * Associative array of HTTP headers to send with the form. + * + * @var array + */ + protected $httpHeaders = []; + /** * SiteConfig constructor. Sets the properties by name given a hash. * @@ -260,4 +267,16 @@ class SiteConfig return $this; } + + public function getHttpHeaders(): array + { + return $this->httpHeaders; + } + + public function setHttpHeaders(array $httpHeaders): self + { + $this->httpHeaders = $httpHeaders; + + return $this; + } } diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index f45bcec45..65cb8d7ee 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -24,10 +24,10 @@ class Utils * * @param string $text * - * @return float + * @return int */ public static function getReadingTime($text) { - return floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200); + return (int) floor(\count(preg_split('~([^\p{L}\p{N}\']+|(\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}){1,2})~u', strip_tags($text))) / 200); } } diff --git a/src/Twig/WallabagExtension.php b/src/Twig/WallabagExtension.php index 5b37a4408..604b5c526 100644 --- a/src/Twig/WallabagExtension.php +++ b/src/Twig/WallabagExtension.php @@ -9,26 +9,21 @@ use Twig\Extension\GlobalsInterface; use Twig\TwigFilter; use Twig\TwigFunction; use Wallabag\Entity\User; +use Wallabag\Repository\AnnotationRepository; use Wallabag\Repository\EntryRepository; use Wallabag\Repository\TagRepository; class WallabagExtension extends AbstractExtension implements GlobalsInterface { - private $tokenStorage; - private $entryRepository; - private $tagRepository; - private $lifeTime; - private $translator; - private $projectDir; - - public function __construct(EntryRepository $entryRepository, TagRepository $tagRepository, TokenStorageInterface $tokenStorage, $lifeTime, TranslatorInterface $translator, string $projectDir) - { - $this->entryRepository = $entryRepository; - $this->tagRepository = $tagRepository; - $this->tokenStorage = $tokenStorage; - $this->lifeTime = $lifeTime; - $this->translator = $translator; - $this->projectDir = $projectDir; + public function __construct( + private readonly EntryRepository $entryRepository, + private readonly AnnotationRepository $annotationRepository, + private readonly TagRepository $tagRepository, + private readonly TokenStorageInterface $tokenStorage, + private $lifeTime, + private readonly TranslatorInterface $translator, + private readonly string $projectDir, + ) { } public function getGlobals(): array @@ -36,23 +31,23 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface return []; } - public function getFilters() + public function getFilters(): array { return [ - new TwigFilter('removeWww', [$this, 'removeWww']), - new TwigFilter('removeScheme', [$this, 'removeScheme']), - new TwigFilter('removeSchemeAndWww', [$this, 'removeSchemeAndWww']), + new TwigFilter('removeWww', $this->removeWww(...)), + new TwigFilter('removeScheme', $this->removeScheme(...)), + new TwigFilter('removeSchemeAndWww', $this->removeSchemeAndWww(...)), ]; } - public function getFunctions() + public function getFunctions(): array { return [ - new TwigFunction('count_entries', [$this, 'countEntries']), - new TwigFunction('count_tags', [$this, 'countTags']), - new TwigFunction('display_stats', [$this, 'displayStats']), - new TwigFunction('asset_file_exists', [$this, 'assetFileExists']), - new TwigFunction('theme_class', [$this, 'themeClass']), + new TwigFunction('count_entries', $this->countEntries(...)), + new TwigFunction('count_tags', $this->countTags(...)), + new TwigFunction('display_stats', $this->displayStats(...)), + new TwigFunction('asset_file_exists', $this->assetFileExists(...)), + new TwigFunction('theme_class', $this->themeClass(...)), ]; } @@ -94,30 +89,16 @@ class WallabagExtension extends AbstractExtension implements GlobalsInterface return 0; } - switch ($type) { - case 'starred': - $qb = $this->entryRepository->getCountBuilderForStarredByUser($user->getId()); - break; - case 'archive': - $qb = $this->entryRepository->getCountBuilderForArchiveByUser($user->getId()); - break; - case 'unread': - $qb = $this->entryRepository->getCountBuilderForUnreadByUser($user->getId()); - break; - case 'annotated': - $qb = $this->entryRepository->getCountBuilderForAnnotationsByUser($user->getId()); - break; - case 'all': - $qb = $this->entryRepository->getCountBuilderForAllByUser($user->getId()); - break; - default: - throw new \InvalidArgumentException(\sprintf('Type "%s" is not implemented.', $type)); - } - - $query = $qb - ->select('COUNT(e.id)') - ->getQuery(); + $qb = match ($type) { + 'starred' => $this->entryRepository->getCountBuilderForStarredByUser($user->getId())->select('COUNT(e.id)'), + 'archive' => $this->entryRepository->getCountBuilderForArchiveByUser($user->getId())->select('COUNT(e.id)'), + 'unread' => $this->entryRepository->getCountBuilderForUnreadByUser($user->getId())->select('COUNT(e.id)'), + 'annotated' => $this->annotationRepository->getCountBuilderByUser($user->getId())->select('COUNT(DISTINCT e.entry)'), + 'all' => $this->entryRepository->getCountBuilderForAllByUser($user->getId())->select('COUNT(e.id)'), + default => throw new \InvalidArgumentException(\sprintf('Type "%s" is not implemented.', $type)), + }; + $query = $qb->getQuery(); $query->useQueryCache(true); $query->enableResultCache($this->lifeTime); diff --git a/templates/Authentication/form.html.twig b/templates/Authentication/form.html.twig index e3d1f2f6a..e6c896ca5 100644 --- a/templates/Authentication/form.html.twig +++ b/templates/Authentication/form.html.twig @@ -21,9 +21,11 @@ {% if displayTrustedOption %} -
- - +
+
{% endif %}
diff --git a/templates/Config/index.html.twig b/templates/Config/index.html.twig index 20e78e785..8abeaf15a 100644 --- a/templates/Config/index.html.twig +++ b/templates/Config/index.html.twig @@ -10,7 +10,7 @@
-
+
{{ form_start(form.config) }} {{ form_errors(form.config) }} @@ -32,7 +32,7 @@ {{ form_label(form.config.items_per_page) }}
@@ -41,11 +41,13 @@
{{ form_errors(form.config.display_thumbnails) }} - {{ form_widget(form.config.display_thumbnails) }} - {{ form_label(form.config.display_thumbnails, null, {'label_attr': {'class': 'settings-checkbox-label'}}) }} +
@@ -62,14 +64,14 @@

-
+
{{ form_errors(form.config.action_mark_as_read) }} {{ form_widget(form.config.action_mark_as_read) }} {{ form_label(form.config.action_mark_as_read) }} @@ -77,13 +79,13 @@
-
+
{{ form_errors(form.config.language) }} {{ form_widget(form.config.language) }} {{ form_label(form.config.language) }}
@@ -100,7 +102,7 @@

@@ -110,34 +112,31 @@
{{ 'config.form_settings.android_configuration'|trans }}
{{ 'config.form_settings.android_instruction'|trans }} - {{ 'config.otp.app.qrcode_label'|trans }} + {{ 'config.otp.app.qrcode_label'|trans }}
-
{{ 'config.tab_menu.article_display'|trans }}
-
+
{{ form_errors(form.config.font) }} - {{ form_widget(form.config.font) }} + {{ form_widget(form.config.font, {attr: {'data-config-target': 'font', 'data-action': 'config#updatePreview'}}) }} {{ form_label(form.config.font) }}
{{ form_errors(form.config.lineHeight) }} - {{ form_widget(form.config.lineHeight) }} + {{ form_widget(form.config.lineHeight, {attr: {'data-config-target': 'lineHeight', 'data-action': 'config#updatePreview'}}) }} {{ form_label(form.config.lineHeight, null, {'label_attr': {'class': 'settings-range-label'}}) }}
@@ -146,22 +145,22 @@
{{ form_errors(form.config.fontsize) }} - {{ form_widget(form.config.fontsize) }} + {{ form_widget(form.config.fontsize, {attr: {'data-config-target': 'fontSize', 'data-action': 'config#updatePreview'}}) }} {{ form_label(form.config.fontsize, null, {'label_attr': {'class': 'settings-range-label'}}) }}
{{ form_errors(form.config.maxWidth) }} - {{ form_widget(form.config.maxWidth) }} + {{ form_widget(form.config.maxWidth, {attr: {'data-config-target': 'maxWidth', 'data-action': 'config#updatePreview'}}) }} {{ form_label(form.config.maxWidth, null, {'label_attr': {'class': 'settings-range-label'}}) }}
@@ -169,8 +168,8 @@
-
-
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
@@ -183,48 +182,63 @@
+
+
+ {{ 'config.form_feed.description'|trans }} +
+
+ +
+
+
{{ 'config.form_feed.token_label'|trans }}
+
+ {% if feed.token %} + {{ feed.token }} + {% else %} + {{ 'config.form_feed.no_token'|trans }} + {% endif %} + + {% if feed.token %} + – +
+ + + +
+ – +
+ + + +
+ {% else %} + – +
+ + + +
+ {% endif %} +
+
+
+ {% if feed.token %} + + {% endif %} + {{ form_start(form.feed) }} {{ form_errors(form.feed) }} -
-
- {{ 'config.form_feed.description'|trans }} -
-
- -
-
-
{{ 'config.form_feed.token_label'|trans }}
-
- {% if feed.token %} - {{ feed.token }} - {% else %} - {{ 'config.form_feed.no_token'|trans }} - {% endif %} - - {% if feed.token %} - – {{ 'config.form_feed.token_reset'|trans }} - – {{ 'config.form_feed.token_revoke'|trans }} - {% else %} - – {{ 'config.form_feed.token_create'|trans }} - {% endif %} -
-
-
- {% if feed.token %} - - {% endif %} -
{{ form_label(form.feed.feed_limit) }} @@ -382,12 +396,16 @@ {{ 'config.form_rules.then_tag_as_label'|trans }} « {{ tagging_rule.tags|join(', ') }} » - + mode_edit - - delete - +
+ + + +
{% endfor %} @@ -562,12 +580,16 @@
  • {{ 'config.form_rules.if_label'|trans }} « {{ ignore_origin_rule.rule }} » - + mode_edit - - delete - +
    + + + +
  • {% endfor %} diff --git a/templates/Config/otp_app.html.twig b/templates/Config/otp_app.html.twig index c1808b943..96df3955a 100644 --- a/templates/Config/otp_app.html.twig +++ b/templates/Config/otp_app.html.twig @@ -15,20 +15,17 @@

    {{ 'config.otp.app.two_factor_code_description_2'|trans }}

    - - +

    -

    - {{ 'config.otp.app.two_factor_code_description_5'|trans }}

    {{ secret }}
    -

    +
    + {{ 'config.otp.app.two_factor_code_description_5'|trans }}
    {{ secret }}
    +
  • {{ 'config.otp.app.two_factor_code_description_3'|trans }}

    -

    {{ backupCodes|join("\n") }}

    +
    {{ backupCodes|join("\n") }}
  • {{ 'config.otp.app.two_factor_code_description_4'|trans }}

    @@ -39,13 +36,13 @@
  • {% endfor %} -
    +
    - +
    diff --git a/templates/Developer/client_parameters.html.twig b/templates/Developer/client_parameters.html.twig index b17d3ea34..70fe3943d 100644 --- a/templates/Developer/client_parameters.html.twig +++ b/templates/Developer/client_parameters.html.twig @@ -18,14 +18,14 @@ {{ 'developer.client_parameter.field_id'|trans }} {{ client_id }} - + {{ 'developer.client_parameter.field_secret'|trans }} {{ client_secret }} - + diff --git a/templates/Developer/howto_app.html.twig b/templates/Developer/howto_app.html.twig index d4795d5c4..f2d9a6ac2 100644 --- a/templates/Developer/howto_app.html.twig +++ b/templates/Developer/howto_app.html.twig @@ -12,16 +12,16 @@

    {{ 'developer.howto.description.paragraph_2'|trans }}

    {{ 'developer.howto.description.paragraph_3'|trans({'%link%': path('developer_create_client')})|raw }}

    {{ 'developer.howto.description.paragraph_4'|trans }}

    -

    +

    http POST {{ wallabag_url }}/oauth/v2/token \
         grant_type=password \
         client_id=12_5um6nz50ceg4088c0840wwc0kgg44g00kk84og044ggkscso0k \
         client_secret=3qd12zpeaxes8cwg8c0404g888co4wo8kc4gcw0occww8cgw4k \
         username=yourUsername \
         password=yourPassw0rd
    -

    +

    {{ 'developer.howto.description.paragraph_5'|trans }}

    -

    +

    HTTP/1.1 200 OK
     Cache-Control: no-store, private
     Connection: close
    @@ -39,12 +39,12 @@ X-Powered-By: PHP/5.5.9-1ubuntu4.13
         "scope": null,
         "token_type": "bearer"
     }
    -

    +

    {{ 'developer.howto.description.paragraph_6'|trans }}

    -

    +

    http GET {{ wallabag_url }}/api/entries.json \
         "Authorization:Bearer ZWFjNjA3ZWMwYWVmYzRkYTBlMmQ3NTllYmVhOGJiZDE0ZTg1NjE4MjczOTVlNzM0ZTRlMWQ0MmRlMmYwNTk5Mw"
    -

    +

    {{ 'developer.howto.description.paragraph_7'|trans }}

    {{ 'developer.howto.description.paragraph_8'|trans({'%link%': path('nelmio_api_doc.swagger_ui')})|raw }}

    {{ 'developer.howto.back'|trans }}

    diff --git a/templates/Developer/index.html.twig b/templates/Developer/index.html.twig index fa437d0ae..8ca4767fb 100644 --- a/templates/Developer/index.html.twig +++ b/templates/Developer/index.html.twig @@ -25,7 +25,7 @@

    {{ 'developer.existing_clients.title'|trans }}

    {% if clients %} -
      +
        {% for client in clients %}
      • {{ client.name }} - #{{ client.id }}
        @@ -35,14 +35,14 @@ {{ 'developer.existing_clients.field_id'|trans }} {{ client.clientId }} - + {{ 'developer.existing_clients.field_secret'|trans }} {{ client.secret }} - + diff --git a/templates/Entry/Card/_content.html.twig b/templates/Entry/Card/_content.html.twig index 2d3088368..a34f9507e 100644 --- a/templates/Entry/Card/_content.html.twig +++ b/templates/Entry/Card/_content.html.twig @@ -4,10 +4,10 @@ {% endif %} {% if is_granted('VIEW', entry) %} - {{ entry.title|striptags|u.truncate(80, '…', false)|default('entry.default_title'|trans)|raw }} + {{ entry.title|striptags|default('entry.default_title'|trans)|raw }} {% else %} - {{ entry.title|striptags|u.truncate(80, '…', false)|default('entry.default_title'|trans)|raw }} + {{ entry.title|striptags|default('entry.default_title'|trans)|raw }} {% endif %}
        diff --git a/templates/Entry/Card/_mass_checkbox.html.twig b/templates/Entry/Card/_mass_checkbox.html.twig index 5e4fe8f6d..dbd1d80b8 100644 --- a/templates/Entry/Card/_mass_checkbox.html.twig +++ b/templates/Entry/Card/_mass_checkbox.html.twig @@ -1,3 +1,3 @@ diff --git a/templates/Entry/_card_actions.html.twig b/templates/Entry/_card_actions.html.twig index f3f18ba95..0111f9b00 100644 --- a/templates/Entry/_card_actions.html.twig +++ b/templates/Entry/_card_actions.html.twig @@ -7,7 +7,7 @@
    - {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %} + {% set current_path = app.request.requesturi %} diff --git a/templates/Entry/_card_list.html.twig b/templates/Entry/_card_list.html.twig index 6aaabbd2d..2710455f1 100644 --- a/templates/Entry/_card_list.html.twig +++ b/templates/Entry/_card_list.html.twig @@ -14,7 +14,7 @@ {% endif %} {% include "Entry/Card/_content.html.twig" with {'entry': entry, 'withMetadata': true, 'subClass': 'metadata'} only %} - {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %} + {% set current_path = app.request.requesturi %} diff --git a/templates/Entry/_tags.html.twig b/templates/Entry/_tags.html.twig index 2ab67e1c8..c23286d77 100644 --- a/templates/Entry/_tags.html.twig +++ b/templates/Entry/_tags.html.twig @@ -3,11 +3,15 @@ {% for tag in tags %}
  • {{ tag.label }} - {% if withRemove is defined and withRemove == true %} + {% if withRemove is defined and withRemove == true and is_granted('DELETE', tag) %} {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %} - - delete - +
    + + + +
    {% endif %}
  • {% endfor %} diff --git a/templates/Entry/_title.html.twig b/templates/Entry/_title.html.twig index 81f682ef1..78acd4047 100644 --- a/templates/Entry/_title.html.twig +++ b/templates/Entry/_title.html.twig @@ -9,7 +9,7 @@ {% elseif current_route == 'search' %} {{ 'entry.page_titles.filtered_search'|trans }} {{ filter }} {% elseif current_route == 'tag_entries' %} - {{ 'entry.page_titles.filtered_tags'|trans }} {{ filter }} + {{ 'entry.page_titles.filtered_tags'|trans }} {{ tag.label }} {% elseif current_route == 'untagged' %} {{ 'entry.page_titles.untagged'|trans }} {% elseif current_route == 'same_domain' %} diff --git a/templates/Entry/entries.html.twig b/templates/Entry/entries.html.twig index 1dc4f16fd..0b7daefaa 100644 --- a/templates/Entry/entries.html.twig +++ b/templates/Entry/entries.html.twig @@ -1,5 +1,8 @@ {% extends "layout.html.twig" %} +{% set has_filters = form is not null %} +{% set has_exports = craue_setting('export_epub') or craue_setting('export_pdf') or craue_setting('export_json') or craue_setting('export_csv') or craue_setting('export_txt') or craue_setting('export_xml') or craue_setting('export_md') %} + {% block head %} {{ parent() }} {% if tag is defined and app.user.config.feedToken %} @@ -18,100 +21,141 @@ {% include "Entry/_title.html.twig" with {'filter': filter} %} {% endblock %} +{% block nav_panel_extra_actions %} + {% if active_route %} +
  • + + casino + +
  • + {% endif %} + {% if has_filters %} +
  • + + filter_list + +
  • + {% endif %} + {% if has_exports %} +
  • + + file_download + +
  • + {% endif %} +{% endblock %} + {% block content %} - {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %} + {% set current_path = app.request.requesturi %} {% set list_mode = app.user.config.listMode %} {% set entries_with_archived_class_routes = ['tag_entries', 'search', 'all'] %} {% set current_route = app.request.attributes.get('_route') %} {% if current_route == 'homepage' %} {% set current_route = 'unread' %} {% endif %} -
    -
    -
    - {{ 'entry.list.number_on_the_page'|trans({'%count%': entries.count}) }} - {% if entries.count > 0 %} - {% if list_mode == 0 %}view_list{% else %}view_module{% endif %} +
    + + + +
    +
    + {{ 'entry.list.number_on_the_page'|trans({'%count%': entries.count}) }} + {% if entries.count > 0 %} +
    + + + +
    + {% endif %} + {% if entries.count > 0 and is_granted('EDIT_ENTRIES') %} + + {% endif %} + {% if app.user.config.feedToken %} + {% include "Entry/_feed_link.html.twig" %} + {% endif %} +
    + {% if current_route == 'search' and is_granted('CREATE_TAGS') %} +
    + + + +
    {% endif %} - {% if entries.count > 0 and is_granted('EDIT_ENTRIES') %} - - {% endif %} - {% if app.user.config.feedToken %} - {% include "Entry/_feed_link.html.twig" %} + {% if entries.getNbPages > 1 %} + {{ pagerfanta(entries, 'default_wallabag') }} {% endif %}
    - {% if current_route == 'search' %}{% endif %} + + {% if entries.count > 0 %} + +
    +
    + + + + +
    + +
    + + +
    +
    + +
      + + {% for entry in entries %} +
    1. + {% if list_mode == 1 %} + {% include "Entry/_card_list.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} + {% elseif not entry.previewPicture is null and entry.mimetype starts with 'image/' %} + {% include "Entry/_card_full_image.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} + {% else %} + {% include "Entry/_card_preview.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} + {% endif %} +
    2. + {% endfor %} +
    + {% endif %} + {% if entries.getNbPages > 1 %} - {{ pagerfanta(entries, 'default_wallabag') }} +
    + {{ pagerfanta(entries, 'default_wallabag') }} +
    {% endif %}
    - {% if entries.count > 0 %} - -
    -
    - - - - -
    - -
    - - -
    -
    - -
      - - {% for entry in entries %} -
    1. - {% if list_mode == 1 %} - {% include "Entry/_card_list.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} - {% elseif not entry.previewPicture is null and entry.mimetype starts with 'image/' %} - {% include "Entry/_card_full_image.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} - {% else %} - {% include "Entry/_card_preview.html.twig" with {'entry': entry, 'currentRoute': current_route, 'routes': entries_with_archived_class_routes} only %} - {% endif %} -
    2. - {% endfor %} -
    - {% endif %} - - - {% if entries.getNbPages > 1 %} -
    - {{ pagerfanta(entries, 'default_wallabag') }} -
    - {% endif %} - -
    - {% set current_tag = null %} - {% if tag is defined %} - {% set current_tag = tag.slug %} + {% if has_exports and is_granted('EXPORT_ENTRIES') %} +
    + {% set current_tag = null %} + {% if tag is defined %} + {% set current_tag = tag.slug %} + {% endif %} + {% set export_search_term = null %} + {% if searchTerm is defined %} + {% set export_search_term = searchTerm %} + {% endif %} + {% set entry = app.request.attributes.get('entry') %} + {% set previous_route = app.request.attributes.get('currentRoute') %} +

    {{ 'entry.list.export_title'|trans }}

    +
      + {% if craue_setting('export_epub') %}
    • EPUB
    • {% endif %} + {% if craue_setting('export_pdf') %}
    • PDF
    • {% endif %} + {% if craue_setting('export_json') %}
    • JSON
    • {% endif %} + {% if craue_setting('export_csv') %}
    • CSV
    • {% endif %} + {% if craue_setting('export_txt') %}
    • TXT
    • {% endif %} + {% if craue_setting('export_xml') %}
    • XML
    • {% endif %} + {% if craue_setting('export_md') %}
    • Markdown
    • {% endif %} +
    +
    {% endif %} - {% set export_search_term = null %} - {% if searchTerm is defined %} - {% set export_search_term = searchTerm %} - {% endif %} - {% set entry = app.request.attributes.get('id') %} - {% set previous_route = app.request.attributes.get('currentRoute') %} -

    {{ 'entry.list.export_title'|trans }}

    -
      - {% if craue_setting('export_epub') %}
    • EPUB
    • {% endif %} - {% if craue_setting('export_pdf') %}
    • PDF
    • {% endif %} - {% if craue_setting('export_json') %}
    • JSON
    • {% endif %} - {% if craue_setting('export_csv') %}
    • CSV
    • {% endif %} - {% if craue_setting('export_txt') %}
    • TXT
    • {% endif %} - {% if craue_setting('export_xml') %}
    • XML
    • {% endif %} - {% if craue_setting('export_md') %}
    • Markdown
    • {% endif %} -
    -
    - {% if form is not null and is_granted('LIST_ENTRIES') %} -
    + {% if has_filters %} +

    {{ 'entry.filters.title'|trans }}

    @@ -128,28 +172,38 @@
    - {{ form_widget(form.isArchived) }} - {{ form_label(form.isArchived) }} +
    - {{ form_widget(form.isStarred) }} - {{ form_label(form.isStarred) }} +
    - {{ form_widget(form.isUnread) }} - {{ form_label(form.isUnread) }} +
    - {{ form_widget(form.isAnnotated) }} - {{ form_label(form.isAnnotated) }} +
    - {{ form_widget(form.isNotParsed) }} - {{ form_label(form.isNotParsed) }} +
    @@ -157,8 +211,10 @@
    - {{ form_widget(form.previewPicture) }} - {{ form_label(form.previewPicture) }} +
    @@ -166,8 +222,10 @@
    - {{ form_widget(form.isPublic) }} - {{ form_label(form.isPublic) }} +
    @@ -209,17 +267,17 @@ {{ form_label(form.createdAt) }}
    -
    - {{ form_widget(form.createdAt.left_date, {'type': 'date', 'attr': {'class': 'datepicker', 'data-value': form.createdAt.left_date.vars.value}}) }} +
    + {{ form_widget(form.createdAt.left_date, {'type': 'date', 'attr': {'data-value': form.createdAt.left_date.vars.value}}) }}
    -
    - {{ form_widget(form.createdAt.right_date, {'type': 'date', 'attr': {'class': 'datepicker', 'data-value': form.createdAt.right_date.vars.value}}) }} +
    + {{ form_widget(form.createdAt.right_date, {'type': 'date', 'attr': {'data-value': form.createdAt.right_date.vars.value}}) }}
    - {{ 'entry.filters.action.clear'|trans }} +
    diff --git a/templates/Entry/entries.xml.twig b/templates/Entry/entries.xml.twig index 087b61be3..b286517b7 100644 --- a/templates/Entry/entries.xml.twig +++ b/templates/Entry/entries.xml.twig @@ -3,10 +3,10 @@ {% if type != 'tag' %} wallabag — {{ type }} feed Atom feed for {{ type }} entries - wallabag:{{ domainName|removeScheme|removeWww }}:{{ user }}:{{ type }} + wallabag:{{ wallabag_url|removeScheme|removeWww }}:{{ user }}:{{ type }} {% else %} - wallabag:{{ domainName|removeScheme|removeWww }}:{{ user }}:{{ type }}:{{ tag }} + wallabag:{{ wallabag_url|removeScheme|removeWww }}:{{ user }}:{{ type }}:{{ tag }} wallabag — {{ type }} {{ tag }} feed Atom feed for entries tagged with {{ tag }} @@ -31,12 +31,10 @@ {% for entry in entries %} <![CDATA[{{ entry.title|e }}]]> - - + - - wallabag:{{ domainName|removeScheme|removeWww }}:{{ user }}:entry:{{ entry.id }} + wallabag:{{ wallabag_url|removeScheme|removeWww }}:{{ user }}:entry:{{ entry.id }} {{ entry.updatedAt|date('c') }} {{ entry.createdAt|date('c') }} {% for tag in entry.tags %} diff --git a/templates/Entry/entry.html.twig b/templates/Entry/entry.html.twig index d3714acf6..8faef53a9 100644 --- a/templates/Entry/entry.html.twig +++ b/templates/Entry/entry.html.twig @@ -8,13 +8,13 @@ {% block menu %}
    -
    +
    -
    - + {% if is_granted('TAG', entry) %} + + {% endif %} - +
    {{ entry.content|raw }}
    -
    +
    menu
    - - - {% endblock %} {% block footer %} diff --git a/templates/Entry/new_form.html.twig b/templates/Entry/new_form.html.twig index e572d9f50..f1a9cf45e 100644 --- a/templates/Entry/new_form.html.twig +++ b/templates/Entry/new_form.html.twig @@ -1,4 +1,4 @@ - diff --git a/templates/Entry/search_form.html.twig b/templates/Entry/search_form.html.twig index 451a1f8ba..ebd10e70a 100644 --- a/templates/Entry/search_form.html.twig +++ b/templates/Entry/search_form.html.twig @@ -1,4 +1,4 @@ - diff --git a/templates/Entry/share.html.twig b/templates/Entry/share.html.twig index c8cf32eaf..794518f57 100644 --- a/templates/Entry/share.html.twig +++ b/templates/Entry/share.html.twig @@ -17,18 +17,14 @@ - {% if app.debug %} - - {% else %} - - {% endif %} + {{ encore_entry_link_tags('public') }}

    {{ entry.title|e|raw }}

    {{ entry.domainName|removeWww }} -

    {{ "entry.public.shared_by_wallabag"|trans({'%wallabag_instance%': url('homepage'), '%username%': entry.user.username|escape})|raw }}.

    +

    {{ "entry.public.shared_by_wallabag"|trans({'%wallabag_instance%': url('homepage'), '%username%': entry.user.name|escape})|raw }}.

    {{ entry.content|raw }} diff --git a/templates/Import/Chrome/index.html.twig b/templates/Import/Chrome/index.html.twig index f97aff670..ecf37ec95 100644 --- a/templates/Import/Chrome/index.html.twig +++ b/templates/Import/Chrome/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Delicious/index.html.twig b/templates/Import/Delicious/index.html.twig index 445e1aa6e..e9356f6b9 100644 --- a/templates/Import/Delicious/index.html.twig +++ b/templates/Import/Delicious/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Elcurator/index.html.twig b/templates/Import/Elcurator/index.html.twig index 7236f7be5..d0479a8d0 100644 --- a/templates/Import/Elcurator/index.html.twig +++ b/templates/Import/Elcurator/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Firefox/index.html.twig b/templates/Import/Firefox/index.html.twig index f16604fe3..239b9a3ee 100644 --- a/templates/Import/Firefox/index.html.twig +++ b/templates/Import/Firefox/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Instapaper/index.html.twig b/templates/Import/Instapaper/index.html.twig index 4fdc5ed96..ea9039c22 100644 --- a/templates/Import/Instapaper/index.html.twig +++ b/templates/Import/Instapaper/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Omnivore/index.html.twig b/templates/Import/Omnivore/index.html.twig index 045668321..3aa09e25c 100644 --- a/templates/Import/Omnivore/index.html.twig +++ b/templates/Import/Omnivore/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Pinboard/index.html.twig b/templates/Import/Pinboard/index.html.twig index 458a089f4..58a956d77 100644 --- a/templates/Import/Pinboard/index.html.twig +++ b/templates/Import/Pinboard/index.html.twig @@ -26,10 +26,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Pocket/index.html.twig b/templates/Import/Pocket/index.html.twig index 11eda98a9..50995941f 100644 --- a/templates/Import/Pocket/index.html.twig +++ b/templates/Import/Pocket/index.html.twig @@ -21,10 +21,12 @@
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Readability/index.html.twig b/templates/Import/Readability/index.html.twig index 12831e1bf..f97cf77e8 100644 --- a/templates/Import/Readability/index.html.twig +++ b/templates/Import/Readability/index.html.twig @@ -26,10 +26,12 @@ -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/Shaarli/index.html.twig b/templates/Import/Shaarli/index.html.twig index 2091d6838..6cbf5049f 100644 --- a/templates/Import/Shaarli/index.html.twig +++ b/templates/Import/Shaarli/index.html.twig @@ -26,10 +26,12 @@ -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/WallabagV1/index.html.twig b/templates/Import/WallabagV1/index.html.twig index d9a87a73c..e48f797d5 100644 --- a/templates/Import/WallabagV1/index.html.twig +++ b/templates/Import/WallabagV1/index.html.twig @@ -26,10 +26,12 @@ -
    -
    {{ 'import.form.mark_as_read_title'|trans }}
    - {{ form_widget(form.mark_as_read) }} - {{ form_label(form.mark_as_read) }} +
    {{ 'import.form.mark_as_read_title'|trans }}
    +
    +
    diff --git a/templates/Import/check_queue.html.twig b/templates/Import/check_queue.html.twig index a26336989..4d8e924ad 100644 --- a/templates/Import/check_queue.html.twig +++ b/templates/Import/check_queue.html.twig @@ -1,13 +1,9 @@ {% if nbRedisMessages is defined and nbRedisMessages > 0 %} - + {% endif %} {% if nbRabbitMessages is defined and nbRabbitMessages > 0 %} - + {% endif %} {% if redisNotInstalled is defined and redisNotInstalled %} diff --git a/templates/Static/about.html.twig b/templates/Static/about.html.twig index 02805405b..b814280f5 100644 --- a/templates/Static/about.html.twig +++ b/templates/Static/about.html.twig @@ -10,7 +10,7 @@
    -
      +
      • {{ 'about.top_menu.who_behind_wallabag'|trans }}
      • {{ 'about.top_menu.getting_help'|trans }}
      • {{ 'about.top_menu.helping'|trans }}
      • @@ -20,10 +20,11 @@
        {{ 'about.who_behind_wallabag.developped_by'|trans }}
        -
        Nicolas Lœuillet{{ 'about.who_behind_wallabag.website'|trans }}
        +
        Nicolas Lœuillet — {{ 'about.who_behind_wallabag.website'|trans }}
        Thomas Citharel — {{ 'about.who_behind_wallabag.website'|trans }}
        Jérémy Benoist — {{ 'about.who_behind_wallabag.website'|trans }}
        Kevin Decherf — {{ 'about.who_behind_wallabag.website'|trans }}
        +
        Yassine Guedidi
        {{ 'about.who_behind_wallabag.many_contributors'|trans|raw }}
        {{ 'about.who_behind_wallabag.project_website'|trans }}
        https://www.wallabag.org
        @@ -33,11 +34,7 @@
        -
        {{ 'about.getting_help.documentation'|trans }}
        -
        english
        -
        français
        -
        deutsch
        -
        italiano
        +
        {{ 'about.getting_help.documentation'|trans }}
        {{ 'about.getting_help.bug_reports'|trans }}
        {{ 'about.getting_help.support'|trans|raw }}
        diff --git a/templates/Static/howto.html.twig b/templates/Static/howto.html.twig index 493961294..6b9412595 100644 --- a/templates/Static/howto.html.twig +++ b/templates/Static/howto.html.twig @@ -10,7 +10,7 @@
        -
        - + + {% endif %}
      • @@ -45,22 +47,24 @@
      • -
      • -
        -
        - {{ 'quickstart.migrate.title'|trans }} -

        {{ 'quickstart.migrate.description'|trans }}

        + {% if is_granted('IMPORT_ENTRIES') %} +
      • +
        +
        + {{ 'quickstart.migrate.title'|trans }} +

        {{ 'quickstart.migrate.description'|trans }}

        +
        +
        - -
      • - + + {% endif %}
      • @@ -71,7 +75,6 @@ diff --git a/templates/Tag/new_form.html.twig b/templates/Tag/new_form.html.twig index 033ea8722..175a2af34 100644 --- a/templates/Tag/new_form.html.twig +++ b/templates/Tag/new_form.html.twig @@ -7,7 +7,7 @@ {{ form_errors(form.label) }} {% endif %} - {{ form_widget(form.label, {'attr': {'autocomplete': 'off'}}) }} + {{ form_widget(form.label, {'attr': {'autocomplete': 'off', 'data-add-tag-target': 'input'}}) }} {{ form_widget(form.add, {'attr': {'class': 'btn waves-effect waves-light tags-add-form-submit'}}) }} {{ form_widget(form._token) }} diff --git a/templates/Tag/tags.html.twig b/templates/Tag/tags.html.twig index fb3b3ed2d..dd9281aee 100644 --- a/templates/Tag/tags.html.twig +++ b/templates/Tag/tags.html.twig @@ -6,7 +6,7 @@ {% set current_path = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
        - {{ 'tag.list.number_on_the_page'|trans({'%count%': tags|length}) }} + {{ 'tag.list.number_on_the_page'|trans({'%count%': allTagsWithNbEntries|length}) }}
      • {% endif %} - {% for tag in tags %} -
      • - - {{ tag.label }} ({{ tag.nbEntries }}) + {% for tagWithNbEntries in allTagsWithNbEntries %} + {% set tag = tagWithNbEntries.tag %} + {% set nbEntries = tagWithNbEntries.nbEntries %} +
      • + + {{ tag.label }} ({{ nbEntries }}) - {% if renameForms is defined and renameForms[tag.id] is defined %} - - + + {% endif %} + {% if is_granted('DELETE', tag) %} +
        + + + +
        {% endif %} - - delete - {% if app.user.config.feedToken %} rss_feed {% endif %} diff --git a/templates/User/edit.html.twig b/templates/User/edit.html.twig index 7bf52f77a..475472d50 100644 --- a/templates/User/edit.html.twig +++ b/templates/User/edit.html.twig @@ -41,22 +41,25 @@
        - {{ form_widget(edit_form.enabled) }} - {{ form_label(edit_form.enabled) }} - {{ form_errors(edit_form.enabled) }} +
        -
        +
        - {{ form_widget(edit_form.emailTwoFactor) }} - {{ form_label(edit_form.emailTwoFactor) }} - {{ form_errors(edit_form.emailTwoFactor) }} +
        - {{ form_widget(edit_form.googleTwoFactor) }} - {{ form_label(edit_form.googleTwoFactor) }} - {{ form_errors(edit_form.googleTwoFactor) }} +
        diff --git a/templates/base.html.twig b/templates/base.html.twig index 00e66482f..43d75fc9f 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -4,7 +4,7 @@ - + {% block head %} diff --git a/templates/bundles/CraueConfigBundle/Settings/modify.html.twig b/templates/bundles/CraueConfigBundle/Settings/modify.html.twig index 809c6dff1..83d9a6fa6 100644 --- a/templates/bundles/CraueConfigBundle/Settings/modify.html.twig +++ b/templates/bundles/CraueConfigBundle/Settings/modify.html.twig @@ -11,7 +11,7 @@
        -
          +
            {% for section in sections|craue_sortSections %}
          • {{ section|trans({}, 'CraueConfigBundle') }}
          • {% endfor %} @@ -22,9 +22,13 @@
            {% for setting in form.settings %} {% if setting.vars.value.section == section %} - {{ form_row(setting.value, { - 'label': setting.vars.value.name|trans({}, 'CraueConfigBundle'), - }) }} +
            +
            + {{ form_row(setting.value, { + 'label': setting.vars.value.name|trans({}, 'CraueConfigBundle'), + }) }} +
            +
            {% endif %} {% endfor %}
            diff --git a/templates/bundles/FOSUserBundle/Security/login.html.twig b/templates/bundles/FOSUserBundle/Security/login.html.twig index 291a0a428..c25b106ff 100644 --- a/templates/bundles/FOSUserBundle/Security/login.html.twig +++ b/templates/bundles/FOSUserBundle/Security/login.html.twig @@ -5,11 +5,11 @@
            {% if error %} - + {% endif %} {% for flash_message in app.session.flashbag.get('notice') %} - + {% endfor %}
            @@ -24,9 +24,11 @@
            -
            - - +
            +
            diff --git a/templates/bundles/FOSUserBundle/layout.html.twig b/templates/bundles/FOSUserBundle/layout.html.twig index c424f42ea..a2d865d9b 100644 --- a/templates/bundles/FOSUserBundle/layout.html.twig +++ b/templates/bundles/FOSUserBundle/layout.html.twig @@ -16,9 +16,23 @@ {% endblock fos_user_content %}
            - Deutsch – - English – - Français +
            + + + +
            + – +
            + + + +
            + – +
            + + + +
        diff --git a/templates/layout.html.twig b/templates/layout.html.twig index 98e1adfaa..b0c2de45c 100644 --- a/templates/layout.html.twig +++ b/templates/layout.html.twig @@ -1,15 +1,33 @@ {% extends "base.html.twig" %} +{% set current_route = app.request.attributes.get('_route') %} +{% set current_route_from_query_params = app.request.query.get('currentRoute') %} + +{% set active_route = null %} +{% if current_route == 'all' or current_route_from_query_params == 'all' %} + {% set active_route = 'all' %} +{% elseif current_route == 'annotated' or current_route_from_query_params == 'annotated' %} + {% set active_route = 'annotated' %} +{% elseif current_route == 'archive' or current_route_from_query_params == 'archive' %} + {% set active_route = 'archive' %} +{% elseif current_route == 'starred' or current_route_from_query_params == 'starred' %} + {% set active_route = 'starred' %} +{% elseif current_route == 'unread' or current_route == 'homepage' or current_route_from_query_params == 'unread' %} + {% set active_route = 'unread' %} +{% elseif current_route == 'untagged' %} + {% set active_route = 'untagged' %} +{% endif %} + {% block css %} {{ parent() }} - {% if not app.debug %} - - {% endif %} + + {{ encore_entry_link_tags('main') }} {% endblock %} {% block scripts %} {{ parent() }} - + + {{ encore_entry_script_tags('main') }} {% endblock %} {% block header %} @@ -17,9 +35,7 @@ {% block messages %} {% for flash_message in app.session.flashbag.get('notice') %} - + {% endfor %} {% endblock %} @@ -27,7 +43,7 @@ {% block menu %}