mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 13:23:33 +03:00
Ensured custom slugs are case sensitive
This commit is contained in:
parent
aa413dab6d
commit
d7e89ebdae
5 changed files with 95 additions and 39 deletions
23
config/autoload/slugify.global.php
Normal file
23
config/autoload/slugify.global.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'slugify_options' => [
|
||||
'lowercase' => false,
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Slugify::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Slugify::class => ['config.slugify_options'],
|
||||
],
|
||||
|
||||
];
|
|
@ -1,13 +1,12 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Action;
|
||||
use Shlinkio\Shlink\Core\Middleware;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Zend\Expressive\Router\RouterInterface;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
@ -16,12 +15,13 @@ return [
|
|||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
NotFoundHandler::class => ConfigAbstractFactory::class,
|
||||
|
||||
Options\AppOptions::class => ConfigAbstractFactory::class,
|
||||
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
|
||||
Options\NotFoundShortUrlOptions::class => ConfigAbstractFactory::class,
|
||||
NotFoundHandler::class => ConfigAbstractFactory::class,
|
||||
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
||||
|
||||
// Services
|
||||
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
||||
Service\VisitsTracker::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrlService::class => ConfigAbstractFactory::class,
|
||||
|
@ -29,11 +29,11 @@ return [
|
|||
Service\Tag\TagService::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||
|
||||
// Middleware
|
||||
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
||||
Action\PixelAction::class => ConfigAbstractFactory::class,
|
||||
Action\QrCodeAction::class => ConfigAbstractFactory::class,
|
||||
Action\PreviewAction::class => ConfigAbstractFactory::class,
|
||||
|
||||
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
@ -44,21 +44,15 @@ return [
|
|||
Options\AppOptions::class => ['config.app_options'],
|
||||
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],
|
||||
Options\NotFoundShortUrlOptions::class => ['config.url_shortener.not_found_short_url'],
|
||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
|
||||
// Services
|
||||
Service\UrlShortener::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'config.url_shortener.validate_url',
|
||||
'config.url_shortener.shortcode_chars',
|
||||
],
|
||||
Service\UrlShortener::class => ['httpClient', 'em', Options\UrlShortenerOptions::class, Slugify::class],
|
||||
Service\VisitsTracker::class => ['em'],
|
||||
Service\ShortUrlService::class => ['em'],
|
||||
Service\VisitService::class => ['em'],
|
||||
Service\Tag\TagService::class => ['em'],
|
||||
Service\ShortUrl\DeleteShortUrlService::class => ['em', Options\DeleteShortUrlsOptions::class],
|
||||
|
||||
// Middleware
|
||||
Action\RedirectAction::class => [
|
||||
Service\UrlShortener::class,
|
||||
Service\VisitsTracker::class,
|
||||
|
@ -74,6 +68,7 @@ return [
|
|||
],
|
||||
Action\QrCodeAction::class => [RouterInterface::class, Service\UrlShortener::class, 'Logger_Shlink'],
|
||||
Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class, 'Logger_Shlink'],
|
||||
|
||||
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
|
||||
],
|
||||
|
||||
|
|
40
module/Core/src/Options/UrlShortenerOptions.php
Normal file
40
module/Core/src/Options/UrlShortenerOptions.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Zend\Stdlib\AbstractOptions;
|
||||
|
||||
class UrlShortenerOptions extends AbstractOptions
|
||||
{
|
||||
public const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
||||
|
||||
// phpcs:disable
|
||||
protected $__strictMode__ = false;
|
||||
// phpcs:enable
|
||||
|
||||
private $shortcodeChars = self::DEFAULT_CHARS;
|
||||
private $validateUrl = true;
|
||||
|
||||
public function getChars(): string
|
||||
{
|
||||
return $this->shortcodeChars;
|
||||
}
|
||||
|
||||
protected function setShortcodeChars(string $shortcodeChars): self
|
||||
{
|
||||
$this->shortcodeChars = empty($shortcodeChars) ? self::DEFAULT_CHARS : $shortcodeChars;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isUrlValidationEnabled(): bool
|
||||
{
|
||||
return $this->validateUrl;
|
||||
}
|
||||
|
||||
protected function setValidateUrl($validateUrl): self
|
||||
{
|
||||
$this->validateUrl = (bool) $validateUrl;
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Cocur\Slugify\SlugifyInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
|
@ -17,6 +16,7 @@ use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
|||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Throwable;
|
||||
|
@ -29,32 +29,29 @@ class UrlShortener implements UrlShortenerInterface
|
|||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
public const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
||||
/** @deprecated */
|
||||
public const DEFAULT_CHARS = UrlShortenerOptions::DEFAULT_CHARS;
|
||||
private const ID_INCREMENT = 200000;
|
||||
|
||||
/** @var ClientInterface */
|
||||
private $httpClient;
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
/** @var string */
|
||||
private $chars;
|
||||
/** @var SlugifyInterface */
|
||||
private $slugger;
|
||||
/** @var bool */
|
||||
private $urlValidationEnabled;
|
||||
/** @var UrlShortenerOptions */
|
||||
private $options;
|
||||
|
||||
public function __construct(
|
||||
ClientInterface $httpClient,
|
||||
EntityManagerInterface $em,
|
||||
$urlValidationEnabled,
|
||||
$chars = self::DEFAULT_CHARS,
|
||||
SlugifyInterface $slugger = null
|
||||
UrlShortenerOptions $options,
|
||||
SlugifyInterface $slugger
|
||||
) {
|
||||
$this->httpClient = $httpClient;
|
||||
$this->em = $em;
|
||||
$this->urlValidationEnabled = (bool) $urlValidationEnabled;
|
||||
$this->chars = empty($chars) ? self::DEFAULT_CHARS : $chars;
|
||||
$this->slugger = $slugger ?: new Slugify();
|
||||
$this->options = $options;
|
||||
$this->slugger = $slugger;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,7 +68,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||
?int $maxVisits = null
|
||||
): ShortUrl {
|
||||
// If the URL validation is enabled, check that the URL actually exists
|
||||
if ($this->urlValidationEnabled) {
|
||||
if ($this->options->isUrlValidationEnabled()) {
|
||||
$this->checkUrlExists($url);
|
||||
}
|
||||
$customSlug = $this->processCustomSlug($customSlug);
|
||||
|
@ -121,16 +118,18 @@ class UrlShortener implements UrlShortenerInterface
|
|||
private function convertAutoincrementIdToShortCode(float $id): string
|
||||
{
|
||||
$id += self::ID_INCREMENT; // Increment the Id so that the generated shortcode is not too short
|
||||
$length = strlen($this->chars);
|
||||
$chars = $this->options->getChars();
|
||||
|
||||
$length = strlen($chars);
|
||||
$code = '';
|
||||
|
||||
while ($id > 0) {
|
||||
// Determine the value of the next higher character in the short code and prepend it
|
||||
$code = $this->chars[(int) fmod($id, $length)] . $code;
|
||||
$code = $chars[(int) fmod($id, $length)] . $code;
|
||||
$id = floor($id / $length);
|
||||
}
|
||||
|
||||
return $this->chars[(int) $id] . $code;
|
||||
return $chars[(int) $id] . $code;
|
||||
}
|
||||
|
||||
private function processCustomSlug(?string $customSlug): ?string
|
||||
|
@ -157,9 +156,11 @@ class UrlShortener implements UrlShortenerInterface
|
|||
*/
|
||||
public function shortCodeToUrl(string $shortCode): ShortUrl
|
||||
{
|
||||
$chars = $this->options->getChars();
|
||||
|
||||
// Validate short code format
|
||||
if (! preg_match('|[' . $this->chars . ']+|', $shortCode)) {
|
||||
throw InvalidShortCodeException::fromCharset($shortCode, $this->chars);
|
||||
if (! preg_match('|[' . $chars . ']+|', $shortCode)) {
|
||||
throw InvalidShortCodeException::fromCharset($shortCode, $chars);
|
||||
}
|
||||
|
||||
/** @var ShortUrlRepository $shortUrlRepo */
|
||||
|
|
|
@ -17,6 +17,7 @@ use Prophecy\Prophecy\MethodProphecy;
|
|||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
@ -57,16 +58,12 @@ class UrlShortenerTest extends TestCase
|
|||
$this->setUrlShortener(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $urlValidationEnabled
|
||||
*/
|
||||
public function setUrlShortener($urlValidationEnabled)
|
||||
public function setUrlShortener(bool $urlValidationEnabled)
|
||||
{
|
||||
$this->urlShortener = new UrlShortener(
|
||||
$this->httpClient->reveal(),
|
||||
$this->em->reveal(),
|
||||
$urlValidationEnabled,
|
||||
UrlShortener::DEFAULT_CHARS,
|
||||
new UrlShortenerOptions(['validate_url' => $urlValidationEnabled]),
|
||||
$this->slugger->reveal()
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue