diff --git a/CHANGELOG.md b/CHANGELOG.md index 4026134..c8d22ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [9.2.0] - 2024-08-11 +### Added +* Add `ROBOTS_ALLOW_ALL_SHORT_URLS` config option. +* Add `ROBOTS_USER_AGENTS` config option. +* Add support for `laminas/laminas-servicemanager` v4.x. + +### Changed +* Update to PHPUnit 11 +* Update to PHPStan 1.11 + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [9.1.0] - 2024-04-14 ### Added * Add `MEMORY_LIMIT` config option. diff --git a/composer.json b/composer.json index 485b0c5..62b003d 100644 --- a/composer.json +++ b/composer.json @@ -14,22 +14,22 @@ "require": { "php": "^8.2", "laminas/laminas-config": "^3.9", - "laminas/laminas-config-aggregator": "^1.14", - "laminas/laminas-servicemanager": "^3.22", + "laminas/laminas-config-aggregator": "^1.15", + "laminas/laminas-servicemanager": "^4.2 || ^3.22", "laminas/laminas-stdlib": "^3.19", - "shlinkio/shlink-config": "^3.0 || ^2.5", - "symfony/console": "^7.0 || ^6.4", - "symfony/filesystem": "^7.0 || ^6.4", - "symfony/process": "^7.0 || ^6.4" + "shlinkio/shlink-config": "^3.1", + "symfony/console": "^7.1", + "symfony/filesystem": "^7.1", + "symfony/process": "^7.1" }, "require-dev": { "devster/ubench": "^2.1", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-phpunit": "^1.4", + "phpunit/phpunit": "^11.3", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.3.0", - "symfony/var-dumper": "^7.0 || ^6.4" + "symfony/var-dumper": "^7.1" }, "autoload": { "psr-4": { @@ -49,8 +49,8 @@ ], "cs": "phpcs", "cs:fix": "phpcbf", - "stan": "phpstan analyse src test test-resources config --level=8", - "test": "phpunit --order-by=random --testdox --colors=always", + "stan": "phpstan analyse", + "test": "phpunit --order-by=random --testdox --testdox-summary", "test:ci": "@test --coverage-clover=build/clover.xml", "test:pretty": "@test --coverage-html=build/coverage-html" }, diff --git a/config/config.php b/config/config.php index 855af5d..d3771e2 100644 --- a/config/config.php +++ b/config/config.php @@ -96,6 +96,10 @@ 'QR codes > Enabled for disabled short URLs' => Config\Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class, ], + 'ROBOTS' => [ + 'Robots.txt > allow all' => Config\Option\Robots\RobotsAllowAllShortUrlsConfigOption::class, + 'Robots.txt > user agents' => Config\Option\Robots\RobotsUserAgentsConfigOption::class, + ], 'APPLICATION' => [ 'Delete short URLs > Visits threshold' => Config\Option\Visit\VisitsThresholdConfigOption::class, 'Base path' => Config\Option\BasePathConfigOption::class, @@ -148,6 +152,7 @@ Config\Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class => InvokableFactory::class, Config\Option\UrlShortener\EnableTrailingSlashConfigOption::class => InvokableFactory::class, Config\Option\UrlShortener\ShortUrlModeConfigOption::class => InvokableFactory::class, + Config\Option\Robots\RobotsAllowAllShortUrlsConfigOption::class => InvokableFactory::class, Config\Option\Redis\RedisServersConfigOption::class => InvokableFactory::class, Config\Option\Redis\RedisSentinelServiceConfigOption::class => InvokableFactory::class, Config\Option\Redis\RedisPubSubConfigOption::class => InvokableFactory::class, diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist index 4a667f1..f823f01 100644 --- a/docker-compose.override.yml.dist +++ b/docker-compose.override.yml.dist @@ -1,5 +1,3 @@ -version: '3' - services: shlink_installer_php: user: 1000:1000 diff --git a/docker-compose.yml b/docker-compose.yml index 9220c3a..b0fcb7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: shlink_installer_php: container_name: shlink_installer_php diff --git a/phpstan.neon b/phpstan.neon index 88060ed..920bb1a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,12 @@ includes: - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon parameters: - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false + level: 8 + paths: + - config + - src + - test + - test-resources + ignoreErrors: + - identifier: missingType.iterableValue diff --git a/src/Config/ConfigOptionsManager.php b/src/Config/ConfigOptionsManager.php index f91a3bc..91082fd 100644 --- a/src/Config/ConfigOptionsManager.php +++ b/src/Config/ConfigOptionsManager.php @@ -5,8 +5,31 @@ namespace Shlinkio\Shlink\Installer\Config; use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\Exception\InvalidServiceException; +use function get_debug_type; +use function sprintf; + +/** + * @extends AbstractPluginManager + * @todo Extend from AbstractSingleInstancePluginManager once servicemanager 3 is no longer supported + */ class ConfigOptionsManager extends AbstractPluginManager implements ConfigOptionsManagerInterface { + /** @var class-string */ protected $instanceOf = Option\ConfigOptionInterface::class; // phpcs:ignore + + public function validate(mixed $instance): void + { + if ($instance instanceof $this->instanceOf) { + return; + } + + throw new InvalidServiceException(sprintf( + 'Plugin manager "%s" expected an instance of type "%s", but "%s" was received', + static::class, + $this->instanceOf, + get_debug_type($instance), + )); + } } diff --git a/src/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOption.php b/src/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOption.php new file mode 100644 index 0000000..4435ece --- /dev/null +++ b/src/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOption.php @@ -0,0 +1,25 @@ +confirm( + 'Do you want all short URLs to be crawlable/allowed by the robots.txt file? ' + . 'You can still allow them individually, regardless of this.', + default: false, + ); + } +} diff --git a/src/Config/Option/Robots/RobotsUserAgentsConfigOption.php b/src/Config/Option/Robots/RobotsUserAgentsConfigOption.php new file mode 100644 index 0000000..566e23f --- /dev/null +++ b/src/Config/Option/Robots/RobotsUserAgentsConfigOption.php @@ -0,0 +1,23 @@ +ask( + 'Provide a comma-separated list of user agents for your robots.txt file. Defaults to all user agents (*)', + ); + } +} diff --git a/test/Config/ConfigOptionsManagerFactoryTest.php b/test/Config/ConfigOptionsManagerFactoryTest.php index df563c6..03f360a 100644 --- a/test/Config/ConfigOptionsManagerFactoryTest.php +++ b/test/Config/ConfigOptionsManagerFactoryTest.php @@ -9,7 +9,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -use ReflectionObject; use Shlinkio\Shlink\Installer\Config\ConfigOptionsManagerFactory; use Shlinkio\Shlink\Installer\Config\Option\ConfigOptionInterface; @@ -25,23 +24,22 @@ public function setUp(): void } #[Test, DataProvider('provideConfigs')] - public function createsServiceWithExpectedPlugins(callable $configCreator, int $expectedSize): void + public function createsServiceWithExpectedPlugins(callable $configCreator, bool $servicesExist): void { $config = $configCreator($this); $this->container->expects($this->once())->method('get')->with('config')->willReturn($config); - $service = ($this->factory)($this->container); - $ref = new ReflectionObject($service); - $servicesProp = $ref->getProperty('services'); - $servicesProp->setAccessible(true); + $manager = ($this->factory)($this->container); - self::assertCount($expectedSize, $servicesProp->getValue($service)); + self::assertEquals($servicesExist, $manager->has('a')); + self::assertEquals($servicesExist, $manager->has('b')); + self::assertEquals($servicesExist, $manager->has('c')); } public static function provideConfigs(): iterable { - yield 'config_options not defined' => [static fn (TestCase $test) => [], 0]; - yield 'config_options empty' => [static fn (TestCase $test) => ['config_options' => []], 0]; + yield 'config_options not defined' => [static fn (TestCase $test) => [], false]; + yield 'config_options empty' => [static fn (TestCase $test) => ['config_options' => []], false]; yield 'config_options with values' => [ static fn (TestCase $test) => [ 'config_options' => [ @@ -52,7 +50,7 @@ public static function provideConfigs(): iterable ], ], ], - 3, + true, ]; } } diff --git a/test/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOptionTest.php b/test/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOptionTest.php new file mode 100644 index 0000000..a76cafd --- /dev/null +++ b/test/Config/Option/Robots/RobotsAllowAllShortUrlsConfigOptionTest.php @@ -0,0 +1,41 @@ +configOption = new RobotsAllowAllShortUrlsConfigOption(); + } + + #[Test] + public function returnsExpectedEnvVar(): void + { + self::assertEquals('ROBOTS_ALLOW_ALL_SHORT_URLS', $this->configOption->getEnvVar()); + } + + #[Test] + public function expectedQuestionIsAsked(): void + { + $io = $this->createMock(StyleInterface::class); + $io->expects($this->once())->method('confirm')->with( + 'Do you want all short URLs to be crawlable/allowed by the robots.txt file? ' + . 'You can still allow them individually, regardless of this.', + false, + )->willReturn(true); + + $answer = $this->configOption->ask($io, []); + + self::assertTrue($answer); + } +} diff --git a/test/Config/Option/Robots/RobotsUserAgentsConfigOptionTest.php b/test/Config/Option/Robots/RobotsUserAgentsConfigOptionTest.php new file mode 100644 index 0000000..97a7b3c --- /dev/null +++ b/test/Config/Option/Robots/RobotsUserAgentsConfigOptionTest.php @@ -0,0 +1,39 @@ +configOption = new RobotsUserAgentsConfigOption(); + } + + #[Test] + public function returnsExpectedEnvVar(): void + { + self::assertEquals('ROBOTS_USER_AGENTS', $this->configOption->getEnvVar()); + } + + #[Test] + public function expectedQuestionIsAsked(): void + { + $io = $this->createMock(StyleInterface::class); + $io->expects($this->once())->method('ask')->with( + 'Provide a comma-separated list of user agents for your robots.txt file. Defaults to all user agents (*)', + )->willReturn('foo,bar'); + + $answer = $this->configOption->ask($io, []); + + self::assertEquals('foo,bar', $answer); + } +}