Skip to content

Commit

Permalink
Merge pull request #16 from kodedphp/v3
Browse files Browse the repository at this point in the history
v3.0.0
  • Loading branch information
kodeart authored May 3, 2021
2 parents d2aa893 + e4dcc76 commit bbfa5f4
Show file tree
Hide file tree
Showing 71 changed files with 1,144 additions and 1,237 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ vendor
.idea
.tmp
composer.lock
*.cache
2 changes: 1 addition & 1 deletion .scrutinizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build:
- php-scrutinizer-run
environment:
php:
version: '7.3'
version: '8.0.1'

before_commands:
- 'composer update -o --prefer-source --no-interaction'
Expand Down
8 changes: 3 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
language: php
os: linux
dist: xenial
dist: bionic

notifications:
email: false

php:
- 7.2
- 7.3
- 7.4
- 8.0.1
- nightly

cache:
Expand All @@ -27,7 +25,7 @@ install:
- composer update -o --prefer-source --no-interaction

script:
- vendor/bin/phpunit --coverage-clover build/clover.xml
- vendor/bin/phpunit --exclude-group internet --coverage-clover build/clover.xml

after_script:
- wget https://scrutinizer-ci.com/ocular.phar
Expand Down
152 changes: 61 additions & 91 deletions AcceptHeaderNegotiator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,56 +25,52 @@
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
*/

use Generator;
use InvalidArgumentException;
use Koded\Http\Interfaces\HttpStatus;

