mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 07:13:11 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8966cf9910 | ||
|
|
4eb4df9ca2 | ||
|
|
32861b1c72 | ||
|
|
7248ca2e9b | ||
|
|
a6ec93f883 | ||
|
|
a28c1d17c5 | ||
|
|
fb705b44a4 | ||
|
|
a32bab9fd0 | ||
|
|
6396e7f964 | ||
|
|
c898cef277 | ||
|
|
baeba54b06 | ||
|
|
f5ee5bf7fb | ||
|
|
73605414f9 | ||
|
|
6045c371e1 | ||
|
|
97a9289d5f | ||
|
|
1983fc9b67 | ||
|
|
bb40d84212 | ||
|
|
46a35c553e | ||
|
|
080943e810 | ||
|
|
62fb3863c6 | ||
|
|
2db03a163d | ||
|
|
9e3dd82efe | ||
|
|
9f1989bfef | ||
|
|
c0bdd8fc77 | ||
|
|
8a23c90e46 | ||
|
|
9095e5b057 | ||
|
|
52c18115af | ||
|
|
737137b19f | ||
|
|
7b78bee135 | ||
|
|
accda36a7b | ||
|
|
69dd9eb067 | ||
|
|
a562bc661d | ||
|
|
258f12f684 | ||
|
|
4dc8d77a5a | ||
|
|
7c5825d1bc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,5 +6,5 @@ vendor/
|
||||
.env
|
||||
data/database.sqlite
|
||||
data/GeoLite2-City.mmdb
|
||||
docs/swagger-ui
|
||||
docs/swagger-ui*
|
||||
docker-compose.override.yml
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -41,11 +41,17 @@ before_deploy:
|
||||
- ./build.sh ${TRAVIS_TAG#?}
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
- provider: script
|
||||
script: bash data/travis/trigger_docker_build.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## 1.15.1 - 2018-12-16
|
||||
|
||||
#### Added
|
||||
|
||||
* [#162](https://github.com/shlinkio/shlink/issues/162) Added non-rest endpoints to swagger definition.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#312](https://github.com/shlinkio/shlink/issues/312) Now all config files both in `php` and `json` format are loaded from `config/params` folder, easing users to provided customizations to docker image.
|
||||
* [#226](https://github.com/shlinkio/shlink/issues/226) Updated how table are rendered in CLI commands, making use of new features in Symfony 4.2.
|
||||
* [#321](https://github.com/shlinkio/shlink/issues/321) Extracted entities mappings from entities to external config files.
|
||||
* [#308](https://github.com/shlinkio/shlink/issues/308) Automated docker image building.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#301](https://github.com/shlinkio/shlink/issues/301) Removed custom `AccessLogFactory` in favor of the implementation included in [zendframework/zend-expressive-swoole](https://github.com/zendframework/zend-expressive-swoole) v2.2.0
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#309](https://github.com/shlinkio/shlink/issues/309) Added missing favicon to prevent 404 errors logged when an error page is loaded in a browser.
|
||||
* [#310](https://github.com/shlinkio/shlink/issues/310) Fixed execution context not being properly detected, making `CloseDbConnectionMiddlware` to be always piped. Now the check is not even made, which simplifies everything.
|
||||
|
||||
|
||||
## 1.15.0 - 2018-12-02
|
||||
|
||||
#### Added
|
||||
|
||||
13
README.md
13
README.md
@@ -202,11 +202,12 @@ In future versions, it is planed that, when using **swoole** to serve shlink, so
|
||||
|
||||
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
|
||||
|
||||
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`)
|
||||
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`)
|
||||
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`).
|
||||
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`).
|
||||
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
|
||||
4. If you are using shlink with swoole, restart the service by running `/etc/init.d/shlink_swoole restart`.
|
||||
|
||||
The script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some the assets neccessary for Shlink to function.
|
||||
The `bin/update` script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some assets shlink needs to work.
|
||||
|
||||
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
|
||||
|
||||
@@ -214,11 +215,9 @@ Right now, it does not import cached info (like website previews), but it will.
|
||||
|
||||
## Using a docker image
|
||||
|
||||
Currently there's no official docker image, but there's a work in progress alpha version you can find [here](https://hub.docker.com/r/shlinkio/shlink/).
|
||||
Starting with version 1.15.0, an official docker image is provided. You can find the docs on how to use it [here](https://hub.docker.com/r/shlinkio/shlink/).
|
||||
|
||||
The idea will be that you can just generate a container using the image and provide predefined config files via volumes or CLI arguments, so that you get shlink up and running.
|
||||
|
||||
Currently the image does not expose an entry point which let's you interact with shlink's CLI interface, nor allows configuration to be passed.
|
||||
The idea is that you can just generate a container using the image and provide custom config via env vars.
|
||||
|
||||
## Using shlink
|
||||
|
||||
|
||||
3
bin/cli
3
bin/cli
@@ -3,11 +3,8 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
|
||||
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::CLI));
|
||||
$container->get(CliApp::class)->run();
|
||||
|
||||
5
build.sh
5
build.sh
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
if [[ "$#" -ne 1 ]]; then
|
||||
echo "Usage:" >&2
|
||||
echo " $0 {version}" >&2
|
||||
exit 1
|
||||
@@ -10,7 +10,7 @@ fi
|
||||
version=$1
|
||||
builtcontent="./build/shlink_${version}_dist"
|
||||
projectdir=$(pwd)
|
||||
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
||||
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
||||
|
||||
# Copy project content to temp dir
|
||||
echo 'Copying project files...'
|
||||
@@ -18,6 +18,7 @@ rm -rf "${builtcontent}"
|
||||
mkdir -p "${builtcontent}"
|
||||
rsync -av * "${builtcontent}" \
|
||||
--exclude=data/infra \
|
||||
--exclude=data/travis \
|
||||
--exclude=data/migrations_template.txt \
|
||||
--exclude=data/GeoLite2-City.mmdb \
|
||||
--exclude=**/.gitignore \
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"monolog/monolog": "^1.21",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^4.1",
|
||||
"symfony/filesystem": "^4.1",
|
||||
"symfony/lock": "^4.1",
|
||||
"symfony/process": "^4.1",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/filesystem": "^4.2",
|
||||
"symfony/lock": "^4.2",
|
||||
"symfony/process": "^4.2",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-expressive-swoole": "^2.1",
|
||||
"zendframework/zend-expressive-swoole": "^2.2",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
@@ -57,8 +57,8 @@
|
||||
"phpunit/phpcov": "^5.0",
|
||||
"phpunit/phpunit": "^7.3",
|
||||
"shlinkio/php-coding-standard": "~1.0.0",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/var-dumper": "^4.0",
|
||||
"symfony/dotenv": "^4.2",
|
||||
"symfony/var-dumper": "^4.2",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
'secret_key' => env('SECRET_KEY', ''),
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor;
|
||||
use Zend\Expressive\Swoole\Log\AccessLogInterface;
|
||||
use const PHP_EOL;
|
||||
|
||||
return [
|
||||
@@ -32,7 +31,6 @@ return [
|
||||
'class' => StreamHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'stream' => 'php://stdout',
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -61,15 +59,13 @@ return [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
|
||||
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
|
||||
|
||||
AccessLogInterface::class => Common\Logger\Swoole\AccessLogFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger_name' => 'Logger_Swoole',
|
||||
'logger-name' => 'Logger_Swoole',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -10,18 +10,11 @@ return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'pre-routing' => [
|
||||
'middleware' => (function () {
|
||||
$middleware = [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
];
|
||||
|
||||
if (Common\Exec\ExecutionContext::currentContextIsSwoole()) {
|
||||
$middleware[] = Common\Middleware\CloseDbConnectionMiddleware::class;
|
||||
}
|
||||
|
||||
return $middleware;
|
||||
})(),
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Common\Middleware\CloseDbConnectionMiddleware::class,
|
||||
],
|
||||
'priority' => 12,
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
|
||||
@@ -9,9 +9,6 @@ return [
|
||||
'swoole-http-server' => [
|
||||
'host' => '0.0.0.0',
|
||||
'process-name' => 'shlink',
|
||||
'static-files' => [
|
||||
'enable' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -19,5 +19,6 @@ return (new ConfigAggregator\ConfigAggregator([
|
||||
CLI\ConfigProvider::class,
|
||||
Installer\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||
|
||||
20
data/travis/trigger_docker_build.sh
Normal file
20
data/travis/trigger_docker_build.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get latest commit in master, in plain text
|
||||
LATEST_MASTER_COMMIT=$(curl -H "Accept: application/vnd.github.sha" -X GET https://api.github.com/repos/shlinkio/shlink-docker-image/commits/master)
|
||||
|
||||
# Create new tag and a ref to the tag, which will trigger image build on it
|
||||
curl -u acelaya:${GITHUB_OAUTH_KEY} \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{ \"tag\": \"${TRAVIS_TAG}\", \"message\": \"${TRAVIS_TAG}\", \"object\": \"${LATEST_MASTER_COMMIT}\", \"type\": \"commit\" }" \
|
||||
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/tags
|
||||
curl -u acelaya:${GITHUB_OAUTH_KEY} \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{ \"ref\": \"refs/tags/${TRAVIS_TAG}\", \"sha\": \"${LATEST_MASTER_COMMIT}\" }" \
|
||||
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/refs
|
||||
|
||||
# Trigger image build for "latest
|
||||
curl -H "Content-Type: application/json" \
|
||||
--data '{ "docker_tag": "latest" }' \
|
||||
-X POST https://registry.hub.docker.com/u/shlinkio/shlink/trigger/${DOCKER_TRIGGER_TOKEN}/
|
||||
36
docs/swagger/paths/{shortCode}.json
Normal file
36
docs/swagger/paths/{shortCode}.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrl",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL",
|
||||
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Visit properly tracked and redirected"
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
docs/swagger/paths/{shortCode}_preview.json
Normal file
44
docs/swagger/paths/{shortCode}_preview.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrlPreview",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL preview image",
|
||||
"description": "Returns the preview of the page behind a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
docs/swagger/paths/{shortCode}_qr-code.json
Normal file
56
docs/swagger/paths/{shortCode}_qr-code.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrlQrCode",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL QR code",
|
||||
"description": "Generates a QR code image pointing to a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"in": "path",
|
||||
"description": "The size of the image to be returned.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 50,
|
||||
"maximum": 1000,
|
||||
"default": 300
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "QR code in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
docs/swagger/paths/{shortCode}_track.json
Normal file
44
docs/swagger/paths/{shortCode}_track.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "trackShortUrl",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL tracking pixel",
|
||||
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image in GIF format",
|
||||
"content": {
|
||||
"image/gif": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,20 @@
|
||||
"version": "1.0"
|
||||
},
|
||||
|
||||
"externalDocs": {
|
||||
"url": "https://shlink.io/api-docs",
|
||||
"description": "Find more info on how to start using this API here"
|
||||
},
|
||||
|
||||
"servers": [
|
||||
{
|
||||
"url": "{schema}://{server}/rest",
|
||||
"url": "{scheme}://{host}",
|
||||
"variables": {
|
||||
"schema": {
|
||||
"scheme": {
|
||||
"default": "https",
|
||||
"enum": ["https", "http"]
|
||||
},
|
||||
"server": {
|
||||
"host": {
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
@@ -51,35 +56,52 @@
|
||||
"name": "Visits",
|
||||
"description": "Operations to manage visits on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "URL Shortener",
|
||||
"description": "Non-rest endpoints, used to be publicly exposed"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Authentication-related endpoints"
|
||||
"description": "**[Deprecated]** Authentication-related endpoints"
|
||||
}
|
||||
],
|
||||
|
||||
"paths": {
|
||||
"/v1/short-urls": {
|
||||
"/rest/v1/short-urls": {
|
||||
"$ref": "paths/v1_short-urls.json"
|
||||
},
|
||||
"/v1/short-urls/shorten": {
|
||||
"/rest/v1/short-urls/shorten": {
|
||||
"$ref": "paths/v1_short-urls_shorten.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}": {
|
||||
"/rest/v1/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}/tags": {
|
||||
"/rest/v1/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/v1/tags": {
|
||||
"/rest/v1/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/v1/short-urls/{shortCode}/visits": {
|
||||
"/rest/v1/short-urls/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||
},
|
||||
|
||||
"/v1/authenticate": {
|
||||
"/{shortCode}": {
|
||||
"$ref": "paths/{shortCode}.json"
|
||||
},
|
||||
"/{shortCode}/track": {
|
||||
"$ref": "paths/{shortCode}_track.json"
|
||||
},
|
||||
"/{shortCode}/qr-code": {
|
||||
"$ref": "paths/{shortCode}_qr-code.json"
|
||||
},
|
||||
"/{shortCode}/preview": {
|
||||
"$ref": "paths/{shortCode}_preview.json"
|
||||
},
|
||||
|
||||
"/rest/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ $config['entity_manager']['connection'] = [
|
||||
$sm->setService('config', $config);
|
||||
|
||||
// Create database
|
||||
$process = new Process('vendor/bin/doctrine orm:schema-tool:create --no-interaction -q --test', __DIR__);
|
||||
$process = new Process(['vendor/bin/doctrine', 'orm:schema-tool:create', '--no-interaction', '-q', '--test'], __DIR__);
|
||||
$process->inheritEnvironmentVariables()
|
||||
->mustRun();
|
||||
|
||||
|
||||
3
indocker
3
indocker
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run docker containers if they are not up yet
|
||||
if [[ $(docker ps | grep shlink_swoole) ]]; then :
|
||||
else
|
||||
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
@@ -46,7 +46,6 @@ class ListKeysCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
@@ -62,7 +61,7 @@ class ListKeysCommand extends Command
|
||||
return $rowData;
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
$io->table(array_filter([
|
||||
ShlinkTable::fromOutput($output)->render(array_filter([
|
||||
'Key',
|
||||
! $enabledOnly ? 'Is enabled' : null,
|
||||
'Expiration date',
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
@@ -69,7 +70,6 @@ class GetVisitsCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
@@ -82,7 +82,7 @@ class GetVisitsCommand extends Command
|
||||
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||
}, $visits);
|
||||
$io->table(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
}
|
||||
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
|
||||
@@ -3,8 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -12,6 +14,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Paginator\Paginator;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
@@ -78,39 +81,56 @@ class ListShortUrlsCommand extends Command
|
||||
$searchTerm = $input->getOption('searchTerm');
|
||||
$tags = $input->getOption('tags');
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
$showTags = $input->getOption('showTags');
|
||||
$showTags = (bool) $input->getOption('showTags');
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
|
||||
$page++;
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
$headers[] = 'Tags';
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = array_values($shortUrl);
|
||||
}
|
||||
$io->table($headers, $rows);
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$io->success('Short URLs properly listed');
|
||||
} else {
|
||||
$continue = $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
|
||||
}
|
||||
$continue = $this->isLastPage($result)
|
||||
? false
|
||||
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
|
||||
} while ($continue);
|
||||
|
||||
$io->newLine();
|
||||
$io->success('Short URLs properly listed');
|
||||
}
|
||||
|
||||
private function renderPage(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
int $page,
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
bool $showTags,
|
||||
DataTransformerInterface $transformer
|
||||
): Paginator {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
$headers[] = 'Tags';
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = array_values($shortUrl);
|
||||
}
|
||||
|
||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
||||
$result,
|
||||
'Page %s of %s'
|
||||
));
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function processOrderBy(InputInterface $input)
|
||||
|
||||
@@ -3,12 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function Functional\map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
@@ -33,8 +33,7 @@ class ListTagsCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table(['Name'], $this->getTagsRows());
|
||||
ShlinkTable::fromOutput($output)->render(['Name'], $this->getTagsRows());
|
||||
}
|
||||
|
||||
private function getTagsRows(): array
|
||||
|
||||
41
module/Common/src/Console/ShlinkTable.php
Normal file
41
module/Common/src/Console/ShlinkTable.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Console;
|
||||
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class ShlinkTable
|
||||
{
|
||||
private const DEFAULT_STYLE_NAME = 'default';
|
||||
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
||||
|
||||
/** @var Table|null */
|
||||
private $baseTable;
|
||||
|
||||
public function __construct(Table $baseTable)
|
||||
{
|
||||
$this->baseTable = $baseTable;
|
||||
}
|
||||
|
||||
public static function fromOutput(OutputInterface $output): self
|
||||
{
|
||||
return new self(new Table($output));
|
||||
}
|
||||
|
||||
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
|
||||
{
|
||||
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
|
||||
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
|
||||
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
|
||||
|
||||
$table = clone $this->baseTable;
|
||||
$table->setStyle($style)
|
||||
->setHeaders($headers)
|
||||
->setRows($rows)
|
||||
->setFooterTitle($footerTitle)
|
||||
->setHeaderTitle($headerTitle)
|
||||
->render();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Exec;
|
||||
|
||||
use const PHP_SAPI;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
abstract class ExecutionContext
|
||||
{
|
||||
public const WEB = 'shlink_web';
|
||||
public const CLI = 'shlink_cli';
|
||||
|
||||
public static function currentContextIsSwoole(): bool
|
||||
{
|
||||
return PHP_SAPI === 'cli' && env('CURRENT_SHLINK_CONTEXT', self::WEB) === self::WEB;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
@@ -23,6 +24,7 @@ class CacheFactory implements FactoryInterface
|
||||
Cache\PhpFileCache::class,
|
||||
Cache\MemcachedCache::class,
|
||||
];
|
||||
private const DEFAULT_MEMCACHED_PORT = 11211;
|
||||
|
||||
/**
|
||||
* Create an object
|
||||
@@ -40,7 +42,7 @@ class CacheFactory implements FactoryInterface
|
||||
{
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$adapter = $this->getAdapter($container);
|
||||
$adapter->setNamespace($appOptions->__toString());
|
||||
$adapter->setNamespace((string) $appOptions);
|
||||
|
||||
return $adapter;
|
||||
}
|
||||
@@ -65,25 +67,35 @@ class CacheFactory implements FactoryInterface
|
||||
return new $cacheConfig['adapter']();
|
||||
case Cache\FilesystemCache::class:
|
||||
case Cache\PhpFileCache::class:
|
||||
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
||||
return new $cacheConfig['adapter']($cacheConfig['options']['dir'] ?? sys_get_temp_dir());
|
||||
case Cache\MemcachedCache::class:
|
||||
$memcached = new Memcached();
|
||||
$servers = $cacheConfig['options']['servers'] ?? [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (! isset($server['host'])) {
|
||||
continue;
|
||||
}
|
||||
$port = isset($server['port']) ? (int) $server['port'] : 11211;
|
||||
|
||||
$memcached->addServer($server['host'], $port);
|
||||
}
|
||||
|
||||
$cache = new Cache\MemcachedCache();
|
||||
$cache->setMemcached($memcached);
|
||||
$cache->setMemcached($this->buildMemcached($cacheConfig));
|
||||
return $cache;
|
||||
default:
|
||||
return new Cache\ArrayCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildMemcached(array $cacheConfig): Memcached
|
||||
{
|
||||
$memcached = new Memcached();
|
||||
$servers = $cacheConfig['options']['servers'] ?? [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$this->addMemcachedServer($memcached, $server);
|
||||
}
|
||||
|
||||
return $memcached;
|
||||
}
|
||||
|
||||
private function addMemcachedServer(Memcached $memcached, array $server): void
|
||||
{
|
||||
if (! isset($server['host'])) {
|
||||
return;
|
||||
}
|
||||
$port = (int) ($server['port'] ?? self::DEFAULT_MEMCACHED_PORT);
|
||||
|
||||
$memcached->addServer($server['host'], $port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\Common\Persistence\Mapping\Driver\PHPDriver;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
@@ -37,12 +38,9 @@ class EntityManagerFactory implements FactoryInterface
|
||||
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
||||
}
|
||||
|
||||
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
$ormConfig['entities_paths'] ?? [],
|
||||
$isDevMode,
|
||||
$ormConfig['proxies_dir'] ?? null,
|
||||
$cache,
|
||||
false
|
||||
));
|
||||
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
|
||||
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
|
||||
|
||||
return EntityManager::create($connectionConfig, $config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Logger\Swoole;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Zend\Expressive\Swoole\Log;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class AccessLogFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
$config = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
|
||||
|
||||
return new Log\Psr3AccessLogDecorator(
|
||||
$this->getLogger($container, $config),
|
||||
$this->getFormatter($container, $config),
|
||||
$config['use-hostname-lookups'] ?? false
|
||||
);
|
||||
}
|
||||
|
||||
private function getLogger(ContainerInterface $container, array $config): LoggerInterface
|
||||
{
|
||||
$loggerName = $config['logger_name'] ?? LoggerInterface::class;
|
||||
return $container->has($loggerName) ? $container->get($loggerName) : new Log\StdoutLogger();
|
||||
}
|
||||
|
||||
private function getFormatter(ContainerInterface $container, array $config): Log\AccessLogFormatterInterface
|
||||
{
|
||||
if ($container->has(Log\AccessLogFormatterInterface::class)) {
|
||||
return $container->get(Log\AccessLogFormatterInterface::class);
|
||||
}
|
||||
|
||||
return new Log\AccessLogFormatter($config['format'] ?? Log\AccessLogFormatter::FORMAT_COMMON);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Zend\Paginator\Paginator;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
trait PaginatorUtilsTrait
|
||||
{
|
||||
@@ -39,4 +40,9 @@ trait PaginatorUtilsTrait
|
||||
{
|
||||
return $paginator->getCurrentPageNumber() >= $paginator->count();
|
||||
}
|
||||
|
||||
private function formatCurrentPageMessage(Paginator $paginator, string $pattern): string
|
||||
{
|
||||
return sprintf($pattern, $paginator->getCurrentPageNumber(), $paginator->count());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@ class TranslatorExtension implements ExtensionInterface
|
||||
public function register(Engine $engine): void
|
||||
{
|
||||
$engine->registerFunction('translate', [$this->translator, 'translate']);
|
||||
$engine->registerFunction('locale', [$this->translator, 'getLocale']);
|
||||
}
|
||||
}
|
||||
|
||||
70
module/Common/test/Console/ShlinkTableTest.php
Normal file
70
module/Common/test/Console/ShlinkTableTest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Console;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableStyle;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ShlinkTableTest extends TestCase
|
||||
{
|
||||
/** @var ShlinkTable */
|
||||
private $shlinkTable;
|
||||
/** @var ObjectProphecy */
|
||||
private $baseTable;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->baseTable = $this->prophesize(Table::class);
|
||||
$this->shlinkTable = new ShlinkTable($this->baseTable->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function renderMakesTableToBeRenderedWithProvidedInfo()
|
||||
{
|
||||
$headers = [];
|
||||
$rows = [[]];
|
||||
$headerTitle = 'Header';
|
||||
$footerTitle = 'Footer';
|
||||
|
||||
$setStyle = $this->baseTable->setStyle(Argument::type(TableStyle::class))->willReturn(
|
||||
$this->baseTable->reveal()
|
||||
);
|
||||
$setHeaders = $this->baseTable->setHeaders($headers)->willReturn($this->baseTable->reveal());
|
||||
$setRows = $this->baseTable->setRows($rows)->willReturn($this->baseTable->reveal());
|
||||
$setFooterTitle = $this->baseTable->setFooterTitle($footerTitle)->willReturn($this->baseTable->reveal());
|
||||
$setHeaderTitle = $this->baseTable->setHeaderTitle($headerTitle)->willReturn($this->baseTable->reveal());
|
||||
$render = $this->baseTable->render()->willReturn($this->baseTable->reveal());
|
||||
|
||||
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
|
||||
|
||||
$setStyle->shouldHaveBeenCalledOnce();
|
||||
$setHeaders->shouldHaveBeenCalledOnce();
|
||||
$setRows->shouldHaveBeenCalledOnce();
|
||||
$setFooterTitle->shouldHaveBeenCalledOnce();
|
||||
$setHeaderTitle->shouldHaveBeenCalledOnce();
|
||||
$render->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function newTableIsCreatedForFactoryMethod()
|
||||
{
|
||||
$instance = ShlinkTable::fromOutput($this->prophesize(OutputInterface::class)->reveal());
|
||||
|
||||
$ref = new ReflectionObject($instance);
|
||||
$baseTable = $ref->getProperty('baseTable');
|
||||
$baseTable->setAccessible(true);
|
||||
|
||||
$this->assertInstanceOf(Table::class, $baseTable->getValue($instance));
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Logger\Swoole;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Common\Logger\Swoole\AccessLogFactory;
|
||||
use Zend\Expressive\Swoole\Log\AccessLogFormatter;
|
||||
use Zend\Expressive\Swoole\Log\AccessLogFormatterInterface;
|
||||
use Zend\Expressive\Swoole\Log\Psr3AccessLogDecorator;
|
||||
use Zend\Expressive\Swoole\Log\StdoutLogger;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
use function is_string;
|
||||
|
||||
class AccessLogFactoryTest extends TestCase
|
||||
{
|
||||
/** @var AccessLogFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new AccessLogFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function createsService()
|
||||
{
|
||||
$service = ($this->factory)(new ServiceManager(), '');
|
||||
$this->assertInstanceOf(Psr3AccessLogDecorator::class, $service);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideLoggers
|
||||
* @param array $config
|
||||
* @param string|LoggerInterface $expectedLogger
|
||||
*/
|
||||
public function wrapsProperLogger(array $config, $expectedLogger)
|
||||
{
|
||||
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
|
||||
|
||||
$ref = new ReflectionObject($service);
|
||||
$loggerProp = $ref->getProperty('logger');
|
||||
$loggerProp->setAccessible(true);
|
||||
$logger = $loggerProp->getValue($service);
|
||||
|
||||
if (is_string($expectedLogger)) {
|
||||
$this->assertInstanceOf($expectedLogger, $logger);
|
||||
} else {
|
||||
$this->assertSame($expectedLogger, $logger);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideLoggers(): iterable
|
||||
{
|
||||
yield 'without-any-logger' => [[], StdoutLogger::class];
|
||||
yield 'with-standard-logger' => (function () {
|
||||
$logger = new NullLogger();
|
||||
return [[LoggerInterface::class => $logger], $logger];
|
||||
})();
|
||||
yield 'with-custom-logger' => (function () {
|
||||
$logger = new NullLogger();
|
||||
return [[
|
||||
'config' => [
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger_name' => 'my-logger',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'my-logger' => $logger,
|
||||
], $logger];
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFormatters
|
||||
* @param array $config
|
||||
* @param string|AccessLogFormatterInterface $expectedFormatter
|
||||
*/
|
||||
public function wrappsProperFormatter(array $config, $expectedFormatter, string $expectedFormat)
|
||||
{
|
||||
$service = ($this->factory)(new ServiceManager(['services' => $config]), '');
|
||||
|
||||
$ref = new ReflectionObject($service);
|
||||
$formatterProp = $ref->getProperty('formatter');
|
||||
$formatterProp->setAccessible(true);
|
||||
$formatter = $formatterProp->getValue($service);
|
||||
|
||||
$ref = new ReflectionObject($formatter);
|
||||
$formatProp = $ref->getProperty('format');
|
||||
$formatProp->setAccessible(true);
|
||||
$format = $formatProp->getValue($formatter);
|
||||
|
||||
if (is_string($expectedFormatter)) {
|
||||
$this->assertInstanceOf($expectedFormatter, $formatter);
|
||||
} else {
|
||||
$this->assertSame($expectedFormatter, $formatter);
|
||||
}
|
||||
$this->assertSame($expectedFormat, $format);
|
||||
}
|
||||
|
||||
public function provideFormatters(): iterable
|
||||
{
|
||||
yield 'with-registered-formatter-and-default-format' => (function () {
|
||||
$formatter = new AccessLogFormatter();
|
||||
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_COMMON];
|
||||
})();
|
||||
yield 'with-registered-formatter-and-custom-format' => (function () {
|
||||
$formatter = new AccessLogFormatter(AccessLogFormatter::FORMAT_AGENT);
|
||||
return [[AccessLogFormatterInterface::class => $formatter], $formatter, AccessLogFormatter::FORMAT_AGENT];
|
||||
})();
|
||||
yield 'with-no-formatter-and-not-configured-format' => [
|
||||
[],
|
||||
AccessLogFormatter::class,
|
||||
AccessLogFormatter::FORMAT_COMMON,
|
||||
];
|
||||
yield 'with-no-formatter-and-configured-format' => [[
|
||||
'config' => [
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'format' => AccessLogFormatter::FORMAT_COMBINED_DEBIAN,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
], AccessLogFormatter::class, AccessLogFormatter::FORMAT_COMBINED_DEBIAN];
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,14 @@ class TranslatorExtensionTest extends TestCase
|
||||
public function properFunctionsAreReturned()
|
||||
{
|
||||
$engine = $this->prophesize(Engine::class);
|
||||
$registerFunction = $engine->registerFunction('translate', Argument::type('callable'))->will(function () {
|
||||
$registerTranslate = $engine->registerFunction('translate', Argument::type('callable'))->will(function () {
|
||||
});
|
||||
$registerLocale = $engine->registerFunction('locale', Argument::type('array'))->will(function () {
|
||||
});
|
||||
|
||||
$this->extension->register($engine->reveal());
|
||||
|
||||
$registerFunction->shouldHaveBeenCalledOnce();
|
||||
$registerTranslate->shouldHaveBeenCalledOnce();
|
||||
$registerLocale->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('short_urls')
|
||||
->setCustomRepositoryClass(Repository\ShortUrlRepository::class);
|
||||
|
||||
$builder->createField('id', Type::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('longUrl', Type::STRING)
|
||||
->columnName('original_url')
|
||||
->length(1024)
|
||||
->build();
|
||||
|
||||
$builder->createField('shortCode', Type::STRING)
|
||||
->columnName('short_code')
|
||||
->unique()
|
||||
->length(255)
|
||||
->build();
|
||||
|
||||
$builder->createField('dateCreated', ChronosDateTimeType::CHRONOS_DATETIME)
|
||||
->columnName('date_created')
|
||||
->build();
|
||||
|
||||
$builder->createField('validSince', ChronosDateTimeType::CHRONOS_DATETIME)
|
||||
->columnName('valid_since')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('validUntil', ChronosDateTimeType::CHRONOS_DATETIME)
|
||||
->columnName('valid_until')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('maxVisits', Type::INTEGER)
|
||||
->columnName('max_visits')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('visits', Entity\Visit::class)
|
||||
->mappedBy('shortUrl')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createManyToMany('tags', Entity\Tag::class)
|
||||
->setJoinTable('short_urls_in_tags')
|
||||
->addInverseJoinColumn('tag_id', 'id')
|
||||
->addJoinColumn('short_url_id', 'id')
|
||||
->build();
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('tags')
|
||||
->setCustomRepositoryClass(Repository\TagRepository::class);
|
||||
|
||||
$builder->createField('id', Type::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('name', Type::STRING)
|
||||
->unique()
|
||||
->build();
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('visits')
|
||||
->setCustomRepositoryClass(Repository\VisitRepository::class);
|
||||
|
||||
$builder->createField('id', Type::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('referer', Type::STRING)
|
||||
->nullable()
|
||||
->length(256)
|
||||
->build();
|
||||
|
||||
$builder->createField('date', ChronosDateTimeType::CHRONOS_DATETIME)
|
||||
->columnName('`date`')
|
||||
->build();
|
||||
|
||||
$builder->createField('remoteAddr', Type::STRING)
|
||||
->columnName('remote_addr')
|
||||
->length(256)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('userAgent', Type::STRING)
|
||||
->columnName('user_agent')
|
||||
->length(512)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('shortUrl', Entity\ShortUrl::class)
|
||||
->addJoinColumn('short_url_id', 'id', false)
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('visitLocation', Entity\VisitLocation::class)
|
||||
->addJoinColumn('visit_location_id', 'id')
|
||||
->cascadePersist()
|
||||
->build();
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('visit_locations');
|
||||
|
||||
$builder->createField('id', Type::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$columns = [
|
||||
'country_code' => 'countryCode',
|
||||
'country_name' => 'countryName',
|
||||
'region_name' => 'regionName',
|
||||
'city_name' => 'cityName',
|
||||
'latitude' => 'latitude',
|
||||
'longitude' => 'longitude',
|
||||
'timezone' => 'timezone',
|
||||
];
|
||||
|
||||
foreach ($columns as $columnName => $fieldName) {
|
||||
$builder->createField($fieldName, Type::STRING)
|
||||
->columnName($columnName)
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
@@ -5,8 +5,8 @@ return [
|
||||
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'entities_paths' => [
|
||||
__DIR__ . '/../src/Entity',
|
||||
'entities_mappings' => [
|
||||
__DIR__ . '/../config/entities-mappings',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -6,72 +6,27 @@ namespace Shlinkio\Shlink\Core\Entity;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Class ShortUrl
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity(repositoryClass=ShortUrlRepository::class)
|
||||
* @ORM\Table(name="short_urls")
|
||||
*/
|
||||
class ShortUrl extends AbstractEntity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="original_url", type="string", nullable=false, length=1024)
|
||||
*/
|
||||
/** @var string */
|
||||
private $longUrl;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(
|
||||
* name="short_code",
|
||||
* type="string",
|
||||
* nullable=false,
|
||||
* length=255,
|
||||
* unique=true
|
||||
* )
|
||||
*/
|
||||
/** @var string */
|
||||
private $shortCode;
|
||||
/**
|
||||
* @var Chronos
|
||||
* @ORM\Column(name="date_created", type="chronos_datetime")
|
||||
*/
|
||||
/** @var Chronos */
|
||||
private $dateCreated;
|
||||
/**
|
||||
* @var Collection|Visit[]
|
||||
* @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
/** @var Collection|Visit[] */
|
||||
private $visits;
|
||||
/**
|
||||
* @var Collection|Tag[]
|
||||
* @ORM\ManyToMany(targetEntity=Tag::class, cascade={"persist"})
|
||||
* @ORM\JoinTable(name="short_urls_in_tags", joinColumns={
|
||||
* @ORM\JoinColumn(name="short_url_id", referencedColumnName="id")
|
||||
* }, inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||
* })
|
||||
*/
|
||||
/** @var Collection|Tag[] */
|
||||
private $tags;
|
||||
/**
|
||||
* @var Chronos|null
|
||||
* @ORM\Column(name="valid_since", type="chronos_datetime", nullable=true)
|
||||
*/
|
||||
/** @var Chronos|null */
|
||||
private $validSince;
|
||||
/**
|
||||
* @var Chronos|null
|
||||
* @ORM\Column(name="valid_until", type="chronos_datetime", nullable=true)
|
||||
*/
|
||||
/** @var Chronos|null */
|
||||
private $validUntil;
|
||||
/**
|
||||
* @var integer
|
||||
* @ORM\Column(name="max_visits", type="integer", nullable=true)
|
||||
*/
|
||||
/** @var integer|null */
|
||||
private $maxVisits;
|
||||
|
||||
public function __construct(string $longUrl, ShortUrlMeta $meta = null)
|
||||
|
||||
@@ -3,25 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JsonSerializable;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
|
||||
/**
|
||||
* Class Tag
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity(repositoryClass=TagRepository::class)
|
||||
* @ORM\Table(name="tags")
|
||||
*/
|
||||
class Tag extends AbstractEntity implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(unique=true)
|
||||
*/
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
|
||||
@@ -4,55 +4,27 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JsonSerializable;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
|
||||
|
||||
/**
|
||||
* Class Visit
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity(repositoryClass=VisitRepository::class)
|
||||
* @ORM\Table(name="visits")
|
||||
*/
|
||||
class Visit extends AbstractEntity implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(type="string", length=256, nullable=true)
|
||||
*/
|
||||
/** @var string */
|
||||
private $referer;
|
||||
/**
|
||||
* @var Chronos
|
||||
* @ORM\Column(type="chronos_datetime", nullable=false)
|
||||
*/
|
||||
/** @var Chronos */
|
||||
private $date;
|
||||
/**
|
||||
* @var string|null
|
||||
* @ORM\Column(type="string", length=256, name="remote_addr", nullable=true)
|
||||
*/
|
||||
/** @var string|null */
|
||||
private $remoteAddr;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(type="string", length=512, name="user_agent", nullable=true)
|
||||
*/
|
||||
/** @var string */
|
||||
private $userAgent;
|
||||
/**
|
||||
* @var ShortUrl
|
||||
* @ORM\ManyToOne(targetEntity=ShortUrl::class)
|
||||
* @ORM\JoinColumn(name="short_url_id", referencedColumnName="id")
|
||||
*/
|
||||
/** @var ShortUrl */
|
||||
private $shortUrl;
|
||||
/**
|
||||
* @var VisitLocation
|
||||
* @ORM\ManyToOne(targetEntity=VisitLocation::class, cascade={"persist"})
|
||||
* @ORM\JoinColumn(name="visit_location_id", referencedColumnName="id", nullable=true)
|
||||
*/
|
||||
/** @var VisitLocation */
|
||||
private $visitLocation;
|
||||
|
||||
public function __construct(ShortUrl $shortUrl, Visitor $visitor, ?Chronos $date = null)
|
||||
@@ -88,9 +60,9 @@ class Visit extends AbstractEntity implements JsonSerializable
|
||||
return ! empty($this->remoteAddr);
|
||||
}
|
||||
|
||||
public function getVisitLocation(): VisitLocation
|
||||
public function getVisitLocation(): VisitLocationInterface
|
||||
{
|
||||
return $this->visitLocation;
|
||||
return $this->visitLocation ?? new UnknownVisitLocation();
|
||||
}
|
||||
|
||||
public function locate(VisitLocation $visitLocation): self
|
||||
|
||||
@@ -3,55 +3,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JsonSerializable;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Class VisitLocation
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="visit_locations")
|
||||
*/
|
||||
class VisitLocation extends AbstractEntity implements JsonSerializable
|
||||
class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="country_code")
|
||||
*/
|
||||
/** @var string */
|
||||
private $countryCode;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="country_name")
|
||||
*/
|
||||
/** @var string */
|
||||
private $countryName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="region_name")
|
||||
*/
|
||||
/** @var string */
|
||||
private $regionName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="city_name")
|
||||
*/
|
||||
/** @var string */
|
||||
private $cityName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="latitude")
|
||||
*/
|
||||
/** @var string */
|
||||
private $latitude;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="longitude")
|
||||
*/
|
||||
/** @var string */
|
||||
private $longitude;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true, name="timezone")
|
||||
*/
|
||||
/** @var string */
|
||||
private $timezone;
|
||||
|
||||
public function __construct(array $locationInfo)
|
||||
|
||||
@@ -15,69 +15,48 @@ class AppOptions extends AbstractOptions
|
||||
private $name = '';
|
||||
/** @var string */
|
||||
private $version = '1.0';
|
||||
/** @var string */
|
||||
/**
|
||||
* @var string
|
||||
* @deprecated
|
||||
*/
|
||||
private $secretKey = '';
|
||||
/** @var string|null */
|
||||
private $disableTrackParam;
|
||||
|
||||
/**
|
||||
* AppOptions constructor.
|
||||
* @param array|null|\Traversable $options
|
||||
*/
|
||||
public function __construct($options = null)
|
||||
{
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
protected function setName($name)
|
||||
protected function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion()
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return $this
|
||||
*/
|
||||
protected function setVersion($version)
|
||||
protected function setVersion(string $version): self
|
||||
{
|
||||
$this->version = $version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @deprecated
|
||||
*/
|
||||
public function getSecretKey()
|
||||
public function getSecretKey(): string
|
||||
{
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $secretKey
|
||||
* @return $this
|
||||
* @deprecated
|
||||
*/
|
||||
protected function setSecretKey($secretKey)
|
||||
protected function setSecretKey(string $secretKey): self
|
||||
{
|
||||
$this->secretKey = $secretKey;
|
||||
return $this;
|
||||
@@ -86,25 +65,18 @@ class AppOptions extends AbstractOptions
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDisableTrackParam()
|
||||
public function getDisableTrackParam(): ?string
|
||||
{
|
||||
return $this->disableTrackParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $disableTrackParam
|
||||
* @return $this|self
|
||||
*/
|
||||
protected function setDisableTrackParam($disableTrackParam): self
|
||||
protected function setDisableTrackParam(?string $disableTrackParam): self
|
||||
{
|
||||
$this->disableTrackParam = $disableTrackParam;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('%s:v%s', $this->name, $this->version);
|
||||
}
|
||||
|
||||
47
module/Core/src/Visit/Model/UnknownVisitLocation.php
Normal file
47
module/Core/src/Visit/Model/UnknownVisitLocation.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Visit\Model;
|
||||
|
||||
final class UnknownVisitLocation implements VisitLocationInterface
|
||||
{
|
||||
public function getCountryName(): string
|
||||
{
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
public function getLatitude(): string
|
||||
{
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
public function getLongitude(): string
|
||||
{
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
public function getCityName(): string
|
||||
{
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'countryCode' => 'Unknown',
|
||||
'countryName' => 'Unknown',
|
||||
'regionName' => 'Unknown',
|
||||
'cityName' => 'Unknown',
|
||||
'latitude' => '0.0',
|
||||
'longitude' => '0.0',
|
||||
'timezone' => 'Unknown',
|
||||
];
|
||||
}
|
||||
}
|
||||
17
module/Core/src/Visit/Model/VisitLocationInterface.php
Normal file
17
module/Core/src/Visit/Model/VisitLocationInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Visit\Model;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
interface VisitLocationInterface extends JsonSerializable
|
||||
{
|
||||
public function getCountryName(): string;
|
||||
|
||||
public function getLatitude(): string;
|
||||
|
||||
public function getLongitude(): string;
|
||||
|
||||
public function getCityName(): string;
|
||||
}
|
||||
@@ -1,31 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><?= $this->section('title', '') ?> | URL shortener</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
|
||||
<style>
|
||||
body {padding-top: 60px;}
|
||||
.app {display: flex; min-height: 100vh; flex-direction: column;}
|
||||
.app-content {flex: 1;}
|
||||
.app-footer p {margin-bottom: 20px;}
|
||||
</style>
|
||||
<?= $this->section('stylesheets', '') ?>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="app-content">
|
||||
<main class="container">
|
||||
<?= $this->section('main', '') ?>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="container">
|
||||
<hr />
|
||||
<p>© <?= date('Y') ?> <a href="https://shlink.io">Shlink</a></p>
|
||||
<html lang="<?= $this->locale() ?>">
|
||||
<head>
|
||||
<title><?= $this->section('title', '') ?> | URL shortener</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
<style>
|
||||
body {padding-top: 60px;}
|
||||
.app {display: flex; min-height: 100vh; flex-direction: column;}
|
||||
.app-content {flex: 1;}
|
||||
.app-footer p {margin-bottom: 20px;}
|
||||
</style>
|
||||
<?= $this->section('stylesheets', '') ?>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="app-content">
|
||||
<main class="container">
|
||||
<?= $this->section('main', '') ?>
|
||||
</main>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="container">
|
||||
<hr>
|
||||
<p>© <?= date('Y') ?> <a href="https://shlink.io">Shlink</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,7 +20,8 @@ use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
use function sprintf;
|
||||
use function array_unshift;
|
||||
use function implode;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
@@ -133,7 +134,7 @@ class InstallCommand extends Command
|
||||
if (! $this->isUpdate) {
|
||||
$this->io->write('Initializing database...');
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
|
||||
['vendor/doctrine/orm/bin/doctrine.php', 'orm:schema-tool:create'],
|
||||
'Error generating database.',
|
||||
$output
|
||||
)) {
|
||||
@@ -144,7 +145,7 @@ class InstallCommand extends Command
|
||||
// Run database migrations
|
||||
$this->io->write('Updating database...');
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
|
||||
['vendor/doctrine/migrations/bin/doctrine-migrations.php', 'migrations:migrate'],
|
||||
'Error updating database.',
|
||||
$output
|
||||
)) {
|
||||
@@ -154,16 +155,16 @@ class InstallCommand extends Command
|
||||
// Generate proxies
|
||||
$this->io->write('Generating proxies...');
|
||||
if (! $this->execPhp(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
|
||||
['vendor/doctrine/orm/bin/doctrine.php', 'orm:generate-proxies'],
|
||||
'Error generating proxies.',
|
||||
$output
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download GeoLite2 db filte
|
||||
// Download GeoLite2 db file
|
||||
$this->io->write('Downloading GeoLite2 db...');
|
||||
if (! $this->execPhp('bin/cli visit:update-db', 'Error downloading GeoLite2 db.', $output)) {
|
||||
if (! $this->execPhp(['bin/cli', 'visit:update-db'], 'Error downloading GeoLite2 db.', $output)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,7 +216,7 @@ class InstallCommand extends Command
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function execPhp(string $command, string $errorMessage, OutputInterface $output): bool
|
||||
private function execPhp(array $command, string $errorMessage, OutputInterface $output): bool
|
||||
{
|
||||
if ($this->processHelper === null) {
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
@@ -225,12 +226,13 @@ class InstallCommand extends Command
|
||||
$this->phpBinary = $this->phpFinder->find(false) ?: 'php';
|
||||
}
|
||||
|
||||
array_unshift($command, $this->phpBinary);
|
||||
$this->io->write(
|
||||
' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ',
|
||||
' <options=bold>[Running "' . implode(' ', $command) . '"]</> ',
|
||||
false,
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
|
||||
$process = $this->processHelper->run($output, $command);
|
||||
if ($process->isSuccessful()) {
|
||||
$this->io->writeln(' <info>Success!</info>');
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
/** @var $metadata ClassMetadata */
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('api_keys');
|
||||
|
||||
$builder->createField('id', Type::BIGINT)
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('key', Type::STRING)
|
||||
->columnName('`key`')
|
||||
->unique()
|
||||
->build();
|
||||
|
||||
$builder->createField('expirationDate', ChronosDateTimeType::CHRONOS_DATETIME)
|
||||
->columnName('expiration_date')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('enabled', Type::BOOLEAN)
|
||||
->build();
|
||||
@@ -5,8 +5,8 @@ return [
|
||||
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'entities_paths' => [
|
||||
__DIR__ . '/../src/Entity',
|
||||
'entities_mappings' => [
|
||||
__DIR__ . '/../config/entities-mappings',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -4,36 +4,18 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Rest\Entity;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* Class ApiKey
|
||||
* @author Shlink
|
||||
* @link http://shlink.io
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="api_keys")
|
||||
*/
|
||||
class ApiKey extends AbstractEntity
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="`key`", nullable=false, unique=true)
|
||||
*/
|
||||
/** @var string */
|
||||
private $key;
|
||||
/**
|
||||
* @var Chronos|null
|
||||
* @ORM\Column(name="expiration_date", nullable=true, type="chronos_datetime")
|
||||
*/
|
||||
/** @var Chronos|null */
|
||||
private $expirationDate;
|
||||
/**
|
||||
* @var bool
|
||||
* @ORM\Column(type="boolean")
|
||||
*/
|
||||
/** @var bool */
|
||||
private $enabled;
|
||||
|
||||
public function __construct(?Chronos $expirationDate = null)
|
||||
|
||||
@@ -18,5 +18,4 @@
|
||||
|
||||
<!-- Paths to exclude -->
|
||||
<exclude-pattern>config/params/*</exclude-pattern>
|
||||
<exclude-pattern>public/index.php</exclude-pattern>
|
||||
</ruleset>
|
||||
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -2,11 +2,8 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
|
||||
use Zend\Expressive\Application;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
|
||||
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::WEB));
|
||||
$container->get(Application::class)->run();
|
||||
|
||||
Reference in New Issue
Block a user