Compare commits

...

91 Commits

Author SHA1 Message Date
Alejandro Celaya
bf24660ddb Merge pull request #526 from acelaya-forks/feature/enhanced-not-found-redirect
Feature/enhanced not found redirect
2019-11-02 21:24:34 +01:00
Alejandro Celaya
b66268867a Updated to shlink-installer 3 2019-11-02 21:15:26 +01:00
Alejandro Celaya
9abaa243e0 Some extra minor improvements 2019-11-02 19:08:07 +01:00
Alejandro Celaya
7030138ff4 Updated changelog 2019-11-02 18:57:16 +01:00
Alejandro Celaya
906dfe60f8 Simplified code to render not-foubnd templates by infering the template to be used inside NotFoundHandler 2019-11-02 18:49:24 +01:00
Alejandro Celaya
01f60614ef Removed hardcoded route names for core routes and used action class names instead 2019-11-02 18:36:15 +01:00
Alejandro Celaya
eeb5306883 Moved all logic to redirect to specific URLs when a 404 is found to the NotFoundHandler 2019-11-02 18:33:26 +01:00
Alejandro Celaya
24c3a3e84c Defined new options for new redirect configuration 2019-11-02 17:23:21 +01:00
Alejandro Celaya
8b9663aea0 Created DeprecatedConfigParserTest 2019-11-02 17:04:49 +01:00
Alejandro Celaya
b59f4e2805 Defined new configs for not found redirects 2019-11-02 11:30:09 +01:00
Alejandro Celaya
6293d57fde Merge pull request #523 from acelaya-forks/feature/refactorings
Feature/refactorings
2019-11-01 17:31:43 +01:00
Alejandro Celaya
39ac2efe26 Updated to latest shlink-common with bug fixes 2019-11-01 17:16:56 +01:00
Alejandro Celaya
1f449e8ce1 Disabled coroutines on swoole during API tests 2019-11-01 10:10:43 +01:00
Alejandro Celaya
ad906000c7 Removed typehint making phpstan throw false positive 2019-11-01 10:04:25 +01:00
Alejandro Celaya
5361f33cc1 Some more refactorings 2019-11-01 09:52:56 +01:00
Alejandro Celaya
1937f3ea22 Trying to automatically persist tags 2019-10-26 09:01:51 +02:00
Alejandro Celaya
f4e9d0c8fe Merge pull request #517 from acelaya-forks/feature/too-long-lock
Updated to shlink-ip-geolocation v1.1
2019-10-25 20:12:54 +02:00
Alejandro Celaya
9b767ee9f3 Updated to shlink-ip-geolocation v1.1 2019-10-25 20:00:26 +02:00
Alejandro Celaya
43cb91bf52 Merge pull request #516 from acelaya-forks/feature/qr-code-with-domain
Feature/qr code with domain
2019-10-22 20:03:37 +02:00
Alejandro Celaya
f784ee5b28 Fixed unit tests 2019-10-22 19:52:28 +02:00
Alejandro Celaya
cd6f067fe5 Ensured domain is taken into account when generating QR codes 2019-10-22 19:43:53 +02:00
Alejandro Celaya
6d366188c9 Added github funding 2019-10-22 19:37:35 +02:00
Alejandro Celaya
c4ca59dcb0 Merge pull request #515 from acelaya-forks/feature/host-and-port
Feature/host and port
2019-10-20 11:14:44 +02:00
Alejandro Celaya
74675ad314 Updated changelog 2019-10-20 10:30:51 +02:00
Alejandro Celaya
b5e4da847a Allowed port number on domain field when creating shotr URLs 2019-10-20 10:30:11 +02:00
Alejandro Celaya
232bf5a68b Merge pull request #513 from acelaya-forks/feature/fix-long-urls
Feature/fix long urls
2019-10-20 10:15:38 +02:00
Alejandro Celaya
67958a78d3 Updated changelog 2019-10-20 09:55:00 +02:00
Alejandro Celaya
b8cdc29d8f Increased long URL size in DB to 2048 characters 2019-10-20 09:53:11 +02:00
Alejandro Celaya
30e4ddb950 Updated to latest infection version 2019-10-20 09:42:46 +02:00
Alejandro Celaya
a61a7db275 Merge pull request #510 from acelaya-forks/feature/update-common
Updated common and qr-code
2019-10-15 19:17:44 +02:00
Alejandro Celaya
0f5e4e7fa2 Updated common and qr-code 2019-10-15 19:06:37 +02:00
Alejandro Celaya
eb17eae781 Merge pull request #506 from acelaya-forks/feature/improved-shortcodes
Feature/improved shortcodes
2019-10-11 12:51:24 +02:00
Alejandro Celaya
740a65f880 Updated references to SHORTCODE_CHARS in docker docs 2019-10-11 11:41:14 +02:00
Alejandro Celaya
5bd7b53e0a Added more tests for new logics 2019-10-11 11:28:53 +02:00
Alejandro Celaya
9538f474de Added logic to check if a short code is in use and regenerate it otherwise 2019-10-11 11:09:33 +02:00
Alejandro Celaya
8f2e78c946 Moved logic to generate random short codes to external function 2019-10-11 09:35:09 +02:00
Alejandro Celaya
2f09ff456c Updated logic to generate random short codes, increasing entropy 2019-10-11 09:14:25 +02:00
Alejandro Celaya
c8d950e04d Merge pull request #504 from acelaya-forks/feature/mariadb
Feature/mariadb
2019-10-06 12:37:22 +02:00
Alejandro Celaya
0f6a0da7a4 Added MariaDB to documentation 2019-10-06 12:27:11 +02:00
Alejandro Celaya
4300fb225f Ensured tests on maria DB are not run on travis, since it would conflict with MySQL 2019-10-06 12:23:02 +02:00
Alejandro Celaya
159021e87c Added MariaDB to issue templates 2019-10-06 11:39:53 +02:00
Alejandro Celaya
9dc6ea9eeb Documented how to use maria db with docker image 2019-10-06 11:38:23 +02:00
Alejandro Celaya
42e84e526e Added mariadb container for development environment 2019-10-06 11:21:41 +02:00
Alejandro Celaya
700ee40109 Merge pull request #503 from acelaya-forks/feature/update-coding-standard
Feature/update coding standard
2019-10-05 18:07:12 +02:00
Alejandro Celaya
4909bd9550 Updated changelog 2019-10-05 17:29:07 +02:00
Alejandro Celaya
e0d20bf8ff Updated to coding styles v2 2019-10-05 17:26:10 +02:00
Alejandro Celaya
09a5284675 Merge pull request #501 from acelaya-forks/feature/v1.19-final
Updated changelog adding final release 1.19.0
2019-10-05 11:09:12 +02:00
Alejandro Celaya
1112f3acdd Updated changelog adding final release 1.19.0 2019-10-05 11:08:50 +02:00
Alejandro Celaya
05e3071db2 Merge pull request #500 from acelaya-forks/feature/multiple-domains
Feature/multiple domains
2019-10-04 23:39:11 +02:00
Alejandro Celaya
403773bc17 Documented new feature in CHANGELOG 2019-10-04 21:46:41 +02:00
Alejandro Celaya
636df2a736 Read request's authority when tracking a visit and passed it down 2019-10-04 21:36:54 +02:00
Alejandro Celaya
baf3093893 Added support for domain param to command and action to resolve a short URL 2019-10-04 21:17:02 +02:00
Alejandro Celaya
8d3a49a319 Fixed issue with postgres when fetching resultset ordering by nullable column 2019-10-04 18:07:26 +02:00
Alejandro Celaya
eced1af21d Added more database cases covering different combinations of finding short URL by short code and domain 2019-10-04 17:34:34 +02:00
Alejandro Celaya
49c3c9bec1 Ensured domain is taken into account when looking for a short URL 2019-10-04 17:21:22 +02:00
Alejandro Celaya
2ffaabe594 Added option to define domain to GenerateShortUrlCommand 2019-10-02 20:29:13 +02:00
Alejandro Celaya
f31dc6c6e5 Added missing return type hints 2019-10-02 20:15:14 +02:00
Alejandro Celaya
f067d0e831 Allowed to provide the domain when creating a short URL 2019-10-02 20:13:25 +02:00
Alejandro Celaya
a892f72425 Added migration to make the combination of slug+domain unique 2019-10-02 20:01:15 +02:00
Alejandro Celaya
25f64a2fc4 Added check for domain when matching an existing short URL 2019-10-01 22:15:11 +02:00
Alejandro Celaya
fd1fe90731 Created tests for new domain resolvers 2019-10-01 22:00:46 +02:00
Alejandro Celaya
495643f4f1 Ensured domain is taken into account when checking if a slug is in use 2019-10-01 21:42:35 +02:00
Alejandro Celaya
8da6b336f5 Added API test which checks short URLs with a domain are parsed as such 2019-10-01 20:24:11 +02:00
Alejandro Celaya
d0bb86ca8f Added simple way to resolve domains from entity manager when creating a short URL 2019-10-01 20:16:27 +02:00
Alejandro Celaya
1085809fa5 Moved code to convert a ShortUrl into a full link as string to the entity itself 2019-09-30 20:01:36 +02:00
Alejandro Celaya
7b1857dcda Added entities config for domains 2019-09-30 19:42:27 +02:00
Alejandro Celaya
6f38790d47 Created migration which adds domains table 2019-09-30 19:15:14 +02:00
Alejandro Celaya
a81ac85af6 Merge pull request #498 from acelaya-forks/feature/improved-issue-templates
Feature/improved issue templates
2019-09-29 09:38:53 +02:00
Alejandro Celaya
8f4d5b6fce Added funding config file 2019-09-29 09:30:18 +02:00
Alejandro Celaya
8468a48eaa Added generic issue template 2019-09-29 09:23:37 +02:00
Alejandro Celaya
fc0885e5d5 Removed shlink set-up info from feature request issue template 2019-09-29 09:21:56 +02:00
Alejandro Celaya
e1a9e347c3 Merge pull request #496 from acelaya-forks/feature/installer2
Feature/installer2
2019-09-28 09:45:09 +02:00
Alejandro Celaya
1b0e3b686d Ignored false positive in phpstan 2019-09-28 09:35:59 +02:00
Alejandro Celaya
a09208582e Updated changelog 2019-09-28 09:31:41 +02:00
Alejandro Celaya
df1de020d1 Updated to shlink installer 2 2019-09-28 09:30:20 +02:00
Alejandro Celaya
9b363368a2 Merge pull request #490 from acelaya-forks/feature/issue-template
Feature/issue template
2019-09-17 20:17:51 +02:00
Alejandro Celaya
9fac69675a Updated how shlink info is requested to users on issue templates 2019-09-17 20:10:27 +02:00
Alejandro Celaya
1d2cfde7f7 Created individual issue templates 2019-09-17 20:04:04 +02:00
Alejandro Celaya
452612ee00 Merge pull request #485 from acelaya-forks/feature/base-path
Feature/base path
2019-09-13 20:54:45 +02:00
Alejandro Celaya
8d74e0c3ff Fixed undefined-index errors in BasePathPrefixerTest 2019-09-13 20:46:49 +02:00
Alejandro Celaya
0a1786c89a Added support for basepath on docker image 2019-09-13 20:36:40 +02:00
Alejandro Celaya
bc07d77d06 Removed duplicated code from BasePathPrefixer 2019-09-13 20:22:41 +02:00
Alejandro Celaya
6e38457655 Created BasePathPrefixerTest 2019-09-13 20:17:30 +02:00
Alejandro Celaya
d7a3aeb0a2 Created a config prost-processor which adds the base path on every applicable configuration 2019-09-13 20:03:53 +02:00
Alejandro Celaya
76541d5563 Merge pull request #484 from acelaya-forks/feature/disable-asking-for-lang
Ensured installer does not ask for the locale
2019-09-13 19:02:34 +02:00
Alejandro Celaya
28b5d8445e Ensured installer does not ask for the locale 2019-09-13 18:51:51 +02:00
Alejandro Celaya
d17533fd0f Merge pull request #483 from acelaya-forks/feature/remoive-translations
Feature/remoive translations
2019-09-12 20:30:45 +02:00
Alejandro Celaya
01d62b7aea Removed escape characters no longer needed in templates 2019-09-12 19:45:24 +02:00
Alejandro Celaya
bd97804ca6 Updated changelog 2019-09-12 08:10:41 +02:00
Alejandro Celaya
7b0ccc9f69 Removed references to anything related with translations 2019-09-12 08:09:17 +02:00
Alejandro Celaya
fdb98fa2a9 Added service aliases that were removed from shlink-common 2019-09-11 20:25:04 +02:00
346 changed files with 2299 additions and 729 deletions

View File

@@ -3,11 +3,6 @@ APP_ENV=
SECRET_KEY=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
# Language
DEFAULT_LOCALE=
CLI_LOCALE=
# Database
DB_USER=

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: ['acelaya']
custom: ['https://acel.me/donate']

View File

@@ -2,5 +2,5 @@
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for a project to cover all use cases.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
-->

37
.github/ISSUE_TEMPLATE/Bug.md vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Bug report
about: Something on shlink is broken or not working as documented?
labels: bug
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### How Shlink is set-up
* Shlink Version: x.y.z
* PHP Version: x.y.z
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted swoole|Docker image
* Database engine used: MySQL|MariaDB|PostgreSQL|SQLite (x.y.z)
#### Summary
<!-- Provide a summary describing the problem you are experiencing. -->
#### Current behavior
<!-- How is it actually behaving (and it shouldn't)? -->
#### Expected behavior
<!-- How did you expected to behave? -->
#### How to reproduce
<!-- Provide steps to reproduce the bug. -->

View File

@@ -0,0 +1,18 @@
---
name: Feature request
about: Do you find shlink is missing some important feature that would make it more useful?
labels: feature
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### Summary
<!-- Describe the new feature you would like to request. -->

View File

@@ -0,0 +1,25 @@
---
name: Question - Support
about: Do you have a problem setting up or using shlink?
labels: question
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### How Shlink is set-up
* Shlink Version: x.y.z
* PHP Version: x.y.z
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted swoole|Docker image
* Database engine used: MySQL|MariaDB|PostgreSQL|SQLite (x.y.z)
#### Summary
<!-- Describe the issue you are facing here. -->

View File

@@ -4,6 +4,93 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## 1.20.0 - 2019-11-02
#### Added
* [#491](https://github.com/shlinkio/shlink/issues/491) Added improved short code generation logic.
Now, short codes are truly random, which removes the guessability factor existing in previous versions.
Generated short codes have 5 characters, and shlink makes sure they keep unique, while making it backwards-compatible.
* [#418](https://github.com/shlinkio/shlink/issues/418) and [#419](https://github.com/shlinkio/shlink/issues/419) Added support to redirect any 404 error to a custom URL.
It was already possible to configure this but only for invalid short URLs. Shlink now also support configuring redirects for the base URL and any other kind of "not found" error.
The three URLs can be different, and it is already possible to pass them to the docker image via configuration or env vars.
The installer also asks for these two new configuration options.
* [#497](https://github.com/shlinkio/shlink/issues/497) Officially added support for MariaDB.
#### Changed
* [#458](https://github.com/shlinkio/shlink/issues/458) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.0.0.
#### Deprecated
* *Nothing*
#### Removed
* * *Nothing*
#### Fixed
* [#507](https://github.com/shlinkio/shlink/issues/507) Fixed error with too long original URLs by increasing size to the maximum value (2048) based on [the standard](https://stackoverflow.com/a/417184).
* [#502](https://github.com/shlinkio/shlink/issues/502) Fixed error when providing the port as part of the domain on short URLs.
* [#509](https://github.com/shlinkio/shlink/issues/509) Fixed error when trying to generate a QR code for a short URL which uses a custom domain.
* [#522](https://github.com/shlinkio/shlink/issues/522) Highly mitigated errors thrown when lots of short URLs are created concurrently including new and existing tags.
## 1.19.0 - 2019-10-05
#### Added
* [#482](https://github.com/shlinkio/shlink/issues/482) Added support to serve shlink under a sub path.
The `router.base_path` config option can be defined now to set the base path from which shlink is served.
```php
return [
'router' => [
'base_path' => '/foo/bar',
],
];
```
This option will also be available on shlink-installer 1.3.0, so the installer will ask for it. It can also be provided for the docker image as the `BASE_PATH` env var.
* [#479](https://github.com/shlinkio/shlink/issues/479) Added preliminary support for multiple domains.
Endpoints and commands which create short URLs support providing the `domain` now (via query param or CLI flag). If not provided, the short URLs will still be "attached" to the default domain.
Custom slugs can be created on multiple domains, allowing to share links like `https://doma.in/my-compaign` and `https://example.com/my-campaign`, under the same shlink instance.
When resolving a short URL to redirect end users, the following rules are applied:
* If the domain used for the request plus the short code/slug are found, the user is redirected to that long URL and the visit is tracked.
* If the domain is not known but the short code/slug is defined for default domain, the user is redirected there and the visit is tracked.
* In any other case, no redirection happens and no visit is tracked (if a fall back redirection is configured for not-found URLs, it will still happen).
#### Changed
* [#486](https://github.com/shlinkio/shlink/issues/486) Updated to [shlink-installer](https://github.com/shlinkio/shlink-installer) v2, which supports asking for base path in which shlink is served.
#### Deprecated
* *Nothing*
#### Removed
* [#435](https://github.com/shlinkio/shlink/issues/435) Removed translations for error pages. All error pages are in english now.
#### Fixed
* *Nothing*
## 1.18.1 - 2019-08-24
#### Added

View File

@@ -3,7 +3,7 @@ LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
ARG SHLINK_VERSION=1.18.1
ENV SHLINK_VERSION ${SHLINK_VERSION}
ENV SWOOLE_VERSION 4.3.3
ENV SWOOLE_VERSION 4.4.8
ENV COMPOSER_VERSION 1.9.0
WORKDIR /etc/shlink

View File

@@ -22,7 +22,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
First make sure the host where you are going to run shlink fulfills these requirements:
* PHP 7.2 or greater with JSON, APCu, intl, curl, PDO and gd extensions enabled.
* MySQL, PostgreSQL or SQLite.
* MySQL, MariaDB, PostgreSQL or SQLite.
* The web server of your choice with PHP integration (Apache or Nginx recommended).
Then, you will need a built version of the project. There are a few ways to get it.
@@ -49,7 +49,7 @@ Then, you will need a built version of the project. There are a few ways to get
Despite how you built the project, you are going to need to install it now, by following these steps:
* If you are going to use MySQL or PostgreSQL, create an empty database with the name of your choice.
* If you are going to use MySQL, MariaDB or PostgreSQL, create an empty database with the name of your choice.
* Recursively grant write permissions to the `data` directory. Shlink uses it to cache some information.
* Setup the application by running the `bin/install` script. It is a command line tool that will guide you through the installation process. **Take into account that this tool has to be run directly on the server where you plan to host Shlink. Do not run it before uploading/moving it there.**
* Expose shlink to the web, either by using a traditional web server + fast CGI approach, or by using a [swoole](https://www.swoole.co.uk/) non-blocking server.

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env sh
set -e
export APP_ENV=test
export DB_DRIVER=mysql
# Try to stop server just in case it hanged in last execution
vendor/bin/zend-expressive-swoole stop

View File

@@ -23,7 +23,7 @@
"doctrine/dbal": "^2.9",
"doctrine/migrations": "^2.0",
"doctrine/orm": "^2.5",
"endroid/qr-code": "^1.7",
"endroid/qr-code": "^3.6",
"firebase/php-jwt": "^4.0",
"geoip2/geoip2": "^2.9",
"guzzlehttp/guzzle": "^6.3",
@@ -33,10 +33,11 @@
"ocramius/proxy-manager": "~2.2.2",
"phly/phly-event-dispatcher": "^1.0",
"predis/predis": "^1.1",
"shlinkio/shlink-common": "^1.0",
"pugx/shortid-php": "^0.5",
"shlinkio/shlink-common": "^2.2.1",
"shlinkio/shlink-event-dispatcher": "^1.0",
"shlinkio/shlink-installer": "^1.2.1",
"shlinkio/shlink-ip-geolocation": "^1.0",
"shlinkio/shlink-installer": "^3.0",
"shlinkio/shlink-ip-geolocation": "^1.1",
"symfony/console": "^4.3",
"symfony/filesystem": "^4.3",
"symfony/lock": "^4.3",
@@ -50,7 +51,6 @@
"zendframework/zend-expressive-helpers": "^5.3",
"zendframework/zend-expressive-platesrenderer": "^2.1",
"zendframework/zend-expressive-swoole": "^2.4",
"zendframework/zend-i18n": "^2.9",
"zendframework/zend-inputfilter": "^2.10",
"zendframework/zend-paginator": "^2.8",
"zendframework/zend-servicemanager": "^3.4",
@@ -60,12 +60,12 @@
"devster/ubench": "^2.0",
"eaglewu/swoole-ide-helper": "dev-master",
"filp/whoops": "^2.4",
"infection/infection": "^0.13.4",
"phpstan/phpstan": "^0.11.2",
"infection/infection": "^0.14.2",
"phpstan/phpstan": "^0.11.16",
"phpunit/phpcov": "^6.0",
"phpunit/phpunit": "^8.3",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~1.2.2",
"shlinkio/php-coding-standard": "~2.0.0",
"shlinkio/shlink-test-utils": "^1.0",
"symfony/dotenv": "^4.3",
"symfony/var-dumper": "^4.3",
@@ -78,7 +78,10 @@
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
}
},
"files": [
"module/Core/functions/functions.php"
]
},
"autoload-dev": {
"psr-4": {
@@ -111,18 +114,25 @@
],
"test:ci": [
"@test:unit:ci",
"@test:db",
"@test:db:ci",
"@test:api"
],
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml --testdox",
"test:db": [
"@test:db:sqlite",
"@test:db:mysql",
"@test:db:maria",
"@test:db:postgres"
],
"test:db:ci": [
"@test:db:sqlite",
"@test:db:mysql",
"@test:db:postgres"
],
"test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
"test:api": "bin/test/run-api-tests.sh",
@@ -141,7 +151,6 @@
]
},
"scripts-descriptions": {
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test:ci\" and \"infect:ci\"</>",
"cs": "<fg=blue;options=bold>Checks coding styles</>",
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
@@ -150,9 +159,11 @@
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL and PostgreSQL</>",
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL, MariaDB and PostgreSQL</>",
"test:db:ci": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL and PostgreSQL</>",
"test:db:sqlite": "<fg=blue;options=bold>Runs database test suites on a SQLite database</>",
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
"test:db:maria": "<fg=blue;options=bold>Runs database test suites on a MariaDB database</>",
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Zend\ConfigAggregator\ConfigAggregator;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Zend\Expressive;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Installer\Config\Plugin;
@@ -6,17 +7,11 @@ use Shlinkio\Shlink\Installer\Config\Plugin;
return [
'installer_plugins_expected_config' => [
Plugin\LanguageConfigCustomizer::class => [
Plugin\LanguageConfigCustomizer::DEFAULT_LANG,
],
Plugin\UrlShortenerConfigCustomizer::class => [
Plugin\UrlShortenerConfigCustomizer::SCHEMA,
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
Plugin\UrlShortenerConfigCustomizer::CHARS,
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
Plugin\UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION,
Plugin\UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO,
],
Plugin\ApplicationConfigCustomizer::class => [
@@ -24,6 +19,7 @@ return [
Plugin\ApplicationConfigCustomizer::DISABLE_TRACK_PARAM,
Plugin\ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD,
Plugin\ApplicationConfigCustomizer::VISITS_THRESHOLD,
Plugin\ApplicationConfigCustomizer::BASE_PATH,
],
Plugin\DatabaseConfigCustomizer::class => [
@@ -34,6 +30,12 @@ return [
Plugin\DatabaseConfigCustomizer::HOST,
Plugin\DatabaseConfigCustomizer::PORT,
],
Plugin\RedirectsConfigCustomizer::class => [
Plugin\RedirectsConfigCustomizer::INVALID_SHORT_URL_REDIRECT_TO,
Plugin\RedirectsConfigCustomizer::REGULAR_404_REDIRECT_TO,
Plugin\RedirectsConfigCustomizer::BASE_URL_REDIRECT_TO,
],
],
'installation_commands' => [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common\Cache\RedisFactory;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
@@ -7,6 +8,7 @@ use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor;
use Psr\Log\LoggerInterface;
use const PHP_EOL;
@@ -61,6 +63,11 @@ return [
'Logger_Shlink' => Common\Logger\LoggerFactory::class,
'Logger_Access' => Common\Logger\LoggerFactory::class,
],
'aliases' => [
'logger' => 'Logger_Shlink',
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
],
],
'zend-expressive-swoole' => [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
@@ -47,9 +48,6 @@ return [
'post-routing' => [
'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class,
// Only if a not found error is triggered, set-up the locale to be used
Common\Middleware\LocaleMiddleware::class,
Core\Response\NotFoundHandler::class,
],
'priority' => 1,

View File

@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
/** @deprecated */
/* @deprecated */
return [
'preview_generation' => [

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
return [
'not_found_redirects' => [
'invalid_short_url' => null, // Formerly url_shortener.not_found_short_url.redirect_to
'regular_404' => null,
'base_url' => null,
],
];

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Zend\Expressive\Router\FastRouteRouter;
@@ -6,6 +7,8 @@ use Zend\Expressive\Router\FastRouteRouter;
return [
'router' => [
'base_path' => '',
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

@@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common;
return [
'translator' => [
'locale' => Common\env('DEFAULT_LOCALE', 'en'),
],
];

View File

@@ -1,7 +1,6 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
@@ -12,12 +11,7 @@ return [
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortenerOptions::DEFAULT_CHARS),
'validate_url' => true,
'not_found_short_url' => [
'enable_redirection' => false,
'redirect_to' => null,
],
],
];

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\EntityManager;
@@ -6,8 +7,10 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
/** @var ContainerInterface|ServiceManager $container */
$container = include __DIR__ . '/container.php';
$em = $container->get(EntityManager::class);
return (function () {
/** @var ContainerInterface|ServiceManager $container */
$container = include __DIR__ . '/container.php';
$em = $container->get(EntityManager::class);
return ConsoleRunner::createHelperSet($em);
return ConsoleRunner::createHelperSet($em);
})();

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
@@ -28,5 +29,7 @@ return (new ConfigAggregator\ConfigAggregator([
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
], 'data/cache/app_config.php', [
Core\SimplifiedConfigParser::class,
Core\Config\SimplifiedConfigParser::class,
Core\Config\BasePathPrefixer::class,
Core\Config\DeprecatedConfigParser::class,
]))->getMergedConfig();

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Symfony\Component\Dotenv\Dotenv;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\TestUtils;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\TestUtils;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
@@ -15,40 +16,41 @@ use function sys_get_temp_dir;
$swooleTestingHost = '127.0.0.1';
$swooleTestingPort = 9999;
$buildDbConnection = function () {
$buildDbConnection = function (): array {
$driver = env('DB_DRIVER', 'sqlite');
$isCi = env('TRAVIS', false);
$getMysqlHost = function (string $driver) {
return sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
};
switch ($driver) {
case 'sqlite':
return [
'driver' => 'pdo_sqlite',
'path' => sys_get_temp_dir() . '/shlink-tests.db',
];
case 'mysql':
return [
'driver' => 'pdo_mysql',
'host' => $isCi ? '127.0.0.1' : 'shlink_db',
'user' => 'root',
'password' => $isCi ? '' : 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
];
case 'postgres':
return [
'driver' => 'pdo_pgsql',
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
'user' => 'postgres',
'password' => $isCi ? '' : 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
];
default:
return [];
}
$driverConfigMap = [
'sqlite' => [
'driver' => 'pdo_sqlite',
'path' => sys_get_temp_dir() . '/shlink-tests.db',
],
'mysql' => [
'driver' => 'pdo_mysql',
'host' => $isCi ? '127.0.0.1' : $getMysqlHost($driver),
'user' => 'root',
'password' => $isCi ? '' : 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
],
'postgres' => [
'driver' => 'pdo_pgsql',
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
'user' => 'postgres',
'password' => $isCi ? '' : 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
],
];
$driverConfigMap['maria'] = $driverConfigMap['mysql'];
return $driverConfigMap[$driver] ?? [];
};
return [
@@ -64,6 +66,7 @@ return [
],
'zend-expressive-swoole' => [
'enable_coroutine' => false,
'swoole-http-server' => [
'host' => $swooleTestingHost,
'port' => $swooleTestingPort,
@@ -72,6 +75,7 @@ return [
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
'worker_num' => 1,
'task_worker_num' => 1,
'enable_coroutine' => false,
],
],
],

2
data/infra/database_maria/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -4,6 +4,7 @@ MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.16
ENV APCU_BC_VERSION 1.0.4
ENV INOTIFY_VERSION 2.0.0
ENV SWOOLE_VERSION 4.4.8
RUN apk update
@@ -66,7 +67,7 @@ RUN rm /tmp/inotify.tar.gz
# Install swoole
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
pecl install swoole && \
pecl install swoole-${SWOOLE_VERSION} && \
docker-php-ext-enable swoole && \
apk del .phpize-deps

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type;
use Doctrine\Migrations\AbstractMigration;
final class Version20190930165521 extends AbstractMigration
{
/**
* @throws SchemaException
*/
public function up(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
if ($shortUrls->hasColumn('domain_id')) {
return;
}
$domains = $schema->createTable('domains');
$domains->addColumn('id', Type::BIGINT, [
'unsigned' => true,
'autoincrement' => true,
'notnull' => true,
]);
$domains->addColumn('authority', Type::STRING, [
'length' => 512,
'notnull' => true,
]);
$domains->addUniqueIndex(['authority']);
$domains->setPrimaryKey(['id']);
$shortUrls->addColumn('domain_id', Type::BIGINT, [
'unsigned' => true,
'notnull' => false,
]);
$shortUrls->addForeignKeyConstraint('domains', ['domain_id'], ['id'], [
'onDelete' => 'RESTRICT',
'onUpdate' => 'RESTRICT',
]);
}
/**
* @throws SchemaException
*/
public function down(Schema $schema): void
{
$schema->getTable('short_urls')->dropColumn('domain_id');
$schema->dropTable('domains');
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
use function array_reduce;
final class Version20191001201532 extends AbstractMigration
{
/**
* @throws SchemaException
*/
public function up(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
if ($shortUrls->hasIndex('unique_short_code_plus_domain')) {
return;
}
/** @var Index|null $shortCodesIndex */
$shortCodesIndex = array_reduce($shortUrls->getIndexes(), function (?Index $found, Index $current) {
[$column] = $current->getColumns();
return $column === 'short_code' ? $current : $found;
});
if ($shortCodesIndex === null) {
return;
}
$shortUrls->dropIndex($shortCodesIndex->getName());
$shortUrls->addUniqueIndex(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
}
/**
* @throws SchemaException
*/
public function down(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
$shortUrls->dropIndex('unique_short_code_plus_domain');
$shortUrls->addUniqueIndex(['short_code']);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration;
final class Version20191020074522 extends AbstractMigration
{
/**
* @throws SchemaException
*/
public function up(Schema $schema): void
{
$this->getOriginalUrlColumn($schema)->setLength(2048);
}
/**
* @throws SchemaException
*/
public function down(Schema $schema): void
{
$this->getOriginalUrlColumn($schema)->setLength(1024);
}
/**
* @throws SchemaException
*/
private function getOriginalUrlColumn(Schema $schema): Column
{
return $schema->getTable('short_urls')->getColumn('original_url');
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace <namespace>;

View File

@@ -24,3 +24,9 @@ services:
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_db_maria:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro

View File

@@ -24,6 +24,7 @@ services:
links:
- shlink_db
- shlink_db_postgres
- shlink_db_maria
- shlink_redis
shlink_swoole:
@@ -33,11 +34,13 @@ services:
dockerfile: ./data/infra/swoole.Dockerfile
ports:
- "8080:8080"
- "9001:9001"
volumes:
- ./:/home/shlink
links:
- shlink_db
- shlink_db_postgres
- shlink_db_maria
- shlink_redis
shlink_db:
@@ -65,6 +68,19 @@ services:
POSTGRES_DB: shlink
PGDATA: /var/lib/postgresql/data/pgdata
shlink_db_maria:
container_name: shlink_db_maria
image: mariadb:10.2
ports:
- "3308:3306"
volumes:
- ./:/home/shlink/www
- ./data/infra/database_maria:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: shlink
MYSQL_INITDB_SKIP_TZINFO: 1
shlink_redis:
container_name: shlink_redis
image: redis:5.0-alpine

View File

@@ -56,16 +56,16 @@ docker exec -it shlink_container shlink
The image comes with a working sqlite database, but in production you will probably want to usa a distributed database.
It is possible to use a set of env vars to make this shlink instance interact with an external MySQL or PostgreSQL database.
It is possible to use a set of env vars to make this shlink instance interact with an external MySQL, MariaDB or PostgreSQL database.
* `DB_DRIVER`: **[Mandatory]**. Use the value **mysql** or **postgres** to prevent the sqlite database to be used.
* `DB_DRIVER`: **[Mandatory]**. Use the value **mysql**, **maria** or **postgres** to prevent the sqlite database to be used.
* `DB_NAME`: [Optional]. The database name to be used. Defaults to **shlink**.
* `DB_USER`: **[Mandatory]**. The username credential for the database server.
* `DB_PASSWORD`: **[Mandatory]**. The password credential for the database server.
* `DB_HOST`: **[Mandatory]**. The host name of the server running the database engine.
* `DB_PORT`: [Optional]. The port in which the database service is running.
* Default value is based on the driver:
* **mysql** -> `3306`
* **mysql** or **maria** -> `3306`
* **postgres** -> `5432`
> PostgreSQL is supported since v1.16.1 of this image. Do not try to use it with previous versions.
@@ -92,8 +92,7 @@ This is the complete list of supported env vars:
* `SHORT_DOMAIN_HOST`: The custom short domain used for this shlink instance. For example **doma.in**.
* `SHORT_DOMAIN_SCHEMA`: Either **http** or **https**.
* `SHORTCODE_CHARS`: A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
* `DB_DRIVER`: **sqlite** (which is the default value), **mysql** or **postgres**.
* `DB_DRIVER`: **sqlite** (which is the default value), **mysql**, **maria** or **postgres**.
* `DB_NAME`: The database name to be used when using an external database driver. Defaults to **shlink**.
* `DB_USER`: The username credential to be used when using an external database driver.
* `DB_PASSWORD`: The password credential to be used when using an external database driver.
@@ -101,9 +100,11 @@ This is the complete list of supported env vars:
* `DB_PORT`: The port in which the database service is running when using an external database driver. Defaults to **3306**.
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
* `LOCALE`: Defines the default language for error pages when a user accesses a short URL which does not exist. Supported values are **es** and **en**. Defaults to **en**.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`.
* `NOT_FOUND_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `INVALID_SHORT_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `REGULAR_404_REDIRECT_TO`: If a URL is provided here, when a user tries to access a URL not matching any one supported by the router, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `BASE_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access Shlink's base URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `BASE_PATH`: The base path from which you plan to serve shlink, in case you don't want to serve it from the root of the domain. Defaults to `''`.
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately.
@@ -112,6 +113,9 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink.
* `NOT_FOUND_REDIRECT_TO`: **Deprecated since v1.20 in favor of `INVALID_SHORT_URL_REDIRECT_TO`** If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
An example using all env vars could look like this:
```bash
@@ -128,10 +132,12 @@ docker run \
-e DB_PORT=3306 \
-e DISABLE_TRACK_PARAM="no-track" \
-e DELETE_SHORT_URL_THRESHOLD=30 \
-e LOCALE=es \
-e VALIDATE_URLS=false \
-e "NOT_FOUND_REDIRECT_TO=https://www.google.com" \
-e "INVALID_SHORT_URL_REDIRECT_TO=https://my-landing-page.com" \
-e "REGULAR_404_REDIRECT_TO=https://my-landing-page.com" \
-e "BASE_URL_REDIRECT_TO=https://my-landing-page.com" \
-e "REDIS_SERVERS=tcp://172.20.0.1:6379,tcp://172.20.0.2:6379" \
-e "BASE_PATH=/my-campaign" \
shlinkio/shlink
```
@@ -147,11 +153,12 @@ The whole configuration should have this format, but it can be split into multip
{
"disable_track_param": "my_param",
"delete_short_url_threshold": 30,
"locale": "es",
"short_domain_schema": "https",
"short_domain_host": "doma.in",
"validate_url": false,
"not_found_redirect_to": "https://my-landing-page.com",
"invalid_short_url_redirect_to": "https://my-landing-page.com",
"regular_404_redirect_to": "https://my-landing-page.com",
"base_url_redirect_to": "https://my-landing-page.com",
"redis_servers": [
"tcp://172.20.0.1:6379",
"tcp://172.20.0.2:6379"
@@ -163,11 +170,13 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc",
"host": "something.rds.amazonaws.com",
"port": "3306"
}
},
"not_found_redirect_to": "https://my-landing-page.com"
}
```
> This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes).
> The `not_found_redirect_to` option has been deprecated when `regular_404_redirect_to` and `base_url_redirect_to` have been introduced. Use `invalid_short_url_redirect_to` instead (however, it will still work for backwards compatibility).
Once created just run shlink with the volume:
@@ -179,7 +188,13 @@ docker run --name shlink -p 8080:8080 -v ${PWD}/my/config/dir:/etc/shlink/config
These are some considerations to take into account when running multiple instances of shlink.
* The first time shlink is run, it generates a charset used to generate short codes, which is a shuffled base62 charset.
* Some operations performed by Shlink should never be run more than once at the same time (like creating the database for the first time, or downloading the GeoLite2 database). For this reason, Shlink uses a locking system.
However, these locks are locally scoped to each Shlink instance by default.
You can (and should) make the locks to be shared by all Shlink instances by using a redis server/cluster. Just define the `REDIS_SERVERS` env var with the list of servers.
* **Ignore this if using Shlink 1.20 or newer**. The first time shlink is run, it generates a charset used to generate short codes, which is a shuffled base62 charset.
If you are using several shlink instances, you will probably want all of them to use the same charset.
@@ -187,12 +202,6 @@ These are some considerations to take into account when running multiple instanc
If you don't do this, each shlink instance will use a different charset. However this shouldn't be a problem in practice, since the chances to get a collision will be very low.
* Some operations performed by Shlink should never be run more than once at the same time (like creating the database for the first time, or downloading the GeoLite2 database). For this reason, Shlink uses a locking system.
However, these locks are locally scoped to each Shlink instance by default.
You can (and should) make the locks to be shared by all Shlink instances by using a redis server/cluster. Just define the `REDIS_SERVERS` env var with the list of servers.
## Versions
Versions of this image match the shlink version it contains.

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
@@ -10,6 +11,7 @@ use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function Functional\contains;
use function implode;
use function Shlinkio\Shlink\Common\env;
use function sprintf;
@@ -21,24 +23,24 @@ $helper = new class {
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql',
'postgres' => 'pdo_pgsql',
];
private const DB_PORTS_MAP = [
'mysql' => '3306',
'maria' => '3306',
'postgres' => '5432',
];
/** @var string */
private $charset;
/** @var string */
private $secretKey;
public function __construct()
{
[$this->charset, $this->secretKey] = $this->initShlinkKeys();
[, $this->secretKey] = $this->initShlinkSecretKey();
}
private function initShlinkKeys(): array
private function initShlinkSecretKey(): array
{
$keysFile = sprintf('%s/shlink.keys', sys_get_temp_dir());
if (file_exists($keysFile)) {
@@ -46,29 +48,19 @@ $helper = new class {
}
$keys = [
env('SHORTCODE_CHARS', $this->generateShortcodeChars()),
env('SECRET_KEY', $this->generateSecretKey()),
'', // This was the SHORTCODE_CHARS. Kept as empty string for BC
env('SECRET_KEY', $this->generateSecretKey()), // Deprecated
];
file_put_contents($keysFile, implode(',', $keys));
return $keys;
}
private function generateShortcodeChars(): string
{
return str_shuffle(self::BASE62);
}
private function generateSecretKey(): string
{
return substr(str_shuffle(self::BASE62), 0, 32);
}
public function getShortcodeChars(): string
{
return $this->charset;
}
public function getSecretKey(): string
{
return $this->secretKey;
@@ -84,8 +76,8 @@ $helper = new class {
];
}
$driverOptions = $driver !== 'mysql' ? [] : [
// PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
$driverOptions = ! contains(['maria', 'mysql'], $driver) ? [] : [
// 1002 -> PDO::MYSQL_ATTR_INIT_COMMAND
1002 => 'SET NAMES utf8',
];
return [
@@ -99,13 +91,12 @@ $helper = new class {
];
}
public function getNotFoundConfig(): array
public function getNotFoundRedirectsConfig(): array
{
$notFoundRedirectTo = env('NOT_FOUND_REDIRECT_TO');
return [
'enable_redirection' => $notFoundRedirectTo !== null,
'redirect_to' => $notFoundRedirectTo,
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO', env('NOT_FOUND_REDIRECT_TO')),
'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'),
];
}
};
@@ -124,10 +115,6 @@ return [
'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', 15),
],
'translator' => [
'locale' => env('LOCALE', 'en'),
],
'entity_manager' => [
'connection' => $helper->getDbConfig(),
],
@@ -137,11 +124,11 @@ return [
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
'hostname' => env('SHORT_DOMAIN_HOST', ''),
],
'shortcode_chars' => $helper->getShortcodeChars(),
'validate_url' => (bool) env('VALIDATE_URLS', true),
'not_found_short_url' => $helper->getNotFoundConfig(),
],
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
'logger' => [
'handlers' => [
'shlink_rotating_handler' => [
@@ -172,4 +159,8 @@ return [
'servers' => env('REDIS_SERVERS'),
],
'router' => [
'base_path' => env('BASE_PATH', ''),
],
];

View File

@@ -216,6 +216,10 @@
"findIfExists": {
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
"type": "boolean"
},
"domain": {
"description": "The domain to which the short URL will be attached",
"type": "string"
}
}
}

View File

@@ -15,6 +15,15 @@
"schema": {
"type": "string"
}
},
{
"name": "domain",
"in": "query",
"description": "The domain in which the short code should be searched for. Will fall back to default domain if not found.",
"required": false,
"schema": {
"type": "string"
}
}
],
"security": [

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;

View File

@@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -17,6 +17,7 @@ use function str_shuffle;
class GenerateCharsetCommand extends Command
{
public const NAME = 'config:generate-charset';
private const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected function configure(): void
{
@@ -25,14 +26,14 @@ class GenerateCharsetCommand extends Command
->setDescription(sprintf(
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
UrlShortenerOptions::DEFAULT_CHARS
self::DEFAULT_CHARS
))
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$charSet = str_shuffle(UrlShortenerOptions::DEFAULT_CHARS);
$charSet = str_shuffle(self::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
return ExitCodes::EXIT_SUCCESS;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
@@ -9,7 +10,6 @@ use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -26,8 +26,6 @@ use function sprintf;
class GenerateShortUrlCommand extends Command
{
use ShortUrlBuilderTrait;
public const NAME = 'short-url:generate';
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
@@ -87,6 +85,12 @@ class GenerateShortUrlCommand extends Command
'f',
InputOption::VALUE_NONE,
'This will force existing matching URL to be returned if found, instead of creating a new one.'
)
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain to which this short URL will be attached.'
);
}
@@ -119,7 +123,7 @@ class GenerateShortUrlCommand extends Command
$maxVisits = $input->getOption('maxVisits');
try {
$shortCode = $this->urlShortener->urlToShortCode(
$shortUrl = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
$tags,
ShortUrlMeta::createFromParams(
@@ -127,14 +131,14 @@ class GenerateShortUrlCommand extends Command
$this->getOptionalDate($input, 'validUntil'),
$customSlug,
$maxVisits !== null ? (int) $maxVisits : null,
$input->getOption('findIfExists')
$input->getOption('findIfExists'),
$input->getOption('domain')
)
)->getShortCode();
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
);
$io->writeln([
sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Generated short URL: <info>%s</info>', $shortUrl),
sprintf('Generated short URL: <info>%s</info>', $shortUrl->toString($this->domainConfig)),
]);
return ExitCodes::EXIT_SUCCESS;
} catch (InvalidUrlException $e) {

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
@@ -10,6 +11,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -35,7 +37,8 @@ class ResolveUrlCommand extends Command
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse')
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain to which the short URL is attached.');
}
protected function interact(InputInterface $input, OutputInterface $output): void
@@ -56,9 +59,10 @@ class ResolveUrlCommand extends Command
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
$domain = $input->getOption('domain');
try {
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$url = $this->urlShortener->shortCodeToUrl($shortCode, $domain);
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
return ExitCodes::EXIT_SUCCESS;
} catch (InvalidShortCodeException $e) {

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Exception;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Exception;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Factory;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Config;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Db;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Db;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
@@ -9,14 +10,21 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class GenerateShortUrlCommandTest extends TestCase
{
private const DOMAIN_CONFIG = [
'schema' => 'http',
'hostname' => 'foo.com',
];
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
@@ -25,21 +33,17 @@ class GenerateShortUrlCommandTest extends TestCase
public function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
'schema' => 'http',
'hostname' => 'foo.com',
]);
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn(
(new ShortUrl(''))->setShortCode('abc123')
);
$shortUrl = new ShortUrl('');
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn($shortUrl);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
@@ -47,27 +51,43 @@ class GenerateShortUrlCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('http://foo.com/abc123', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
/** @test */
public function exceptionWhileParsingLongUrlOutputsError()
public function exceptionWhileParsingLongUrlOutputsError(): void
{
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
->shouldBeCalledOnce();
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid']);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(
'Provided URL "http://domain.com/invalid" is invalid.',
$output
);
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
$this->assertStringContainsString('Provided URL "http://domain.com/invalid" is invalid.', $output);
}
/** @test */
public function properlyProcessesProvidedTags()
public function providingNonUniqueSlugOutputsError(): void
{
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
NonUniqueSlugException::class
);
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
$output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
$this->assertStringContainsString('Provided slug "my-slug" is already in use', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
/** @test */
public function properlyProcessesProvidedTags(): void
{
$shortUrl = new ShortUrl('');
$urlToShortCode = $this->urlShortener->urlToShortCode(
Argument::type(UriInterface::class),
Argument::that(function (array $tags) {
@@ -75,7 +95,7 @@ class GenerateShortUrlCommandTest extends TestCase
return $tags;
}),
Argument::cetera()
)->willReturn((new ShortUrl(''))->setShortCode('abc123'));
)->willReturn($shortUrl);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
@@ -83,7 +103,8 @@ class GenerateShortUrlCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('http://foo.com/abc123', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
}

Some files were not shown because too many files have changed in this diff Show More