mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-03 22:03:13 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15d49e97c0 | ||
|
|
d5e7ce38ac | ||
|
|
162d0560db | ||
|
|
1de05047ca | ||
|
|
2af5de1199 | ||
|
|
e66a724d2b | ||
|
|
9f4c2ac8d7 | ||
|
|
44f0011445 | ||
|
|
545094cddf | ||
|
|
99f45d8853 | ||
|
|
c25b5f9938 | ||
|
|
db1304c11a | ||
|
|
57714b373c | ||
|
|
5be7f839f3 | ||
|
|
aa441eb58b | ||
|
|
e6b6a40fa6 | ||
|
|
f6dde6f4c1 |
10
.travis.yml
10
.travis.yml
@@ -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:
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -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
|
||||
|
||||
55
README.md
55
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
extension="apcu.so"
|
||||
@@ -1 +0,0 @@
|
||||
extension="memcached.so"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
28
module/Common/src/Middleware/IpAddressMiddlewareFactory.php
Normal file
28
module/Common/src/Middleware/IpAddressMiddlewareFactory.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
60
module/Core/src/Model/Visitor.php
Normal file
60
module/Core/src/Model/Visitor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user