Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

19
vendor/league/event/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2014 Frank de Jonge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,64 @@
<?php
namespace League\Event;
abstract class AbstractEvent implements EventInterface
{
/**
* Has propagation stopped?
*
* @var bool
*/
protected $propagationStopped = false;
/**
* The emitter instance.
*
* @var EmitterInterface|null
*/
protected $emitter;
/**
* @inheritdoc
*/
public function setEmitter(EmitterInterface $emitter)
{
$this->emitter = $emitter;
return $this;
}
/**
* @inheritdoc
*/
public function getEmitter()
{
return $this->emitter;
}
/**
* @inheritdoc
*/
public function stopPropagation()
{
$this->propagationStopped = true;
return $this;
}
/**
* @inheritdoc
*/
public function isPropagationStopped()
{
return $this->propagationStopped;
}
/**
* @inheritdoc
*/
public function getName()
{
return get_class($this);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace League\Event;
abstract class AbstractListener implements ListenerInterface
{
/**
* @inheritdoc
*/
public function isListener($listener)
{
return $this === $listener;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace League\Event;
class BufferedEmitter extends Emitter
{
/**
* @var EventInterface[]
*/
protected $bufferedEvents = [];
/**
* @inheritdoc
*/
public function emit($event)
{
$this->bufferedEvents[] = $event;
return $event;
}
/**
* @inheritdoc
*/
public function emitBatch(array $events)
{
foreach ($events as $event) {
$this->bufferedEvents[] = $event;
}
return $events;
}
/**
* Emit the buffered events.
*
* @return array
*/
public function emitBufferedEvents()
{
$result = [];
while ($event = array_shift($this->bufferedEvents)) {
$result[] = parent::emit($event);
}
return $result;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace League\Event;
class CallbackListener implements ListenerInterface
{
/**
* The callback.
*
* @var callable
*/
protected $callback;
/**
* Create a new callback listener instance.
*
* @param callable $callback
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* Get the callback.
*
* @return callable
*/
public function getCallback()
{
return $this->callback;
}
/**
* @inheritdoc
*/
public function handle(EventInterface $event)
{
call_user_func_array($this->callback, func_get_args());
}
/**
* @inheritdoc
*/
public function isListener($listener)
{
if ($listener instanceof CallbackListener) {
$listener = $listener->getCallback();
}
return $this->callback === $listener;
}
/**
* Named constructor
*
* @param callable $callable
*
* @return static
*/
public static function fromCallable(callable $callable)
{
return new static($callable);
}
}

268
vendor/league/event/src/Emitter.php vendored Normal file
View File

@@ -0,0 +1,268 @@
<?php
namespace League\Event;
use InvalidArgumentException;
class Emitter implements EmitterInterface
{
/**
* The registered listeners.
*
* @var array
*/
protected $listeners = [];
/**
* The sorted listeners
*
* Listeners will get sorted and stored for re-use.
*
* @var ListenerInterface[]
*/
protected $sortedListeners = [];
/**
* @inheritdoc
*/
public function addListener($event, $listener, $priority = self::P_NORMAL)
{
$listener = $this->ensureListener($listener);
$this->listeners[$event][$priority][] = $listener;
$this->clearSortedListeners($event);
return $this;
}
/**
* @inheritdoc
*/
public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL)
{
$listener = $this->ensureListener($listener);
$listener = new OneTimeListener($listener);
return $this->addListener($event, $listener, $priority);
}
/**
* @inheritdoc
*/
public function useListenerProvider(ListenerProviderInterface $provider)
{
$acceptor = new ListenerAcceptor($this);
$provider->provideListeners($acceptor);
return $this;
}
/**
* @inheritdoc
*/
public function removeListener($event, $listener)
{
$this->clearSortedListeners($event);
$listeners = $this->hasListeners($event)
? $this->listeners[$event]
: [];
$filter = function ($registered) use ($listener) {
return ! $registered->isListener($listener);
};
foreach ($listeners as $priority => $collection) {
$listeners[$priority] = array_filter($collection, $filter);
}
$this->listeners[$event] = $listeners;
return $this;
}
/**
* @inheritdoc
*/
public function removeAllListeners($event)
{
$this->clearSortedListeners($event);
if ($this->hasListeners($event)) {
unset($this->listeners[$event]);
}
return $this;
}
/**
* Ensure the input is a listener.
*
* @param ListenerInterface|callable $listener
*
* @throws InvalidArgumentException
*
* @return ListenerInterface
*/
protected function ensureListener($listener)
{
if ($listener instanceof ListenerInterface) {
return $listener;
}
if (is_callable($listener)) {
return CallbackListener::fromCallable($listener);
}
throw new InvalidArgumentException('Listeners should be ListenerInterface, Closure or callable. Received type: '.gettype($listener));
}
/**
* @inheritdoc
*/
public function hasListeners($event)
{
if (! isset($this->listeners[$event]) || count($this->listeners[$event]) === 0) {
return false;
}
return true;
}
/**
* @inheritdoc
*/
public function getListeners($event)
{
if (array_key_exists($event, $this->sortedListeners)) {
return $this->sortedListeners[$event];
}
return $this->sortedListeners[$event] = $this->getSortedListeners($event);
}
/**
* Get the listeners sorted by priority for a given event.
*
* @param string $event
*
* @return ListenerInterface[]
*/
protected function getSortedListeners($event)
{
if (! $this->hasListeners($event)) {
return [];
}
$listeners = $this->listeners[$event];
krsort($listeners);
return call_user_func_array('array_merge', $listeners);
}
/**
* @inheritdoc
*/
public function emit($event)
{
list($name, $event) = $this->prepareEvent($event);
$arguments = [$event] + func_get_args();
$this->invokeListeners($name, $event, $arguments);
$this->invokeListeners('*', $event, $arguments);
return $event;
}
/**
* @inheritdoc
*/
public function emitBatch(array $events)
{
$results = [];
foreach ($events as $event) {
$results[] = $this->emit($event);
}
return $results;
}
/**
* @inheritdoc
*/
public function emitGeneratedEvents(GeneratorInterface $generator)
{
$events = $generator->releaseEvents();
return $this->emitBatch($events);
}
/**
* Invoke the listeners for an event.
*
* @param string $name
* @param EventInterface $event
* @param array $arguments
*
* @return void
*/
protected function invokeListeners($name, EventInterface $event, array $arguments)
{
$listeners = $this->getListeners($name);
foreach ($listeners as $listener) {
if ($event->isPropagationStopped()) {
break;
}
call_user_func_array([$listener, 'handle'], $arguments);
}
}
/**
* Prepare an event for emitting.
*
* @param string|EventInterface $event
*
* @return array
*/
protected function prepareEvent($event)
{
$event = $this->ensureEvent($event);
$name = $event->getName();
$event->setEmitter($this);
return [$name, $event];
}
/**
* Ensure event input is of type EventInterface or convert it.
*
* @param string|EventInterface $event
*
* @throws InvalidArgumentException
*
* @return EventInterface
*/
protected function ensureEvent($event)
{
if (is_string($event)) {
return Event::named($event);
}
if (! $event instanceof EventInterface) {
throw new InvalidArgumentException('Events should be provides as Event instances or string, received type: '.gettype($event));
}
return $event;
}
/**
* Clear the sorted listeners for an event
*
* @param $event
*/
protected function clearSortedListeners($event)
{
unset($this->sortedListeners[$event]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace League\Event;
interface EmitterAwareInterface
{
/**
* Set the Emitter.
*
* @param EmitterInterface $emitter
*
* @return $this
*/
public function setEmitter(?EmitterInterface $emitter = null);
/**
* Get the Emitter.
*
* @return EmitterInterface
*/
public function getEmitter();
}

View File

@@ -0,0 +1,41 @@
<?php
namespace League\Event;
trait EmitterAwareTrait
{
/**
* The emitter instance.
*
* @var EmitterInterface|null
*/
protected $emitter;
/**
* Set the Emitter.
*
* @param EmitterInterface|null $emitter
*
* @return $this
*/
public function setEmitter(?EmitterInterface $emitter = null)
{
$this->emitter = $emitter;
return $this;
}
/**
* Get the Emitter.
*
* @return EmitterInterface
*/
public function getEmitter()
{
if (! $this->emitter) {
$this->emitter = new Emitter();
}
return $this->emitter;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace League\Event;
interface EmitterInterface extends ListenerAcceptorInterface
{
/**
* Remove a specific listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable".
*
* @param string $event
* @param ListenerInterface|callable $listener
*
* @return $this
*/
public function removeListener($event, $listener);
/**
* Use a provider to add listeners.
*
* @param ListenerProviderInterface $provider
*
* @return $this
*/
public function useListenerProvider(ListenerProviderInterface $provider);
/**
* Remove all listeners for an event.
*
* The first parameter should be the event name. All event listeners will
* be removed.
*
* @param string $event
*
* @return $this
*/
public function removeAllListeners($event);
/**
* Check whether an event has listeners.
*
* The first parameter should be the event name. We'll return true if the
* event has one or more registered even listeners, and false otherwise.
*
* @param string $event
*
* @return bool
*/
public function hasListeners($event);
/**
* Get all the listeners for an event.
*
* The first parameter should be the event name. We'll return an array of
* all the registered even listeners, or an empty array if there are none.
*
* @param string $event
*
* @return array
*/
public function getListeners($event);
/**
* Emit an event.
*
* @param string|EventInterface $event
*
* @return EventInterface
*/
public function emit($event);
/**
* Emit a batch of events.
*
* @param array $events
*
* @return array
*/
public function emitBatch(array $events);
/**
* Release all events stored in a generator
*
* @param GeneratorInterface $generator
*
* @return EventInterface[]
*/
public function emitGeneratedEvents(GeneratorInterface $generator);
}

113
vendor/league/event/src/EmitterTrait.php vendored Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace League\Event;
trait EmitterTrait
{
use EmitterAwareTrait;
/**
* Add a listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable".
*
* @param string $event
* @param ListenerInterface|callable $listener
* @param int $priority
*
* @return $this
*/
public function addListener($event, $listener, $priority = ListenerAcceptorInterface::P_NORMAL)
{
$this->getEmitter()->addListener($event, $listener, $priority);
return $this;
}
/**
* Add a one time listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable".
*
* @param string $event
* @param ListenerInterface|callable $listener
* @param int $priority
*
* @return $this
*/
public function addOneTimeListener($event, $listener, $priority = ListenerAcceptorInterface::P_NORMAL)
{
$this->getEmitter()->addOneTimeListener($event, $listener, $priority);
return $this;
}
/**
* Remove a specific listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable".
*
* @param string $event
* @param ListenerInterface|callable $listener
*
* @return $this
*/
public function removeListener($event, $listener)
{
$this->getEmitter()->removeListener($event, $listener);
return $this;
}
/**
* Remove all listeners for an event.
*
* The first parameter should be the event name. All event listeners will
* be removed.
*
* @param string $event
*
* @return $this
*/
public function removeAllListeners($event)
{
$this->getEmitter()->removeAllListeners($event);
return $this;
}
/**
* Add listeners from a provider.
*
* @param ListenerProviderInterface $provider
*
* @return $this
*/
public function useListenerProvider(ListenerProviderInterface $provider)
{
$this->getEmitter()->useListenerProvider($provider);
return $this;
}
/**
* Emit an event.
*
* @param string|EventInterface $event
*
* @return EventInterface
*/
public function emit($event)
{
$emitter = $this->getEmitter();
$arguments = [$event] + func_get_args();
return call_user_func_array([$emitter, 'emit'], $arguments);
}
}

43
vendor/league/event/src/Event.php vendored Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace League\Event;
class Event extends AbstractEvent
{
/**
* The event name.
*
* @var string
*/
protected $name;
/**
* Create a new event instance.
*
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* @inheritdoc
*/
public function getName()
{
return $this->name;
}
/**
* Create a new event instance.
*
* @param string $name
*
* @return static
*/
public static function named($name)
{
return new static($name);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace League\Event;
interface EventInterface
{
/**
* Set the Emitter.
*
* @param EmitterInterface $emitter
*
* @return $this
*/
public function setEmitter(EmitterInterface $emitter);
/**
* Get the Emitter.
*
* @return EmitterInterface
*/
public function getEmitter();
/**
* Stop event propagation.
*
* @return $this
*/
public function stopPropagation();
/**
* Check whether propagation was stopped.
*
* @return bool
*/
public function isPropagationStopped();
/**
* Get the event name.
*
* @return string
*/
public function getName();
}

10
vendor/league/event/src/Generator.php vendored Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace League\Event;
class Generator implements GeneratorInterface
{
use GeneratorTrait {
addEvent as public;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace League\Event;
interface GeneratorInterface
{
/**
* Release all the added events.
*
* @return EventInterface[]
*/
public function releaseEvents();
}

View File

@@ -0,0 +1,40 @@
<?php
namespace League\Event;
trait GeneratorTrait
{
/**
* The registered events.
*
* @var EventInterface[]
*/
protected $events = [];
/**
* Add an event.
*
* @param EventInterface $event
*
* @return $this
*/
protected function addEvent(EventInterface $event)
{
$this->events[] = $event;
return $this;
}
/**
* Release all the added events.
*
* @return EventInterface[]
*/
public function releaseEvents()
{
$events = $this->events;
$this->events = [];
return $events;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace League\Event;
class ListenerAcceptor implements ListenerAcceptorInterface
{
/**
* The emitter instance.
*
* @var EmitterInterface|null
*/
protected $emitter;
/**
* Constructor
*
* @param EmitterInterface $emitter
*/
public function __construct(EmitterInterface $emitter)
{
$this->emitter = $emitter;
}
/**
* @inheritdoc
*/
public function addListener($event, $listener, $priority = self::P_NORMAL)
{
$this->emitter->addListener($event, $listener, $priority);
return $this;
}
/**
* @inheritdoc
*/
public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL)
{
$this->emitter->addOneTimeListener($event, $listener, $priority);
return $this;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace League\Event;
interface ListenerAcceptorInterface
{
/**
* High priority.
*
* @const int
*/
const P_HIGH = 100;
/**
* Normal priority.
*
* @const int
*/
const P_NORMAL = 0;
/**
* Low priority.
*
* @const int
*/
const P_LOW = -100;
/**
* Add a listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable". In this case, the priority emitter also accepts
* an optional third parameter specifying the priority as an integer. You
* may use one of our predefined constants here if you want.
*
* @param string $event
* @param ListenerInterface|callable $listener
* @param int $priority
*
* @return $this
*/
public function addListener($event, $listener, $priority = self::P_NORMAL);
/**
* Add a one time listener for an event.
*
* The first parameter should be the event name, and the second should be
* the event listener. It may implement the League\Event\ListenerInterface
* or simply be "callable".
*
* @param string $event
* @param ListenerInterface|callable $listener
* @param int $priority
*
* @return $this
*/
public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace League\Event;
interface ListenerInterface
{
/**
* Handle an event.
*
* @param EventInterface $event
*
* @return void
*/
public function handle(EventInterface $event);
/**
* Check whether the listener is the given parameter.
*
* @param mixed $listener
*
* @return bool
*/
public function isListener($listener);
}

View File

@@ -0,0 +1,15 @@
<?php
namespace League\Event;
interface ListenerProviderInterface
{
/**
* Provide event
*
* @param ListenerAcceptorInterface $listenerAcceptor
*
* @return $this
*/
public function provideListeners(ListenerAcceptorInterface $listenerAcceptor);
}

View File

@@ -0,0 +1,57 @@
<?php
namespace League\Event;
class OneTimeListener implements ListenerInterface
{
/**
* The listener instance.
*
* @var ListenerInterface
*/
protected $listener;
/**
* Create a new one time listener instance.
*
* @param ListenerInterface $listener
*/
public function __construct(ListenerInterface $listener)
{
$this->listener = $listener;
}
/**
* Get the wrapped listener.
*
* @return ListenerInterface
*/
public function getWrappedListener()
{
return $this->listener;
}
/**
* @inheritdoc
*/
public function handle(EventInterface $event)
{
$name = $event->getName();
$emitter = $event->getEmitter();
$emitter->removeListener($name, $this->listener);
return call_user_func_array([$this->listener, 'handle'], func_get_args());
}
/**
* @inheritdoc
*/
public function isListener($listener)
{
if ($listener instanceof OneTimeListener) {
$listener = $listener->getWrappedListener();
}
return $this->listener->isListener($listener);
}
}

20
vendor/league/oauth2-server/LICENSE vendored Normal file
View File

@@ -0,0 +1,20 @@
MIT License
Copyright (C) Alex Bilbie
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,252 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use DateInterval;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\GrantTypeInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\AbstractResponseType;
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class AuthorizationServer implements EmitterAwareInterface
{
use EmitterAwareTrait;
/**
* @var GrantTypeInterface[]
*/
protected $enabledGrantTypes = [];
/**
* @var DateInterval[]
*/
protected $grantTypeAccessTokenTTL = [];
/**
* @var CryptKey
*/
protected $privateKey;
/**
* @var CryptKey
*/
protected $publicKey;
/**
* @var ResponseTypeInterface
*/
protected $responseType;
/**
* @var ClientRepositoryInterface
*/
private $clientRepository;
/**
* @var AccessTokenRepositoryInterface
*/
private $accessTokenRepository;
/**
* @var ScopeRepositoryInterface
*/
private $scopeRepository;
/**
* @var string|Key
*/
private $encryptionKey;
/**
* @var string
*/
private $defaultScope = '';
/**
* @var bool
*/
private $revokeRefreshTokens = true;
/**
* New server instance.
*
* @param ClientRepositoryInterface $clientRepository
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param ScopeRepositoryInterface $scopeRepository
* @param CryptKey|string $privateKey
* @param string|Key $encryptionKey
* @param null|ResponseTypeInterface $responseType
*/
public function __construct(
ClientRepositoryInterface $clientRepository,
AccessTokenRepositoryInterface $accessTokenRepository,
ScopeRepositoryInterface $scopeRepository,
$privateKey,
$encryptionKey,
?ResponseTypeInterface $responseType = null
) {
$this->clientRepository = $clientRepository;
$this->accessTokenRepository = $accessTokenRepository;
$this->scopeRepository = $scopeRepository;
if ($privateKey instanceof CryptKey === false) {
$privateKey = new CryptKey($privateKey);
}
$this->privateKey = $privateKey;
$this->encryptionKey = $encryptionKey;
if ($responseType === null) {
$responseType = new BearerTokenResponse();
} else {
$responseType = clone $responseType;
}
$this->responseType = $responseType;
}
/**
* Enable a grant type on the server.
*
* @param GrantTypeInterface $grantType
* @param null|DateInterval $accessTokenTTL
*/
public function enableGrantType(GrantTypeInterface $grantType, ?DateInterval $accessTokenTTL = null)
{
if ($accessTokenTTL === null) {
$accessTokenTTL = new DateInterval('PT1H');
}
$grantType->setAccessTokenRepository($this->accessTokenRepository);
$grantType->setClientRepository($this->clientRepository);
$grantType->setScopeRepository($this->scopeRepository);
$grantType->setDefaultScope($this->defaultScope);
$grantType->setPrivateKey($this->privateKey);
$grantType->setEmitter($this->getEmitter());
$grantType->setEncryptionKey($this->encryptionKey);
$grantType->revokeRefreshTokens($this->revokeRefreshTokens);
$this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType;
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL;
}
/**
* Validate an authorization request
*
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*
* @return AuthorizationRequest
*/
public function validateAuthorizationRequest(ServerRequestInterface $request)
{
foreach ($this->enabledGrantTypes as $grantType) {
if ($grantType->canRespondToAuthorizationRequest($request)) {
return $grantType->validateAuthorizationRequest($request);
}
}
throw OAuthServerException::unsupportedGrantType();
}
/**
* Complete an authorization request
*
* @param AuthorizationRequest $authRequest
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function completeAuthorizationRequest(AuthorizationRequest $authRequest, ResponseInterface $response)
{
return $this->enabledGrantTypes[$authRequest->getGrantTypeId()]
->completeAuthorizationRequest($authRequest)
->generateHttpResponse($response);
}
/**
* Return an access token response.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
*
* @throws OAuthServerException
*
* @return ResponseInterface
*/
public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response)
{
foreach ($this->enabledGrantTypes as $grantType) {
if (!$grantType->canRespondToAccessTokenRequest($request)) {
continue;
}
$tokenResponse = $grantType->respondToAccessTokenRequest(
$request,
$this->getResponseType(),
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
);
if ($tokenResponse instanceof ResponseTypeInterface) {
return $tokenResponse->generateHttpResponse($response);
}
}
throw OAuthServerException::unsupportedGrantType();
}
/**
* Get the token type that grants will return in the HTTP response.
*
* @return ResponseTypeInterface
*/
protected function getResponseType()
{
$responseType = clone $this->responseType;
if ($responseType instanceof AbstractResponseType) {
$responseType->setPrivateKey($this->privateKey);
}
$responseType->setEncryptionKey($this->encryptionKey);
return $responseType;
}
/**
* Set the default scope for the authorization server.
*
* @param string $defaultScope
*/
public function setDefaultScope($defaultScope)
{
$this->defaultScope = $defaultScope;
}
/**
* Sets whether to revoke refresh tokens or not (for all grant types).
*
* @param bool $revokeRefreshTokens
*/
public function revokeRefreshTokens(bool $revokeRefreshTokens): void
{
$this->revokeRefreshTokens = $revokeRefreshTokens;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\AuthorizationValidators;
use Psr\Http\Message\ServerRequestInterface;
interface AuthorizationValidatorInterface
{
/**
* Determine the access token in the authorization header and append OAUth properties to the request
* as attributes.
*
* @param ServerRequestInterface $request
*
* @return ServerRequestInterface
*/
public function validateAuthorization(ServerRequestInterface $request);
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\AuthorizationValidators;
use DateTimeZone;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;
class BearerTokenValidator implements AuthorizationValidatorInterface
{
use CryptTrait;
/**
* @var AccessTokenRepositoryInterface
*/
private $accessTokenRepository;
/**
* @var CryptKey
*/
protected $publicKey;
/**
* @var Configuration
*/
private $jwtConfiguration;
/**
* @var \DateInterval|null
*/
private $jwtValidAtDateLeeway;
/**
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param \DateInterval|null $jwtValidAtDateLeeway
*/
public function __construct(AccessTokenRepositoryInterface $accessTokenRepository, ?\DateInterval $jwtValidAtDateLeeway = null)
{
$this->accessTokenRepository = $accessTokenRepository;
$this->jwtValidAtDateLeeway = $jwtValidAtDateLeeway;
}
/**
* Set the public key
*
* @param CryptKey $key
*/
public function setPublicKey(CryptKey $key)
{
$this->publicKey = $key;
$this->initJwtConfiguration();
}
/**
* Initialise the JWT configuration.
*/
private function initJwtConfiguration()
{
$this->jwtConfiguration = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText('empty', 'empty')
);
$clock = new SystemClock(new DateTimeZone(\date_default_timezone_get()));
$this->jwtConfiguration->setValidationConstraints(
new LooseValidAt($clock, $this->jwtValidAtDateLeeway),
new SignedWith(
new Sha256(),
InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '')
)
);
}
/**
* {@inheritdoc}
*/
public function validateAuthorization(ServerRequestInterface $request)
{
if ($request->hasHeader('authorization') === false) {
throw OAuthServerException::accessDenied('Missing "Authorization" header');
}
$header = $request->getHeader('authorization');
$jwt = \trim((string) \preg_replace('/^\s*Bearer\s/', '', $header[0]));
try {
// Attempt to parse the JWT
$token = $this->jwtConfiguration->parser()->parse($jwt);
} catch (\Lcobucci\JWT\Exception $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
}
try {
// Attempt to validate the JWT
$constraints = $this->jwtConfiguration->validationConstraints();
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception);
}
$claims = $token->claims();
// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}
// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $claims->get('jti'))
->withAttribute('oauth_client_id', $this->convertSingleRecordAudToString($claims->get('aud')))
->withAttribute('oauth_user_id', $claims->get('sub'))
->withAttribute('oauth_scopes', $claims->get('scopes'));
}
/**
* Convert single record arrays into strings to ensure backwards compatibility between v4 and v3.x of lcobucci/jwt
*
* @param mixed $aud
*
* @return array|string
*/
private function convertSingleRecordAudToString($aud)
{
return \is_array($aud) && \count($aud) === 1 ? $aud[0] : $aud;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
interface CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod();
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge);
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
class PlainVerifier implements CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod()
{
return 'plain';
}
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge)
{
return \hash_equals($codeVerifier, $codeChallenge);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @author Lukáš Unger <lookymsc@gmail.com>
* @copyright Copyright (c) Lukáš Unger
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\CodeChallengeVerifiers;
class S256Verifier implements CodeChallengeVerifierInterface
{
/**
* Return code challenge method.
*
* @return string
*/
public function getMethod()
{
return 'S256';
}
/**
* Verify the code challenge.
*
* @param string $codeVerifier
* @param string $codeChallenge
*
* @return bool
*/
public function verifyCodeChallenge($codeVerifier, $codeChallenge)
{
return \hash_equals(
\strtr(\rtrim(\base64_encode(\hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
$codeChallenge
);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* Cryptography key holder.
*
* @author Julián Gutiérrez <juliangut@gmail.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use LogicException;
class CryptKey
{
/** @deprecated left for backward compatibility check */
const RSA_KEY_PATTERN =
'/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----)\R.*(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)\R?$/s';
private const FILE_PREFIX = 'file://';
/**
* @var string Key contents
*/
protected $keyContents;
/**
* @var string
*/
protected $keyPath;
/**
* @var null|string
*/
protected $passPhrase;
/**
* @param string $keyPath
* @param null|string $passPhrase
* @param bool $keyPermissionsCheck
*/
public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true)
{
$this->passPhrase = $passPhrase;
if (\strpos($keyPath, self::FILE_PREFIX) !== 0 && $this->isValidKey($keyPath, $this->passPhrase ?? '')) {
$this->keyContents = $keyPath;
$this->keyPath = '';
// There's no file, so no need for permission check.
$keyPermissionsCheck = false;
} elseif (\is_file($keyPath)) {
if (\strpos($keyPath, self::FILE_PREFIX) !== 0) {
$keyPath = self::FILE_PREFIX . $keyPath;
}
if (!\is_readable($keyPath)) {
throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath));
}
$this->keyContents = \file_get_contents($keyPath);
$this->keyPath = $keyPath;
if (!$this->isValidKey($this->keyContents, $this->passPhrase ?? '')) {
throw new LogicException('Unable to read key from file ' . $keyPath);
}
} else {
throw new LogicException('Invalid key supplied');
}
if ($keyPermissionsCheck === true) {
// Verify the permissions of the key
$keyPathPerms = \decoct(\fileperms($this->keyPath) & 0777);
if (\in_array($keyPathPerms, ['400', '440', '600', '640', '660'], true) === false) {
\trigger_error(
\sprintf(
'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s',
$this->keyPath,
$keyPathPerms
),
E_USER_NOTICE
);
}
}
}
/**
* Get key contents
*
* @return string Key contents
*/
public function getKeyContents(): string
{
return $this->keyContents;
}
/**
* Validate key contents.
*
* @param string $contents
* @param string $passPhrase
*
* @return bool
*/
private function isValidKey($contents, $passPhrase)
{
$pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents);
if ($pkey === false) {
return false;
}
$details = \openssl_pkey_get_details($pkey);
return $details !== false && \in_array(
$details['type'] ?? -1,
[OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC],
true
);
}
/**
* Retrieve key path.
*
* @return string
*/
public function getKeyPath()
{
return $this->keyPath;
}
/**
* Retrieve key pass phrase.
*
* @return null|string
*/
public function getPassPhrase()
{
return $this->passPhrase;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Encrypt/decrypt with encryptionKey.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use Exception;
use LogicException;
trait CryptTrait
{
/**
* @var string|Key|null
*/
protected $encryptionKey;
/**
* Encrypt data with encryptionKey.
*
* @param string $unencryptedData
*
* @throws LogicException
*
* @return string
*/
protected function encrypt($unencryptedData)
{
try {
if ($this->encryptionKey instanceof Key) {
return Crypto::encrypt($unencryptedData, $this->encryptionKey);
}
if (\is_string($this->encryptionKey)) {
return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey);
}
throw new LogicException('Encryption key not set when attempting to encrypt');
} catch (Exception $e) {
throw new LogicException($e->getMessage(), 0, $e);
}
}
/**
* Decrypt data with encryptionKey.
*
* @param string $encryptedData
*
* @throws LogicException
*
* @return string
*/
protected function decrypt($encryptedData)
{
try {
if ($this->encryptionKey instanceof Key) {
return Crypto::decrypt($encryptedData, $this->encryptionKey);
}
if (\is_string($this->encryptionKey)) {
return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey);
}
throw new LogicException('Encryption key not set when attempting to decrypt');
} catch (Exception $e) {
throw new LogicException($e->getMessage(), 0, $e);
}
}
/**
* Set the encryption key
*
* @param string|Key $key
*/
public function setEncryptionKey($key = null)
{
$this->encryptionKey = $key;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
use League\OAuth2\Server\CryptKey;
interface AccessTokenEntityInterface extends TokenInterface
{
/**
* Set a private key used to encrypt the access token.
*/
public function setPrivateKey(CryptKey $privateKey);
/**
* Generate a string representation of the access token.
*/
public function __toString();
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
interface AuthCodeEntityInterface extends TokenInterface
{
/**
* @return string|null
*/
public function getRedirectUri();
/**
* @param string $uri
*/
public function setRedirectUri($uri);
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
interface ClientEntityInterface
{
/**
* Get the client's identifier.
*
* @return string
*/
public function getIdentifier();
/**
* Get the client's name.
*
* @return string
*/
public function getName();
/**
* Returns the registered redirect URI (as a string).
*
* Alternatively return an indexed array of redirect URIs.
*
* @return string|string[]
*/
public function getRedirectUri();
/**
* Returns true if the client is confidential.
*
* @return bool
*/
public function isConfidential();
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
use DateTimeImmutable;
interface RefreshTokenEntityInterface
{
/**
* Get the token's identifier.
*
* @return string
*/
public function getIdentifier();
/**
* Set the token's identifier.
*
* @param mixed $identifier
*/
public function setIdentifier($identifier);
/**
* Get the token's expiry date time.
*
* @return DateTimeImmutable
*/
public function getExpiryDateTime();
/**
* Set the date time when the token expires.
*
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(DateTimeImmutable $dateTime);
/**
* Set the access token that the refresh token was associated with.
*
* @param AccessTokenEntityInterface $accessToken
*/
public function setAccessToken(AccessTokenEntityInterface $accessToken);
/**
* Get the access token that the refresh token was originally associated with.
*
* @return AccessTokenEntityInterface
*/
public function getAccessToken();
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
use JsonSerializable;
interface ScopeEntityInterface extends JsonSerializable
{
/**
* Get the scope's identifier.
*
* @return string
*/
public function getIdentifier();
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
use DateTimeImmutable;
interface TokenInterface
{
/**
* Get the token's identifier.
*
* @return string
*/
public function getIdentifier();
/**
* Set the token's identifier.
*
* @param mixed $identifier
*/
public function setIdentifier($identifier);
/**
* Get the token's expiry date time.
*
* @return DateTimeImmutable
*/
public function getExpiryDateTime();
/**
* Set the date time when the token expires.
*
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(DateTimeImmutable $dateTime);
/**
* Set the identifier of the user associated with the token.
*
* @param string|int|null $identifier The identifier of the user
*/
public function setUserIdentifier($identifier);
/**
* Get the token user's identifier.
*
* @return string|int|null
*/
public function getUserIdentifier();
/**
* Get the client that the token was issued to.
*
* @return ClientEntityInterface
*/
public function getClient();
/**
* Set the client that the token was issued to.
*
* @param ClientEntityInterface $client
*/
public function setClient(ClientEntityInterface $client);
/**
* Associate a scope with the token.
*
* @param ScopeEntityInterface $scope
*/
public function addScope(ScopeEntityInterface $scope);
/**
* Return an array of scopes associated with the token.
*
* @return ScopeEntityInterface[]
*/
public function getScopes();
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
trait AccessTokenTrait
{
/**
* @var CryptKey
*/
private $privateKey;
/**
* @var Configuration
*/
private $jwtConfiguration;
/**
* Set the private key used to encrypt this access token.
*/
public function setPrivateKey(CryptKey $privateKey)
{
$this->privateKey = $privateKey;
}
/**
* Initialise the JWT Configuration.
*/
public function initJwtConfiguration()
{
$this->jwtConfiguration = Configuration::forAsymmetricSigner(
new Sha256(),
InMemory::plainText($this->privateKey->getKeyContents(), $this->privateKey->getPassPhrase() ?? ''),
InMemory::plainText('empty', 'empty')
);
}
/**
* Generate a JWT from the access token
*
* @return Token
*/
private function convertToJWT()
{
$this->initJwtConfiguration();
return $this->jwtConfiguration->builder()
->permittedFor($this->getClient()->getIdentifier())
->identifiedBy($this->getIdentifier())
->issuedAt(new DateTimeImmutable())
->canOnlyBeUsedAfter(new DateTimeImmutable())
->expiresAt($this->getExpiryDateTime())
->relatedTo((string) $this->getUserIdentifier())
->withClaim('scopes', $this->getScopes())
->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
}
/**
* Generate a string representation from the access token
*/
public function __toString()
{
return $this->convertToJWT()->toString();
}
/**
* @return ClientEntityInterface
*/
abstract public function getClient();
/**
* @return DateTimeImmutable
*/
abstract public function getExpiryDateTime();
/**
* @return string|int
*/
abstract public function getUserIdentifier();
/**
* @return ScopeEntityInterface[]
*/
abstract public function getScopes();
/**
* @return string
*/
abstract public function getIdentifier();
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
trait AuthCodeTrait
{
/**
* @var null|string
*/
protected $redirectUri;
/**
* @return string|null
*/
public function getRedirectUri()
{
return $this->redirectUri;
}
/**
* @param string $uri
*/
public function setRedirectUri($uri)
{
$this->redirectUri = $uri;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
trait ClientTrait
{
/**
* @var string
*/
protected $name;
/**
* @var string|string[]
*/
protected $redirectUri;
/**
* @var bool
*/
protected $isConfidential = false;
/**
* Get the client's name.
*
* @return string
*
* @codeCoverageIgnore
*/
public function getName()
{
return $this->name;
}
/**
* Returns the registered redirect URI (as a string).
*
* Alternatively return an indexed array of redirect URIs.
*
* @return string|string[]
*/
public function getRedirectUri()
{
return $this->redirectUri;
}
/**
* Returns true if the client is confidential.
*
* @return bool
*/
public function isConfidential()
{
return $this->isConfidential;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
trait EntityTrait
{
/**
* @var string
*/
protected $identifier;
/**
* @return mixed
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* @param mixed $identifier
*/
public function setIdentifier($identifier)
{
$this->identifier = $identifier;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
trait RefreshTokenTrait
{
/**
* @var AccessTokenEntityInterface
*/
protected $accessToken;
/**
* @var DateTimeImmutable
*/
protected $expiryDateTime;
/**
* {@inheritdoc}
*/
public function setAccessToken(AccessTokenEntityInterface $accessToken)
{
$this->accessToken = $accessToken;
}
/**
* {@inheritdoc}
*/
public function getAccessToken()
{
return $this->accessToken;
}
/**
* Get the token's expiry date time.
*
* @return DateTimeImmutable
*/
public function getExpiryDateTime()
{
return $this->expiryDateTime;
}
/**
* Set the date time when the token expires.
*
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(DateTimeImmutable $dateTime)
{
$this->expiryDateTime = $dateTime;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* @author Andrew Millington <andrew@noexceptions.io>
* @copyright Copyright (c) Andrew Millington
* @license http://mit-license.org
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
trait ScopeTrait
{
/**
* Serialize the object to the scopes string identifier when using json_encode().
*
* @return string
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->getIdentifier();
}
/**
* @return string
*/
abstract public function getIdentifier();
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities\Traits;
use DateTimeImmutable;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
trait TokenEntityTrait
{
/**
* @var ScopeEntityInterface[]
*/
protected $scopes = [];
/**
* @var DateTimeImmutable
*/
protected $expiryDateTime;
/**
* @var string|int|null
*/
protected $userIdentifier;
/**
* @var ClientEntityInterface
*/
protected $client;
/**
* Associate a scope with the token.
*
* @param ScopeEntityInterface $scope
*/
public function addScope(ScopeEntityInterface $scope)
{
$this->scopes[$scope->getIdentifier()] = $scope;
}
/**
* Return an array of scopes associated with the token.
*
* @return ScopeEntityInterface[]
*/
public function getScopes()
{
return \array_values($this->scopes);
}
/**
* Get the token's expiry date time.
*
* @return DateTimeImmutable
*/
public function getExpiryDateTime()
{
return $this->expiryDateTime;
}
/**
* Set the date time when the token expires.
*
* @param DateTimeImmutable $dateTime
*/
public function setExpiryDateTime(DateTimeImmutable $dateTime)
{
$this->expiryDateTime = $dateTime;
}
/**
* Set the identifier of the user associated with the token.
*
* @param string|int|null $identifier The identifier of the user
*/
public function setUserIdentifier($identifier)
{
$this->userIdentifier = $identifier;
}
/**
* Get the token user's identifier.
*
* @return string|int|null
*/
public function getUserIdentifier()
{
return $this->userIdentifier;
}
/**
* Get the client that the token was issued to.
*
* @return ClientEntityInterface
*/
public function getClient()
{
return $this->client;
}
/**
* Set the client that the token was issued to.
*
* @param ClientEntityInterface $client
*/
public function setClient(ClientEntityInterface $client)
{
$this->client = $client;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Entities;
interface UserEntityInterface
{
/**
* Return the user's identifier.
*
* @return mixed
*/
public function getIdentifier();
}

View File

@@ -0,0 +1,416 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Exception;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
class OAuthServerException extends Exception
{
/**
* @var int
*/
private $httpStatusCode;
/**
* @var string
*/
private $errorType;
/**
* @var null|string
*/
private $hint;
/**
* @var null|string
*/
private $redirectUri;
/**
* @var array
*/
private $payload;
/**
* @var ServerRequestInterface
*/
private $serverRequest;
/**
* Throw a new exception.
*
* @param string $message Error message
* @param int $code Error code
* @param string $errorType Error type
* @param int $httpStatusCode HTTP status code to send (default = 400)
* @param null|string $hint A helper hint
* @param null|string $redirectUri A HTTP URI to redirect the user back to
* @param Throwable $previous Previous exception
*/
public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->httpStatusCode = $httpStatusCode;
$this->errorType = $errorType;
$this->hint = $hint;
$this->redirectUri = $redirectUri;
$this->payload = [
'error' => $errorType,
'error_description' => $message,
];
if ($hint !== null) {
$this->payload['hint'] = $hint;
}
}
/**
* Returns the current payload.
*
* @return array
*/
public function getPayload()
{
$payload = $this->payload;
// The "message" property is deprecated and replaced by "error_description"
// TODO: remove "message" property
if (isset($payload['error_description']) && !isset($payload['message'])) {
$payload['message'] = $payload['error_description'];
}
return $payload;
}
/**
* Updates the current payload.
*
* @param array $payload
*/
public function setPayload(array $payload)
{
$this->payload = $payload;
}
/**
* Set the server request that is responsible for generating the exception
*
* @param ServerRequestInterface $serverRequest
*/
public function setServerRequest(ServerRequestInterface $serverRequest)
{
$this->serverRequest = $serverRequest;
}
/**
* Unsupported grant type error.
*
* @return static
*/
public static function unsupportedGrantType()
{
$errorMessage = 'The authorization grant type is not supported by the authorization server.';
$hint = 'Check that all required parameters have been provided';
return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint);
}
/**
* Invalid request error.
*
* @param string $parameter The invalid parameter
* @param null|string $hint
* @param Throwable $previous Previous exception
*
* @return static
*/
public static function invalidRequest($parameter, $hint = null, ?Throwable $previous = null)
{
$errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' .
'includes a parameter more than once, or is otherwise malformed.';
$hint = ($hint === null) ? \sprintf('Check the `%s` parameter', $parameter) : $hint;
return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous);
}
/**
* Invalid client error.
*
* @param ServerRequestInterface $serverRequest
*
* @return static
*/
public static function invalidClient(ServerRequestInterface $serverRequest)
{
$exception = new static('Client authentication failed', 4, 'invalid_client', 401);
$exception->setServerRequest($serverRequest);
return $exception;
}
/**
* Invalid scope error.
*
* @param string $scope The bad scope
* @param null|string $redirectUri A HTTP URI to redirect the user back to
*
* @return static
*/
public static function invalidScope($scope, $redirectUri = null)
{
$errorMessage = 'The requested scope is invalid, unknown, or malformed';
if (empty($scope)) {
$hint = 'Specify a scope in the request or set a default scope';
} else {
$hint = \sprintf(
'Check the `%s` scope',
\htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false)
);
}
return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri);
}
/**
* Invalid credentials error.
*
* @return static
*/
public static function invalidCredentials()
{
return new static('The user credentials were incorrect.', 6, 'invalid_grant', 400);
}
/**
* Server error.
*
* @param string $hint
* @param Throwable $previous
*
* @return static
*
* @codeCoverageIgnore
*/
public static function serverError($hint, ?Throwable $previous = null)
{
return new static(
'The authorization server encountered an unexpected condition which prevented it from fulfilling'
. ' the request: ' . $hint,
7,
'server_error',
500,
null,
null,
$previous
);
}
/**
* Invalid refresh token.
*
* @param null|string $hint
* @param Throwable $previous
*
* @return static
*/
public static function invalidRefreshToken($hint = null, ?Throwable $previous = null)
{
return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint, null, $previous);
}
/**
* Access denied.
*
* @param null|string $hint
* @param null|string $redirectUri
* @param Throwable $previous
*
* @return static
*/
public static function accessDenied($hint = null, $redirectUri = null, ?Throwable $previous = null)
{
return new static(
'The resource owner or authorization server denied the request.',
9,
'access_denied',
401,
$hint,
$redirectUri,
$previous
);
}
/**
* Invalid grant.
*
* @param string $hint
*
* @return static
*/
public static function invalidGrant($hint = '')
{
return new static(
'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token '
. 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, '
. 'or was issued to another client.',
10,
'invalid_grant',
400,
$hint
);
}
/**
* @return string
*/
public function getErrorType()
{
return $this->errorType;
}
/**
* Generate a HTTP response.
*
* @param ResponseInterface $response
* @param bool $useFragment True if errors should be in the URI fragment instead of query string
* @param int $jsonOptions options passed to json_encode
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response, $useFragment = false, $jsonOptions = 0)
{
$headers = $this->getHttpHeaders();
$payload = $this->getPayload();
if ($this->redirectUri !== null) {
if ($useFragment === true) {
$this->redirectUri .= (\strstr($this->redirectUri, '#') === false) ? '#' : '&';
} else {
$this->redirectUri .= (\strstr($this->redirectUri, '?') === false) ? '?' : '&';
}
return $response->withStatus(302)->withHeader('Location', $this->redirectUri . \http_build_query($payload));
}
foreach ($headers as $header => $content) {
$response = $response->withHeader($header, $content);
}
$responseBody = \json_encode($payload, $jsonOptions) ?: 'JSON encoding of payload failed';
$response->getBody()->write($responseBody);
return $response->withStatus($this->getHttpStatusCode());
}
/**
* Get all headers that have to be send with the error response.
*
* @return array Array with header values
*/
public function getHttpHeaders()
{
$headers = [
'Content-type' => 'application/json',
];
// Add "WWW-Authenticate" header
//
// RFC 6749, section 5.2.:
// "If the client attempted to authenticate via the 'Authorization'
// request header field, the authorization server MUST
// respond with an HTTP 401 (Unauthorized) status code and
// include the "WWW-Authenticate" response header field
// matching the authentication scheme used by the client.
if ($this->errorType === 'invalid_client' && $this->requestHasAuthorizationHeader()) {
$authScheme = \strpos($this->serverRequest->getHeader('Authorization')[0], 'Bearer') === 0 ? 'Bearer' : 'Basic';
$headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"';
}
return $headers;
}
/**
* Check if the exception has an associated redirect URI.
*
* Returns whether the exception includes a redirect, since
* getHttpStatusCode() doesn't return a 302 when there's a
* redirect enabled. This helps when you want to override local
* error pages but want to let redirects through.
*
* @return bool
*/
public function hasRedirect()
{
return $this->redirectUri !== null;
}
/**
* Returns the Redirect URI used for redirecting.
*
* @return string|null
*/
public function getRedirectUri()
{
return $this->redirectUri;
}
/**
* Returns the HTTP status code to send when the exceptions is output.
*
* @return int
*/
public function getHttpStatusCode()
{
return $this->httpStatusCode;
}
/**
* @return null|string
*/
public function getHint()
{
return $this->hint;
}
/**
* Check if the request has a non-empty 'Authorization' header value.
*
* Returns true if the header is present and not an empty string, false
* otherwise.
*
* @return bool
*/
private function requestHasAuthorizationHeader()
{
if (!$this->serverRequest->hasHeader('Authorization')) {
return false;
}
$authorizationHeader = $this->serverRequest->getHeader('Authorization');
// Common .htaccess configurations yield an empty string for the
// 'Authorization' header when one is not provided by the client.
// For practical purposes that case should be treated as though the
// header isn't present.
// See https://github.com/thephpleague/oauth2-server/issues/1162
if (empty($authorizationHeader) || empty($authorizationHeader[0])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* @author Ivan Kurnosov <zerkms@zerkms.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Exception;
class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException
{
/**
* @return UniqueTokenIdentifierConstraintViolationException
*/
public static function create()
{
$errorMessage = 'Could not create unique access token identifier';
return new static($errorMessage, 100, 'access_token_duplicate', 500);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Abstract authorization grant.
*
* @author Julián Gutiérrez <juliangut@gmail.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
abstract class AbstractAuthorizeGrant extends AbstractGrant
{
/**
* @param string $uri
* @param array $params
* @param string $queryDelimiter
*
* @return string
*/
public function makeRedirectUri($uri, $params = [], $queryDelimiter = '?')
{
$uri .= (\strstr($uri, $queryDelimiter) === false) ? $queryDelimiter : '&';
return $uri . \http_build_query($params);
}
}

View File

@@ -0,0 +1,621 @@
<?php
/**
* OAuth 2.0 Abstract grant.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use DateTimeImmutable;
use Error;
use Exception;
use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use TypeError;
/**
* Abstract grant class.
*/
abstract class AbstractGrant implements GrantTypeInterface
{
use EmitterAwareTrait, CryptTrait;
const SCOPE_DELIMITER_STRING = ' ';
const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10;
/**
* @var ClientRepositoryInterface
*/
protected $clientRepository;
/**
* @var AccessTokenRepositoryInterface
*/
protected $accessTokenRepository;
/**
* @var ScopeRepositoryInterface
*/
protected $scopeRepository;
/**
* @var AuthCodeRepositoryInterface
*/
protected $authCodeRepository;
/**
* @var RefreshTokenRepositoryInterface
*/
protected $refreshTokenRepository;
/**
* @var UserRepositoryInterface
*/
protected $userRepository;
/**
* @var DateInterval
*/
protected $refreshTokenTTL;
/**
* @var CryptKey
*/
protected $privateKey;
/**
* @var string
*/
protected $defaultScope;
/**
* @var bool
*/
protected $revokeRefreshTokens;
/**
* @param ClientRepositoryInterface $clientRepository
*/
public function setClientRepository(ClientRepositoryInterface $clientRepository)
{
$this->clientRepository = $clientRepository;
}
/**
* @param AccessTokenRepositoryInterface $accessTokenRepository
*/
public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
{
$this->accessTokenRepository = $accessTokenRepository;
}
/**
* @param ScopeRepositoryInterface $scopeRepository
*/
public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
{
$this->scopeRepository = $scopeRepository;
}
/**
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
*/
public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
{
$this->refreshTokenRepository = $refreshTokenRepository;
}
/**
* @param AuthCodeRepositoryInterface $authCodeRepository
*/
public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
{
$this->authCodeRepository = $authCodeRepository;
}
/**
* @param UserRepositoryInterface $userRepository
*/
public function setUserRepository(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* {@inheritdoc}
*/
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
{
$this->refreshTokenTTL = $refreshTokenTTL;
}
/**
* Set the private key
*
* @param CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{
$this->privateKey = $key;
}
/**
* @param string $scope
*/
public function setDefaultScope($scope)
{
$this->defaultScope = $scope;
}
/**
* @param bool $revokeRefreshTokens
*/
public function revokeRefreshTokens(bool $revokeRefreshTokens)
{
$this->revokeRefreshTokens = $revokeRefreshTokens;
}
/**
* Validate the client.
*
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*
* @return ClientEntityInterface
*/
protected function validateClient(ServerRequestInterface $request)
{
[$clientId, $clientSecret] = $this->getClientCredentials($request);
if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
$client = $this->getClientEntityOrFail($clientId, $request);
// If a redirect URI is provided ensure it matches what is pre-registered
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
if ($redirectUri !== null) {
if (!\is_string($redirectUri)) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
$this->validateRedirectUri($redirectUri, $client, $request);
}
return $client;
}
/**
* Wrapper around ClientRepository::getClientEntity() that ensures we emit
* an event and throw an exception if the repo doesn't return a client
* entity.
*
* This is a bit of defensive coding because the interface contract
* doesn't actually enforce non-null returns/exception-on-no-client so
* getClientEntity might return null. By contrast, this method will
* always either return a ClientEntityInterface or throw.
*
* @param string $clientId
* @param ServerRequestInterface $request
*
* @return ClientEntityInterface
*/
protected function getClientEntityOrFail($clientId, ServerRequestInterface $request)
{
$client = $this->clientRepository->getClientEntity($clientId);
if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
return $client;
}
/**
* Gets the client credentials from the request from the request body or
* the Http Basic Authorization header
*
* @param ServerRequestInterface $request
*
* @return array
*/
protected function getClientCredentials(ServerRequestInterface $request)
{
[$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request);
$clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
if (\is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
$clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
if ($clientSecret !== null && !\is_string($clientSecret)) {
throw OAuthServerException::invalidRequest('client_secret');
}
return [$clientId, $clientSecret];
}
/**
* Validate redirectUri from the request.
* If a redirect URI is provided ensure it matches what is pre-registered
*
* @param string $redirectUri
* @param ClientEntityInterface $client
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*/
protected function validateRedirectUri(
string $redirectUri,
ClientEntityInterface $client,
ServerRequestInterface $request
) {
$validator = new RedirectUriValidator($client->getRedirectUri());
if (!$validator->validateRedirectUri($redirectUri)) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
}
/**
* Validate scopes in the request.
*
* @param string|array|null $scopes
* @param string $redirectUri
*
* @throws OAuthServerException
*
* @return ScopeEntityInterface[]
*/
public function validateScopes($scopes, $redirectUri = null)
{
if ($scopes === null) {
$scopes = [];
} elseif (\is_string($scopes)) {
$scopes = $this->convertScopesQueryStringToArray($scopes);
}
if (!\is_array($scopes)) {
throw OAuthServerException::invalidRequest('scope');
}
$validScopes = [];
foreach ($scopes as $scopeItem) {
$scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
if ($scope instanceof ScopeEntityInterface === false) {
throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
}
$validScopes[] = $scope;
}
return $validScopes;
}
/**
* Converts a scopes query string to an array to easily iterate for validation.
*
* @param string $scopes
*
* @return array
*/
private function convertScopesQueryStringToArray(string $scopes)
{
return \array_filter(\explode(self::SCOPE_DELIMITER_STRING, \trim($scopes)), function ($scope) {
return $scope !== '';
});
}
/**
* Retrieve request parameter.
*
* @param string $parameter
* @param ServerRequestInterface $request
* @param mixed $default
*
* @return null|string
*/
protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
{
$requestParameters = (array) $request->getParsedBody();
return $requestParameters[$parameter] ?? $default;
}
/**
* Retrieve HTTP Basic Auth credentials with the Authorization header
* of a request. First index of the returned array is the username,
* second is the password (so list() will work). If the header does
* not exist, or is otherwise an invalid HTTP Basic header, return
* [null, null].
*
* @param ServerRequestInterface $request
*
* @return string[]|null[]
*/
protected function getBasicAuthCredentials(ServerRequestInterface $request)
{
if (!$request->hasHeader('Authorization')) {
return [null, null];
}
$header = $request->getHeader('Authorization')[0];
if (\strpos($header, 'Basic ') !== 0) {
return [null, null];
}
if (!($decoded = \base64_decode(\substr($header, 6)))) {
return [null, null];
}
if (\strpos($decoded, ':') === false) {
return [null, null]; // HTTP Basic header without colon isn't valid
}
return \explode(':', $decoded, 2);
}
/**
* Retrieve query string parameter.
*
* @param string $parameter
* @param ServerRequestInterface $request
* @param mixed $default
*
* @return null|string
*/
protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
{
return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
}
/**
* Retrieve cookie parameter.
*
* @param string $parameter
* @param ServerRequestInterface $request
* @param mixed $default
*
* @return null|string
*/
protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
{
return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
}
/**
* Retrieve server parameter.
*
* @param string $parameter
* @param ServerRequestInterface $request
* @param mixed $default
*
* @return null|string
*/
protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
{
return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
}
/**
* Issue an access token.
*
* @param DateInterval $accessTokenTTL
* @param ClientEntityInterface $client
* @param string|null $userIdentifier
* @param ScopeEntityInterface[] $scopes
*
* @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException
*
* @return AccessTokenEntityInterface
*/
protected function issueAccessToken(
DateInterval $accessTokenTTL,
ClientEntityInterface $client,
$userIdentifier,
array $scopes = []
) {
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
$accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
$accessToken->setPrivateKey($this->privateKey);
while ($maxGenerationAttempts-- > 0) {
$accessToken->setIdentifier($this->generateUniqueIdentifier());
try {
$this->accessTokenRepository->persistNewAccessToken($accessToken);
return $accessToken;
} catch (UniqueTokenIdentifierConstraintViolationException $e) {
if ($maxGenerationAttempts === 0) {
throw $e;
}
}
}
}
/**
* Issue an auth code.
*
* @param DateInterval $authCodeTTL
* @param ClientEntityInterface $client
* @param string $userIdentifier
* @param string|null $redirectUri
* @param ScopeEntityInterface[] $scopes
*
* @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException
*
* @return AuthCodeEntityInterface
*/
protected function issueAuthCode(
DateInterval $authCodeTTL,
ClientEntityInterface $client,
$userIdentifier,
$redirectUri,
array $scopes = []
) {
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$authCode = $this->authCodeRepository->getNewAuthCode();
$authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
$authCode->setClient($client);
$authCode->setUserIdentifier($userIdentifier);
if ($redirectUri !== null) {
$authCode->setRedirectUri($redirectUri);
}
foreach ($scopes as $scope) {
$authCode->addScope($scope);
}
while ($maxGenerationAttempts-- > 0) {
$authCode->setIdentifier($this->generateUniqueIdentifier());
try {
$this->authCodeRepository->persistNewAuthCode($authCode);
return $authCode;
} catch (UniqueTokenIdentifierConstraintViolationException $e) {
if ($maxGenerationAttempts === 0) {
throw $e;
}
}
}
}
/**
* @param AccessTokenEntityInterface $accessToken
*
* @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException
*
* @return RefreshTokenEntityInterface|null
*/
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
{
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
if ($refreshToken === null) {
return null;
}
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
$refreshToken->setAccessToken($accessToken);
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
while ($maxGenerationAttempts-- > 0) {
$refreshToken->setIdentifier($this->generateUniqueIdentifier());
try {
$this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
return $refreshToken;
} catch (UniqueTokenIdentifierConstraintViolationException $e) {
if ($maxGenerationAttempts === 0) {
throw $e;
}
}
}
}
/**
* Generate a new unique identifier.
*
* @param int $length
*
* @throws OAuthServerException
*
* @return string
*/
protected function generateUniqueIdentifier($length = 40)
{
try {
return \bin2hex(\random_bytes($length));
// @codeCoverageIgnoreStart
} catch (TypeError $e) {
throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (Error $e) {
throw OAuthServerException::serverError('An unexpected error has occurred', $e);
} catch (Exception $e) {
// If you get this message, the CSPRNG failed hard.
throw OAuthServerException::serverError('Could not generate a random string', $e);
}
// @codeCoverageIgnoreEnd
}
/**
* {@inheritdoc}
*/
public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
{
$requestParameters = (array) $request->getParsedBody();
return (
\array_key_exists('grant_type', $requestParameters)
&& $requestParameters['grant_type'] === $this->getIdentifier()
);
}
/**
* {@inheritdoc}
*/
public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
{
return false;
}
/**
* {@inheritdoc}
*/
public function validateAuthorizationRequest(ServerRequestInterface $request)
{
throw new LogicException('This grant cannot validate an authorization request');
}
/**
* {@inheritdoc}
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
throw new LogicException('This grant cannot complete an authorization request');
}
}

View File

@@ -0,0 +1,424 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use DateTimeImmutable;
use Exception;
use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestAccessTokenEvent;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestRefreshTokenEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use stdClass;
class AuthCodeGrant extends AbstractAuthorizeGrant
{
/**
* @var DateInterval
*/
private $authCodeTTL;
/**
* @var bool
*/
private $requireCodeChallengeForPublicClients = true;
/**
* @var CodeChallengeVerifierInterface[]
*/
private $codeChallengeVerifiers = [];
/**
* @param AuthCodeRepositoryInterface $authCodeRepository
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
* @param DateInterval $authCodeTTL
*
* @throws Exception
*/
public function __construct(
AuthCodeRepositoryInterface $authCodeRepository,
RefreshTokenRepositoryInterface $refreshTokenRepository,
DateInterval $authCodeTTL
) {
$this->setAuthCodeRepository($authCodeRepository);
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->authCodeTTL = $authCodeTTL;
$this->refreshTokenTTL = new DateInterval('P1M');
if (\in_array('sha256', \hash_algos(), true)) {
$s256Verifier = new S256Verifier();
$this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
}
$plainVerifier = new PlainVerifier();
$this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
}
/**
* Disable the requirement for a code challenge for public clients.
*/
public function disableRequireCodeChallengeForPublicClients()
{
$this->requireCodeChallengeForPublicClients = false;
}
/**
* Respond to an access token request.
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param DateInterval $accessTokenTTL
*
* @throws OAuthServerException
*
* @return ResponseTypeInterface
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
list($clientId) = $this->getClientCredentials($request);
$client = $this->getClientEntityOrFail($clientId, $request);
// Only validate the client if it is confidential
if ($client->isConfidential()) {
$this->validateClient($request);
}
$encryptedAuthCode = $this->getRequestParameter('code', $request, null);
if (!\is_string($encryptedAuthCode)) {
throw OAuthServerException::invalidRequest('code');
}
try {
$authCodePayload = \json_decode($this->decrypt($encryptedAuthCode));
$this->validateAuthorizationCode($authCodePayload, $client, $request);
$scopes = $this->scopeRepository->finalizeScopes(
$this->validateScopes($authCodePayload->scopes),
$this->getIdentifier(),
$client,
$authCodePayload->user_id
);
} catch (LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
}
$codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
// If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
if (empty($authCodePayload->code_challenge) && $codeVerifier !== null) {
throw OAuthServerException::invalidRequest(
'code_challenge',
'code_verifier received when no code_challenge is present'
);
}
if (!empty($authCodePayload->code_challenge)) {
$this->validateCodeChallenge($authCodePayload, $codeVerifier);
}
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}
// Revoke used auth code
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
return $responseType;
}
private function validateCodeChallenge($authCodePayload, $codeVerifier)
{
if ($codeVerifier === null) {
throw OAuthServerException::invalidRequest('code_verifier');
}
// Validate code_verifier according to RFC-7636
// @see: https://tools.ietf.org/html/rfc7636#section-4.1
if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
throw OAuthServerException::invalidRequest(
'code_verifier',
'Code Verifier must follow the specifications of RFC-7636.'
);
}
if (\property_exists($authCodePayload, 'code_challenge_method')) {
if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
$codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
}
} else {
throw OAuthServerException::serverError(
\sprintf(
'Unsupported code challenge method `%s`',
$authCodePayload->code_challenge_method
)
);
}
}
}
/**
* Validate the authorization code.
*
* @param stdClass $authCodePayload
* @param ClientEntityInterface $client
* @param ServerRequestInterface $request
*/
private function validateAuthorizationCode(
$authCodePayload,
ClientEntityInterface $client,
ServerRequestInterface $request
) {
if (!\property_exists($authCodePayload, 'auth_code_id')) {
throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
}
if (\time() > $authCodePayload->expire_time) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
}
if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
}
if ($authCodePayload->client_id !== $client->getIdentifier()) {
throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
}
// The redirect URI is required in this request
$redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
if ($authCodePayload->redirect_uri !== $redirectUri) {
throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
}
}
/**
* Return the grant identifier that can be used in matching up requests.
*
* @return string
*/
public function getIdentifier()
{
return 'authorization_code';
}
/**
* {@inheritdoc}
*/
public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
{
return (
\array_key_exists('response_type', $request->getQueryParams())
&& $request->getQueryParams()['response_type'] === 'code'
&& isset($request->getQueryParams()['client_id'])
);
}
/**
* {@inheritdoc}
*/
public function validateAuthorizationRequest(ServerRequestInterface $request)
{
$clientId = $this->getQueryStringParameter(
'client_id',
$request,
$this->getServerParameter('PHP_AUTH_USER', $request)
);
if ($clientId === null) {
throw OAuthServerException::invalidRequest('client_id');
}
$client = $this->getClientEntityOrFail($clientId, $request);
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
if ($redirectUri !== null) {
if (!\is_string($redirectUri)) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
$this->validateRedirectUri($redirectUri, $client, $request);
} elseif (empty($client->getRedirectUri()) ||
(\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
$defaultClientRedirectUri = \is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri ?? $defaultClientRedirectUri
);
$stateParameter = $this->getQueryStringParameter('state', $request);
$authorizationRequest = new AuthorizationRequest();
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($scopes);
$codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
if ($codeChallenge !== null) {
$codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
throw OAuthServerException::invalidRequest(
'code_challenge_method',
'Code challenge method must be one of ' . \implode(', ', \array_map(
function ($method) {
return '`' . $method . '`';
},
\array_keys($this->codeChallengeVerifiers)
))
);
}
// Validate code_challenge according to RFC-7636
// @see: https://tools.ietf.org/html/rfc7636#section-4.2
if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
throw OAuthServerException::invalidRequest(
'code_challenge',
'Code challenge must follow the specifications of RFC-7636.'
);
}
$authorizationRequest->setCodeChallenge($codeChallenge);
$authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
} elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
}
return $authorizationRequest;
}
/**
* {@inheritdoc}
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
}
$finalRedirectUri = $authorizationRequest->getRedirectUri()
?? $this->getClientRedirectUri($authorizationRequest);
// The user approved the client, redirect them back with an auth code
if ($authorizationRequest->isAuthorizationApproved() === true) {
$authCode = $this->issueAuthCode(
$this->authCodeTTL,
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier(),
$authorizationRequest->getRedirectUri(),
$authorizationRequest->getScopes()
);
$payload = [
'client_id' => $authCode->getClient()->getIdentifier(),
'redirect_uri' => $authCode->getRedirectUri(),
'auth_code_id' => $authCode->getIdentifier(),
'scopes' => $authCode->getScopes(),
'user_id' => $authCode->getUserIdentifier(),
'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
'code_challenge' => $authorizationRequest->getCodeChallenge(),
'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
];
$jsonPayload = \json_encode($payload);
if ($jsonPayload === false) {
throw new LogicException('An error was encountered when JSON encoding the authorization request response');
}
$response = new RedirectResponse();
$response->setRedirectUri(
$this->makeRedirectUri(
$finalRedirectUri,
[
'code' => $this->encrypt($jsonPayload),
'state' => $authorizationRequest->getState(),
]
)
);
return $response;
}
// The user denied the client, redirect them back with an error
throw OAuthServerException::accessDenied(
'The user denied the request',
$this->makeRedirectUri(
$finalRedirectUri,
[
'state' => $authorizationRequest->getState(),
]
)
);
}
/**
* Get the client redirect URI if not set in the request.
*
* @param AuthorizationRequest $authorizationRequest
*
* @return string
*/
private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
{
return \is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri();
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* OAuth 2.0 Client credentials grant.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestAccessTokenEvent;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Client credentials grant class.
*/
class ClientCredentialsGrant extends AbstractGrant
{
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
list($clientId) = $this->getClientCredentials($request);
$client = $this->getClientEntityOrFail($clientId, $request);
if (!$client->isConfidential()) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}
// Validate request
$this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);
// Issue and persist access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);
// Send event to emitter
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
// Inject access token into response type
$responseType->setAccessToken($accessToken);
return $responseType;
}
/**
* {@inheritdoc}
*/
public function getIdentifier()
{
return 'client_credentials';
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* OAuth 2.0 Grant type interface.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use Defuse\Crypto\Key;
use League\Event\EmitterAwareInterface;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Grant type interface.
*/
interface GrantTypeInterface extends EmitterAwareInterface
{
/**
* Set refresh token TTL.
*
* @param DateInterval $refreshTokenTTL
*/
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL);
/**
* Return the grant identifier that can be used in matching up requests.
*
* @return string
*/
public function getIdentifier();
/**
* Respond to an incoming request.
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param DateInterval $accessTokenTTL
*
* @return ResponseTypeInterface
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
);
/**
* The grant type should return true if it is able to response to an authorization request
*
* @param ServerRequestInterface $request
*
* @return bool
*/
public function canRespondToAuthorizationRequest(ServerRequestInterface $request);
/**
* If the grant can respond to an authorization request this method should be called to validate the parameters of
* the request.
*
* If the validation is successful an AuthorizationRequest object will be returned. This object can be safely
* serialized in a user's session, and can be used during user authentication and authorization.
*
* @param ServerRequestInterface $request
*
* @return AuthorizationRequest
*/
public function validateAuthorizationRequest(ServerRequestInterface $request);
/**
* Once a user has authenticated and authorized the client the grant can complete the authorization request.
* The AuthorizationRequest object's $userId property must be set to the authenticated user and the
* $authorizationApproved property must reflect their desire to authorize or deny the client.
*
* @param AuthorizationRequest $authorizationRequest
*
* @return ResponseTypeInterface
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest);
/**
* The grant type should return true if it is able to respond to this request.
*
* For example most grant types will check that the $_POST['grant_type'] property matches it's identifier property.
*
* @param ServerRequestInterface $request
*
* @return bool
*/
public function canRespondToAccessTokenRequest(ServerRequestInterface $request);
/**
* Set the client repository.
*
* @param ClientRepositoryInterface $clientRepository
*/
public function setClientRepository(ClientRepositoryInterface $clientRepository);
/**
* Set the access token repository.
*
* @param AccessTokenRepositoryInterface $accessTokenRepository
*/
public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository);
/**
* Set the scope repository.
*
* @param ScopeRepositoryInterface $scopeRepository
*/
public function setScopeRepository(ScopeRepositoryInterface $scopeRepository);
/**
* Set the default scope.
*
* @param string $scope
*/
public function setDefaultScope($scope);
/**
* Set the path to the private key.
*
* @param CryptKey $privateKey
*/
public function setPrivateKey(CryptKey $privateKey);
/**
* Set the encryption key
*
* @param string|Key|null $key
*/
public function setEncryptionKey($key = null);
}

View File

@@ -0,0 +1,232 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
class ImplicitGrant extends AbstractAuthorizeGrant
{
/**
* @var DateInterval
*/
private $accessTokenTTL;
/**
* @var string
*/
private $queryDelimiter;
/**
* @param DateInterval $accessTokenTTL
* @param string $queryDelimiter
*/
public function __construct(DateInterval $accessTokenTTL, $queryDelimiter = '#')
{
$this->accessTokenTTL = $accessTokenTTL;
$this->queryDelimiter = $queryDelimiter;
}
/**
* @param DateInterval $refreshTokenTTL
*
* @throw LogicException
*/
public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
{
throw new LogicException('The Implicit Grant does not return refresh tokens');
}
/**
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
*
* @throw LogicException
*/
public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
{
throw new LogicException('The Implicit Grant does not return refresh tokens');
}
/**
* {@inheritdoc}
*/
public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
{
return false;
}
/**
* Return the grant identifier that can be used in matching up requests.
*
* @return string
*/
public function getIdentifier()
{
return 'implicit';
}
/**
* Respond to an incoming request.
*
* @param ServerRequestInterface $request
* @param ResponseTypeInterface $responseType
* @param DateInterval $accessTokenTTL
*
* @return ResponseTypeInterface
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
throw new LogicException('This grant does not used this method');
}
/**
* {@inheritdoc}
*/
public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
{
return (
isset($request->getQueryParams()['response_type'])
&& $request->getQueryParams()['response_type'] === 'token'
&& isset($request->getQueryParams()['client_id'])
);
}
/**
* {@inheritdoc}
*/
public function validateAuthorizationRequest(ServerRequestInterface $request)
{
$clientId = $this->getQueryStringParameter(
'client_id',
$request,
$this->getServerParameter('PHP_AUTH_USER', $request)
);
if (\is_null($clientId)) {
throw OAuthServerException::invalidRequest('client_id');
}
$client = $this->getClientEntityOrFail($clientId, $request);
$redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
if ($redirectUri !== null) {
if (!\is_string($redirectUri)) {
throw OAuthServerException::invalidRequest('redirect_uri');
}
$this->validateRedirectUri($redirectUri, $client, $request);
} elseif (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1
|| empty($client->getRedirectUri())) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
} else {
$redirectUri = \is_array($client->getRedirectUri())
? $client->getRedirectUri()[0]
: $client->getRedirectUri();
}
$scopes = $this->validateScopes(
$this->getQueryStringParameter('scope', $request, $this->defaultScope),
$redirectUri
);
$stateParameter = $this->getQueryStringParameter('state', $request);
if ($stateParameter !== null && !\is_string($stateParameter)) {
throw OAuthServerException::invalidRequest('state');
}
$authorizationRequest = new AuthorizationRequest();
$authorizationRequest->setGrantTypeId($this->getIdentifier());
$authorizationRequest->setClient($client);
$authorizationRequest->setRedirectUri($redirectUri);
if ($stateParameter !== null) {
$authorizationRequest->setState($stateParameter);
}
$authorizationRequest->setScopes($scopes);
return $authorizationRequest;
}
/**
* {@inheritdoc}
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
}
$finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
? \is_array($authorizationRequest->getClient()->getRedirectUri())
? $authorizationRequest->getClient()->getRedirectUri()[0]
: $authorizationRequest->getClient()->getRedirectUri()
: $authorizationRequest->getRedirectUri();
// The user approved the client, redirect them back with an access token
if ($authorizationRequest->isAuthorizationApproved() === true) {
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes(
$authorizationRequest->getScopes(),
$this->getIdentifier(),
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier()
);
$accessToken = $this->issueAccessToken(
$this->accessTokenTTL,
$authorizationRequest->getClient(),
$authorizationRequest->getUser()->getIdentifier(),
$finalizedScopes
);
$response = new RedirectResponse();
$response->setRedirectUri(
$this->makeRedirectUri(
$finalRedirectUri,
[
'access_token' => (string) $accessToken,
'token_type' => 'Bearer',
'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - \time(),
'state' => $authorizationRequest->getState(),
],
$this->queryDelimiter
)
);
return $response;
}
// The user denied the client, redirect them back with an error
throw OAuthServerException::accessDenied(
'The user denied the request',
$this->makeRedirectUri(
$finalRedirectUri,
[
'state' => $authorizationRequest->getState(),
]
)
);
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* OAuth 2.0 Password grant.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\RequestAccessTokenEvent;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestRefreshTokenEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Password grant class.
*/
class PasswordGrant extends AbstractGrant
{
/**
* @param UserRepositoryInterface $userRepository
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
*/
public function __construct(
UserRepositoryInterface $userRepository,
RefreshTokenRepositoryInterface $refreshTokenRepository
) {
$this->setUserRepository($userRepository);
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
$user = $this->validateUser($request, $client);
// Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}
return $responseType;
}
/**
* @param ServerRequestInterface $request
* @param ClientEntityInterface $client
*
* @throws OAuthServerException
*
* @return UserEntityInterface
*/
protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
$username = $this->getRequestParameter('username', $request);
if (!\is_string($username)) {
throw OAuthServerException::invalidRequest('username');
}
$password = $this->getRequestParameter('password', $request);
if (!\is_string($password)) {
throw OAuthServerException::invalidRequest('password');
}
$user = $this->userRepository->getUserEntityByUserCredentials(
$username,
$password,
$this->getIdentifier(),
$client
);
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
return $user;
}
/**
* {@inheritdoc}
*/
public function getIdentifier()
{
return 'password';
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* OAuth 2.0 Refresh token grant.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Grant;
use DateInterval;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestAccessTokenEvent;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\RequestRefreshTokenEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Refresh token grant.
*/
class RefreshTokenGrant extends AbstractGrant
{
/**
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
*/
public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository)
{
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier());
$scopes = $this->validateScopes(
$this->getRequestParameter(
'scope',
$request,
\implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes'])
)
);
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($scopes as $scope) {
if (\in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) {
throw OAuthServerException::invalidScope($scope->getIdentifier());
}
}
// Expire old tokens
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
if ($this->revokeRefreshTokens) {
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
}
// Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
$this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
if ($this->revokeRefreshTokens) {
$refreshToken = $this->issueRefreshToken($accessToken);
if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
$responseType->setRefreshToken($refreshToken);
}
}
return $responseType;
}
/**
* @param ServerRequestInterface $request
* @param string $clientId
*
* @throws OAuthServerException
*
* @return array
*/
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId)
{
$encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request);
if (!\is_string($encryptedRefreshToken)) {
throw OAuthServerException::invalidRequest('refresh_token');
}
// Validate refresh token
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
}
$refreshTokenData = \json_decode($refreshToken, true);
if ($refreshTokenData['client_id'] !== $clientId) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
}
if ($refreshTokenData['expire_time'] < \time()) {
throw OAuthServerException::invalidRefreshToken('Token has expired');
}
if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
}
return $refreshTokenData;
}
/**
* {@inheritdoc}
*/
public function getIdentifier()
{
return 'refresh_token';
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class AuthorizationServerMiddleware
{
/**
* @var AuthorizationServer
*/
private $server;
/**
* @param AuthorizationServer $server
*/
public function __construct(AuthorizationServer $server)
{
$this->server = $server;
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
*
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
try {
$response = $this->server->respondToAccessTokenRequest($request, $response);
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart
} catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response);
// @codeCoverageIgnoreEnd
}
// Pass the request and response on to the next responder in the chain
return $next($request, $response);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Middleware;
use Exception;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class ResourceServerMiddleware
{
/**
* @var ResourceServer
*/
private $server;
/**
* @param ResourceServer $server
*/
public function __construct(ResourceServer $server)
{
$this->server = $server;
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
*
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
try {
$request = $this->server->validateAuthenticatedRequest($request);
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
// @codeCoverageIgnoreStart
} catch (Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response);
// @codeCoverageIgnoreEnd
}
// Pass the request and response on to the next responder in the chain
return $next($request, $response);
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* @author Sebastiano Degan <sebdeg87@gmail.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\RedirectUriValidators;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Uri;
class RedirectUriValidator implements RedirectUriValidatorInterface
{
/**
* @var array
*/
private $allowedRedirectUris;
/**
* New validator instance for the given uri
*
* @param string|array $allowedRedirectUris
*/
public function __construct($allowedRedirectUri)
{
if (\is_string($allowedRedirectUri)) {
$this->allowedRedirectUris = [$allowedRedirectUri];
} elseif (\is_array($allowedRedirectUri)) {
$this->allowedRedirectUris = $allowedRedirectUri;
} else {
$this->allowedRedirectUris = [];
}
}
/**
* Validates the redirect uri.
*
* @param string $redirectUri
*
* @return bool Return true if valid, false otherwise
*/
public function validateRedirectUri($redirectUri)
{
if ($this->isLoopbackUri($redirectUri)) {
return $this->matchUriExcludingPort($redirectUri);
}
return $this->matchExactUri($redirectUri);
}
/**
* According to section 7.3 of rfc8252, loopback uris are:
* - "http://127.0.0.1:{port}/{path}" for IPv4
* - "http://[::1]:{port}/{path}" for IPv6
*
* @param string $redirectUri
*
* @return bool
*/
private function isLoopbackUri($redirectUri)
{
try {
$uri = Uri::createFromString($redirectUri);
} catch (SyntaxError $e) {
return false;
}
return $uri->getScheme() === 'http'
&& (\in_array($uri->getHost(), ['127.0.0.1', '[::1]'], true));
}
/**
* Find an exact match among allowed uris
*
* @param string $redirectUri
*
* @return bool Return true if an exact match is found, false otherwise
*/
private function matchExactUri($redirectUri)
{
return \in_array($redirectUri, $this->allowedRedirectUris, true);
}
/**
* Find a match among allowed uris, allowing for different port numbers
*
* @param string $redirectUri
*
* @return bool Return true if a match is found, false otherwise
*/
private function matchUriExcludingPort($redirectUri)
{
$parsedUrl = $this->parseUrlAndRemovePort($redirectUri);
foreach ($this->allowedRedirectUris as $allowedRedirectUri) {
if ($parsedUrl === $this->parseUrlAndRemovePort($allowedRedirectUri)) {
return true;
}
}
return false;
}
/**
* Parse an url like \parse_url, excluding the port
*
* @param string $url
*
* @return array
*/
private function parseUrlAndRemovePort($url)
{
$uri = Uri::createFromString($url);
return (string) $uri->withPort(null);
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @author Sebastiano Degan <sebdeg87@gmail.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\RedirectUriValidators;
interface RedirectUriValidatorInterface
{
/**
* Validates the redirect uri.
*
* @param string $redirectUri
*
* @return bool Return true if valid, false otherwise
*/
public function validateRedirectUri($redirectUri);
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Access token interface.
*/
interface AccessTokenRepositoryInterface extends RepositoryInterface
{
/**
* Create a new access token
*
* @param ClientEntityInterface $clientEntity
* @param ScopeEntityInterface[] $scopes
* @param mixed $userIdentifier
*
* @return AccessTokenEntityInterface
*/
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null);
/**
* Persists a new access token to permanent storage.
*
* @param AccessTokenEntityInterface $accessTokenEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity);
/**
* Revoke an access token.
*
* @param string $tokenId
*/
public function revokeAccessToken($tokenId);
/**
* Check if the access token has been revoked.
*
* @param string $tokenId
*
* @return bool Return true if this token has been revoked
*/
public function isAccessTokenRevoked($tokenId);
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Auth code storage interface.
*/
interface AuthCodeRepositoryInterface extends RepositoryInterface
{
/**
* Creates a new AuthCode
*
* @return AuthCodeEntityInterface
*/
public function getNewAuthCode();
/**
* Persists a new auth code to permanent storage.
*
* @param AuthCodeEntityInterface $authCodeEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity);
/**
* Revoke an auth code.
*
* @param string $codeId
*/
public function revokeAuthCode($codeId);
/**
* Check if the auth code has been revoked.
*
* @param string $codeId
*
* @return bool Return true if this code has been revoked
*/
public function isAuthCodeRevoked($codeId);
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\ClientEntityInterface;
/**
* Client storage interface.
*/
interface ClientRepositoryInterface extends RepositoryInterface
{
/**
* Get a client.
*
* @param string $clientIdentifier The client's identifier
*
* @return ClientEntityInterface|null
*/
public function getClientEntity($clientIdentifier);
/**
* Validate a client's secret.
*
* @param string $clientIdentifier The client's identifier
* @param null|string $clientSecret The client's secret (if sent)
* @param null|string $grantType The type of grant the client is using (if sent)
*
* @return bool
*/
public function validateClient($clientIdentifier, $clientSecret, $grantType);
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
/**
* Refresh token interface.
*/
interface RefreshTokenRepositoryInterface extends RepositoryInterface
{
/**
* Creates a new refresh token
*
* @return RefreshTokenEntityInterface|null
*/
public function getNewRefreshToken();
/**
* Create a new refresh token_name.
*
* @param RefreshTokenEntityInterface $refreshTokenEntity
*
* @throws UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity);
/**
* Revoke the refresh token.
*
* @param string $tokenId
*/
public function revokeRefreshToken($tokenId);
/**
* Check if the refresh token has been revoked.
*
* @param string $tokenId
*
* @return bool Return true if this token has been revoked
*/
public function isRefreshTokenRevoked($tokenId);
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
/**
* Repository interface.
*/
interface RepositoryInterface
{
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
/**
* Scope interface.
*/
interface ScopeRepositoryInterface extends RepositoryInterface
{
/**
* Return information about a scope.
*
* @param string $identifier The scope identifier
*
* @return ScopeEntityInterface|null
*/
public function getScopeEntityByIdentifier($identifier);
/**
* Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally
* append additional scopes or remove requested scopes.
*
* @param ScopeEntityInterface[] $scopes
* @param string $grantType
* @param ClientEntityInterface $clientEntity
* @param null|string $userIdentifier
*
* @return ScopeEntityInterface[]
*/
public function finalizeScopes(
array $scopes,
$grantType,
ClientEntityInterface $clientEntity,
$userIdentifier = null
);
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\Repositories;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
interface UserRepositoryInterface extends RepositoryInterface
{
/**
* Get a user entity.
*
* @param string $username
* @param string $password
* @param string $grantType The grant type used
* @param ClientEntityInterface $clientEntity
*
* @return UserEntityInterface|null
*/
public function getUserEntityByUserCredentials(
$username,
$password,
$grantType,
ClientEntityInterface $clientEntity
);
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use Psr\Http\Message\ServerRequestInterface;
class RequestAccessTokenEvent extends RequestEvent
{
/**
* @var AccessTokenEntityInterface
*/
private $accessToken;
/**
* @param string $name
* @param ServerRequestInterface $request
*/
public function __construct($name, ServerRequestInterface $request, AccessTokenEntityInterface $accessToken)
{
parent::__construct($name, $request);
$this->accessToken = $accessToken;
}
/**
* @return AccessTokenEntityInterface
*
* @codeCoverageIgnore
*/
public function getAccessToken()
{
return $this->accessToken;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use League\Event\Event;
use Psr\Http\Message\ServerRequestInterface;
class RequestEvent extends Event
{
const CLIENT_AUTHENTICATION_FAILED = 'client.authentication.failed';
const USER_AUTHENTICATION_FAILED = 'user.authentication.failed';
const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed';
const REFRESH_TOKEN_ISSUED = 'refresh_token.issued';
const ACCESS_TOKEN_ISSUED = 'access_token.issued';
/**
* @var ServerRequestInterface
*/
private $request;
/**
* RequestEvent constructor.
*
* @param string $name
* @param ServerRequestInterface $request
*/
public function __construct($name, ServerRequestInterface $request)
{
parent::__construct($name);
$this->request = $request;
}
/**
* @return ServerRequestInterface
*
* @codeCoverageIgnore
*/
public function getRequest()
{
return $this->request;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use Psr\Http\Message\ServerRequestInterface;
class RequestRefreshTokenEvent extends RequestEvent
{
/**
* @var RefreshTokenEntityInterface
*/
private $refreshToken;
/**
* @param string $name
* @param ServerRequestInterface $request
*/
public function __construct($name, ServerRequestInterface $request, RefreshTokenEntityInterface $refreshToken)
{
parent::__construct($name, $request);
$this->refreshToken = $refreshToken;
}
/**
* @return RefreshTokenEntityInterface
*
* @codeCoverageIgnore
*/
public function getRefreshToken()
{
return $this->refreshToken;
}
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\RequestTypes;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
class AuthorizationRequest
{
/**
* The grant type identifier
*
* @var string
*/
protected $grantTypeId;
/**
* The client identifier
*
* @var ClientEntityInterface
*/
protected $client;
/**
* The user identifier
*
* @var UserEntityInterface
*/
protected $user;
/**
* An array of scope identifiers
*
* @var ScopeEntityInterface[]
*/
protected $scopes = [];
/**
* Has the user authorized the authorization request
*
* @var bool
*/
protected $authorizationApproved = false;
/**
* The redirect URI used in the request
*
* @var string|null
*/
protected $redirectUri;
/**
* The state parameter on the authorization request
*
* @var string|null
*/
protected $state;
/**
* The code challenge (if provided)
*
* @var string
*/
protected $codeChallenge;
/**
* The code challenge method (if provided)
*
* @var string
*/
protected $codeChallengeMethod;
/**
* @return string
*/
public function getGrantTypeId()
{
return $this->grantTypeId;
}
/**
* @param string $grantTypeId
*/
public function setGrantTypeId($grantTypeId)
{
$this->grantTypeId = $grantTypeId;
}
/**
* @return ClientEntityInterface
*/
public function getClient()
{
return $this->client;
}
/**
* @param ClientEntityInterface $client
*/
public function setClient(ClientEntityInterface $client)
{
$this->client = $client;
}
/**
* @return UserEntityInterface|null
*/
public function getUser()
{
return $this->user;
}
/**
* @param UserEntityInterface $user
*/
public function setUser(UserEntityInterface $user)
{
$this->user = $user;
}
/**
* @return ScopeEntityInterface[]
*/
public function getScopes()
{
return $this->scopes;
}
/**
* @param ScopeEntityInterface[] $scopes
*/
public function setScopes(array $scopes)
{
$this->scopes = $scopes;
}
/**
* @return bool
*/
public function isAuthorizationApproved()
{
return $this->authorizationApproved;
}
/**
* @param bool $authorizationApproved
*/
public function setAuthorizationApproved($authorizationApproved)
{
$this->authorizationApproved = $authorizationApproved;
}
/**
* @return string|null
*/
public function getRedirectUri()
{
return $this->redirectUri;
}
/**
* @param string|null $redirectUri
*/
public function setRedirectUri($redirectUri)
{
$this->redirectUri = $redirectUri;
}
/**
* @return string|null
*/
public function getState()
{
return $this->state;
}
/**
* @param string $state
*/
public function setState($state)
{
$this->state = $state;
}
/**
* @return string
*/
public function getCodeChallenge()
{
return $this->codeChallenge;
}
/**
* @param string $codeChallenge
*/
public function setCodeChallenge($codeChallenge)
{
$this->codeChallenge = $codeChallenge;
}
/**
* @return string
*/
public function getCodeChallengeMethod()
{
return $this->codeChallengeMethod;
}
/**
* @param string $codeChallengeMethod
*/
public function setCodeChallengeMethod($codeChallengeMethod)
{
$this->codeChallengeMethod = $codeChallengeMethod;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server;
use League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;
class ResourceServer
{
/**
* @var AccessTokenRepositoryInterface
*/
private $accessTokenRepository;
/**
* @var CryptKey
*/
private $publicKey;
/**
* @var null|AuthorizationValidatorInterface
*/
private $authorizationValidator;
/**
* New server instance.
*
* @param AccessTokenRepositoryInterface $accessTokenRepository
* @param CryptKey|string $publicKey
* @param null|AuthorizationValidatorInterface $authorizationValidator
*/
public function __construct(
AccessTokenRepositoryInterface $accessTokenRepository,
$publicKey,
?AuthorizationValidatorInterface $authorizationValidator = null
) {
$this->accessTokenRepository = $accessTokenRepository;
if ($publicKey instanceof CryptKey === false) {
$publicKey = new CryptKey($publicKey);
}
$this->publicKey = $publicKey;
$this->authorizationValidator = $authorizationValidator;
}
/**
* @return AuthorizationValidatorInterface
*/
protected function getAuthorizationValidator()
{
if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) {
$this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
}
if ($this->authorizationValidator instanceof BearerTokenValidator === true) {
$this->authorizationValidator->setPublicKey($this->publicKey);
}
return $this->authorizationValidator;
}
/**
* Determine the access token validity.
*
* @param ServerRequestInterface $request
*
* @throws OAuthServerException
*
* @return ServerRequestInterface
*/
public function validateAuthenticatedRequest(ServerRequestInterface $request)
{
return $this->getAuthorizationValidator()->validateAuthorization($request);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* OAuth 2.0 Abstract Response Type.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
abstract class AbstractResponseType implements ResponseTypeInterface
{
use CryptTrait;
/**
* @var AccessTokenEntityInterface
*/
protected $accessToken;
/**
* @var RefreshTokenEntityInterface
*/
protected $refreshToken;
/**
* @var CryptKey
*/
protected $privateKey;
/**
* {@inheritdoc}
*/
public function setAccessToken(AccessTokenEntityInterface $accessToken)
{
$this->accessToken = $accessToken;
}
/**
* {@inheritdoc}
*/
public function setRefreshToken(RefreshTokenEntityInterface $refreshToken)
{
$this->refreshToken = $refreshToken;
}
/**
* Set the private key
*
* @param CryptKey $key
*/
public function setPrivateKey(CryptKey $key)
{
$this->privateKey = $key;
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* OAuth 2.0 Bearer Token Response.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use LogicException;
use Psr\Http\Message\ResponseInterface;
class BearerTokenResponse extends AbstractResponseType
{
/**
* {@inheritdoc}
*/
public function generateHttpResponse(ResponseInterface $response)
{
$expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp();
$responseParams = [
'token_type' => 'Bearer',
'expires_in' => $expireDateTime - \time(),
'access_token' => (string) $this->accessToken,
];
if ($this->refreshToken instanceof RefreshTokenEntityInterface) {
$refreshTokenPayload = \json_encode([
'client_id' => $this->accessToken->getClient()->getIdentifier(),
'refresh_token_id' => $this->refreshToken->getIdentifier(),
'access_token_id' => $this->accessToken->getIdentifier(),
'scopes' => $this->accessToken->getScopes(),
'user_id' => $this->accessToken->getUserIdentifier(),
'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(),
]);
if ($refreshTokenPayload === false) {
throw new LogicException('Error encountered JSON encoding the refresh token payload');
}
$responseParams['refresh_token'] = $this->encrypt($refreshTokenPayload);
}
$responseParams = \json_encode(\array_merge($this->getExtraParams($this->accessToken), $responseParams));
if ($responseParams === false) {
throw new LogicException('Error encountered JSON encoding response parameters');
}
$response = $response
->withStatus(200)
->withHeader('pragma', 'no-cache')
->withHeader('cache-control', 'no-store')
->withHeader('content-type', 'application/json; charset=UTF-8');
$response->getBody()->write($responseParams);
return $response;
}
/**
* Add custom fields to your Bearer Token response here, then override
* AuthorizationServer::getResponseType() to pull in your version of
* this class rather than the default.
*
* @param AccessTokenEntityInterface $accessToken
*
* @return array
*/
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
{
return [];
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* OAuth 2.0 Redirect Response.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\ResponseTypes;
use Psr\Http\Message\ResponseInterface;
class RedirectResponse extends AbstractResponseType
{
/**
* @var string
*/
private $redirectUri;
/**
* @param string $redirectUri
*/
public function setRedirectUri($redirectUri)
{
$this->redirectUri = $redirectUri;
}
/**
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response)
{
return $response->withStatus(302)->withHeader('Location', $this->redirectUri);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* OAuth 2.0 Response Type Interface.
*
* @author Alex Bilbie <hello@alexbilbie.com>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/
namespace League\OAuth2\Server\ResponseTypes;
use Defuse\Crypto\Key;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use Psr\Http\Message\ResponseInterface;
interface ResponseTypeInterface
{
/**
* @param AccessTokenEntityInterface $accessToken
*/
public function setAccessToken(AccessTokenEntityInterface $accessToken);
/**
* @param RefreshTokenEntityInterface $refreshToken
*/
public function setRefreshToken(RefreshTokenEntityInterface $refreshToken);
/**
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response);
/**
* Set the encryption key
*
* @param string|Key|null $key
*/
public function setEncryptionKey($key = null);
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
interface AuthorityInterface extends UriComponentInterface
{
/**
* Returns the host component of the authority.
*/
public function getHost(): ?string;
/**
* Returns the port component of the authority.
*/
public function getPort(): ?int;
/**
* Returns the user information component of the authority.
*/
public function getUserInfo(): ?string;
/**
* Returns an associative array containing all the Authority components.
*
* The returned a hashmap similar to PHP's parse_url return value
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return array{user: ?string, pass : ?string, host: ?string, port: ?int}
*/
public function components(): array;
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* A null value provided for the host is equivalent to removing the host
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
* @throws MissingFeature for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(Stringable|string|null $host): self;
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPort(?int $port): self;
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; a null value for the user is equivalent to removing user
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self;
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use SplFileObject;
use Stringable;
interface DataPathInterface extends PathInterface
{
/**
* Retrieve the data mime type associated to the URI.
*
* If no mimetype is present, this method MUST return the default mimetype 'text/plain'.
*
* @see http://tools.ietf.org/html/rfc2397#section-2
*/
public function getMimeType(): string;
/**
* Retrieve the parameters associated with the Mime Type of the URI.
*
* If no parameters is present, this method MUST return the default parameter 'charset=US-ASCII'.
*
* @see http://tools.ietf.org/html/rfc2397#section-2
*/
public function getParameters(): string;
/**
* Retrieve the mediatype associated with the URI.
*
* If no mediatype is present, this method MUST return the default parameter 'text/plain;charset=US-ASCII'.
*
* @see http://tools.ietf.org/html/rfc2397#section-3
*
* @return string The URI scheme.
*/
public function getMediaType(): string;
/**
* Retrieves the data string.
*
* Retrieves the data part of the path. If no data part is provided return
* an empty string
*/
public function getData(): string;
/**
* Tells whether the data is binary safe encoded.
*/
public function isBinaryData(): bool;
/**
* Save the data to a specific file.
*/
public function save(string $path, string $mode = 'w'): SplFileObject;
/**
* Returns an instance where the data part is base64 encoded.
*
* This method MUST retain the state of the current instance, and return
* an instance where the data part is base64 encoded
*/
public function toBinary(): self;
/**
* Returns an instance where the data part is url encoded following RFC3986 rules.
*
* This method MUST retain the state of the current instance, and return
* an instance where the data part is url encoded
*/
public function toAscii(): self;
/**
* Return an instance with the specified mediatype parameters.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified mediatype parameters.
*
* Users must provide encoded characters.
*
* An empty parameters value is equivalent to removing the parameter.
*/
public function withParameters(Stringable|string $parameters): self;
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<string>
*/
interface DomainHostInterface extends Countable, HostInterface, IteratorAggregate
{
/**
* Returns the labels total number.
*/
public function count(): int;
/**
* Iterate over the Domain labels.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Retrieves a single host label.
*
* If the label offset has not been set, returns the null value.
*/
public function get(int $offset): ?string;
/**
* Returns the associated key for a specific label or all the keys.
*
* @return int[]
*/
public function keys(?string $label = null): array;
/**
* Tells whether the domain is absolute.
*/
public function isAbsolute(): bool;
/**
* Prepends a label to the host.
*/
public function prepend(Stringable|string $label): self;
/**
* Appends a label to the host.
*/
public function append(Stringable|string $label): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Domain.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Returns an instance with its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withRootLabel(): self;
/**
* Returns an instance without its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withoutRootLabel(): self;
/**
* Returns an instance with the modified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new label
*
* If $key is non-negative, the added label will be the label at $key position from the start.
* If $key is negative, the added label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withLabel(int $key, Stringable|string $label): self;
/**
* Returns an instance without the specified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed label will be the label at $key position from the start.
* If $key is negative, the removed label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutLabel(int ...$keys): self;
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface FragmentInterface extends UriComponentInterface
{
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string;
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface HostInterface extends UriComponentInterface
{
/**
* Returns the ascii representation.
*/
public function toAscii(): ?string;
/**
* Returns the unicode representation.
*/
public function toUnicode(): ?string;
/**
* Returns the IP version.
*
* If the host is a not an IP this method will return null
*/
public function getIpVersion(): ?string;
/**
* Returns the IP component If the Host is an IP address.
*
* If the host is a not an IP this method will return null
*/
public function getIp(): ?string;
/**
* Tells whether the host is a domain name.
*/
public function isDomain(): bool;
/**
* Tells whether the host is an IP Address.
*/
public function isIp(): bool;
/**
* Tells whether the host is a registered name.
*/
public function isRegisteredName(): bool;
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface IpHostInterface extends HostInterface
{
/**
* Tells whether the host is an IPv4 address.
*/
public function isIpv4(): bool;
/**
* Tells whether the host is an IPv6 address.
*/
public function isIpv6(): bool;
/**
* Tells whether the host is an IPv6 address.
*/
public function isIpFuture(): bool;
/**
* Tells whether the host has a ZoneIdentifier.
*
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
public function hasZoneIdentifier(): bool;
/**
* Returns a host without its zone identifier according to RFC6874.
*
* This method MUST retain the state of the current instance, and return
* an instance without the host zone identifier according to RFC6874
*
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
public function withoutZoneIdentifier(): self;
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\SyntaxError;
interface PathInterface extends UriComponentInterface
{
/**
* Returns the decoded path.
*/
public function decoded(): string;
/**
* Tells whether the path is absolute or relative.
*/
public function isAbsolute(): bool;
/**
* Tells whether the path has a trailing slash.
*/
public function hasTrailingSlash(): bool;
/**
* Returns an instance without dot segments.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* the dot segment.
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutDotSegments(): self;
/**
* Returns an instance with a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withLeadingSlash(): self;
/**
* Returns an instance without a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutLeadingSlash(): self;
/**
* Returns an instance with a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withTrailingSlash(): self;
/**
* Returns an instance without a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutTrailingSlash(): self;
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface PortInterface extends UriComponentInterface
{
/**
* Returns the integer representation of the Port.
*/
public function toInt(): ?int;
}

View File

@@ -0,0 +1,253 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use Stringable;
/**
* @extends IteratorAggregate<array{0:string, 1:string|null}>
*
* @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys.
* @method self withoutPairByValue(Stringable|string|int|bool|null ...$values) Returns an instance without pairs with the specified values.
* @method self withoutPairByKeyValue(string $key, Stringable|string|int|bool|null $value) Returns an instance without pairs with the specified key/value pair
* @method bool hasPair(string $key, ?string $value) Tells whether the pair exists in the query.
* @method ?string toFormData() Returns the string representation using the applicat/www-form-urlencoded rules
* @method ?string toRFC3986() Returns the string representation using RFC3986 rules
*/
interface QueryInterface extends Countable, IteratorAggregate, UriComponentInterface
{
/**
* Returns the query separator.
*
* @return non-empty-string
*/
public function getSeparator(): string;
/**
* Returns the number of key/value pairs present in the object.
*/
public function count(): int;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The pair is represented as an array where the first value is the pair key
* and the second value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return Iterator<int, array{0:string, 1:string|null}>
*/
public function getIterator(): Iterator;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The return type is as an Iterator where its offset is the pair key and its value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return iterable<string, string|null>
*/
public function pairs(): iterable;
/**
* Tells whether a list of pair with a specific key exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function has(string ...$keys): bool;
/**
* Returns the first value associated to the given pair name.
*
* If no value is found null is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-get
*/
public function get(string $key): ?string;
/**
* Returns all the values associated to the given pair name as an array or all
* the instance pairs.
*
* If no value is found an empty array is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-getall
*
* @return array<int, string|null>
*/
public function getAll(string $key): array;
/**
* Returns the store PHP variables as elements of an array.
*
* The result is similar as PHP parse_str when used with its
* second argument with the difference that variable names are
* not mangled.
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return array the collection of stored PHP variables or the empty array if no input is given,
*/
public function parameters(): array;
/**
* Returns the value attached to the specific key.
*
* The result is similar to PHP parse_str with the difference that variable
* names are not mangled.
*
* If a key is submitted it will return the value attached to it or null
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return mixed the collection of stored PHP variables or the empty array if no input is given,
* the single value of a stored PHP variable or null if the variable is not present in the collection
*/
public function parameter(string $name): mixed;
/**
* Tells whether a list of variable with specific names exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function hasParameter(string ...$names): bool;
/**
* Returns the RFC1738 encoded query.
*/
public function toRFC1738(): ?string;
/**
* Returns an instance with a different separator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component with a different separator
*/
public function withSeparator(string $separator): self;
/**
* Returns an instance with the new pairs set to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see ::withPair
*/
public function merge(Stringable|string $query): self;
/**
* Returns an instance with the new pairs appended to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will be added to it.
*/
public function append(Stringable|string $query): self;
/**
* Returns a new instance with a specified key/value pair appended as a new pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*/
public function appendTo(string $key, Stringable|string|int|bool|null $value): self;
/**
* Sorts the query string by offset, maintaining offset to data correlations.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-sort
*/
public function sort(): self;
/**
* Returns an instance without duplicate key/value pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* duplicate pairs whose key/value are the same.
*/
public function withoutDuplicates(): self;
/**
* Returns an instance without empty key/value where the value is the null value.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* empty pairs.
*
* A pair is considered empty if its value is equal to the null value
*/
public function withoutEmptyPairs(): self;
/**
* Returns an instance where numeric indices associated to PHP's array like key are removed.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized so that numeric indexes
* are removed from the pair key value.
*
* i.e.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar
*/
public function withoutNumericIndices(): self;
/**
* Returns an instance with a new key/value pair added to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will replace the existing value.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-set
*/
public function withPair(string $key, Stringable|string|int|float|bool|null $value): self;
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.3.0
* @codeCoverageIgnore
* @see QueryInterface::withoutPairByKey()
*
* Returns an instance without the specified keys.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*/
#[Deprecated(message:'use League\Uri\Contracts\QueryInterface::withoutPairByKey() instead', since:'league/uri-interfaces:7.3.0')]
public function withoutPair(string ...$keys): self;
/**
* Returns an instance without the specified params.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component without PHP's value.
* PHP's mangled is not taken into account.
*/
public function withoutParameters(string ...$names): self;
}

View File

@@ -0,0 +1,149 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<string>
*/
interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInterface
{
/**
* Returns the total number of segments in the path.
*/
public function count(): int;
/**
* Iterate over the path segment.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Returns parent directory's path.
*/
public function getDirname(): string;
/**
* Returns the path basename.
*/
public function getBasename(): string;
/**
* Returns the basename extension.
*/
public function getExtension(): string;
/**
* Retrieves a single path segment.
*
* If the segment offset has not been set, returns null.
*/
public function get(int $offset): ?string;
/**
* Returns the associated key for a specific segment.
*
* If a value is specified only the keys associated with
* the given value will be returned
*
* @return array<int>
*/
public function keys(Stringable|string|null $segment = null): array;
/**
* Appends a segment to the path.
*/
public function append(Stringable|string $segment): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Path.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Prepends a segment to the path.
*/
public function prepend(Stringable|string $segment): self;
/**
* Returns an instance with the modified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new segment
*
* If $key is non-negative, the added segment will be the segment at $key position from the start.
* If $key is negative, the added segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withSegment(int $key, Stringable|string $segment): self;
/**
* Returns an instance without the specified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed segment will be the segment at $key position from the start.
* If $key is negative, the removed segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutSegment(int ...$keys): self;
/**
* Returns an instance without duplicate delimiters.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* multiple consecutive empty segment
*/
public function withoutEmptySegments(): self;
/**
* Returns an instance with the specified parent directory's path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withDirname(Stringable|string $path): self;
/**
* Returns an instance with the specified basename.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withBasename(Stringable|string $basename): self;
/**
* Returns an instance with the specified basename extension.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withExtension(Stringable|string $extension): self;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
interface UriAccess
{
public function getUri(): UriInterface|Psr7UriInterface;
/**
* Returns the RFC3986 string representation of the complete URI.
*/
public function getUriString(): string;
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use Stringable;
interface UriComponentInterface extends JsonSerializable, Stringable
{
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined null is returned
*/
public function value(): ?string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function toString(): string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function __toString(): string;
/**
* Returns the instance json representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986 or RFC 1738.
*
* If the instance is not defined null is returned
*/
public function jsonSerialize(): ?string;
/**
* Returns the instance string representation with its optional URI delimiters.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode any
* characters. To determine what characters to encode, please refer to RFC 3986,
* Sections 2 and 3.
*
* If the instance is not defined an empty string is returned
*/
public function getUriComponent(): string;
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Throwable;
interface UriException extends Throwable
{
}

View File

@@ -0,0 +1,314 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriString;
use Stringable;
/**
* @phpstan-import-type ComponentMap from UriString
*
* @method string|null getUsername() returns the user component of the URI.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
* @method array toComponents() returns an associative array containing all the URI components.
*/
interface UriInterface extends JsonSerializable, Stringable
{
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function __toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @see ::__toString
*/
public function jsonSerialize(): string;
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and MUST NOT be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
*/
public function getScheme(): ?string;
/**
* Retrieve the authority component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If the port component is not set or is the standard port for the current
* scheme, it SHOULD NOT be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
*/
public function getAuthority(): ?string;
/**
* Retrieve the user information component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and MUST
* NOT be added.
*/
public function getUserInfo(): ?string;
/**
* Retrieve the host component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function getHost(): ?string;
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*/
public function getPort(): ?int;
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
*/
public function getPath(): string;
/**
* Retrieve the query string of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
*/
public function getQuery(): ?string;
/**
* Retrieve the fragment component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
*/
public function getFragment(): ?string;
/**
* Returns an associative array containing all the URI components.
*
* The returned array is similar to PHP's parse_url return value with the following
* differences:
*
* <ul>
* <li>All components are present in the returned array</li>
* <li>Empty and undefined component are treated differently. And empty component is
* set to the empty string while an undefined component is set to the `null` value.</li>
* </ul>
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return ComponentMap
*/
public function getComponents(): array;
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* A null value provided for the scheme is equivalent to removing the scheme
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withScheme(Stringable|string|null $scheme): self;
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; a null value for the user is equivalent to removing user
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self;
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* A null value provided for the host is equivalent to removing the host
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
* @throws MissingFeature for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(Stringable|string|null $host): self;
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPort(?int $port): self;
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPath(Stringable|string $path): self;
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* A null value provided for the query is equivalent to removing the query
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withQuery(Stringable|string|null $query): self;
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* A null value provided for the fragment is equivalent to removing the fragment
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withFragment(Stringable|string|null $fragment): self;
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Stringable;
interface UserInfoInterface extends UriComponentInterface
{
/**
* Returns the user component part.
*/
public function getUser(): ?string;
/**
* Returns the pass component part.
*/
public function getPass(): ?string;
/**
* Returns an associative array containing all the User Info components.
*
* The returned a hashmap similar to PHP's parse_url return value
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return array{user: ?string, pass : ?string}
*/
public function components(): array;
/**
* Returns an instance with the specified user and/or pass.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified new username
* otherwise it returns the same instance unchanged.
*
* A variable equal to null is equivalent to removing the complete user information.
*/
public function withUser(Stringable|string|null $username): self;
/**
* Returns an instance with the specified user and/or pass.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified password if the user is specified
* otherwise it returns the same instance unchanged.
*
* An empty user is equivalent to removing the user information.
*/
public function withPass(Stringable|string|null $password): self;
}

176
vendor/league/uri-interfaces/Encoder.php vendored Normal file
View File

@@ -0,0 +1,176 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Closure;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use SensitiveParameter;
use Stringable;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function strtoupper;
final class Encoder
{
private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/';
private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},';
private const REGEXP_CHARS_PREVENTS_DECODING = ',%
2[A-F|1-2|4-9]|
3[0-9|B|D]|
4[1-9|A-F]|
5[0-9|A|F]|
6[1-9|A-F]|
7[0-9|E]
,ix';
private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~';
private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})';
/**
* Encode User.
*
* All generic delimiters MUST be encoded
*/
public static function encodeUser(Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Encode Password.
*
* Generic delimiters ":" MUST NOT be encoded
*/
public static function encodePassword(#[SensitiveParameter] Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Encode Path.
*
* Generic delimiters ":", "@", and "/" MUST NOT be encoded
*/
public static function encodePath(Stringable|string|null $component): string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
return (string) self::encode($component, $pattern);
}
/**
* Encode Query or Fragment.
*
* Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded
*/
public static function encodeQueryOrFragment(Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
public static function encodeQueryKeyValue(mixed $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/';
$encodeMatches = static fn (array $matches): string => match (1) {
preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
default => $matches[0],
};
$component = self::filterComponent($component);
return match (true) {
!is_scalar($component) => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))),
1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => rawurlencode($component),
1 === preg_match($pattern, $component) => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
default => $component,
};
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*/
public static function decodePartial(Stringable|string|int|null $component): ?string
{
$decodeMatches = static fn (array $matches): string => match (1) {
preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]),
default => rawurldecode($matches[0]),
};
return self::decode($component, $decodeMatches);
}
/**
* Decodes all the URI component characters.
*/
public static function decodeAll(Stringable|string|int|null $component): ?string
{
$decodeMatches = static fn (array $matches): string => rawurldecode($matches[0]);
return self::decode($component, $decodeMatches);
}
private static function filterComponent(mixed $component): ?string
{
return match (true) {
true === $component => '1',
false === $component => '0',
$component instanceof UriComponentInterface => $component->value(),
$component instanceof Stringable,
is_scalar($component) => (string) $component,
null === $component => null,
default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))),
};
}
private static function encode(Stringable|string|int|bool|null $component, string $pattern): ?string
{
$component = self::filterComponent($component);
$encodeMatches = static fn (array $matches): string => match (1) {
preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
default => $matches[0],
};
return match (true) {
null === $component,
'' === $component => $component,
default => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
};
}
/**
* Decodes all the URI component characters.
*/
private static function decode(Stringable|string|int|null $component, Closure $decodeMatches): ?string
{
$component = self::filterComponent($component);
return match (true) {
null === $component => null,
1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => throw new SyntaxError('Invalid component string: '.$component.'.'),
1 === preg_match(self::REGEXP_CHARS_ENCODED, $component) => preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decodeMatches(...), $component),
default => $component,
};
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use League\Uri\Idna\Error;
use League\Uri\Idna\Result;
use Stringable;
final class ConversionFailed extends SyntaxError
{
private function __construct(
string $message,
private readonly string $host,
private readonly Result $result
) {
parent::__construct($message);
}
public static function dueToIdnError(Stringable|string $host, Result $result): self
{
$reasons = array_map(fn (Error $error): string => $error->description(), $result->errors());
return new self('Host `'.$host.'` is invalid: '.implode('; ', $reasons).'.', (string) $host, $result);
}
public function getHost(): string
{
return $this->host;
}
public function getResult(): Result
{
return $this->result;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use League\Uri\Contracts\UriException;
use RuntimeException;
class MissingFeature extends RuntimeException implements UriException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
class OffsetOutOfBounds extends SyntaxError
{
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use InvalidArgumentException;
use League\Uri\Contracts\UriException;
class SyntaxError extends InvalidArgumentException implements UriException
{
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use finfo;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;
use const PHP_INT_SIZE;
/**
* Allow detecting features needed to make the packages work.
*/
final class FeatureDetection
{
public static function supportsFileDetection(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? class_exists(finfo::class);
if (!$isSupported) {
throw new MissingFeature('Support for file type detection requires the `fileinfo` extension.');
}
}
public static function supportsIdn(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
if (!$isSupported) {
throw new MissingFeature('Support for IDN host requires the `intl` extension for best performance or run "composer require symfony/polyfill-intl-idn" to install a polyfill.');
}
}
public static function supportsIPv4Conversion(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (extension_loaded('gmp') || extension_loaded('bcmath') || (4 < PHP_INT_SIZE));
if (!$isSupported) {
throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implmentation.');
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use function bcadd;
use function bccomp;
use function bcdiv;
use function bcmod;
use function bcmul;
use function bcpow;
use function bcsub;
use function str_split;
final class BCMathCalculator implements Calculator
{
private const SCALE = 0;
private const CONVERSION_TABLE = [
'0' => '0', '1' => '1', '2' => '2', '3' => '3',
'4' => '4', '5' => '5', '6' => '6', '7' => '7',
'8' => '8', '9' => '9', 'a' => '10', 'b' => '11',
'c' => '12', 'd' => '13', 'e' => '14', 'f' => '15',
];
public function baseConvert(mixed $value, int $base): string
{
$value = (string) $value;
if (10 === $base) {
return $value;
}
$base = (string) $base;
$decimal = '0';
foreach (str_split($value) as $char) {
$decimal = bcadd($this->multiply($decimal, $base), self::CONVERSION_TABLE[$char], self::SCALE);
}
return $decimal;
}
public function pow(mixed $value, int $exponent): string
{
return bcpow((string) $value, (string) $exponent, self::SCALE);
}
public function compare(mixed $value1, $value2): int
{
return bccomp((string) $value1, (string) $value2, self::SCALE);
}
public function multiply(mixed $value1, $value2): string
{
return bcmul((string) $value1, (string) $value2, self::SCALE);
}
public function div(mixed $value, mixed $base): string
{
return bcdiv((string) $value, (string) $base, self::SCALE);
}
public function mod(mixed $value, mixed $base): string
{
return bcmod((string) $value, (string) $base, self::SCALE);
}
public function add(mixed $value1, mixed $value2): string
{
return bcadd((string) $value1, (string) $value2, self::SCALE);
}
public function sub(mixed $value1, mixed $value2): string
{
return bcsub((string) $value1, (string) $value2, self::SCALE);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
interface Calculator
{
/**
* Add numbers.
*
* @param mixed $value1 a number that will be added to $value2
* @param mixed $value2 a number that will be added to $value1
*
* @return mixed the addition result
*/
public function add(mixed $value1, mixed $value2);
/**
* Subtract one number from another.
*
* @param mixed $value1 a number that will be subtracted of $value2
* @param mixed $value2 a number that will be subtracted to $value1
*
* @return mixed the subtraction result
*/
public function sub(mixed $value1, mixed $value2);
/**
* Multiply numbers.
*
* @param mixed $value1 a number that will be multiplied by $value2
* @param mixed $value2 a number that will be multiplied by $value1
*
* @return mixed the multiplication result
*/
public function multiply(mixed $value1, mixed $value2);
/**
* Divide numbers.
*
* @param mixed $value The number being divided.
* @param mixed $base The number that $value is being divided by.
*
* @return mixed the result of the division
*/
public function div(mixed $value, mixed $base);
/**
* Raise an number to the power of exponent.
*
* @param mixed $value scalar, the base to use
*
* @return mixed the value raised to the power of exp.
*/
public function pow(mixed $value, int $exponent);
/**
* Returns the int point remainder (modulo) of the division of the arguments.
*
* @param mixed $value The dividend
* @param mixed $base The divisor
*
* @return mixed the remainder
*/
public function mod(mixed $value, mixed $base);
/**
* Number comparison.
*
* @param mixed $value1 the first value
* @param mixed $value2 the second value
*
* @return int Returns < 0 if value1 is less than value2; > 0 if value1 is greater than value2, and 0 if they are equal.
*/
public function compare(mixed $value1, mixed $value2): int;
/**
* Get the decimal integer value of a variable.
*
* @param mixed $value The scalar value being converted to an integer
*
* @return mixed the integer value
*/
public function baseConvert(mixed $value, int $base);
}

View File

@@ -0,0 +1,303 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\FeatureDetection;
use Stringable;
use function array_pop;
use function count;
use function explode;
use function extension_loaded;
use function ltrim;
use function preg_match;
use function str_ends_with;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
private const REGEXP_IPV4_HOST = '/
(?(DEFINE) # . is missing as it is used to separate labels
(?<hexadecimal>0x[[:xdigit:]]*)
(?<octal>0[0-7]*)
(?<decimal>\d+)
(?<ipv4_part>(?:(?&hexadecimal)|(?&octal)|(?&decimal))*)
)
^(?:(?&ipv4_part)\.){0,3}(?&ipv4_part)\.?$
/x';
private const REGEXP_IPV4_NUMBER_PER_BASE = [
'/^0x(?<number>[[:xdigit:]]*)$/' => 16,
'/^0(?<number>[0-7]*)$/' => 8,
'/^(?<number>\d+)$/' => 10,
];
private const IPV6_6TO4_PREFIX = '2002:';
private const IPV4_MAPPED_PREFIX = '::ffff:';
private readonly mixed $maxIPv4Number;
public function __construct(
private readonly Calculator $calculator
) {
$this->maxIPv4Number = $calculator->sub($calculator->pow(2, 32), 1);
}
/**
* Returns an instance using a GMP calculator.
*/
public static function fromGMP(): self
{
return new self(new GMPCalculator());
}
/**
* Returns an instance using a Bcmath calculator.
*/
public static function fromBCMath(): self
{
return new self(new BCMathCalculator());
}
/**
* Returns an instance using a PHP native calculator (requires 64bits PHP).
*/
public static function fromNative(): self
{
return new self(new NativeCalculator());
}
/**
* Returns an instance using a detected calculator depending on the PHP environment.
*
* @throws MissingFeature If no Calculator implementing object can be used on the platform
*
* @codeCoverageIgnore
*/
public static function fromEnvironment(): self
{
FeatureDetection::supportsIPv4Conversion();
return match (true) {
extension_loaded('gmp') => self::fromGMP(),
extension_loaded('bcmath') => self::fromBCMath(),
default => self::fromNative(),
};
}
public function isIpv4(Stringable|string|null $host): bool
{
if (null === $host) {
return false;
}
if (null !== $this->toDecimal($host)) {
return true;
}
$host = (string) $host;
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return false;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return false !== filter_var(substr($ipAddress, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return false;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
return count($hexParts) > 1
&& false !== long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1]));
}
public function toIPv6Using6to4(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
/** @var array<string> $parts */
$parts = array_map(
fn (string $part): string => sprintf('%02x', $part),
explode('.', $host)
);
return '['.self::IPV6_6TO4_PREFIX.$parts[0].$parts[1].':'.$parts[2].$parts[3].'::]';
}
public function toIPv6UsingMapping(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
return '['.self::IPV4_MAPPED_PREFIX.$host.']';
}
public function toOctal(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => implode('.', array_map(
fn ($value) => str_pad(decoct((int) $value), 4, '0', STR_PAD_LEFT),
explode('.', $host)
)),
};
}
public function toHexadecimal(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => '0x'.implode('', array_map(
fn ($value) => dechex((int) $value),
explode('.', $host)
)),
};
}
/**
* Tries to convert a IPv4 hexadecimal or a IPv4 octal notation into a IPv4 dot-decimal notation if possible
* otherwise returns null.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function toDecimal(Stringable|string|null $host): ?string
{
$host = (string) $host;
if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
$host = substr($host, 1, -1);
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return null;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return substr($ipAddress, 7);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return null;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
return (string) match (true) {
count($hexParts) < 2 => null,
default => long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1])),
};
}
if (1 !== preg_match(self::REGEXP_IPV4_HOST, $host)) {
return null;
}
if (str_ends_with($host, '.')) {
$host = substr($host, 0, -1);
}
$numbers = [];
foreach (explode('.', $host) as $label) {
$number = $this->labelToNumber($label);
if (null === $number) {
return null;
}
$numbers[] = $number;
}
$ipv4 = array_pop($numbers);
$max = $this->calculator->pow(256, 6 - count($numbers));
if ($this->calculator->compare($ipv4, $max) > 0) {
return null;
}
foreach ($numbers as $offset => $number) {
if ($this->calculator->compare($number, 255) > 0) {
return null;
}
$ipv4 = $this->calculator->add($ipv4, $this->calculator->multiply(
$number,
$this->calculator->pow(256, 3 - $offset)
));
}
return $this->long2Ip($ipv4);
}
/**
* Converts a domain label into a IPv4 integer part.
*
* @see https://url.spec.whatwg.org/#ipv4-number-parser
*
* @return mixed returns null if it cannot correctly convert the label
*/
private function labelToNumber(string $label): mixed
{
foreach (self::REGEXP_IPV4_NUMBER_PER_BASE as $regexp => $base) {
if (1 !== preg_match($regexp, $label, $matches)) {
continue;
}
$number = ltrim($matches['number'], '0');
if ('' === $number) {
return 0;
}
$number = $this->calculator->baseConvert($number, $base);
if (0 <= $this->calculator->compare($number, 0) && 0 >= $this->calculator->compare($number, $this->maxIPv4Number)) {
return $number;
}
}
return null;
}
/**
* Generates the dot-decimal notation for IPv4.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*
* @param mixed $ipAddress the number representation of the IPV4address
*/
private function long2Ip(mixed $ipAddress): string
{
$output = '';
for ($offset = 0; $offset < 4; $offset++) {
$output = $this->calculator->mod($ipAddress, 256).$output;
if ($offset < 3) {
$output = '.'.$output;
}
$ipAddress = $this->calculator->div($ipAddress, 256);
}
return $output;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use GMP;
use function gmp_add;
use function gmp_cmp;
use function gmp_div_q;
use function gmp_init;
use function gmp_mod;
use function gmp_mul;
use function gmp_pow;
use function gmp_sub;
use const GMP_ROUND_MINUSINF;
final class GMPCalculator implements Calculator
{
public function baseConvert(mixed $value, int $base): GMP
{
return gmp_init($value, $base);
}
public function pow(mixed $value, int $exponent): GMP
{
return gmp_pow($value, $exponent);
}
public function compare(mixed $value1, mixed $value2): int
{
return gmp_cmp($value1, $value2);
}
public function multiply(mixed $value1, mixed $value2): GMP
{
return gmp_mul($value1, $value2);
}
public function div(mixed $value, mixed $base): GMP
{
return gmp_div_q($value, $base, GMP_ROUND_MINUSINF);
}
public function mod(mixed $value, mixed $base): GMP
{
return gmp_mod($value, $base);
}
public function add(mixed $value1, mixed $value2): GMP
{
return gmp_add($value1, $value2);
}
public function sub(mixed $value1, mixed $value2): GMP
{
return gmp_sub($value1, $value2);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use function floor;
use function intval;
final class NativeCalculator implements Calculator
{
public function baseConvert(mixed $value, int $base): int
{
return intval((string) $value, $base);
}
public function pow(mixed $value, int $exponent)
{
return $value ** $exponent;
}
public function compare(mixed $value1, mixed $value2): int
{
return $value1 <=> $value2;
}
public function multiply(mixed $value1, mixed $value2): int
{
return $value1 * $value2;
}
public function div(mixed $value, mixed $base): int
{
return (int) floor($value / $base);
}
public function mod(mixed $value, mixed $base): int
{
return $value % $base;
}
public function add(mixed $value1, mixed $value2): int
{
return $value1 + $value2;
}
public function sub(mixed $value1, mixed $value2): int
{
return $value1 - $value2;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv6;
use Stringable;
use ValueError;
use function filter_var;
use function implode;
use function inet_pton;
use function str_split;
use function strtolower;
use function unpack;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
/**
* Significant 10 bits of IP to detect Zone ID regular expression pattern.
*
* @var string
*/
private const HOST_ADDRESS_BLOCK = "\xfe\x80";
public static function compressIp(string $ipAddress): string
{
return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
false => throw new ValueError('The submitted IP is not a valid IPv6 address.'),
default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))),
};
}
public static function expandIp(string $ipAddress): string
{
if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new ValueError('The submitted IP is not a valid IPv6 address.');
}
$hex = (array) unpack('H*hex', (string) inet_pton($ipAddress));
return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
}
public static function compress(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}
$components['ipAddress'] = self::compressIp($components['ipAddress']);
return self::build($components);
}
public static function expand(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}
$components['ipAddress'] = self::expandIp($components['ipAddress']);
return self::build($components);
}
private static function build(array $components): string
{
$components['ipAddress'] ??= null;
$components['zoneIdentifier'] ??= null;
if (null === $components['ipAddress']) {
return '';
}
return '['.$components['ipAddress'].match ($components['zoneIdentifier']) {
null => '',
default => '%'.$components['zoneIdentifier'],
}.']';
}
/**]
* @param Stringable|string|null $host
*
* @return array{ipAddress:string|null, zoneIdentifier:string|null}
*/
private static function parse(Stringable|string|null $host): array
{
if (null === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
$host = (string) $host;
if ('' === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_starts_with($host, '[')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_ends_with($host, ']')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
[$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
return match (true) {
null === $zoneIdentifier,
is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier],
default => ['ipAddress' => null, 'zoneIdentifier' => null],
};
}
}

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