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] ## [Unreleased]
### Added ### 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. * [#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. 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', 'mssql' => '1433',
default => '3306', 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) { $resolveConnection = static fn () => match ($driver) {
null, 'sqlite' => [ null, 'sqlite' => [
'driver' => 'pdo_sqlite', 'driver' => 'pdo_sqlite',
@ -34,7 +41,7 @@ return (static function (): array {
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null), 'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', $resolveDefaultPort()), 'port' => env('DB_PORT', $resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null, 'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null,
'charset' => 'utf8', 'charset' => $resolveCharset(),
], ],
}; };

View file

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

View file

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

View file

@ -5,45 +5,45 @@ declare(strict_types=1);
namespace ShlinkMigrations; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
use function is_subclass_of;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
*/ */
class Version20160819142757 extends AbstractMigration class Version20160819142757 extends AbstractMigration
{ {
private const MYSQL = 'mysql';
private const SQLITE = 'sqlite';
/** /**
* @throws Exception * @throws Exception
* @throws SchemaException * @throws SchemaException
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$db = $this->connection->getDatabasePlatform()->getName(); $platformClass = $this->connection->getDatabasePlatform();
$table = $schema->getTable('short_urls'); $table = $schema->getTable('short_urls');
$column = $table->getColumn('short_code'); $column = $table->getColumn('short_code');
if ($db === self::MYSQL) { match (true) {
$column->setPlatformOption('collation', 'utf8_bin'); is_subclass_of($platformClass, MySQLPlatform::class) => $column
} elseif ($db === self::SQLITE) { ->setPlatformOption('charset', 'utf8mb4')
$column->setPlatformOption('collate', 'BINARY'); ->setPlatformOption('collation', 'utf8mb4_bin'),
} is_subclass_of($platformClass, SqlitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
default => null,
};
} }
/**
* @throws Exception
*/
public function down(Schema $schema): void public function down(Schema $schema): void
{ {
$this->connection->getDatabasePlatform()->getName(); // Nothing to roll back
} }
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -76,6 +77,6 @@ class Version20160820191203 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@ -48,6 +49,6 @@ class Version20171021093246 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@ -45,6 +46,6 @@ class Version20171022064541 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -42,6 +43,6 @@ final class Version20180801183328 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
use PDO; use PDO;
@ -69,6 +70,6 @@ final class Version20180913205455 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -50,6 +51,6 @@ final class Version20180915110857 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Table;
@ -58,7 +59,7 @@ final class Version20181020060559 extends AbstractMigration
foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) { foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) {
$qb->set($snakeCaseName, $camelCaseName); $qb->set($snakeCaseName, $camelCaseName);
} }
$qb->execute(); $qb->executeStatement();
} }
public function down(Schema $schema): void public function down(Schema $schema): void
@ -68,6 +69,6 @@ final class Version20181020060559 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -41,6 +42,6 @@ final class Version20181020065148 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20181110175521 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20190824075137 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@ -55,6 +56,6 @@ final class Version20190930165521 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
@ -49,6 +50,6 @@ final class Version20191001201532 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
@ -37,6 +38,6 @@ final class Version20191020074522 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -38,7 +40,7 @@ final class Version20200105165647 extends AbstractMigration
'zeroValue' => '0', 'zeroValue' => '0',
'emptyString' => '', 'emptyString' => '',
]) ])
->execute(); ->executeStatement();
} }
} }
@ -61,14 +63,14 @@ final class Version20200105165647 extends AbstractMigration
*/ */
public function postUp(Schema $schema): void public function postUp(Schema $schema): void
{ {
$platformName = $this->connection->getDatabasePlatform()->getName(); $isPostgres = $this->connection->getDatabasePlatform() instanceof PostgreSQLPlatform;
$castType = $platformName === 'postgres' ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)'; $castType = $isPostgres ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)';
foreach (self::COLUMNS as $newName => $oldName) { foreach (self::COLUMNS as $newName => $oldName) {
$qb = $this->connection->createQueryBuilder(); $qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations') $qb->update('visit_locations')
->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')') ->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')')
->execute(); ->executeStatement();
} }
} }
@ -78,7 +80,7 @@ final class Version20200105165647 extends AbstractMigration
$qb = $this->connection->createQueryBuilder(); $qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations') $qb->update('visit_locations')
->set($oldName, $newName) ->set($oldName, $newName)
->execute(); ->executeStatement();
} }
} }
@ -96,6 +98,6 @@ final class Version20200105165647 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -47,6 +48,6 @@ final class Version20200106215144 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -36,6 +38,9 @@ final class Version20200110182849 extends AbstractMigration
); );
} }
/**
* @throws Exception
*/
public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void
{ {
$qb = $this->connection->createQueryBuilder(); $qb = $this->connection->createQueryBuilder();
@ -43,7 +48,7 @@ final class Version20200110182849 extends AbstractMigration
->set($columnName, ':emptyValue') ->set($columnName, ':emptyValue')
->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE) ->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE)
->where($qb->expr()->isNull($columnName)) ->where($qb->expr()->isNull($columnName))
->execute(); ->executeStatement();
} }
public function down(Schema $schema): void public function down(Schema $schema): void
@ -53,6 +58,6 @@ final class Version20200110182849 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -32,7 +33,7 @@ final class Version20200323190014 extends AbstractMigration
->andWhere($qb->expr()->eq('lon', 0)) ->andWhere($qb->expr()->eq('lon', 0))
->setParameter('isEmpty', true) ->setParameter('isEmpty', true)
->setParameter('emptyString', '') ->setParameter('emptyString', '')
->execute(); ->executeStatement();
} }
public function down(Schema $schema): void public function down(Schema $schema): void
@ -45,6 +46,6 @@ final class Version20200323190014 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -27,6 +28,6 @@ final class Version20200503170404 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -44,6 +45,6 @@ final class Version20201023090929 extends AbstractMigration
public function isTransactional(): bool 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 Cake\Chronos\Chronos;
use Doctrine\DBAL\Driver\Result; use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -86,6 +87,6 @@ final class Version20201102113208 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -52,6 +53,6 @@ final class Version20210102174433 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20210118153932 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -36,6 +37,6 @@ final class Version20210202181026 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -43,6 +44,6 @@ final class Version20210207100807 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -37,6 +38,6 @@ final class Version20210306165711 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20210522051601 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -28,6 +29,6 @@ final class Version20210522124633 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@ -41,6 +42,6 @@ final class Version20210720143824 extends AbstractMigration
public function isTransactional(): bool 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; namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -26,6 +27,6 @@ final class Version20211002072605 extends AbstractMigration
public function isTransactional(): bool 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>; namespace <namespace>;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -21,6 +22,6 @@ final class <className> extends AbstractMigration
public function isTransactional(): bool 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) ->option('unsigned', true)
->build(); ->build();
$builder->createField('authority', Types::STRING) fieldWithUtf8Charset($builder->createField('authority', Types::STRING), $emConfig)
->unique() ->unique()
->build(); ->build();
$builder->createField('baseUrlRedirect', Types::STRING) fieldWithUtf8Charset($builder->createField('baseUrlRedirect', Types::STRING), $emConfig)
->columnName('base_url_redirect') ->columnName('base_url_redirect')
->nullable() ->nullable()
->build(); ->build();
$builder->createField('regular404Redirect', Types::STRING) fieldWithUtf8Charset($builder->createField('regular404Redirect', Types::STRING), $emConfig)
->columnName('regular_not_found_redirect') ->columnName('regular_not_found_redirect')
->nullable() ->nullable()
->build(); ->build();
$builder->createField('invalidShortUrlRedirect', Types::STRING) fieldWithUtf8Charset($builder->createField('invalidShortUrlRedirect', Types::STRING), $emConfig)
->columnName('invalid_short_url_redirect') ->columnName('invalid_short_url_redirect')
->nullable() ->nullable()
->build(); ->build();

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use DateTimeInterface; use DateTimeInterface;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Jaybizzle\CrawlerDetect\CrawlerDetect; use Jaybizzle\CrawlerDetect\CrawlerDetect;
use Laminas\InputFilter\InputFilter; use Laminas\InputFilter\InputFilter;
use PUGX\Shortid\Factory as ShortIdFactory; use PUGX\Shortid\Factory as ShortIdFactory;
@ -13,13 +14,10 @@ use Shlinkio\Shlink\Common\Util\DateRange;
use function Functional\reduce_left; use function Functional\reduce_left;
use function is_array; use function is_array;
use function lcfirst;
use function print_r; use function print_r;
use function Shlinkio\Shlink\Common\buildDateRange; use function Shlinkio\Shlink\Common\buildDateRange;
use function sprintf; use function sprintf;
use function str_repeat; use function str_repeat;
use function str_replace;
use function ucwords;
function generateRandomShortCode(int $length): string function generateRandomShortCode(int $length): string
{ {
@ -34,7 +32,7 @@ function generateRandomShortCode(int $length): string
function parseDateFromQuery(array $query, string $dateName): ?Chronos 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 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 function isCrawler(string $userAgent): bool
{ {
static $detector; static $detector;
@ -114,3 +107,12 @@ function isCrawler(string $userAgent): bool
return $detector->isCrawler($userAgent); 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 public function stringify(ShortUrl $shortUrl): string
{ {
return (new Uri())->withPath($shortUrl->getShortCode()) $uriWithoutShortCode = (new Uri())->withScheme($this->domainConfig['schema'] ?? 'http')
->withScheme($this->domainConfig['schema'] ?? 'http') ->withHost($this->resolveDomain($shortUrl))
->withHost($this->resolveDomain($shortUrl)) ->withPath($this->basePath)
->__toString(); ->__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 private function resolveDomain(ShortUrl $shortUrl): string
{ {
$domain = $shortUrl->getDomain(); return $shortUrl->getDomain()?->getAuthority() ?? $this->domainConfig['hostname'] ?? '';
if ($domain === null) {
return $this->domainConfig['hostname'] ?? '';
}
return sprintf('%s%s', $domain->getAuthority(), $this->basePath);
} }
} }

View file

@ -43,6 +43,18 @@ class ShortUrlStringifierTest extends TestCase
$shortUrlWithShortCode('bar'), $shortUrlWithShortCode('bar'),
'http://example.com/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' => [ yield 'hostname with base path in config' => [
['hostname' => 'example.com/foo/bar'], ['hostname' => 'example.com/foo/bar'],
'', '',

View file

@ -315,11 +315,22 @@ class CreateShortUrlTest extends ApiTestCase
yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481']; 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 { * @return array{int $statusCode, array $payload}
* @var int $statusCode
* @var array $payload
* }
*/ */
private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array
{ {