class AcceptHeaderNegotiator
{
/** @var AcceptHeader[] */
private $supports;
private string $supports = '';

public function __construct(string $supportHeader)
{
$this->supports = $supportHeader;
}


public function match(string $accepts): AcceptHeader
{
return $this->matches($accepts)[0];
}

public function matches(string $accepts): array
{
/** @var AcceptHeader $support */
foreach ($this->parse($accepts) as $accept) {
foreach ($this->parse($this->supports) as $support) {
$support->matches($accept, $types);
$support->matches($accept, $matches);
}
}

usort($types, function(AcceptHeader $a, AcceptHeader $b) {
return $b->weight() <=> $a->weight();
});

if (empty($types)) {
usort($matches, fn(AcceptHeader $a, AcceptHeader $b) => $b->weight() <=> $a->weight());
if (empty($matches)) {
/* Set "q=0", meaning the header is explicitly rejected.
* The consuming clients should handle this according to
* their internal logic. This is much better then throwing
* exceptions which must be handled in every place where
* match() is called. For example, the client may issue a
* 406 status code and be done with it.
*/
$types[] = new class('*;q=0') extends AcceptHeader {};
$matches[] = new class('*;q=0') extends AcceptHeader {};
}

return $types[0];
return $matches;
}

/**
* @param string $header
*
* @return Generator
* @return \Generator
*/
private function parse(string $header): Generator
private function parse(string $header): \Generator
{
foreach (explode(',', $header) as $header) {
foreach (\explode(',', $header) as $header) {
yield new class($header) extends AcceptHeader {};
}
}
Expand All @@ -83,177 +79,151 @@ private function parse(string $header): Generator

abstract class AcceptHeader
{
private $header;
private $separator;
private $type;
private $subtype;
private $quality = 1.0;
private $weight = 0.0;
private $catchAll = false;
private $params = [];
private string $header = '';
private string $separator = '/';
private string $type = '';
private string $subtype = '*';
private float $quality = 1.0;
private float $weight = 0.0;
private bool $catchAll = false;
private array $params = [];

public function __construct(string $header)
{
$this->header = $header;

$header = preg_replace('/[[:space:]]/', '', $header);
$bits = explode(';', $header);
$type = array_shift($bits);

if (!empty($type) && !preg_match('~^(\*|[a-z0-9._]+)([/|_-])?(\*|[a-z0-9.\-_+]+)?$~i', $type, $matches)) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid Access header', $header),
$header = \preg_replace('/[[:space:]]/', '', $header);
$bits = \explode(';', $header);
$type = \array_shift($bits);
if (!empty($type) && !\preg_match('~^(\*|[a-z0-9._]+)([/|_\-])?(\*|[a-z0-9.\-_+]+)?$~i', $type, $matches)) {
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid Access header', $header),
HttpStatus::NOT_ACCEPTABLE);
}

$this->separator = $matches[2] ?? '/';
[$type, $subtype] = explode($this->separator, $type, 2) + [1 => '*'];

[$type, $subtype] = \explode($this->separator, $type, 2) + [1 => '*'];
if ('*' === $type && '*' !== $subtype) {
// @see https://tools.ietf.org/html/rfc7231#section-5.3.2
throw new InvalidArgumentException(sprintf('"%s" is not a valid Access header', $header),
throw new \InvalidArgumentException(\sprintf('"%s" is not a valid Access header', $header),
HttpStatus::NOT_ACCEPTABLE);
}

// @see https://tools.ietf.org/html/rfc7540#section-8.1.2
$this->type = strtolower($type);

/* Uses a simple heuristic to check if subtype is part of
* some obscure media type like "vnd.api-v1+json".
$this->type = \trim(\strtolower($type));
/*
* Uses a simple heuristic to check if subtype is part of
* some convoluted media type like "vnd.api-v1+json".
*
* NOTE: It is a waste of time to negotiate on the basis
* of obscure parameters while using a meaningless media
* type like "vnd.whatever". But the web world is a big mess
* and this module can handle the Dunning-Kruger effect.
* type like "vnd.whatever". The web world is a big mess
* but this module can handle the Dunning-Kruger effect.
*/
$this->subtype = explode('+', $subtype)[1] ?? $subtype;
$this->catchAll = '*' === $this->type && '*' === $this->subtype;

parse_str(join('&', $bits), $this->params);
$this->subtype = \trim(\explode('+', $subtype)[1] ?? $subtype);
$this->catchAll = ('*' === $this->type) && ('*' === $this->subtype);
\parse_str(\join('&', $bits), $this->params);
$this->quality = (float)($this->params['q'] ?? 1);
unset($this->params['q']);
}


public function __toString(): string
{
return $this->value();
}


public function value(): string
{
// The header is explicitly rejected
if (0.0 === $this->quality) {
$this->type = $this->subtype = '';
return '';
}

// If language, encoding or charset
if ('*' === $this->subtype) {
return $this->type;
}

return $this->type . $this->separator . $this->subtype;
}


public function quality(): float
{
return $this->quality;
}


public function weight(): float
{
return $this->weight;
}

public function is(string $type): bool
{
return ($type === $this->subtype) && ($this->subtype !== '*');
}

/**
* @internal
*
* @param AcceptHeader $accept The accept header part
* @param AcceptHeader[] $matches Matched types
*
* @return bool TRUE if the accept header part is a match
* against the supported (this) header part
*
* This method finds the best match for the Accept header,
* including all the nonsense that may be passed by the
* including lots of nonsense that may be passed by the
* developers who do not follow RFC standards.
*
* @internal
*/
public function matches(AcceptHeader $accept, array &$matches = null): bool
public function matches(AcceptHeader $accept, array &$matches = null): void
{
$matches = (array)$matches;
$accept = clone $accept;

$typeMatch = $this->type === $accept->type;

$matches = (array)$matches;
$accept = clone $accept;
$typeMatch = ($this->type === $accept->type);
if (1.0 === $accept->quality) {
$accept->quality = (float)$this->quality;
}

if ($accept->catchAll) {
$accept->type = $this->type;
$accept->subtype = $this->subtype;
$matches[] = $accept;

return true;
return;
}

// Explicitly denied
if (0.0 === $this->quality) {
$matches[] = clone $this;

return true;
return;
}

// Explicitly denied
if (0.0 === $accept->quality) {
$matches[] = $accept;

return true;
return;
}

// Explicit type mismatch (w/o asterisk); bail out
if (false === $typeMatch && '*' !== $this->type) {
return false;
if ((false === $typeMatch) && ('*' !== $this->type)) {
return;
}

if ('*' === $accept->subtype) {
$accept->subtype = $this->subtype;
}

if ($accept->subtype !== $this->subtype && '*' !== $this->subtype) {
return false;
if (($accept->subtype !== $this->subtype) && ('*' !== $this->subtype)) {
return;
}

$matches[] = $this->rank($accept);

return true;
}


private function rank(AcceptHeader $accept): AcceptHeader
{
// +100 if types are exact match w/o asterisk
if ($this->type === $accept->type && '*' !== $accept->type) {
if (($this->type === $accept->type) &&
($this->subtype === $accept->subtype)) {
$accept->weight += 100;
}

$accept->weight += $this->catchAll ? 0.0 : $accept->quality;

$accept->weight += ($this->catchAll ? 0.0 : $accept->quality);
// +1 for each parameter that matches, except "q"
foreach ($this->params as $k => $v) {
if (isset($accept->params[$k]) && $accept->params[$k] === $v) {
if (isset($accept->params[$k]) && ($accept->params[$k] === $v)) {
$accept->weight += 1;
} else {
$accept->weight -= 1;
}
}

// Add "q"
$accept->weight += $accept->quality;

return $accept;
}
}
Loading

0 comments on commit bbfa5f4

Please sign in to comment.