Merge pull request #1319 from acelaya-forks/feature/emoji-support

Feature/emoji support
This commit is contained in:
Alejandro Celaya 2022-01-10 14:51:13 +01:00 committed by GitHub
commit 8cfb14198b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 231 additions and 89 deletions

View file

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
## [Unreleased]
### Added
* [#767](https://github.com/shlinkio/shlink/issues/767) Added full support to use emojis everywhere, whether it is custom slugs, titles, referrers, etc.
* [#1274](https://github.com/shlinkio/shlink/issues/1274) Added support to filter short URLs lists by all provided tags.
The `GET /short-urls` endpoint now accepts a `tagsMode=all` param which will make only short URLs matching **all** the tags in the `tags[]` query param, to be returned.

View file

@ -21,6 +21,13 @@ return (static function (): array {
'mssql' => '1433',
default => '3306',
};
$resolveCharset = static fn () => match ($driver) {
// This does not determine charsets or collations in tables or columns, but the charset used in the data
// flowing in the connection, so it has to match what has been set in the database.
'maria', 'mysql' => 'utf8mb4',
'postgres' => 'utf8',
default => null,
};
$resolveConnection = static fn () => match ($driver) {
null, 'sqlite' => [
'driver' => 'pdo_sqlite',
@ -34,7 +41,7 @@ return (static function (): array {
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', $resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null,
'charset' => 'utf8',
'charset' => $resolveCharset(),
],
};

View file

@ -11,6 +11,7 @@ return [
'driver' => 'pdo_mysql',
'host' => 'shlink_db_mysql',
'dbname' => 'shlink',
'charset' => 'utf8mb4',
],
],

View file

@ -55,6 +55,7 @@ $buildDbConnection = static function (): array {
'user' => 'postgres',
'password' => 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
],
'mssql' => [
'driver' => 'pdo_sqlsrv',
@ -70,6 +71,7 @@ $buildDbConnection = static function (): array {
'user' => 'root',
'password' => 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8mb4',
],
};
};

View file

@ -5,45 +5,45 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
use function is_subclass_of;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160819142757 extends AbstractMigration
{
private const MYSQL = 'mysql';
private const SQLITE = 'sqlite';
/**
* @throws Exception
* @throws SchemaException
*/
public function up(Schema $schema): void
{
$db = $this->connection->getDatabasePlatform()->getName();
$platformClass = $this->connection->getDatabasePlatform();
$table = $schema->getTable('short_urls');
$column = $table->getColumn('short_code');
if ($db === self::MYSQL) {
$column->setPlatformOption('collation', 'utf8_bin');
} elseif ($db === self::SQLITE) {
$column->setPlatformOption('collate', 'BINARY');
}
match (true) {
is_subclass_of($platformClass, MySQLPlatform::class) => $column
->setPlatformOption('charset', 'utf8mb4')
->setPlatformOption('collation', 'utf8mb4_bin'),
is_subclass_of($platformClass, SqlitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
default => null,
};
}
/**
* @throws Exception
*/
public function down(Schema $schema): void
{
$this->connection->getDatabasePlatform()->getName();
// Nothing to roll back
}
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -76,6 +77,6 @@ class Version20160820191203 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types;
@ -48,6 +49,6 @@ class Version20171021093246 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types;
@ -45,6 +46,6 @@ class Version20171022064541 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
@ -42,6 +43,6 @@ final class Version20180801183328 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use PDO;
@ -69,6 +70,6 @@ final class Version20180913205455 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
@ -50,6 +51,6 @@ final class Version20180915110857 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;
@ -58,7 +59,7 @@ final class Version20181020060559 extends AbstractMigration
foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) {
$qb->set($snakeCaseName, $camelCaseName);
}
$qb->execute();
$qb->executeStatement();
}
public function down(Schema $schema): void
@ -68,6 +69,6 @@ final class Version20181020060559 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
@ -41,6 +42,6 @@ final class Version20181020065148 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20181110175521 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20190824075137 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types;
@ -55,6 +56,6 @@ final class Version20190930165521 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
@ -49,6 +50,6 @@ final class Version20191001201532 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20191020074522 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -38,7 +40,7 @@ final class Version20200105165647 extends AbstractMigration
'zeroValue' => '0',
'emptyString' => '',
])
->execute();
->executeStatement();
}
}
@ -61,14 +63,14 @@ final class Version20200105165647 extends AbstractMigration
*/
public function postUp(Schema $schema): void
{
$platformName = $this->connection->getDatabasePlatform()->getName();
$castType = $platformName === 'postgres' ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)';
$isPostgres = $this->connection->getDatabasePlatform() instanceof PostgreSQLPlatform;
$castType = $isPostgres ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)';
foreach (self::COLUMNS as $newName => $oldName) {
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')')
->execute();
->executeStatement();
}
}
@ -78,7 +80,7 @@ final class Version20200105165647 extends AbstractMigration
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($oldName, $newName)
->execute();
->executeStatement();
}
}
@ -96,6 +98,6 @@ final class Version20200105165647 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -47,6 +48,6 @@ final class Version20200106215144 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
@ -36,6 +38,9 @@ final class Version20200110182849 extends AbstractMigration
);
}
/**
* @throws Exception
*/
public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void
{
$qb = $this->connection->createQueryBuilder();
@ -43,7 +48,7 @@ final class Version20200110182849 extends AbstractMigration
->set($columnName, ':emptyValue')
->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE)
->where($qb->expr()->isNull($columnName))
->execute();
->executeStatement();
}
public function down(Schema $schema): void
@ -53,6 +58,6 @@ final class Version20200110182849 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -32,7 +33,7 @@ final class Version20200323190014 extends AbstractMigration
->andWhere($qb->expr()->eq('lon', 0))
->setParameter('isEmpty', true)
->setParameter('emptyString', '')
->execute();
->executeStatement();
}
public function down(Schema $schema): void
@ -45,6 +46,6 @@ final class Version20200323190014 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
@ -27,6 +28,6 @@ final class Version20200503170404 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -44,6 +45,6 @@ final class Version20201023090929 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -6,6 +6,7 @@ namespace ShlinkMigrations;
use Cake\Chronos\Chronos;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -86,6 +87,6 @@ final class Version20201102113208 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -52,6 +53,6 @@ final class Version20210102174433 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20210118153932 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -36,6 +37,6 @@ final class Version20210202181026 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -43,6 +44,6 @@ final class Version20210207100807 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -37,6 +38,6 @@ final class Version20210306165711 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20210522051601 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -28,6 +29,6 @@ final class Version20210522124633 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
@ -41,6 +42,6 @@ final class Version20210720143824 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20211002072605 extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220110113313 extends AbstractMigration
{
private const CHARSET = 'utf8mb4';
private const COLLATIONS = [
'short_urls' => [
'original_url' => 'unicode_ci',
'short_code' => 'bin',
'import_original_short_code' => 'unicode_ci',
'title' => 'unicode_ci',
],
'domains' => [
'authority' => 'unicode_ci',
'base_url_redirect' => 'unicode_ci',
'regular_not_found_redirect' => 'unicode_ci',
'invalid_short_url_redirect' => 'unicode_ci',
],
'tags' => [
'name' => 'unicode_ci',
],
'visits' => [
'referer' => 'unicode_ci',
'user_agent' => 'unicode_ci',
'visited_url' => 'unicode_ci',
],
'visit_locations' => [
'country_code' => 'unicode_ci',
'country_name' => 'unicode_ci',
'region_name' => 'unicode_ci',
'city_name' => 'unicode_ci',
'timezone' => 'unicode_ci',
],
];
public function up(Schema $schema): void
{
$this->skipIf(! $this->isMySql(), 'This only sets MySQL-specific database options');
foreach (self::COLLATIONS as $tableName => $columns) {
$table = $schema->getTable($tableName);
foreach ($columns as $columnName => $collation) {
$table->getColumn($columnName)
->setPlatformOption('charset', self::CHARSET)
->setPlatformOption('collation', self::CHARSET . '_' . $collation);
}
}
}
public function down(Schema $schema): void
{
// No down
}
public function isTransactional(): bool
{
return ! $this->isMySql();
}
private function isMySql(): bool
{
return $this->connection->getDatabasePlatform() instanceof MySQLPlatform;
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace <namespace>;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
@ -21,6 +22,6 @@ final class <className> extends AbstractMigration
public function isTransactional(): bool
{
return $this->connection->getDatabasePlatform()->getName() !== 'mysql';
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View file

@ -21,21 +21,21 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->option('unsigned', true)
->build();
$builder->createField('authority', Types::STRING)
fieldWithUtf8Charset($builder->createField('authority', Types::STRING), $emConfig)
->unique()
->build();
$builder->createField('baseUrlRedirect', Types::STRING)
fieldWithUtf8Charset($builder->createField('baseUrlRedirect', Types::STRING), $emConfig)
->columnName('base_url_redirect')
->nullable()
->build();
$builder->createField('regular404Redirect', Types::STRING)
fieldWithUtf8Charset($builder->createField('regular404Redirect', Types::STRING), $emConfig)
->columnName('regular_not_found_redirect')
->nullable()
->build();
$builder->createField('invalidShortUrlRedirect', Types::STRING)
fieldWithUtf8Charset($builder->createField('invalidShortUrlRedirect', Types::STRING), $emConfig)
->columnName('invalid_short_url_redirect')
->nullable()
->build();

View file

@ -23,12 +23,12 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->option('unsigned', true)
->build();
$builder->createField('longUrl', Types::STRING)
fieldWithUtf8Charset($builder->createField('longUrl', Types::STRING), $emConfig)
->columnName('original_url')
->length(2048)
->build();
$builder->createField('shortCode', Types::STRING)
fieldWithUtf8Charset($builder->createField('shortCode', Types::STRING), $emConfig, 'bin')
->columnName('short_code')
->length(255)
->build();
@ -57,7 +57,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->nullable()
->build();
$builder->createField('importOriginalShortCode', Types::STRING)
fieldWithUtf8Charset($builder->createField('importOriginalShortCode', Types::STRING), $emConfig)
->columnName('import_original_short_code')
->nullable()
->build();
@ -85,7 +85,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
$builder->createField('title', Types::STRING)
fieldWithUtf8Charset($builder->createField('title', Types::STRING), $emConfig)
->columnName('title')
->length(512)
->nullable()

View file

@ -21,7 +21,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->option('unsigned', true)
->build();
$builder->createField('name', Types::STRING)
fieldWithUtf8Charset($builder->createField('name', Types::STRING), $emConfig)
->unique()
->build();

View file

@ -23,7 +23,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->option('unsigned', true)
->build();
$builder->createField('referer', Types::STRING)
fieldWithUtf8Charset($builder->createField('referer', Types::STRING), $emConfig)
->nullable()
->length(Visitor::REFERER_MAX_LENGTH)
->build();
@ -40,7 +40,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->nullable()
->build();
$builder->createField('userAgent', Types::STRING)
fieldWithUtf8Charset($builder->createField('userAgent', Types::STRING), $emConfig)
->columnName('user_agent')
->length(Visitor::USER_AGENT_MAX_LENGTH)
->nullable()
@ -55,7 +55,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->cascadePersist()
->build();
$builder->createField('visitedUrl', Types::STRING)
fieldWithUtf8Charset($builder->createField('visitedUrl', Types::STRING), $emConfig)
->columnName('visited_url')
->length(Visitor::VISITED_URL_MAX_LENGTH)
->nullable()

View file

@ -29,7 +29,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
];
foreach ($columns as $columnName => $fieldName) {
$builder->createField($fieldName, Types::STRING)
fieldWithUtf8Charset($builder->createField($fieldName, Types::STRING), $emConfig)
->columnName($columnName)
->nullable()
->build();

View file

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core;
use Cake\Chronos\Chronos;
use DateTimeInterface;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Jaybizzle\CrawlerDetect\CrawlerDetect;
use Laminas\InputFilter\InputFilter;
use PUGX\Shortid\Factory as ShortIdFactory;
@ -13,13 +14,10 @@ use Shlinkio\Shlink\Common\Util\DateRange;
use function Functional\reduce_left;
use function is_array;
use function lcfirst;
use function print_r;
use function Shlinkio\Shlink\Common\buildDateRange;
use function sprintf;
use function str_repeat;
use function str_replace;
use function ucwords;
function generateRandomShortCode(int $length): string
{
@ -34,7 +32,7 @@ function generateRandomShortCode(int $length): string
function parseDateFromQuery(array $query, string $dateName): ?Chronos
{
return ! isset($query[$dateName]) || empty($query[$dateName]) ? null : Chronos::parse($query[$dateName]);
return empty($query[$dateName] ?? null) ? null : Chronos::parse($query[$dateName]);
}
function parseDateRangeFromQuery(array $query, string $startDateName, string $endDateName): DateRange
@ -100,11 +98,6 @@ function arrayToString(array $array, int $indentSize = 4): string
}, '');
}
function kebabCaseToCamelCase(string $name): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $name))));
}
function isCrawler(string $userAgent): bool
{
static $detector;
@ -114,3 +107,12 @@ function isCrawler(string $userAgent): bool
return $detector->isCrawler($userAgent);
}
function fieldWithUtf8Charset(FieldBuilder $field, array $emConfig, string $collation = 'unicode_ci'): FieldBuilder
{
return match ($emConfig['connection']['driver'] ?? null) {
'pdo_mysql' => $field->option('charset', 'utf8mb4')
->option('collation', 'utf8mb4_' . $collation),
default => $field,
};
}

View file

@ -17,19 +17,17 @@ class ShortUrlStringifier implements ShortUrlStringifierInterface
public function stringify(ShortUrl $shortUrl): string
{
return (new Uri())->withPath($shortUrl->getShortCode())
->withScheme($this->domainConfig['schema'] ?? 'http')
$uriWithoutShortCode = (new Uri())->withScheme($this->domainConfig['schema'] ?? 'http')
->withHost($this->resolveDomain($shortUrl))
->withPath($this->basePath)
->__toString();
// The short code needs to be appended to avoid it from being URL-encoded
return sprintf('%s/%s', $uriWithoutShortCode, $shortUrl->getShortCode());
}
private function resolveDomain(ShortUrl $shortUrl): string
{
$domain = $shortUrl->getDomain();
if ($domain === null) {
return $this->domainConfig['hostname'] ?? '';
}
return sprintf('%s%s', $domain->getAuthority(), $this->basePath);
return $shortUrl->getDomain()?->getAuthority() ?? $this->domainConfig['hostname'] ?? '';
}
}

View file

@ -43,6 +43,18 @@ class ShortUrlStringifierTest extends TestCase
$shortUrlWithShortCode('bar'),
'http://example.com/bar',
];
yield 'special chars in short code' => [
['hostname' => 'example.com'],
'',
$shortUrlWithShortCode('グーグル'),
'http://example.com/グーグル',
];
yield 'emojis in short code' => [
['hostname' => 'example.com'],
'',
$shortUrlWithShortCode('🦣-🍅'),
'http://example.com/🦣-🍅',
];
yield 'hostname with base path in config' => [
['hostname' => 'example.com/foo/bar'],
'',

View file

@ -315,11 +315,22 @@ class CreateShortUrlTest extends ApiTestCase
yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
}
/** @test */
public function canCreateShortUrlsWithEmojis(): void
{
[$statusCode, $payload] = $this->createShortUrl([
'longUrl' => 'https://emojipedia.org/fire/',
'title' => '🔥🔥🔥',
'customSlug' => '🦣🦣🦣',
]);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('🔥🔥🔥', $payload['title']);
self::assertEquals('🦣🦣🦣', $payload['shortCode']);
self::assertEquals('http://doma.in/🦣🦣🦣', $payload['shortUrl']);
}
/**
* @return array {
* @var int $statusCode
* @var array $payload
* }
* @return array{int $statusCode, array $payload}
*/
private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array
{