Compare commits

..

17 Commits

Author SHA1 Message Date
Alejandro Celaya
15d49e97c0 Deleted ocular.phar before building assets for deployment 2018-10-18 21:53:24 +02:00
Alejandro Celaya
d5e7ce38ac Updated changelog 2018-10-18 21:47:00 +02:00
Alejandro Celaya
162d0560db Merge pull request #238 from acelaya/feature/fix-ip-address
Feature/fix ip address
2018-10-18 21:44:15 +02:00
Alejandro Celaya
1de05047ca Merge pull request #235 from tivyhosting/master
Improved update instructions + command fix
2018-10-18 21:28:13 +02:00
tivyhosting
2af5de1199 Made required fixes. 2018-10-18 12:06:07 -07:00
Alejandro Celaya
e66a724d2b Added fix on IP addresses discovery to changelog 2018-10-18 20:34:55 +02:00
Alejandro Celaya
9f4c2ac8d7 Inlined instructions to enable apcu and memcached in travis 2018-10-18 20:26:44 +02:00
Alejandro Celaya
44f0011445 Moved logic to create a visitor from a request to the visitor itself 2018-10-18 20:24:25 +02:00
Alejandro Celaya
545094cddf Used middleware from library to actually find visitor IP addresses 2018-10-18 20:19:29 +02:00
Alejandro Celaya
99f45d8853 Installed and registered new middleware to process IP addresses from request 2018-10-18 19:53:50 +02:00
Alejandro Celaya
c25b5f9938 Allowed failures on PHP 7.3 until a fix is found 2018-10-18 19:36:03 +02:00
Alejandro Celaya
db1304c11a Added unreleased changes to changelog 2018-10-18 19:24:02 +02:00
Alejandro Celaya
57714b373c Added php 7.3 to the travis build matrix 2018-10-18 19:23:07 +02:00
Alejandro Celaya
5be7f839f3 Ensured visits with empty remote address are not tried to be located 2018-10-18 19:22:24 +02:00
tivyhosting
aa441eb58b Update README.md 2018-10-16 11:00:25 -07:00
tivyhosting
e6b6a40fa6 added CLI info 2018-10-16 10:59:33 -07:00
tivyhosting
f6dde6f4c1 Improved update instructions + command fix 2018-10-16 10:56:15 -07:00
19 changed files with 290 additions and 80 deletions

View File

@@ -9,10 +9,15 @@ branches:
php:
- 7.1
- 7.2
- 7.3
matrix:
allow_failures:
- php: 7.3
before_install:
- phpenv config-add data/infra/travis-php/memcached.ini
- phpenv config-add data/infra/travis-php/apcu.ini
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
install:
- composer self-update
@@ -29,6 +34,7 @@ after_success:
# Before deploying, build dist file for current travis tag
before_deploy:
- rm -f ocular.phar
- ./build.sh ${TRAVIS_TAG#?}
deploy:

View File

@@ -1,5 +1,35 @@
# CHANGELOG
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## 1.13.2 - 2018-10-18
#### Added
* [#233](https://github.com/shlinkio/shlink/issues/233) Added PHP 7.3 to build matrix allowing its failure.
#### Changed
* [#235](https://github.com/shlinkio/shlink/issues/235) Improved update instructions (thanks to [tivyhosting](https://github.com/tivyhosting)).
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#237](https://github.com/shlinkio/shlink/issues/233) Solved errors when trying to geo-locate `null` IP addresses.
Also improved how visitor IP addresses are discovered, thanks to [akrabat/ip-address-middleware](https://github.com/akrabat/ip-address-middleware) package.
## 1.13.1 - 2018-10-16
#### Added

View File

@@ -104,21 +104,21 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
* Generate website previews: `/path/to/shlink/bin/cli shortcode:process-previews`
* Generate website previews: `/path/to/shlink/bin/cli short-url:process-previews`
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
## Update to new version
When a new Shlink version is available, you don't need to repeat the whole process yourself.
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
Instead, get the latest version as explained in previous step, and then, run the script `bin/update`.
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`)
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`)
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
The script will ask you for the location from previous shlink version, and use it in order to import the configuration.
The script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some the assets neccessary for Shlink to function.
It will then update the database and generate some assets.
Right now, it does not import cached info (like website previews), but it will. By now you will need to regenerate them again.
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
**Important!** It is recommended that you don't skip any version when using this process. The update gets better on every version, but older versions might make assumptions.
@@ -145,3 +145,44 @@ Once shlink is installed, there are two main ways to interact with it:
However, you probably don't want to consume the raw API yourself. That's why a nice [web client](https://github.com/shlinkio/shlink-web-client) is provided that can be directly used from [https://app.shlink.io](https://app.shlink.io), or you can host it yourself too.
Both the API and CLI allow you to do the same operations, except for API key management, which can be done from the command line interface only.
### Shlink CLI Help
```
Usage:
command [options] [arguments]
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
help Displays help for a command
list Lists commands
api-key
api-key:disable Disables an API key.
api-key:generate Generates a new valid API key.
api-key:list Lists all the available API keys.
config
config:generate-charset Generates a character set sample just by shuffling the default one, "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
config:generate-secret Generates a random secret string that can be used for JWT token encryption
short-url
short-url:delete [short-code:delete] Deletes a short URL
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
short-url:list [shortcode:list|short-code:list] List all short URLs
short-url:parse [shortcode:parse|short-code:parse] Returns the long URL behind a short code
short-url:process-previews [shortcode:process-previews|short-code:process-previews] Processes and generates the previews for every URL, improving performance for later web requests.
short-url:visits [shortcode:visits|short-code:visits] Returns the detailed visits information for provided short code
tag
tag:create Creates one or more tags.
tag:delete Deletes one or more tags.
tag:list Lists existing tags.
tag:rename Renames one existing tag.
visit
visit:process Processes visits where location is not set yet
```

View File

@@ -16,6 +16,7 @@
"ext-json": "*",
"ext-pdo": "*",
"acelaya/ze-content-based-error-handler": "^2.2",
"akrabat/ip-address-middleware": "^1.0",
"cakephp/chronos": "^1.2",
"cocur/slugify": "^3.0",
"doctrine/cache": "^1.6",

View File

@@ -1 +0,0 @@
extension="apcu.so"

View File

@@ -1 +0,0 @@
extension="memcached.so"

View File

@@ -59,6 +59,14 @@ class ProcessVisitsCommand extends Command
$count = 0;
foreach ($visits as $visit) {
if (! $visit->hasRemoteAddr()) {
$io->writeln(
sprintf('<comment>%s</comment>', $this->translator->translate('Ignored visit with no IP address')),
OutputInterface::VERBOSITY_VERBOSE
);
continue;
}
$ipAddr = $visit->getRemoteAddr();
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {

View File

@@ -11,8 +11,11 @@ use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Service\VisitService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use function count;
use function round;
class ProcessVisitsCommandTest extends TestCase
{
@@ -67,15 +70,15 @@ class ProcessVisitsCommandTest extends TestCase
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(0, \strpos($output, 'Processing IP 1.2.3.0'));
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 4.3.2.0'));
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 12.34.56.0'));
$this->assertContains('Processing IP 1.2.3.0', $output);
$this->assertContains('Processing IP 4.3.2.0', $output);
$this->assertContains('Processing IP 12.34.56.0', $output);
}
/**
* @test
*/
public function localhostAddressIsIgnored()
public function localhostAndEmptyAddressIsIgnored()
{
$visits = [
(new Visit())->setRemoteAddr('1.2.3.4'),
@@ -83,19 +86,22 @@ class ProcessVisitsCommandTest extends TestCase
(new Visit())->setRemoteAddr('12.34.56.78'),
(new Visit())->setRemoteAddr('127.0.0.1'),
(new Visit())->setRemoteAddr('127.0.0.1'),
(new Visit())->setRemoteAddr(''),
(new Visit())->setRemoteAddr(null),
];
$this->visitService->getUnlocatedVisits()->willReturn($visits)
->shouldBeCalledTimes(1);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(\count($visits) - 2);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 4);
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
->shouldBeCalledTimes(\count($visits) - 2);
->shouldBeCalledTimes(count($visits) - 4);
$this->commandTester->execute([
'command' => 'visit:process',
]);
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
$this->assertGreaterThan(0, \strpos($output, 'Ignored localhost address'));
$this->assertContains('Ignored localhost address', $output);
$this->assertContains('Ignored visit with no IP address', $output);
}
/**
@@ -130,8 +136,8 @@ class ProcessVisitsCommandTest extends TestCase
'command' => 'visit:process',
]);
$getApiLimit->shouldHaveBeenCalledTimes(\count($visits));
$getApiInterval->shouldHaveBeenCalledTimes(\round(\count($visits) / $apiLimit));
$resolveIpLocation->shouldHaveBeenCalledTimes(\count($visits));
$getApiLimit->shouldHaveBeenCalledTimes(count($visits));
$getApiInterval->shouldHaveBeenCalledTimes(round(count($visits) / $apiLimit));
$resolveIpLocation->shouldHaveBeenCalledTimes(count($visits));
}
}

View File

@@ -1,16 +1,14 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Factory;
use Shlinkio\Shlink\Common\Image;
use Shlinkio\Shlink\Common\Image\ImageBuilder;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Common\Service;
use Shlinkio\Shlink\Common\Template\Extension\TranslatorExtension;
use RKA\Middleware\IpAddress;
use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
@@ -21,14 +19,16 @@ return [
'dependencies' => [
'factories' => [
EntityManager::class => Factory\EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
GuzzleClient::class => InvokableFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Filesystem::class => InvokableFactory::class,
Translator::class => Factory\TranslatorFactory::class,
TranslatorExtension::class => ConfigAbstractFactory::class,
LocaleMiddleware::class => ConfigAbstractFactory::class,
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
@@ -37,7 +37,7 @@ return [
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
'httpClient' => GuzzleClient::class,
'translator' => Translator::class,
'logger' => LoggerInterface::class,
Logger::class => 'Logger_Shlink',
@@ -49,11 +49,11 @@ return [
],
ConfigAbstractFactory::class => [
TranslatorExtension::class => ['translator'],
LocaleMiddleware::class => ['translator'],
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Service\IpApiLocationResolver::class => ['httpClient'],
Service\PreviewGenerator::class => [
ImageBuilder::class,
Image\ImageBuilder::class,
Filesystem::class,
'config.preview_generation.files_location',
],

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Interop\Container\ContainerInterface;
use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class IpAddressMiddlewareFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): IpAddress
{
return new IpAddress(true, [], Visitor::REMOTE_ADDRESS_ATTR);
}
}

View File

@@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Util;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use function count;
use function explode;
use function implode;
use function trim;
final class IpAddress
{
@@ -43,9 +47,9 @@ final class IpAddress
*/
public static function fromString(string $address): self
{
$address = \trim($address);
$parts = \explode('.', $address);
if (\count($parts) !== self::IPV4_PARTS_COUNT) {
$address = trim($address);
$parts = explode('.', $address);
if (count($parts) !== self::IPV4_PARTS_COUNT) {
throw WrongIpException::fromIpAddress($address);
}
@@ -64,7 +68,7 @@ final class IpAddress
public function __toString(): string
{
return \implode('.', [
return implode('.', [
$this->firstOctet,
$this->secondOctet,
$this->thirdOctet,

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Shlinkio\Shlink\Core\Model\Visitor;
use Zend\ServiceManager\ServiceManager;
class IpAddressMiddlewareFactoryTest extends TestCase
{
private $factory;
public function setUp()
{
$this->factory = new IpAddressMiddlewareFactory();
}
/**
* @test
*/
public function returnedInstanceIsProperlyConfigured()
{
$instance = $this->factory->__invoke(new ServiceManager(), '');
$ref = new ReflectionObject($instance);
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
$checkProxyHeaders->setAccessible(true);
$trustedProxies = $ref->getProperty('trustedProxies');
$trustedProxies->setAccessible(true);
$attributeName = $ref->getProperty('attributeName');
$attributeName->setAccessible(true);
$this->assertTrue($checkProxyHeaders->getValue($instance));
$this->assertEquals([], $trustedProxies->getValue($instance));
$this->assertEquals(Visitor::REMOTE_ADDRESS_ATTR, $attributeName->getValue($instance));
}
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Action;
use Shlinkio\Shlink\Core\Middleware;
@@ -10,13 +11,19 @@ return [
[
'name' => 'long-url-redirect',
'path' => '/{shortCode}',
'middleware' => Action\RedirectAction::class,
'middleware' => [
IpAddress::class,
Action\RedirectAction::class,
],
'allowed_methods' => ['GET'],
],
[
'name' => 'pixel-tracking',
'path' => '/{shortCode}/track',
'middleware' => Action\PixelAction::class,
'middleware' => [
IpAddress::class,
Action\PixelAction::class,
],
'allowed_methods' => ['GET'],
],
[

View File

@@ -12,6 +12,7 @@ use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
@@ -69,7 +70,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
// Track visit to this short code
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
$this->visitTracker->track($shortCode, $request);
$this->visitTracker->track($shortCode, Visitor::fromRequest($request));
}
return $this->createResp($url->getLongUrl());

View File

@@ -102,6 +102,11 @@ class Visit extends AbstractEntity implements \JsonSerializable
return $this;
}
public function hasRemoteAddr(): bool
{
return ! empty($this->remoteAddr);
}
private function obfuscateAddress(?string $address): ?string
{
// Localhost addresses do not need to be obfuscated

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface;
final class Visitor
{
public const REMOTE_ADDRESS_ATTR = 'remote_address';
/**
* @var string
*/
private $userAgent;
/**
* @var string
*/
private $referer;
/**
* @var string|null
*/
private $remoteAddress;
public function __construct(string $userAgent, string $referer, ?string $remoteAddress)
{
$this->userAgent = $userAgent;
$this->referer = $referer;
$this->remoteAddress = $remoteAddress;
}
public static function fromRequest(ServerRequestInterface $request): self
{
return new self(
$request->getHeaderLine('User-Agent'),
$request->getHeaderLine('Referer'),
$request->getAttribute(self::REMOTE_ADDRESS_ATTR)
);
}
public static function emptyInstance(): self
{
return new self('', '', null);
}
public function getUserAgent(): string
{
return $this->userAgent;
}
public function getReferer(): string
{
return $this->referer;
}
public function getRemoteAddress(): ?string
{
return $this->remoteAddress;
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service;
use Doctrine\ORM;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
class VisitsTracker implements VisitsTrackerInterface
@@ -24,14 +24,9 @@ class VisitsTracker implements VisitsTrackerInterface
}
/**
* Tracks a new visit to provided short code, using an array of data to look up information
*
* @param string $shortCode
* @param ServerRequestInterface $request
* @throws ORM\ORMInvalidArgumentException
* @throws ORM\OptimisticLockException
* Tracks a new visit to provided short code from provided visitor
*/
public function track($shortCode, ServerRequestInterface $request): void
public function track(string $shortCode, Visitor $visitor): void
{
/** @var ShortUrl $shortUrl */
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
@@ -40,9 +35,9 @@ class VisitsTracker implements VisitsTrackerInterface
$visit = new Visit();
$visit->setShortUrl($shortUrl)
->setUserAgent($request->getHeaderLine('User-Agent'))
->setReferer($request->getHeaderLine('Referer'))
->setRemoteAddr($this->findOutRemoteAddr($request));
->setUserAgent($visitor->getUserAgent())
->setReferer($visitor->getReferer())
->setRemoteAddr($visitor->getRemoteAddress());
/** @var ORM\EntityManager $em */
$em = $this->em;
@@ -50,21 +45,6 @@ class VisitsTracker implements VisitsTrackerInterface
$em->flush($visit);
}
/**
* @param ServerRequestInterface $request
*/
private function findOutRemoteAddr(ServerRequestInterface $request): ?string
{
$forwardedFor = $request->getHeaderLine('X-Forwarded-For');
if (empty($forwardedFor)) {
$serverParams = $request->getServerParams();
return $serverParams['REMOTE_ADDR'] ?? null;
}
$ips = \explode(',', $forwardedFor);
return $ips[0] ?? null;
}
/**
* Returns the visits on certain short code
*

View File

@@ -3,20 +3,17 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Service;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\Visitor;
interface VisitsTrackerInterface
{
/**
* Tracks a new visit to provided short code, using an array of data to look up information
*
* @param string $shortCode
* @param ServerRequestInterface $request
* Tracks a new visit to provided short code from provided visitor
*/
public function track($shortCode, ServerRequestInterface $request): void;
public function track(string $shortCode, Visitor $visitor): void;
/**
* Returns the visits on certain short code

View File

@@ -10,9 +10,9 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Zend\Diactoros\ServerRequestFactory;
class VisitsTrackerTest extends TestCase
{
@@ -44,13 +44,13 @@ class VisitsTrackerTest extends TestCase
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals());
$this->visitsTracker->track($shortCode, Visitor::emptyInstance());
}
/**
* @test
*/
public function trackUsesForwardedForHeaderIfPresent()
public function trackedIpAddressGetsObfuscated()
{
$shortCode = '123ABC';
$test = $this;
@@ -65,9 +65,7 @@ class VisitsTrackerTest extends TestCase
})->shouldBeCalledTimes(1);
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals(
['REMOTE_ADDR' => '1.2.3.4']
)->withHeader('X-Forwarded-For', '4.3.2.1,99.99.99.99'));
$this->visitsTracker->track($shortCode, new Visitor('', '', '4.3.2.1'));
}
/**