diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 015d459e..5d31ae2d 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -20,6 +20,7 @@ return [ '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 40173d69..4ddd52e5 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -126,6 +126,7 @@ return [ '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), + 'track_orphan_visits' => (bool) env('TRACK_ORPHAN_VISITS', true), ], 'not_found_redirects' => $helper->getNotFoundRedirectsConfig(), diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 43586e16..1b83ad7d 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -85,7 +85,7 @@ return [ Visit\VisitsTracker::class => [ 'em', EventDispatcherInterface::class, - 'config.url_shortener.anonymize_remote_addr', + Options\UrlShortenerOptions::class, ], Service\ShortUrlService::class => [ 'em', diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index ebedbf97..e1956203 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -19,6 +19,8 @@ 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 { @@ -62,9 +64,28 @@ class UrlShortenerOptions extends AbstractOptions return $this->autoResolveTitles; } - protected function setAutoResolveTitles(bool $autoResolveTitles): self + protected function setAutoResolveTitles(bool $autoResolveTitles): void { $this->autoResolveTitles = $autoResolveTitles; - return $this; + } + + public function anonymizeRemoteAddr(): bool + { + return $this->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; } } diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index 48157e3b..306da7a9 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -10,41 +10,57 @@ 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; class VisitsTracker implements VisitsTrackerInterface { private ORM\EntityManagerInterface $em; private EventDispatcherInterface $eventDispatcher; - private bool $anonymizeRemoteAddr; + private UrlShortenerOptions $options; public function __construct( ORM\EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, - bool $anonymizeRemoteAddr + UrlShortenerOptions $options ) { $this->em = $em; $this->eventDispatcher = $eventDispatcher; - $this->anonymizeRemoteAddr = $anonymizeRemoteAddr; + $this->options = $options; } public function track(ShortUrl $shortUrl, Visitor $visitor): void { - $this->trackVisit(Visit::forValidShortUrl($shortUrl, $visitor, $this->anonymizeRemoteAddr), $visitor); + $this->trackVisit( + Visit::forValidShortUrl($shortUrl, $visitor, $this->options->anonymizeRemoteAddr()), + $visitor, + ); } public function trackInvalidShortUrlVisit(Visitor $visitor): void { - $this->trackVisit(Visit::forInvalidShortUrl($visitor, $this->anonymizeRemoteAddr), $visitor); + if (! $this->options->trackOrphanVisits()) { + return; + } + + $this->trackVisit(Visit::forInvalidShortUrl($visitor, $this->options->anonymizeRemoteAddr()), $visitor); } public function trackBaseUrlVisit(Visitor $visitor): void { - $this->trackVisit(Visit::forBasePath($visitor, $this->anonymizeRemoteAddr), $visitor); + if (! $this->options->trackOrphanVisits()) { + return; + } + + $this->trackVisit(Visit::forBasePath($visitor, $this->options->anonymizeRemoteAddr()), $visitor); } public function trackRegularNotFoundVisit(Visitor $visitor): void { - $this->trackVisit(Visit::forRegularNotFound($visitor, $this->anonymizeRemoteAddr), $visitor); + if (! $this->options->trackOrphanVisits()) { + return; + } + + $this->trackVisit(Visit::forRegularNotFound($visitor, $this->options->anonymizeRemoteAddr()), $visitor); } private function trackVisit(Visit $visit, Visitor $visitor): void diff --git a/module/Core/test/Visit/VisitsTrackerTest.php b/module/Core/test/Visit/VisitsTrackerTest.php index fd6e341f..118ebc06 100644 --- a/module/Core/test/Visit/VisitsTrackerTest.php +++ b/module/Core/test/Visit/VisitsTrackerTest.php @@ -14,6 +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\Visit\VisitsTracker; class VisitsTrackerTest extends TestCase @@ -23,13 +24,15 @@ class VisitsTrackerTest extends TestCase private VisitsTracker $visitsTracker; private ObjectProphecy $em; private ObjectProphecy $eventDispatcher; + private UrlShortenerOptions $options; public function setUp(): void { $this->em = $this->prophesize(EntityManager::class); $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); + $this->options = new UrlShortenerOptions(); - $this->visitsTracker = new VisitsTracker($this->em->reveal(), $this->eventDispatcher->reveal(), true); + $this->visitsTracker = new VisitsTracker($this->em->reveal(), $this->eventDispatcher->reveal(), $this->options); } /** @@ -53,4 +56,26 @@ class VisitsTrackerTest extends TestCase yield 'trackBaseUrlVisit' => ['trackBaseUrlVisit', [Visitor::emptyInstance()]]; yield 'trackRegularNotFoundVisit' => ['trackRegularNotFoundVisit', [Visitor::emptyInstance()]]; } + + /** + * @test + * @dataProvider provideOrphanTrackingMethodNames + */ + public function orphanVisitsAreNotTrackedWhenDisabled(string $method): void + { + $this->options->trackOrphanVisits = false; + + $this->visitsTracker->{$method}(Visitor::emptyInstance()); + + $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->em->flush()->shouldNotHaveBeenCalled(); + } + + public function provideOrphanTrackingMethodNames(): iterable + { + yield 'trackInvalidShortUrlVisit' => ['trackInvalidShortUrlVisit']; + yield 'trackBaseUrlVisit' => ['trackBaseUrlVisit']; + yield 'trackRegularNotFoundVisit' => ['trackRegularNotFoundVisit']; + } }