diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ba489c..5bcfe116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1059](https://github.com/shlinkio/shlink/issues/1059) Added ability to optionally display author API key and its name when listing short URLs from the command line. * [#1066](https://github.com/shlinkio/shlink/issues/1066) Added support to import short URLs and their visits from another Shlink instance using its API. +* [#898](https://github.com/shlinkio/shlink/issues/898) Improved tracking granularity, allowing to disable visits tracking completely, or just parts of it. + + In order to achieve it, Shlink now supports 4 new tracking-related options, that can be customized via env vars for docker, or via installer: + + * `disable_tracking`: If true, visits will not be tracked at all. + * `disable_ip_tracking`: If true, visits will be tracked, but neither the IP address, nor the location will be resolved. + * `disable_referrer_tracking`: If true, the referrer will not be tracked. + * `disable_ua_tracking`: If true, the user agent will not be tracked. ### Changed * [#1036](https://github.com/shlinkio/shlink/issues/1036) Updated to `happyr/doctrine-specification` 2.0. diff --git a/composer.json b/composer.json index 87275e92..2474cebf 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "shlinkio/shlink-config": "^1.0", "shlinkio/shlink-event-dispatcher": "^2.1", "shlinkio/shlink-importer": "dev-main#39928b6 as 2.3", - "shlinkio/shlink-installer": "dev-develop#aa50ea9 as 5.5", + "shlinkio/shlink-installer": "dev-develop#15cf3b3 as 6.0", "shlinkio/shlink-ip-geolocation": "^1.5", "symfony/console": "^5.1", "symfony/filesystem": "^5.1", diff --git a/config/autoload/app_options.global.php b/config/autoload/app_options.global.php index f64f9cff..0b7ec937 100644 --- a/config/autoload/app_options.global.php +++ b/config/autoload/app_options.global.php @@ -7,7 +7,6 @@ return [ 'app_options' => [ 'name' => 'Shlink', 'version' => '%SHLINK_VERSION%', - 'disable_track_param' => null, ], ]; diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 605b16ce..0a72c6fa 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -27,7 +27,6 @@ return [ Option\Redirect\BaseUrlRedirectConfigOption::class, Option\Redirect\InvalidShortUrlRedirectConfigOption::class, Option\Redirect\Regular404RedirectConfigOption::class, - Option\DisableTrackParamConfigOption::class, Option\Visit\CheckVisitsThresholdConfigOption::class, Option\Visit\VisitsThresholdConfigOption::class, Option\BasePathConfigOption::class, @@ -40,11 +39,16 @@ return [ Option\Mercure\MercureInternalUrlConfigOption::class, Option\Mercure\MercureJwtSecretConfigOption::class, Option\UrlShortener\GeoLiteLicenseKeyConfigOption::class, - Option\UrlShortener\IpAnonymizationConfigOption::class, Option\UrlShortener\RedirectStatusCodeConfigOption::class, Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class, Option\UrlShortener\AutoResolveTitlesConfigOption::class, - Option\UrlShortener\OrphanVisitsTrackingConfigOption::class, + Option\Tracking\IpAnonymizationConfigOption::class, + Option\Tracking\OrphanVisitsTrackingConfigOption::class, + Option\Tracking\DisableTrackParamConfigOption::class, + Option\Tracking\DisableTrackingConfigOption::class, + Option\Tracking\DisableIpTrackingConfigOption::class, + Option\Tracking\DisableReferrerTrackingConfigOption::class, + Option\Tracking\DisableUaTrackingConfigOption::class, ], 'installation_commands' => [ diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php new file mode 100644 index 00000000..4fdf0ba6 --- /dev/null +++ b/config/autoload/tracking.global.php @@ -0,0 +1,31 @@ + [ + // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations + // This applies only if IP address tracking is enabled + 'anonymize_remote_addr' => true, + + // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence + 'track_orphan_visits' => true, + + // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence + 'disable_track_param' => null, + + // If true, visits will not be tracked at all + 'disable_tracking' => false, + + // If true, visits will be tracked, but neither the IP address, nor the location will be resolved + 'disable_ip_tracking' => false, + + // If true, the referrer will not be tracked + 'disable_referrer_tracking' => false, + + // If true, the user agent will not be tracked + 'disable_ua_tracking' => false, + ], + +]; diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 3751b1e9..d7cd8b02 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -14,13 +14,11 @@ return [ 'hostname' => '', ], 'validate_url' => false, // Deprecated - 'anonymize_remote_addr' => true, 'visits_webhooks' => [], 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, 'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE, 'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME, 'auto_resolve_titles' => false, - 'track_orphan_visits' => true, ], ]; diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index acecda14..2a8369d7 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -94,10 +94,6 @@ $helper = new class { return [ - 'app_options' => [ - 'disable_track_param' => env('DISABLE_TRACK_PARAM'), - ], - 'delete_short_urls' => [ 'check_visits_threshold' => true, 'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD), @@ -113,13 +109,21 @@ return [ 'hostname' => env('SHORT_DOMAIN_HOST', ''), ], 'validate_url' => (bool) env('VALIDATE_URLS', false), - 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true), 'visits_webhooks' => $helper->getVisitsWebhooks(), 'default_short_codes_length' => $helper->getDefaultShortCodesLength(), 'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE), 'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME), 'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false), + ], + + 'tracking' => [ + 'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true), 'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true), + 'disable_track_param' => env('DISABLE_TRACK_PARAM'), + 'disable_tracking' => (bool) env('DISABLE_TRACKING', false), + 'disable_ip_tracking' => (bool) env('DISABLE_IP_TRACKING', false), + 'disable_referrer_tracking' => (bool) env('DISABLE_REFERRER_TRACKING', false), + 'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false), ], 'not_found_redirects' => $helper->getNotFoundRedirectsConfig(), diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 479b497a..b84c74a4 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -24,6 +24,7 @@ return [ Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class, Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, + Options\TrackingOptions::class => ConfigAbstractFactory::class, Service\UrlShortener::class => ConfigAbstractFactory::class, Service\ShortUrlService::class => ConfigAbstractFactory::class, @@ -75,6 +76,7 @@ return [ Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'], Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], + Options\TrackingOptions::class => ['config.tracking'], Service\UrlShortener::class => [ ShortUrl\Helper\ShortUrlTitleResolutionHelper::class, @@ -85,7 +87,7 @@ return [ Visit\VisitsTracker::class => [ 'em', EventDispatcherInterface::class, - Options\UrlShortenerOptions::class, + Options\TrackingOptions::class, ], Service\ShortUrlService::class => [ 'em', @@ -112,14 +114,14 @@ return [ Action\RedirectAction::class => [ Service\ShortUrl\ShortUrlResolver::class, Visit\VisitsTracker::class, - Options\AppOptions::class, + Options\TrackingOptions::class, Util\RedirectResponseHelper::class, 'Logger_Shlink', ], Action\PixelAction::class => [ Service\ShortUrl\ShortUrlResolver::class, Visit\VisitsTracker::class, - Options\AppOptions::class, + Options\TrackingOptions::class, 'Logger_Shlink', ], Action\QrCodeAction::class => [ diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index b6a119b2..567e930c 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -18,7 +18,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Options\AppOptions; +use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface; @@ -29,18 +29,18 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet { private ShortUrlResolverInterface $urlResolver; private VisitsTrackerInterface $visitTracker; - private AppOptions $appOptions; + private TrackingOptions $trackingOptions; private LoggerInterface $logger; public function __construct( ShortUrlResolverInterface $urlResolver, VisitsTrackerInterface $visitTracker, - AppOptions $appOptions, + TrackingOptions $trackingOptions, ?LoggerInterface $logger = null ) { $this->urlResolver = $urlResolver; $this->visitTracker = $visitTracker; - $this->appOptions = $appOptions; + $this->trackingOptions = $trackingOptions; $this->logger = $logger ?? new NullLogger(); } @@ -48,7 +48,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet { $identifier = ShortUrlIdentifier::fromRedirectRequest($request); $query = $request->getQueryParams(); - $disableTrackParam = $this->appOptions->getDisableTrackParam(); + $disableTrackParam = $this->trackingOptions->getDisableTrackParam(); try { $shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier); diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index d346456b..7da67b59 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -21,11 +21,11 @@ class RedirectAction extends AbstractTrackingAction implements StatusCodeInterfa public function __construct( ShortUrlResolverInterface $urlResolver, VisitsTrackerInterface $visitTracker, - Options\AppOptions $appOptions, + Options\TrackingOptions $trackingOptions, RedirectResponseHelperInterface $redirectResponseHelper, ?LoggerInterface $logger = null ) { - parent::__construct($urlResolver, $visitTracker, $appOptions, $logger); + parent::__construct($urlResolver, $visitTracker, $trackingOptions, $logger); $this->redirectResponseHelper = $redirectResponseHelper; } diff --git a/module/Core/src/Config/DeprecatedConfigParser.php b/module/Core/src/Config/DeprecatedConfigParser.php index 92074bfc..b3421146 100644 --- a/module/Core/src/Config/DeprecatedConfigParser.php +++ b/module/Core/src/Config/DeprecatedConfigParser.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Config; use function Functional\compose; +/** @deprecated */ class DeprecatedConfigParser { public function __invoke(array $config): array diff --git a/module/Core/src/Config/SimplifiedConfigParser.php b/module/Core/src/Config/SimplifiedConfigParser.php index b578799b..2b0b1d71 100644 --- a/module/Core/src/Config/SimplifiedConfigParser.php +++ b/module/Core/src/Config/SimplifiedConfigParser.php @@ -19,7 +19,7 @@ use function uksort; class SimplifiedConfigParser { private const SIMPLIFIED_CONFIG_MAPPING = [ - 'disable_track_param' => ['app_options', 'disable_track_param'], + 'disable_track_param' => ['tracking', 'disable_track_param'], 'short_domain_schema' => ['url_shortener', 'domain', 'schema'], 'short_domain_host' => ['url_shortener', 'domain', 'hostname'], 'validate_url' => ['url_shortener', 'validate_url'], @@ -38,7 +38,7 @@ class SimplifiedConfigParser 'mercure_public_hub_url' => ['mercure', 'public_hub_url'], 'mercure_internal_hub_url' => ['mercure', 'internal_hub_url'], 'mercure_jwt_secret' => ['mercure', 'jwt_secret'], - 'anonymize_remote_addr' => ['url_shortener', 'anonymize_remote_addr'], + 'anonymize_remote_addr' => ['tracking', 'anonymize_remote_addr'], 'redirect_status_code' => ['url_shortener', 'redirect_status_code'], 'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'], 'port' => ['mezzio-swoole', 'swoole-http-server', 'port'], diff --git a/module/Core/src/Model/Visitor.php b/module/Core/src/Model/Visitor.php index 7438bdce..9564a41c 100644 --- a/module/Core/src/Model/Visitor.php +++ b/module/Core/src/Model/Visitor.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory; +use Shlinkio\Shlink\Core\Options\TrackingOptions; use function substr; @@ -68,4 +69,16 @@ final class Visitor { return $this->visitedUrl; } + + public function normalizeForTrackingOptions(TrackingOptions $options): self + { + $instance = self::emptyInstance(); + + $instance->userAgent = $options->disableUaTracking() ? '' : $this->userAgent; + $instance->referer = $options->disableReferrerTracking() ? '' : $this->referer; + $instance->remoteAddress = $options->disableIpTracking() ? null : $this->remoteAddress; + $instance->visitedUrl = $this->visitedUrl; + + return $instance; + } } diff --git a/module/Core/src/Options/AppOptions.php b/module/Core/src/Options/AppOptions.php index 66d76126..8fde2663 100644 --- a/module/Core/src/Options/AppOptions.php +++ b/module/Core/src/Options/AppOptions.php @@ -12,7 +12,6 @@ class AppOptions extends AbstractOptions { private string $name = ''; private string $version = '1.0'; - private ?string $disableTrackParam = null; public function getName(): string { @@ -36,16 +35,10 @@ class AppOptions extends AbstractOptions return $this; } - /** - */ - public function getDisableTrackParam(): ?string - { - return $this->disableTrackParam; - } - + /** @deprecated */ protected function setDisableTrackParam(?string $disableTrackParam): self { - $this->disableTrackParam = $disableTrackParam; + // Keep just for backwards compatibility during hydration return $this; } diff --git a/module/Core/src/Options/TrackingOptions.php b/module/Core/src/Options/TrackingOptions.php new file mode 100644 index 00000000..98e09085 --- /dev/null +++ b/module/Core/src/Options/TrackingOptions.php @@ -0,0 +1,88 @@ +anonymizeRemoteAddr; + } + + protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void + { + $this->anonymizeRemoteAddr = $anonymizeRemoteAddr; + } + + public function trackOrphanVisits(): bool + { + return $this->trackOrphanVisits; + } + + protected function setTrackOrphanVisits(bool $trackOrphanVisits): void + { + $this->trackOrphanVisits = $trackOrphanVisits; + } + + public function getDisableTrackParam(): ?string + { + return $this->disableTrackParam; + } + + protected function setDisableTrackParam(?string $disableTrackParam): void + { + $this->disableTrackParam = $disableTrackParam; + } + + public function disableTracking(): bool + { + return $this->disableTracking; + } + + protected function setDisableTracking(bool $disableTracking): void + { + $this->disableTracking = $disableTracking; + } + + public function disableIpTracking(): bool + { + return $this->disableIpTracking; + } + + protected function setDisableIpTracking(bool $disableIpTracking): void + { + $this->disableIpTracking = $disableIpTracking; + } + + public function disableReferrerTracking(): bool + { + return $this->disableReferrerTracking; + } + + protected function setDisableReferrerTracking(bool $disableReferrerTracking): void + { + $this->disableReferrerTracking = $disableReferrerTracking; + } + + public function disableUaTracking(): bool + { + return $this->disableUaTracking; + } + + protected function setDisableUaTracking(bool $disableUaTracking): void + { + $this->disableUaTracking = $disableUaTracking; + } +} diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index e1956203..a0005da2 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -19,8 +19,6 @@ class UrlShortenerOptions extends AbstractOptions private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE; private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME; private bool $autoResolveTitles = false; - private bool $anonymizeRemoteAddr = true; - private bool $trackOrphanVisits = true; public function isUrlValidationEnabled(): bool { @@ -69,23 +67,15 @@ class UrlShortenerOptions extends AbstractOptions $this->autoResolveTitles = $autoResolveTitles; } - public function anonymizeRemoteAddr(): bool - { - return $this->anonymizeRemoteAddr; - } - + /** @deprecated */ protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void { - $this->anonymizeRemoteAddr = $anonymizeRemoteAddr; - } - - public function trackOrphanVisits(): bool - { - return $this->trackOrphanVisits; + // Keep just for backwards compatibility during hydration } + /** @deprecated */ protected function setTrackOrphanVisits(bool $trackOrphanVisits): void { - $this->trackOrphanVisits = $trackOrphanVisits; + // Keep just for backwards compatibility during hydration } } diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index f8c82b49..f77cd624 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -10,18 +10,18 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; +use Shlinkio\Shlink\Core\Options\TrackingOptions; class VisitsTracker implements VisitsTrackerInterface { private ORM\EntityManagerInterface $em; private EventDispatcherInterface $eventDispatcher; - private UrlShortenerOptions $options; + private TrackingOptions $options; public function __construct( ORM\EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, - UrlShortenerOptions $options + TrackingOptions $options ) { $this->em = $em; $this->eventDispatcher = $eventDispatcher; @@ -31,40 +31,51 @@ class VisitsTracker implements VisitsTrackerInterface public function track(ShortUrl $shortUrl, Visitor $visitor): void { $this->trackVisit( - Visit::forValidShortUrl($shortUrl, $visitor, $this->options->anonymizeRemoteAddr()), + fn (Visitor $v) => Visit::forValidShortUrl($shortUrl, $v, $this->options->anonymizeRemoteAddr()), $visitor, ); } public function trackInvalidShortUrlVisit(Visitor $visitor): void { - if (! $this->options->trackOrphanVisits()) { - return; - } - - $this->trackVisit(Visit::forInvalidShortUrl($visitor, $this->options->anonymizeRemoteAddr()), $visitor); + $this->trackOrphanVisit( + fn (Visitor $v) => Visit::forInvalidShortUrl($v, $this->options->anonymizeRemoteAddr()), + $visitor, + ); } public function trackBaseUrlVisit(Visitor $visitor): void { - if (! $this->options->trackOrphanVisits()) { - return; - } - - $this->trackVisit(Visit::forBasePath($visitor, $this->options->anonymizeRemoteAddr()), $visitor); + $this->trackOrphanVisit( + fn (Visitor $v) => Visit::forBasePath($v, $this->options->anonymizeRemoteAddr()), + $visitor, + ); } public function trackRegularNotFoundVisit(Visitor $visitor): void + { + $this->trackOrphanVisit( + fn (Visitor $v) => Visit::forRegularNotFound($v, $this->options->anonymizeRemoteAddr()), + $visitor, + ); + } + + private function trackOrphanVisit(callable $createVisit, Visitor $visitor): void { if (! $this->options->trackOrphanVisits()) { return; } - $this->trackVisit(Visit::forRegularNotFound($visitor, $this->options->anonymizeRemoteAddr()), $visitor); + $this->trackVisit($createVisit, $visitor); } - private function trackVisit(Visit $visit, Visitor $visitor): void + private function trackVisit(callable $createVisit, Visitor $visitor): void { + if ($this->options->disableTracking()) { + return; + } + + $visit = $createVisit($visitor->normalizeForTrackingOptions($this->options)); $this->em->transactional(function () use ($visit, $visitor): void { $this->em->persist($visit); $this->em->flush(); diff --git a/module/Core/test/Action/PixelActionTest.php b/module/Core/test/Action/PixelActionTest.php index 065cc2c4..6df2498a 100644 --- a/module/Core/test/Action/PixelActionTest.php +++ b/module/Core/test/Action/PixelActionTest.php @@ -14,7 +14,7 @@ use Shlinkio\Shlink\Common\Response\PixelResponse; use Shlinkio\Shlink\Core\Action\PixelAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; -use Shlinkio\Shlink\Core\Options\AppOptions; +use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Visit\VisitsTracker; @@ -34,7 +34,7 @@ class PixelActionTest extends TestCase $this->action = new PixelAction( $this->urlResolver->reveal(), $this->visitTracker->reveal(), - new AppOptions(), + new TrackingOptions(), ); } diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index f869e2c4..dde9144c 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -42,7 +42,7 @@ class RedirectActionTest extends TestCase $this->action = new RedirectAction( $this->urlResolver->reveal(), $this->visitTracker->reveal(), - new Options\AppOptions(['disableTrackParam' => 'foobar']), + new Options\TrackingOptions(['disableTrackParam' => 'foobar']), $this->redirectRespHelper->reveal(), ); } diff --git a/module/Core/test/Config/SimplifiedConfigParserTest.php b/module/Core/test/Config/SimplifiedConfigParserTest.php index 6f040bb6..f4e5c8f0 100644 --- a/module/Core/test/Config/SimplifiedConfigParserTest.php +++ b/module/Core/test/Config/SimplifiedConfigParserTest.php @@ -22,7 +22,7 @@ class SimplifiedConfigParserTest extends TestCase public function properlyMapsSimplifiedConfig(): void { $config = [ - 'app_options' => [ + 'tracking' => [ 'disable_track_param' => 'foo', ], @@ -70,8 +70,9 @@ class SimplifiedConfigParserTest extends TestCase 'port' => 8888, ]; $expected = [ - 'app_options' => [ + 'tracking' => [ 'disable_track_param' => 'bar', + 'anonymize_remote_addr' => false, ], 'entity_manager' => [ @@ -96,7 +97,6 @@ class SimplifiedConfigParserTest extends TestCase 'https://third-party.io/foo', ], 'default_short_codes_length' => 8, - 'anonymize_remote_addr' => false, 'redirect_status_code' => 301, 'redirect_cache_lifetime' => 90, ], diff --git a/module/Core/test/Model/VisitorTest.php b/module/Core/test/Model/VisitorTest.php index e1003056..50c277c4 100644 --- a/module/Core/test/Model/VisitorTest.php +++ b/module/Core/test/Model/VisitorTest.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Model; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Options\TrackingOptions; use function random_int; use function str_repeat; @@ -71,4 +72,28 @@ class VisitorTest extends TestCase } return $randomString; } + + /** @test */ + public function newNormalizedInstanceIsCreatedFromTrackingOptions(): void + { + $visitor = new Visitor( + $this->generateRandomString(2000), + $this->generateRandomString(2000), + $this->generateRandomString(2000), + $this->generateRandomString(2000), + ); + $normalizedVisitor = $visitor->normalizeForTrackingOptions(new TrackingOptions([ + 'disableIpTracking' => true, + 'disableReferrerTracking' => true, + 'disableUaTracking' => true, + ])); + + self::assertNotSame($visitor, $normalizedVisitor); + self::assertEmpty($normalizedVisitor->getUserAgent()); + self::assertNotEmpty($visitor->getUserAgent()); + self::assertEmpty($normalizedVisitor->getReferer()); + self::assertNotEmpty($visitor->getReferer()); + self::assertNull($normalizedVisitor->getRemoteAddress()); + self::assertNotNull($visitor->getRemoteAddress()); + } } diff --git a/module/Core/test/Visit/VisitsTrackerTest.php b/module/Core/test/Visit/VisitsTrackerTest.php index bba4e919..45188f6c 100644 --- a/module/Core/test/Visit/VisitsTrackerTest.php +++ b/module/Core/test/Visit/VisitsTrackerTest.php @@ -14,7 +14,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; +use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Visit\VisitsTracker; class VisitsTrackerTest extends TestCase @@ -24,7 +24,7 @@ class VisitsTrackerTest extends TestCase private VisitsTracker $visitsTracker; private ObjectProphecy $em; private ObjectProphecy $eventDispatcher; - private UrlShortenerOptions $options; + private TrackingOptions $options; public function setUp(): void { @@ -35,7 +35,7 @@ class VisitsTrackerTest extends TestCase }); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->options = new UrlShortenerOptions(); + $this->options = new TrackingOptions(); $this->visitsTracker = new VisitsTracker($this->em->reveal(), $this->eventDispatcher->reveal(), $this->options); } @@ -57,6 +57,22 @@ class VisitsTrackerTest extends TestCase $this->eventDispatcher->dispatch(Argument::type(UrlVisited::class))->shouldHaveBeenCalled(); } + /** + * @test + * @dataProvider provideTrackingMethodNames + */ + public function trackingIsSkippedCompletelyWhenDisabledFromOptions(string $method, array $args): void + { + $this->options->disableTracking = true; + + $this->visitsTracker->{$method}(...$args); + + $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->em->transactional(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->em->flush()->shouldNotHaveBeenCalled(); + } + public function provideTrackingMethodNames(): iterable { yield 'track' => ['track', [ShortUrl::createEmpty(), Visitor::emptyInstance()]];