mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
commit
86d877b565
36 changed files with 1523 additions and 38 deletions
|
@ -8,3 +8,7 @@ SHORTCODE_CHARS=
|
|||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
|
||||
# Rest authentication
|
||||
REST_USER=
|
||||
REST_PASSWORD=
|
||||
|
|
|
@ -6,7 +6,6 @@ branches:
|
|||
- develop
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7
|
||||
- hhvm
|
||||
|
|
|
@ -11,20 +11,21 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^5.5 || ^7.0",
|
||||
"php": "^5.6 || ^7.0",
|
||||
"zendframework/zend-expressive": "^1.0",
|
||||
"zendframework/zend-expressive-helpers": "^2.0",
|
||||
"zendframework/zend-expressive-fastroute": "^1.1",
|
||||
"zendframework/zend-expressive-twigrenderer": "^1.0",
|
||||
"zendframework/zend-stdlib": "^2.7",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"doctrine/orm": "^2.5",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||
"symfony/console": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8",
|
||||
"phpunit/phpunit": "^5.0",
|
||||
"squizlabs/php_codesniffer": "^2.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
|
|
21
config/autoload/errorhandler.local.php.dist
Normal file
21
config/autoload/errorhandler.local.php.dist
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'services' => [
|
||||
'invokables' => [
|
||||
'Zend\Expressive\Whoops' => Whoops\Run::class,
|
||||
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
|
||||
],
|
||||
'factories' => [
|
||||
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'whoops' => [
|
||||
'json_exceptions' => [
|
||||
'display' => true,
|
||||
'show_trace' => true,
|
||||
'ajax_only' => true,
|
||||
],
|
||||
],
|
||||
];
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Middleware;
|
||||
use Zend\Expressive\Container\ApplicationFactory;
|
||||
use Zend\Expressive\Helper;
|
||||
|
||||
|
@ -15,6 +16,21 @@ return [
|
|||
'routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::ROUTING_MIDDLEWARE,
|
||||
],
|
||||
'priority' => 10,
|
||||
],
|
||||
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Middleware\CheckAuthenticationMiddleware::class,
|
||||
Middleware\CrossDomainMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Helper\UrlHelperMiddleware::class,
|
||||
ApplicationFactory::DISPATCH_MIDDLEWARE,
|
||||
],
|
||||
|
|
9
config/autoload/rest.global.php
Normal file
9
config/autoload/rest.global.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
return [
|
||||
|
||||
'rest' => [
|
||||
'username' => getenv('REST_USER'),
|
||||
'password' => getenv('REST_PASSWORD'),
|
||||
],
|
||||
|
||||
];
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
use Acelaya\UrlShortener\Middleware\Routable;
|
||||
use Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -10,6 +11,38 @@ return [
|
|||
'middleware' => Routable\RedirectMiddleware::class,
|
||||
'allowed_methods' => ['GET'],
|
||||
],
|
||||
|
||||
// Rest
|
||||
[
|
||||
'name' => 'rest-authenticate',
|
||||
'path' => '/rest/authenticate',
|
||||
'middleware' => Rest\AuthenticateMiddleware::class,
|
||||
'allowed_methods' => ['POST', 'OPTIONS'],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-create-shortcode',
|
||||
'path' => '/rest/short-codes',
|
||||
'middleware' => Rest\CreateShortcodeMiddleware::class,
|
||||
'allowed_methods' => ['POST', 'OPTIONS'],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-resolve-url',
|
||||
'path' => '/rest/short-codes/{shortCode}',
|
||||
'middleware' => Rest\ResolveUrlMiddleware::class,
|
||||
'allowed_methods' => ['GET', 'OPTIONS'],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-list-shortened-url',
|
||||
'path' => '/rest/short-codes',
|
||||
'middleware' => Rest\ListShortcodesMiddleware::class,
|
||||
'allowed_methods' => ['GET'],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-get-visits',
|
||||
'path' => '/rest/visits/{shortCode}',
|
||||
'middleware' => Rest\GetVisitsMiddleware::class,
|
||||
'allowed_methods' => ['GET', 'OPTIONS'],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -37,6 +37,8 @@ return [
|
|||
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||
Service\UrlShortener::class => AnnotatedFactory::class,
|
||||
Service\VisitsTracker::class => AnnotatedFactory::class,
|
||||
Service\ShortUrlService::class => AnnotatedFactory::class,
|
||||
Service\RestTokenService::class => AnnotatedFactory::class,
|
||||
Cache::class => CacheFactory::class,
|
||||
|
||||
// Cli commands
|
||||
|
@ -44,11 +46,19 @@ return [
|
|||
|
||||
// Middleware
|
||||
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleHttp\Client::class,
|
||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
||||
]
|
||||
],
|
||||
|
||||
|
|
278
data/docs/rest.md
Normal file
278
data/docs/rest.md
Normal file
|
@ -0,0 +1,278 @@
|
|||
|
||||
# REST API documentation
|
||||
|
||||
## Error management
|
||||
|
||||
Statuses:
|
||||
|
||||
* 400 -> controlled error
|
||||
* 401 -> authentication error
|
||||
* 500 -> unexpected error
|
||||
|
||||
[TODO]
|
||||
|
||||
## Authentication
|
||||
|
||||
[TODO]
|
||||
|
||||
## Endpoints
|
||||
|
||||
#### Authenticate
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `POST` -> `/rest/authenticate`
|
||||
* Params:
|
||||
* username: `string`
|
||||
* password: `string`
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "9f741eb0-33d7-4c56-b8f7-3719e9929946"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_ARGUMENT",
|
||||
"message": "You have to provide both \"username\" and \"password\""
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: Username or password were not provided.
|
||||
* **INVALID_CREDENTIALS**: Username or password are incorrect.
|
||||
|
||||
|
||||
#### Create shortcode
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `POST` -> `/rest/short-codes`
|
||||
* Params:
|
||||
* longUrl: `string` -> The URL to shorten
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"longUrl": "https://www.facebook.com/something/something",
|
||||
"shortUrl": "https://doma.in/rY9Kr",
|
||||
"shortCode": "rY9Kr"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_URL",
|
||||
"message": "Provided URL \"wfwef\" is invalid. Try with a different one."
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: The longUrl was not provided.
|
||||
* **INVALID_URL**: Provided longUrl has an invalid format or does not resolve.
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### Resolve URL
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/short-codes/{shortCode}`
|
||||
* Route params:
|
||||
* shortCode: `string` -> The short code we want to resolve
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"longUrl": "https://www.facebook.com/something/something"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_SHORTCODE",
|
||||
"message": "Provided short code \"abc123\" has an invalid format"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: No longUrl was found for provided shortCode.
|
||||
* **INVALID_SHORTCODE**: Provided shortCode does not match the character set used by the app to generate short codes.
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### List shortened URLs
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/short-codes`
|
||||
* Query params:
|
||||
* page: `integer` -> The page to list. Defaults to 1 if not provided.
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "abc123",
|
||||
"originalUrl": "http://www.alejandrocelaya.com",
|
||||
"dateCreated": "2016-04-30T18:01:47+0200",
|
||||
"visitsCount": 4
|
||||
},
|
||||
{
|
||||
"shortCode": "def456",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/en",
|
||||
"dateCreated": "2016-04-30T18:03:43+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "ghi789",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/es",
|
||||
"dateCreated": "2016-04-30T18:10:38+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "jkl987",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/es/",
|
||||
"dateCreated": "2016-04-30T18:10:57+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "mno654",
|
||||
"originalUrl": "http://blog.alejandrocelaya.com/2016/04/09/improving-zend-service-manager-workflow-with-annotations/",
|
||||
"dateCreated": "2016-04-30T19:21:05+0200",
|
||||
"visitsCount": 1
|
||||
},
|
||||
{
|
||||
"shortCode": "pqr321",
|
||||
"originalUrl": "http://www.google.com",
|
||||
"dateCreated": "2016-05-01T11:19:53+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "stv159",
|
||||
"originalUrl": "http://www.acelaya.com",
|
||||
"dateCreated": "2016-06-12T17:49:21+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "wxy753",
|
||||
"originalUrl": "http://www.atomic-reader.com",
|
||||
"dateCreated": "2016-06-12T17:50:27+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "zab852",
|
||||
"originalUrl": "http://foo.com",
|
||||
"dateCreated": "2016-07-03T09:07:36+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "cde963",
|
||||
"originalUrl": "https://www.facebook.com.com",
|
||||
"dateCreated": "2016-07-03T09:12:35+0200",
|
||||
"visitsCount": 0
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 4,
|
||||
"pagesCount": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "UNKNOWN_ERROR",
|
||||
"message": "Unexpected error occured"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### Get visits
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/visits/{shortCode}`
|
||||
* Route params:
|
||||
* shortCode: `string` -> The shortCode from which we eant to get the visits.
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-06-18T09:32:22+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-04-30T19:20:06+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": "google.com",
|
||||
"date": "2016-04-30T19:19:57+0200",
|
||||
"remoteAddr": "1.2.3.4",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-04-30T19:17:35+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_ARGUMENT",
|
||||
"message": "Provided short code \"abc123\" is invalid"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: The shortcode does not belong to any short URL
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
102
src/Entity/RestToken.php
Normal file
102
src/Entity/RestToken.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Entity;
|
||||
|
||||
use Acelaya\UrlShortener\Util\StringUtilsTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Class RestToken
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="rest_tokens")
|
||||
*/
|
||||
class RestToken extends AbstractEntity
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* The default interval is 20 minutes
|
||||
*/
|
||||
const DEFAULT_INTERVAL = 'PT20M';
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(type="datetime", name="expiration_date", nullable=false)
|
||||
*/
|
||||
protected $expirationDate;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=false)
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->updateExpiration();
|
||||
$this->setRandomTokenKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getExpirationDate()
|
||||
{
|
||||
return $this->expirationDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $expirationDate
|
||||
* @return $this
|
||||
*/
|
||||
public function setExpirationDate($expirationDate)
|
||||
{
|
||||
$this->expirationDate = $expirationDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return $this
|
||||
*/
|
||||
public function setToken($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExpired()
|
||||
{
|
||||
return new \DateTime() > $this->expirationDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the expiration of the token, setting it to the default interval in the future
|
||||
* @return $this
|
||||
*/
|
||||
public function updateExpiration()
|
||||
{
|
||||
return $this->setExpirationDate((new \DateTime())->add(new \DateInterval(self::DEFAULT_INTERVAL)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a random unique token key for this RestToken
|
||||
* @return RestToken
|
||||
*/
|
||||
public function setRandomTokenKey()
|
||||
{
|
||||
return $this->setToken($this->generateV4Uuid());
|
||||
}
|
||||
}
|
|
@ -10,10 +10,10 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Entity(repositoryClass="Acelaya\UrlShortener\Repository\ShortUrlRepository")
|
||||
* @ORM\Table(name="short_urls")
|
||||
*/
|
||||
class ShortUrl extends AbstractEntity
|
||||
class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
|
@ -117,4 +117,21 @@ class ShortUrl extends AbstractEntity
|
|||
$this->visits = $visits;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
* @link http://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 [
|
||||
'shortCode' => $this->shortCode,
|
||||
'originalUrl' => $this->originalUrl,
|
||||
'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null,
|
||||
'visitsCount' => count($this->visits),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
* @ORM\Entity
|
||||
* @ORM\Table(name="visits")
|
||||
*/
|
||||
class Visit extends AbstractEntity
|
||||
class Visit extends AbstractEntity implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
|
@ -134,4 +134,21 @@ class Visit extends AbstractEntity
|
|||
$this->userAgent = $userAgent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
* @link http://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 [
|
||||
'referer' => $this->referer,
|
||||
'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null,
|
||||
'remoteAddr' => $this->remoteAddr,
|
||||
'userAgent' => $this->userAgent,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
10
src/Exception/AuthenticationException.php
Normal file
10
src/Exception/AuthenticationException.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
class AuthenticationException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
public static function fromCredentials($username, $password)
|
||||
{
|
||||
return new self(sprintf('Invalid credentials. Username -> "%s". Password -> "%s"', $username, $password));
|
||||
}
|
||||
}
|
6
src/Exception/InvalidArgumentException.php
Normal file
6
src/Exception/InvalidArgumentException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
102
src/Middleware/CheckAuthenticationMiddleware.php
Normal file
102
src/Middleware/CheckAuthenticationMiddleware.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
|
||||
use Acelaya\UrlShortener\Service\RestTokenService;
|
||||
use Acelaya\UrlShortener\Service\RestTokenServiceInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class CheckAuthenticationMiddleware implements MiddlewareInterface
|
||||
{
|
||||
const AUTH_TOKEN_HEADER = 'X-Auth-Token';
|
||||
|
||||
/**
|
||||
* @var RestTokenServiceInterface
|
||||
*/
|
||||
private $restTokenService;
|
||||
|
||||
/**
|
||||
* CheckAuthenticationMiddleware constructor.
|
||||
* @param RestTokenServiceInterface|RestTokenService $restTokenService
|
||||
*
|
||||
* @Inject({RestTokenService::class})
|
||||
*/
|
||||
public function __construct(RestTokenServiceInterface $restTokenService)
|
||||
{
|
||||
$this->restTokenService = $restTokenService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
// If current route is the authenticate route or an OPTIONS request, continue to the next middleware
|
||||
/** @var RouteResult $routeResult */
|
||||
$routeResult = $request->getAttribute(RouteResult::class);
|
||||
if ((isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate')
|
||||
|| strtolower($request->getMethod()) === 'options'
|
||||
) {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
// Check that the auth header was provided, and that it belongs to a non-expired token
|
||||
if (! $request->hasHeader(self::AUTH_TOKEN_HEADER)) {
|
||||
return $this->createTokenErrorResponse();
|
||||
}
|
||||
|
||||
$authToken = $request->getHeaderLine(self::AUTH_TOKEN_HEADER);
|
||||
try {
|
||||
$restToken = $this->restTokenService->getByToken($authToken);
|
||||
if ($restToken->isExpired()) {
|
||||
return $this->createTokenErrorResponse();
|
||||
}
|
||||
|
||||
// Update the token expiration and continue to next middleware
|
||||
$this->restTokenService->updateExpiration($restToken);
|
||||
return $out($request, $response);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return $this->createTokenErrorResponse();
|
||||
}
|
||||
}
|
||||
|
||||
protected function createTokenErrorResponse()
|
||||
{
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
|
||||
'message' => sprintf(
|
||||
'Missing or invalid auth token provided. Perform a new authentication request and send provided token '
|
||||
. 'on every new request on the "%s" header',
|
||||
self::AUTH_TOKEN_HEADER
|
||||
),
|
||||
], 401);
|
||||
}
|
||||
}
|
|
@ -3,26 +3,10 @@ namespace Acelaya\UrlShortener\Middleware;
|
|||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class CliParamsMiddleware implements MiddlewareInterface
|
||||
class CrossDomainMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $argv;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
private $currentSapi;
|
||||
|
||||
public function __construct(array $argv, $currentSapi)
|
||||
{
|
||||
$this->argv = $argv;
|
||||
$this->currentSapi = $currentSapi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
|
@ -50,22 +34,19 @@ class CliParamsMiddleware implements MiddlewareInterface
|
|||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
// When not in CLI, just call next middleware
|
||||
if ($this->currentSapi !== 'cli') {
|
||||
return $out($request, $response);
|
||||
/** @var Response $response */
|
||||
$response = $out($request, $response);
|
||||
|
||||
if (strtolower($request->getMethod()) === 'options') {
|
||||
$response = $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
->withHeader('Access-Control-Max-Age', '1000')
|
||||
->withHeader(
|
||||
// Allow all requested headers
|
||||
'Access-Control-Allow-Headers',
|
||||
$request->getHeaderLine('Access-Control-Request-Headers')
|
||||
);
|
||||
}
|
||||
|
||||
/** @var RouteResult $routeResult */
|
||||
$routeResult = $request->getAttribute(RouteResult::class);
|
||||
if (! $routeResult->isSuccess()) {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
// Inject ARGV params as request attributes
|
||||
if ($routeResult->getMatchedRouteName() === 'cli-generate-shortcode') {
|
||||
$request = $request->withAttribute('longUrl', isset($this->argv[2]) ? $this->argv[2] : null);
|
||||
}
|
||||
|
||||
return $out($request, $response);
|
||||
return $response->withHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
}
|
51
src/Middleware/Rest/AbstractRestMiddleware.php
Normal file
51
src/Middleware/Rest/AbstractRestMiddleware.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
abstract class AbstractRestMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
if (strtolower($request->getMethod()) === 'options') {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->dispatch($request, $response, $out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
abstract protected function dispatch(Request $request, Response $response, callable $out = null);
|
||||
}
|
57
src/Middleware/Rest/AuthenticateMiddleware.php
Normal file
57
src/Middleware/Rest/AuthenticateMiddleware.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\AuthenticationException;
|
||||
use Acelaya\UrlShortener\Service\RestTokenService;
|
||||
use Acelaya\UrlShortener\Service\RestTokenServiceInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class AuthenticateMiddleware extends AbstractRestMiddleware
|
||||
{
|
||||
/**
|
||||
* @var RestTokenServiceInterface
|
||||
*/
|
||||
private $restTokenService;
|
||||
|
||||
/**
|
||||
* AuthenticateMiddleware constructor.
|
||||
* @param RestTokenServiceInterface|RestTokenService $restTokenService
|
||||
*
|
||||
* @Inject({RestTokenService::class})
|
||||
*/
|
||||
public function __construct(RestTokenServiceInterface $restTokenService)
|
||||
{
|
||||
$this->restTokenService = $restTokenService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$authData = $request->getParsedBody();
|
||||
if (! isset($authData['username'], $authData['password'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => 'You have to provide both "username" and "password"'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$token = $this->restTokenService->createToken($authData['username'], $authData['password']);
|
||||
return new JsonResponse(['token' => $token->getToken()]);
|
||||
} catch (AuthenticationException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => 'Invalid username and/or password',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
}
|
79
src/Middleware/Rest/CreateShortcodeMiddleware.php
Normal file
79
src/Middleware/Rest/CreateShortcodeMiddleware.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidUrlException;
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
class CreateShortcodeMiddleware extends AbstractRestMiddleware
|
||||
{
|
||||
/**
|
||||
* @var UrlShortener|UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
|
||||
/**
|
||||
* GenerateShortcodeMiddleware constructor.
|
||||
*
|
||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||
* @param array $domainConfig
|
||||
*
|
||||
* @Inject({UrlShortener::class, "config.url_shortener.domain"})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$postData = $request->getParsedBody();
|
||||
if (! isset($postData['longUrl'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => 'A URL was not provided',
|
||||
], 400);
|
||||
}
|
||||
$longUrl = $postData['longUrl'];
|
||||
|
||||
try {
|
||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
|
||||
$shortUrl = (new Uri())->withPath($shortCode)
|
||||
->withScheme($this->domainConfig['schema'])
|
||||
->withHost($this->domainConfig['hostname']);
|
||||
|
||||
return new JsonResponse([
|
||||
'longUrl' => $longUrl,
|
||||
'shortUrl' => $shortUrl->__toString(),
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl),
|
||||
], 400);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occured',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
62
src/Middleware/Rest/GetVisitsMiddleware.php
Normal file
62
src/Middleware/Rest/GetVisitsMiddleware.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
|
||||
use Acelaya\UrlShortener\Service\VisitsTracker;
|
||||
use Acelaya\UrlShortener\Service\VisitsTrackerInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class GetVisitsMiddleware extends AbstractRestMiddleware
|
||||
{
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
private $visitsTracker;
|
||||
|
||||
/**
|
||||
* GetVisitsMiddleware constructor.
|
||||
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
|
||||
*
|
||||
* @Inject({VisitsTracker::class})
|
||||
*/
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
|
||||
try {
|
||||
$visits = $this->visitsTracker->info($shortCode);
|
||||
|
||||
return new JsonResponse([
|
||||
'visits' => [
|
||||
'data' => $visits,
|
||||
// 'pagination' => [],
|
||||
]
|
||||
]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided short code "%s" is invalid', $shortCode),
|
||||
], 400);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occured',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
53
src/Middleware/Rest/ListShortcodesMiddleware.php
Normal file
53
src/Middleware/Rest/ListShortcodesMiddleware.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Acelaya\UrlShortener\Paginator\Util\PaginatorSerializerTrait;
|
||||
use Acelaya\UrlShortener\Service\ShortUrlService;
|
||||
use Acelaya\UrlShortener\Service\ShortUrlServiceInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
class ListShortcodesMiddleware extends AbstractRestMiddleware
|
||||
{
|
||||
use PaginatorSerializerTrait;
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
private $shortUrlService;
|
||||
|
||||
/**
|
||||
* ListShortcodesMiddleware constructor.
|
||||
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
|
||||
*
|
||||
* @Inject({ShortUrlService::class})
|
||||
*/
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService)
|
||||
{
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
try {
|
||||
$query = $request->getQueryParams();
|
||||
$shortUrls = $this->shortUrlService->listShortUrls(isset($query['page']) ? $query['page'] : 1);
|
||||
return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occured',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
65
src/Middleware/Rest/ResolveUrlMiddleware.php
Normal file
65
src/Middleware/Rest/ResolveUrlMiddleware.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Middleware\Rest;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
|
||||
use Acelaya\UrlShortener\Util\RestUtils;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class ResolveUrlMiddleware extends AbstractRestMiddleware
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
|
||||
/**
|
||||
* ResolveUrlMiddleware constructor.
|
||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||
*
|
||||
* @Inject({UrlShortener::class})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param callable|null $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
if (! isset($longUrl)) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => sprintf('No URL found for shortcode "%s"', $shortCode),
|
||||
], 400);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'longUrl' => $longUrl,
|
||||
]);
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode),
|
||||
], 400);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occured',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
56
src/Paginator/Adapter/PaginableRepositoryAdapter.php
Normal file
56
src/Paginator/Adapter/PaginableRepositoryAdapter.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Paginator\Adapter;
|
||||
|
||||
use Acelaya\UrlShortener\Repository\PaginableRepositoryInterface;
|
||||
use Zend\Paginator\Adapter\AdapterInterface;
|
||||
|
||||
class PaginableRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
/**
|
||||
* @var PaginableRepositoryInterface
|
||||
*/
|
||||
private $paginableRepository;
|
||||
/**
|
||||
* @var null
|
||||
*/
|
||||
private $searchTerm;
|
||||
/**
|
||||
* @var null
|
||||
*/
|
||||
private $orderBy;
|
||||
|
||||
public function __construct(PaginableRepositoryInterface $paginableRepository, $searchTerm = null, $orderBy = null)
|
||||
{
|
||||
$this->paginableRepository = $paginableRepository;
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->orderBy = $orderBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of items for a page.
|
||||
*
|
||||
* @param int $offset Page offset
|
||||
* @param int $itemCountPerPage Number of items per page
|
||||
* @return array
|
||||
*/
|
||||
public function getItems($offset, $itemCountPerPage)
|
||||
{
|
||||
return $this->paginableRepository->findList($itemCountPerPage, $offset, $this->searchTerm, $this->orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count elements of an object
|
||||
* @link http://php.net/manual/en/countable.count.php
|
||||
* @return int The custom count as an integer.
|
||||
* </p>
|
||||
* <p>
|
||||
* The return value is cast to an integer.
|
||||
* @since 5.1.0
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return $this->paginableRepository->countList($this->searchTerm);
|
||||
}
|
||||
}
|
19
src/Paginator/Util/PaginatorSerializerTrait.php
Normal file
19
src/Paginator/Util/PaginatorSerializerTrait.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Paginator\Util;
|
||||
|
||||
use Zend\Paginator\Paginator;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
trait PaginatorSerializerTrait
|
||||
{
|
||||
protected function serializePaginator(Paginator $paginator)
|
||||
{
|
||||
return [
|
||||
'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()),
|
||||
'pagination' => [
|
||||
'currentPage' => $paginator->getCurrentPageNumber(),
|
||||
'pagesCount' => $paginator->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
24
src/Repository/PaginableRepositoryInterface.php
Normal file
24
src/Repository/PaginableRepositoryInterface.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Repository;
|
||||
|
||||
interface PaginableRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Gets a list of elements using provided filtering data
|
||||
*
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @param string|null $searchTerm
|
||||
* @param string|array|null $orderBy
|
||||
* @return array
|
||||
*/
|
||||
public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null);
|
||||
|
||||
/**
|
||||
* Counts the number of elements in a list using provided filtering data
|
||||
*
|
||||
* @param null $searchTerm
|
||||
* @return int
|
||||
*/
|
||||
public function countList($searchTerm = null);
|
||||
}
|
62
src/Repository/ShortUrlRepository.php
Normal file
62
src/Repository/ShortUrlRepository.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Repository;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @param string|null $searchTerm
|
||||
* @param string|array|null $orderBy
|
||||
* @return ShortUrl[]
|
||||
*/
|
||||
public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('s');
|
||||
|
||||
if (isset($limit)) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if (isset($offset)) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
if (isset($searchTerm)) {
|
||||
// TODO
|
||||
}
|
||||
if (isset($orderBy)) {
|
||||
if (is_string($orderBy)) {
|
||||
$qb->orderBy($orderBy);
|
||||
} elseif (is_array($orderBy)) {
|
||||
$key = key($orderBy);
|
||||
$qb->orderBy($key, $orderBy[$key]);
|
||||
}
|
||||
} else {
|
||||
$qb->orderBy('s.dateCreated');
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of elements in a list using provided filtering data
|
||||
*
|
||||
* @param null $searchTerm
|
||||
* @return int
|
||||
*/
|
||||
public function countList($searchTerm = null)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('COUNT(s)')
|
||||
->from(ShortUrl::class, 's');
|
||||
|
||||
if (isset($searchTerm)) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
}
|
8
src/Repository/ShortUrlRepositoryInterface.php
Normal file
8
src/Repository/ShortUrlRepositoryInterface.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Repository;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
|
||||
interface ShortUrlRepositoryInterface extends ObjectRepository, PaginableRepositoryInterface
|
||||
{
|
||||
}
|
98
src/Service/RestTokenService.php
Normal file
98
src/Service/RestTokenService.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\RestToken;
|
||||
use Acelaya\UrlShortener\Exception\AuthenticationException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class RestTokenService implements RestTokenServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $restConfig;
|
||||
|
||||
/**
|
||||
* ShortUrlService constructor.
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @param array $restConfig
|
||||
* @Inject({"em", "config.rest"})
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em, array $restConfig)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->restConfig = $restConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @return RestToken
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getByToken($token)
|
||||
{
|
||||
$restToken = $this->em->getRepository(RestToken::class)->findOneBy([
|
||||
'token' => $token,
|
||||
]);
|
||||
if (! isset($restToken)) {
|
||||
throw new InvalidArgumentException(sprintf('RestToken not found for token "%s"', $token));
|
||||
}
|
||||
|
||||
return $restToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new RestToken if username and password are correct
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @return RestToken
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function createToken($username, $password)
|
||||
{
|
||||
$this->processCredentials($username, $password);
|
||||
|
||||
$restToken = new RestToken();
|
||||
$this->em->persist($restToken);
|
||||
$this->em->flush();
|
||||
|
||||
return $restToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*/
|
||||
protected function processCredentials($username, $password)
|
||||
{
|
||||
$configUsername = strtolower(trim($this->restConfig['username']));
|
||||
$providedUsername = strtolower(trim($username));
|
||||
$configPassword = trim($this->restConfig['password']);
|
||||
$providedPassword = trim($password);
|
||||
|
||||
if ($configUsername === $providedUsername && $configPassword === $providedPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If credentials are not correct, throw exception
|
||||
throw AuthenticationException::fromCredentials($providedUsername, $providedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the expiration of provided token, extending its life
|
||||
*
|
||||
* @param RestToken $token
|
||||
*/
|
||||
public function updateExpiration(RestToken $token)
|
||||
{
|
||||
$token->updateExpiration();
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
32
src/Service/RestTokenServiceInterface.php
Normal file
32
src/Service/RestTokenServiceInterface.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\RestToken;
|
||||
use Acelaya\UrlShortener\Exception\AuthenticationException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
|
||||
|
||||
interface RestTokenServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param string $token
|
||||
* @return RestToken
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getByToken($token);
|
||||
|
||||
/**
|
||||
* Creates and returns a new RestToken if username and password are correct
|
||||
* @param $username
|
||||
* @param $password
|
||||
* @return RestToken
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function createToken($username, $password);
|
||||
|
||||
/**
|
||||
* Updates the expiration of provided token, extending its life
|
||||
*
|
||||
* @param RestToken $token
|
||||
*/
|
||||
public function updateExpiration(RestToken $token);
|
||||
}
|
43
src/Service/ShortUrlService.php
Normal file
43
src/Service/ShortUrlService.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Acelaya\UrlShortener\Repository\ShortUrlRepository;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* ShortUrlService constructor.
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @Inject({"em"})
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
* @return Paginator|ShortUrl[]
|
||||
*/
|
||||
public function listShortUrls($page = 1)
|
||||
{
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$paginator = new Paginator(new PaginableRepositoryAdapter($repo));
|
||||
$paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE)
|
||||
->setCurrentPageNumber($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
14
src/Service/ShortUrlServiceInterface.php
Normal file
14
src/Service/ShortUrlServiceInterface.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
interface ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $page
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls($page = 1);
|
||||
}
|
|
@ -3,8 +3,11 @@ namespace Acelaya\UrlShortener\Service;
|
|||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Entity\Visit;
|
||||
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class VisitsTracker implements VisitsTrackerInterface
|
||||
{
|
||||
|
@ -58,4 +61,27 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||
{
|
||||
return isset($array[$key]) ? $array[$key] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visits on certain shortcode
|
||||
*
|
||||
* @param $shortCode
|
||||
* @return Paginator|Visit[]
|
||||
*/
|
||||
public function info($shortCode)
|
||||
{
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if (! isset($shortUrl)) {
|
||||
throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode));
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Visit::class)->findBy([
|
||||
'shortUrl' => $shortUrl,
|
||||
], [
|
||||
'date' => 'DESC'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\Visit;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
interface VisitsTrackerInterface
|
||||
{
|
||||
/**
|
||||
|
@ -10,4 +13,12 @@ interface VisitsTrackerInterface
|
|||
* @param array $visitorData Defaults to global $_SERVER
|
||||
*/
|
||||
public function track($shortCode, array $visitorData = null);
|
||||
|
||||
/**
|
||||
* Returns the visits on certain shortcode
|
||||
*
|
||||
* @param $shortCode
|
||||
* @return Paginator|Visit[]
|
||||
*/
|
||||
public function info($shortCode);
|
||||
}
|
||||
|
|
30
src/Util/RestUtils.php
Normal file
30
src/Util/RestUtils.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Util;
|
||||
|
||||
use Acelaya\UrlShortener\Exception;
|
||||
|
||||
class RestUtils
|
||||
{
|
||||
const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
|
||||
const INVALID_URL_ERROR = 'INVALID_URL';
|
||||
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
|
||||
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
|
||||
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN_ERROR';
|
||||
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
|
||||
|
||||
public static function getRestErrorCodeFromException(Exception\ExceptionInterface $e)
|
||||
{
|
||||
switch (true) {
|
||||
case $e instanceof Exception\InvalidShortCodeException:
|
||||
return self::INVALID_SHORTCODE_ERROR;
|
||||
case $e instanceof Exception\InvalidUrlException:
|
||||
return self::INVALID_URL_ERROR;
|
||||
case $e instanceof Exception\InvalidArgumentException:
|
||||
return self::INVALID_ARGUMENT_ERROR;
|
||||
case $e instanceof Exception\AuthenticationException:
|
||||
return self::INVALID_CREDENTIALS_ERROR;
|
||||
default:
|
||||
return self::UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
40
src/Util/StringUtilsTrait.php
Normal file
40
src/Util/StringUtilsTrait.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace Acelaya\UrlShortener\Util;
|
||||
|
||||
trait StringUtilsTrait
|
||||
{
|
||||
protected function generateRandomString($length = 10)
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen($characters);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[rand(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
protected function generateV4Uuid()
|
||||
{
|
||||
return sprintf(
|
||||
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
// 32 bits for "time_low"
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
// 16 bits for "time_mid"
|
||||
mt_rand(0, 0xffff),
|
||||
// 16 bits for "time_hi_and_version",
|
||||
// four most significant bits holds version number 4
|
||||
mt_rand(0, 0x0fff) | 0x4000,
|
||||
// 16 bits, 8 bits for "clk_seq_hi_res",
|
||||
// 8 bits for "clk_seq_low",
|
||||
// two most significant bits holds zero and one for variant DCE1.1
|
||||
mt_rand(0, 0x3fff) | 0x8000,
|
||||
// 48 bits for "node"
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0xffff)
|
||||
);
|
||||
}
|
||||
}
|
49
tests/Service/ShortUrlServiceTest.php
Normal file
49
tests/Service/ShortUrlServiceTest.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
namespace AcelayaTest\UrlShortener\Service;
|
||||
|
||||
use Acelaya\UrlShortener\Entity\ShortUrl;
|
||||
use Acelaya\UrlShortener\Repository\ShortUrlRepository;
|
||||
use Acelaya\UrlShortener\Service\ShortUrlService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
|
||||
class ShortUrlServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ShortUrlService
|
||||
*/
|
||||
protected $service;
|
||||
/**
|
||||
* @var ObjectProphecy|EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->service = new ShortUrlService($this->em->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function listedUrlsAreReturnedFromEntityManager()
|
||||
{
|
||||
$list = [
|
||||
new ShortUrl(),
|
||||
new ShortUrl(),
|
||||
new ShortUrl(),
|
||||
new ShortUrl(),
|
||||
];
|
||||
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$repo->findList(Argument::cetera())->willReturn($list)->shouldBeCalledTimes(1);
|
||||
$repo->countList(Argument::cetera())->willReturn(count($list))->shouldBeCalledTimes(1);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$list = $this->service->listShortUrls();
|
||||
$this->assertEquals(4, $list->getCurrentItemCount());
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue