Giter VIP home page Giter VIP logo

negotiation's Introduction

Negotiation

GitHub Actions Total Downloads Latest Stable Version

Negotiation is a standalone library without any dependencies that allows you to implement content negotiation in your application, whatever framework you use. This library is based on RFC 7231. Negotiation is easy to use, and extensively unit tested!

Important: You are browsing the documentation of Negotiation 3.x+.

Documentation for version 1.x is available here: Negotiation 1.x documentation.

Documentation for version 2.x is available here: Negotiation 2.x documentation.

Installation

The recommended way to install Negotiation is through Composer:

$ composer require willdurand/negotiation

Usage Examples

Media Type Negotiation

$negotiator = new \Negotiation\Negotiator();

$acceptHeader = 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8';
$priorities   = array('text/html; charset=UTF-8', 'application/json', 'application/xml;q=0.5');

$mediaType = $negotiator->getBest($acceptHeader, $priorities);

$value = $mediaType->getValue();
// $value == 'text/html; charset=UTF-8'

The Negotiator returns an instance of Accept, or null if negotiating the best media type has failed.

Language Negotiation

<?php

$negotiator = new \Negotiation\LanguageNegotiator();

$acceptLanguageHeader = 'en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2';
$priorities          = array('de', 'fu', 'en');

$bestLanguage = $negotiator->getBest($acceptLanguageHeader, $priorities);

$type = $bestLanguage->getType();
// $type == 'fu';

$quality = $bestLanguage->getQuality();
// $quality == 0.9

The LanguageNegotiator returns an instance of AcceptLanguage.

Encoding Negotiation

<?php

$negotiator = new \Negotiation\EncodingNegotiator();
$encoding   = $negotiator->getBest($acceptHeader, $priorities);

The EncodingNegotiator returns an instance of AcceptEncoding.

Charset Negotiation

<?php

$negotiator = new \Negotiation\CharsetNegotiator();

$acceptCharsetHeader = 'ISO-8859-1, UTF-8; q=0.9';
$priorities          = array('iso-8859-1;q=0.3', 'utf-8;q=0.9', 'utf-16;q=1.0');

$bestCharset = $negotiator->getBest($acceptCharsetHeader, $priorities);

$type = $bestCharset->getType();
// $type == 'utf-8';

$quality = $bestCharset->getQuality();
// $quality == 0.81

The CharsetNegotiator returns an instance of AcceptCharset.

Accept* Classes

Accept and Accept* classes share common methods such as:

  • getValue() returns the accept value (e.g. text/html; z=y; a=b; c=d)
  • getNormalizedValue() returns the value with parameters sorted (e.g. text/html; a=b; c=d; z=y)
  • getQuality() returns the quality if available (q parameter)
  • getType() returns the accept type (e.g. text/html)
  • getParameters() returns the set of parameters (excluding the q parameter if provided)
  • getParameter() allows to retrieve a given parameter by its name. Fallback to a $default (nullable) value otherwise.
  • hasParameter() indicates whether a parameter exists.

Versioning

Negotiation follows Semantic Versioning.

End Of Life

1.x

As of October 2016, branch 1.x is not supported anymore, meaning major version 1 reached end of life. Last version is: 1.5.0.

2.x

As of November 2020, branch 2.x is not supported anymore, meaning major version 2 reached end of life. Last version is: 2.3.1.

Stable Version

3.x (and dev-master)

Negotiation 3.0 has been released on November 26th, 2020. This is the current stable version and it is in sync with the main branch (a.k.a. master).

Unit Tests

Setup the test suite using Composer:

$ composer install --dev

Run it using PHPUnit:

$ phpunit

Contributing

See CONTRIBUTING file.

Credits

License

Negotiation is released under the MIT License. See the bundled LICENSE file for details.

negotiation's People

Contributors

adammbalogh avatar akrabat avatar cblegare avatar dawehner avatar dracoblue avatar dunglas avatar eduardosoliv avatar guilhemn avatar iby avatar kornrunner avatar liuggio avatar lsmith77 avatar luxifer avatar michalbundyra avatar neural-wetware avatar oscarotero avatar pborreli avatar peter279k avatar pierredup avatar pvankouteren avatar rtm-ctrlz avatar scrutinizer-auto-fixer avatar sgehrig avatar simonsimcity avatar w0rma avatar weierophinney avatar willdurand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

negotiation's Issues

Undefined Offset Error

I'm not sure if this is an issue with my install or something else that I can help resolve. I am occasionally receiving the following error in this class:

PHP Notice: Undefined offset: 1 in /../vendor/willdurand/negotiation/src/Negotiation/Negotiator.php on line 161

I am using Symfony 2.4.1 with FOSRestBundle 0.13.0.

Thanks for the help!

Question: */*;q=0.0

This piece of software works great, however in 1 case, I get something I wouldn't expect to have.

Situation:

accept header => /;q=0.0

So no formats are allowed, could be used in combination with some random formats as well, bottom-line is, the server can't respond with a format, and/or all other formats aren't good enough.

When I fill in my priorities using this library, I also put "/" in it to receive normal results that are provided by default in this library, such as xml or json.

Now when I let it evaluate, I receive a format, with a quality of 0. Shouldn't this return just NULL? If you ask getBestFormat, you just get a format that suits best, this should be NULL as well, going by the rule if no proper response can be sent (user agent wants a format we cannot provide, we "should" provide a 406. If you as a server want to reply with a 406, you'll have to ask for "getBest", then check if the quality is higher than 0 and return an error.

Was this the intentional behaviour in this situation? If so, this can be closed and I'll check with getQuality if I'll have to throw a 406 or not.

Register more formats

Did you considered to provide a format for hal+json by default? Sure we can just call registerFormat and it works fine, but I am just curious whether you would be open for adding more formats.

I know once you start you would never end supporting more and more

forcing quality for media ranges

This code in Negotiator::parseHeader() isn't part of the standard, as far as I know.

if (self::CATCH_ALL_VALUE === $value) {
    $quality = 0.01;
} elseif ('*' === substr($value, -1)) {
    $quality = 0.02;
}

Should be removed?

Default best format

Hi William,

Is it possible to add functionality to return a default format instead of null when calling getBestFormat(). If you like the idea, I can submit a PR for it.

Cheers,
Robin

Quality-of-source factor

Hi,

I'd like to propose a simple way to specify relative priorities of media types, languages, encodings or character sets on the server side, without introducing a backward-compatibility break.

Current behaviour

The programmer specifies a list of available media types, in order of preference, when calling the getBest() function.

The client can specify quality scores against each acceptable type in the Accept header. The default score if not specified is 1.0.

The negotiator finds matches as per RFC 7231, and sets the quality of a match equal to the quality score from the Accept header.

If there is more than one match, the best match is the one with the highest quality score.

In the event of multiple matches with the same quality score, the negotiator returns the match that occurred earliest in the programmer's list.

Proposed behaviour

I'd like to be able to specify a quality of source factor, like in Apache's content negotiation. For example:

$priorities = array('text/html; charset=UTF-8; q=1.0', 'application/pdf; q=0.9');

The negotiator would still find matches as per RFC 7231, but would set the quality of the match equal to the quality score from the Accept header multiplied by the quality-of-source factor specified by the programmer.

The best match is the one with the highest quality.

In the event of multiple matches with the same quality, the negotiator returns the match that occurred earliest in the programmer's list. So if the programmer has not specified any quality-of-source factors, the behaviour would be exactly the same as it is now.

Implementation

Because the priorities are already parsed the same way as the headers, it's a one-line change to implement this. Simply change line 64 of AbstractNegotiator from:

return new Match($header->getQuality(), $score, $index);

to:

return new Match($header->getQuality() * $priority->getQuality(), $score, $index);

Making this change doesn't break any of the existing unit tests. I'm happy to submit a pull request.

Thanks,
Steve

Precedence over wildcards

When given the following header:

Accept: text/xml,text/*;q=0.6,application/pdf;q=0.6

it should prioritize application/pdf over text/html, as application/pdf is matched more specifically. This is taken from RFC 2616, chapter:

If more than one media range applies to a given type, the most specific reference has precedence.

Right now, it prefers text/html.

Regression in LanguageNegotiator

PHP 5.5:

$ phpunit
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from /Users/william/projects/hhvm/Negotiation/phpunit.xml.dist

................................................................. 65 / 69 ( 94%)
....

Time: 172 ms, Memory: 3.50Mb

OK (69 tests, 167 assertions)

HHVM:

$ hhvm vendor/bin/phpunit
PHPUnit 3.7.28 by Sebastian Bergmann.

Configuration read from /vagrant/Negotiation/phpunit.xml.dist

.....................................F........................... 65 / 69 ( 94%)
....

Time: 241 ms, Memory: 3.08Mb

There was 1 failure:

1) Negotiation\Tests\LanguageNegotiatorTest::testGetBest with data set #1 ('es-ES;q=0.7, es; q=0.6 ,fr; q=1.0, en; q=0.5,dk , fr-CH', 'fr-CH')
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'fr-CH'
+'fr'

/vagrant/Negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php:57
/vagrant/Negotiation/vendor/bin/phpunit:63

FAILURES!
Tests: 69, Assertions: 167, Failures: 1.

getBest returns a value when no acceptable headers match any of the priorities

I'm about to make a pull request for a small bug while discovered the following and want to clarify first. Currently there's only one place where this is found in LanguageNegotiatorTest:

public function testGetBestDoesNotMatchPriorities()
{
    $acceptLanguageHeader = 'en, de';
    $priorities           = array('fr');
    $acceptHeader = $this->negotiator->getBest($acceptLanguageHeader, $priorities);
    $this->assertInstanceOf('Negotiation\AcceptHeader', $acceptHeader);
    $this->assertEquals('en', $acceptHeader->getValue());
}

Basically what happens is: the client says I will accept only en or de, but the only thing we can offer is fr. The rational action is to return null as we can't satisfy the request, but we return en instead (which we can't provide). I am assuming the way this works is wrong?

Access to sorted Accept Headers array?

Negotiator::getBest() is the only public method and only returns the best candidate from parsed Accept/Accept-*` header.

Wouldn't be useful to get the whole sorted array (basically what Negotiator::sortAcceptHeaders() returns)?

Skip invalid headers

Commit 4e48025 silently skips invalid headers. I want this library to catch invalid headers so I can 406. It seems silly that I should have to parse the headers again to determine if they are invalid when the library is already doing this.

What do you think about reversing this change or adding a flag to getBest() to enable/disable this? I'm willing to implement this change.

Allow to get a collection of ordered accept headers

Hey!

Context

We have an API which deals with the Accept-Language header in order to negotiate the best locales to use. In our model we does not priorize locales, we just have a set of locales the application can deal with (not always available) and a default/fallback one (always available).

Use case

We want to use your library to parse and retrieve an order collection of AcceptLanguage if is provided. Then, we can inject this ordered locales in our model which will iterate on provided locales and try to find an available translation transparently. If none exists, we though an exception which will end with a 4xx explaining the reason and the available locales for this specific resource.

Proposal

For now, we was using the 1.x branch of this library and we override the Negotiator to introduce a method which return all ordered locales (not the best one as we don't use priorities). It worked great by using the methods you provided internally such as parseHeader, ...

Now, we just upgrade to the 2.x and we notice that you close the parseHeader method by moving it to private. So, we have to copy/paste your code from the this method to our own and I don't really like it...

Now my code looks like:

public function parse($header)
{
    if (!$header) {
        throw new InvalidArgument('The header string should not be empty.');
    }

    $acceptHeaders = array_map(array($this, 'acceptFactory'), $this->parseHeader($header));

    uasort($acceptHeaders, function (AcceptLanguage $a, AcceptLanguage $b) {
         return $a->getQuality() < $b->getQuality();
    });

    return $acceptHeaders;
}

Basically, I would like to introduce a new method allowing to parse and sort all accept headers without dealing with priorities... Maybe it's out of scope of this library as it is not really a negotiation but much more a parsing/sorting process.

Anyway, I just would like to get your opinion about it as it is something which is handled internally by the library and so can maybe get exposed to end users. Let's if you would accept such change or if I should live with my current code base :)

Cheers!

Dealing with wildcards

Hi William,

Nice job on the content negotiation library. I'm having a few hickups using it, which I can work around. But perhaps my feedback below can be of value.

When submitting getBest('/',array('application/json')) a null value is returned. Submitting getBest('*',array('application/json')) however results in an object containing application/json as value.

Is this expected behaviour? I would think that the first example would return application/json, and that the second isn't a valid wildcard to use.

Also partial wildcards don't seem to work, such as getBest('application/*',array('application/json'))

Any chance of you looking into this, or maybe accepting a PR?

AcceptLanguage issue with 3 parts lang in header

Here's my stack trace:

Stacktrace (most recent call first):

  File "/filer/xotelia/releases/20151104160916/vendor/willdurand/negotiation/src/Negotiation/AcceptLanguage.php", line 26, in __construct
    throw new InvalidLanguage();
  File "/filer/xotelia/releases/20151104160916/vendor/willdurand/negotiation/src/Negotiation/LanguageNegotiator.php", line 12, in acceptFactory
    return new AcceptLanguage($accept);
  File "/filer/xotelia/releases/20151104160916/vendor/willdurand/negotiation/src/Negotiation/AbstractNegotiator.php", line 27, in getBest
    $headers    = array_map(array($this, 'acceptFactory'), $headers);
  File "/filer/xotelia/releases/20151104160916/src/Xotelia/FrameworkBundle/Listener/LocaleListener.php", line 40, in onKernelRequest
    $bestLocale = $negotiator->getBest($request->headers->get('Accept-Language'), $isoCodes);
  File "/filer/xotelia/releases/20151104160916/.software/.cache/prod/classes.php", line 2654, in doDispatch
    call_user_func($listener, $event, $eventName, $this);
  File "/filer/xotelia/releases/20151104160916/.software/.cache/prod/classes.php", line 2583, in dispatch
    $this->doDispatch($listeners, $eventName, $event);
  File "/filer/xotelia/releases/20151104160916/app/bootstrap.php.cache", line 3098, in handleRaw
    $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);
  File "/filer/xotelia/releases/20151104160916/app/bootstrap.php.cache", line 3071, in handle
    return $this->handleRaw($request, $type);
  File "/filer/xotelia/releases/20151104160916/app/bootstrap.php.cache", line 3222, in handle
    $response = parent::handle($request, $type, $catch);
  File "/filer/xotelia/releases/20151104160916/app/bootstrap.php.cache", line 2444, in handle
    return $this->getHttpKernel()->handle($request, $type, $catch);
  File "/filer/xotelia/releases/20151104160916/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php", line 487, in forward
    $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
  File "/filer/xotelia/releases/20151104160916/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php", line 60, in forward
    return parent::forward($request, $raw, $entry);
  File "/filer/xotelia/releases/20151104160916/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php", line 444, in fetch
    $response = $this->forward($subRequest, $catch);
  File "/filer/xotelia/releases/20151104160916/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php", line 344, in lookup
    return $this->fetch($request, $catch);
  File "/filer/xotelia/releases/20151104160916/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php", line 210, in handle
    $response = $this->lookup($request, $catch);
  File "/filer/xotelia/releases/20151104160916/web/index.php", line 36, in null
    $response = $kernel->handle($request);

Here's the context for the last statement:

$value: zh-Hans-CN;q=0.3

The accept-language header passed to the negotiator is:

fr-FR,fr;q=0.8,en-US;q=0.7,en;q=0.5,zh-Hans-CN;q=0.3,zh-Hans;q=0.2

and the list of priorities is :

fr, en, de, es, it, da, pt, ru, sv, nk, th

My version of this package is v2.0.1

My guess is that a language code can have more than 2 parts separated by a dash - specifically for asian languages

Does not match generic language priorities with specific Accept

Given this code:

$negotiator = new \Negotiation\LanguageNegotiator();
$language = $negotiator->getBest('en-US', array('fi','sv','en'));
echo $language->getValue() . "\n";

the result is "en-US", so the returned result is not among the given priorities.

I guess this is sort of allowed by RFC2616, as it doesn't consider an available generic language resource (e.g. "en") to be a match for a requested variant (e.g. "en-US"). But I think it is rather unfortunate that getBest() returns a result that is not even available on the server...

This is due to this line in Negotiator.php setting the default for $best:
$best = reset($acceptHeaders);
and when all matching fails, this is what eventually gets returned by getBest().

I think the correct response in this case would be either "en" (though it sort of violates RFC2616), or NULL.

If you agree with my reasoning I can produce a pull request implementing the chosen solution.

Throw exception for invalid input

If the user give and invalid Accept header or priorities that aren't valid media types, the library should throw exceptions.

Currently, we cannot distinguish between a 406 and bad input because getBest() will just return null in both cases.

LanguageNegotiator does not match plain language

According to the HTTP specification about header fields,

Given the Accept-Language header is "fr-FR, en;q=0.8"
And the priorities are "en-US" and "de-DE"
Then the best language should be "en-US"

The LanguageNegotiator do not match the priority en-US to the header part en. It returns reset($acceptHeaders), that is fr-FR.

EDIT:
One might consider that the best language should be en as it is the token provided by the user agent. What do you think ?

support for wildcard priorities

The code and unit tests allow for cases where the server sets */* or something/* as a priority. I don't think this makes sense and it greatly overcomplicates the negotiation logic.

The server needs to return an actual media type. If the server sets */* as a preference, it could get any media type from bestFit(). Then the server must then manually check if it supports the returned media type, which is supposed to be the job of the library.

It makes more sense for the server to just list all supported media types in the priorities array. These should be actual media types and not media ranges.

I understand that this could break existing clients that are using the library in some esoteric way. Could removing wildcard priorities considered for the new version?

Double wildcard */* doesn't match anything

When the client says I will accept anything and the server has available options the following returns nothing null.

$negotiator->getBest('*/*', array('foo, bar, yo'));

Using */*, * solves the problem, but I guess it's not how this is supposed to work.

Order or parameters should be ignored

If the order of the parameters in the header is different from the order in the server priority, this library fails to match the priority.

<?
use \Negotiation;

include './Negotiation/src/Negotiation/NegotiatorInterface.php' ;
include './Negotiation/src/Negotiation/Negotiator.php' ;
include './Negotiation/src/Negotiation/FormatNegotiatorInterface.php' ;
include './Negotiation/src/Negotiation/FormatNegotiator.php' ;
include './Negotiation/src/Negotiation/AcceptHeader.php' ;

$neg = new \Negotiation\FormatNegotiator();
$best = $neg->getBest('text/html; foo=bar; biz=baz', array('text/plain', 'text/html; foo=bar; biz=baz'));
var_dump($best);

$neg = new \Negotiation\FormatNegotiator();
$best = $neg->getBest('text/html; foo=bar; biz=baz', array('text/plain', 'text/html; biz=baz; foo=bar'));
var_dump($best);

I think that the match should still occur regardless of the parameter order.

Fix parsing

Got great feedback related to how I implemented the spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. I choose to not exactly fit the ABNF which is not perfect, but pragmatic. Anyway, that would be nice to rewrite this part. It should be BC.

For the record:

Yeh in the real world those flaws are so minor they're not even worth the bother to fix
(although the ;q= can be fixed with a simple preg_split('/;\s_q=/')), and similarly in real
world the regexp performance problem is a total non-problem. I haven't totally pulled
it apart as yet, those were just a couple of things that leapt out at me.
One thing I would say is it would be worth having separate routines for different
Accept-_ headers, as they do all have a well defined-but-slightly-different format for
the field values
Oh also the parameter names are case insensitive so that should be /;\s*q=/i

getBest() not working correctly?

So I have this code based on the JSON API spec.

$negotiator = new Negotiator();
$priority = 'application/vnd.api+json';

if ($contentTypeHeader = $request->headers->get('Content-Type')) {
    $contentType = $negotiator->getBest($contentTypeHeader, [$priority]);

    if ($contentType && $contentType->getValue() === $priority && count($contentType->getParameters())) {
        throw new ApiException(415, ErrorCode::UNSUPPORTED_MEDIA_TYPE);
    }
}

Which checks the Content-Type header in the request and throws and error if any parameters exist.

When testing, I set Content-Type to application/vnd.api+json;version=1, text/html, which seems legitimate, but it never matches as the "best". The $contentType variable is always null.

When trying to debug, it seems like AbstractNegotiator::match() is never called, but findMatches() is called. Not sure where the disconnect is, or if I'm doing this incorrectly.

LanguageNegociation fail with Safari (iOS9, Safari.9.1.1)

Hi there !

We are encountering some troubles with LanguageNegociator under Safari.

Here's the Header sent by Safari on mobile & desktop : fr-fr

Accept-Language : fr-fr
$locale = $this->negotiator->getBest($request->getHeaderLine('Accept-Language'), $locales);

with

$locales = ["fr", "en", "it", "de"]

$locale is NULL after that, or it should be fr right ?

Any ideas on what's the deal ?
Thank's !

Take into account some browser bugs

Drupal has a quite complex test scenario for various crazy browser vendors

    $tests = array(
      // @see https://bugs.webkit.org/show_bug.cgi?id=27267
      'Firefox 3.5 (2009)' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      'IE8 (2009)' => 'image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, */*',
      'Opera (2009)' => 'text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1',
      'Chrome (2009)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
      // @see https://github.com/symfony/symfony/pull/564
      'Firefox 3.6 (2010)' => 'text/html,application/xhtml+xml,application/json,application/xml;q=0.9,*/*;q=0.8',
      'Safari (2010)' => '*/*',
      'Opera (2010)' => 'image/jpeg,image/gif,image/x-xbitmap,text/html,image/webp,image/png,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.1',
      // @see http://drupal.org/node/1716790
      'Safari (2010), iOS 4.2.1 (2012)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
      'Android #1 (2012)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
      'Android #2 (2012)' => 'text/xml,text/html,application/xhtml+xml,image/png,text/plain,*/*;q=0.8',
    );

all of them actually expects HTML but I wonder which of those do you want to support. The following
ones fail on your implementation:

  • IE8 (2009)
  • Chrome (2009)
  • Opera (2010)
  • Safari (2010), iOS 4.2.1 (2012)
  • Android #1 (2012)
  • Android #2 (2012)

I wonder whether we could provide workarounds in this library to return 'html' in those cases

'*' preferred over earlier specified language

Given a list of languages like:

['en-US' => '...', 'en-UK' => '....']

And a language string of:

en-US, *

I get the value for en-UK using getBest and based on my (admittedly new) reading of the RFC suggests that I should get the 'en-US' value since it is listed first and they have the same quality factor. (Common sense suggests that is the better value.)

Switching the accept string to be en-US, *;q=0 resolves the matter but seems either incorrect or at least unnecessary.

Am I doing something wrong or is this a bug? It looked similar to some of the other open issues, but not quite the same. It also wasn't a problem in the 1.x release series.

Format matching doesn't include parameters plus more

Hi,

I'm having a look at this library and tried it for a few use cases. I like it so far, but I'd like to raise some points that might need to be addressed. It might be better to create separate issues on the things we agree on, but…I'm lazy atm and thus here are my issues/questions:

Incomplete custom format matching

Registering formats with media types that need parameters to distinguish different behaviour/response formats works, but doesn't match correctly later. Example media types are ODATA and ATOM variants. A few examples for registering formats with valid media types follow (excuse the formatting):

$negotiator->registerFormat('odata-v4', array(
    'application/json;odata.metadata=full',
    'application/json;odata.metadata=none',
    'application/json;odata.metadata=minimal'
), true);
$negotiator->registerFormat('odata-v4-minimal-streaming', array(
    'application/json;odata.metadata=minimal;odata.streaming=true'
), true);
$negotiator->registerFormat('odata-v3-verbose', array(
    'application/json;odata=verbose'
), true);
$negotiator->registerFormat('atom-entry', array(
    'application/atom+xml;type=entry'
), true);

When you try to match the above formats you'll likely get weird results, as the format of an Accept header that contains application/json;odata.metadata=minimal;odata.streaming=true would just be a simple json which is not what I would've expected. When I saw this correctly the parameters are just stripped prior to format matching which explains this behaviour. I'm wondering what the reason for this is and would suggest doing some slightly more complex matching. The above use cases are important to handle for REST applications as they e.g. aren't allowed to stream content to clients that are not requesting that via the Accept header media type parameter.

More information on Accept header values

Is the handling of types, subtypes, parameters and tokens planned in a way that one can get detailed information from the parsed Accept header value? I'd find it nice to get information like type, subtype, type range etc.. Tokens are probably ignored because of YAGNI?

Subtype handling and +formats

On a related note there doesn't seem support for subtypes (or formats?) like +json or +xml within the library. Wouldn't that be something that would be cool and a matching feature? I guess the AcceptHeader class might get a few more properties or a slightly different implementation. It would be nice to get e.g. the information that application/vnd.collection+json has a (data format) subtype of json while the matching (registered) format might by collectionjson or similar.

Predefined formats

I saw a closed issue on this iirc, but would still like to ask why it's not possible to start with an empty predefined formats array. It's nice, that I can overwrite the defaults, but I'd still have formats in there, that my application doesn't even want to support. The defaults are also a bit off, as strictly speaking application/xhtml+xml is xml instead of html even though user agents and symfony may be unified in handling it as some normal html dialect (e.g. when it comes to strict validation and error handling).

Resources

refactor API to make it possible to fetch the best accept header while using format priorities

this relates to FriendsOfSymfony/FOSRestBundle#612
right now there is getBest() and getBestFormat(). The former requires the priorities as media types and the later as formats. The former returns the best media type while the later returns a format.

for the above linked PR we however need a way to get the media type while still only providing a list of formats.

semi related to this. it would actually be cool if one could provide an array of both formats and media types. this way FOSRestBundle could allow people to specify either or. ideally such conversions should be done at container compile time but likely its not so easily and flexible to do this, so it has to be done at run time.

Upgrade to 1.2.3 "breaks" content type on CSS case

I'm using negotation together with FOSRestBundle, with the following configuration:

- { path: ^/, priorities: ['html', '*/*'], fallback_format: html, prefer_extension: true }

I have a manual test that shows the problem (I will have to open a PR with a failing test case):

$ php composer.phar show -i | grep negotiation
willdurand/negotiation                   1.2.2              Content Negotiation tools for PHP provided as a standalone library.
$ curl -v http://eb.dev/app_dev.php/css/min/base_bootstrap-responsive.min_3.css
* About to connect() to eb.dev port 80 (#0)
*   Trying 127.0.0.1...
* connected
* Connected to eb.dev (127.0.0.1) port 80 (#0)
> GET /app_dev.php/css/min/base_bootstrap-responsive.min_3.css HTTP/1.1
> User-Agent: curl/7.27.0
> Host: eb.dev
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx
< Content-Type: text/css; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/5.4.13
< Set-Cookie: PHPSESSID=j5bcf9hu961ohoilc5ee8re855; path=/; HttpOnly
< Cache-Control: private, must-revalidate
< Date: Tue, 04 Mar 2014 15:10:07 GMT
< Expires: Tue, 04 Mar 2014 15:10:07 GMT

After upgrade to 1.2.3

$ php composer.phar show -i | grep negotiation
willdurand/negotiation                   1.2.3              Content Negotiation tools for PHP provided as a standalone library.
$ curl -v http://eb.dev/app_dev.php/css/min/base_bootstrap-responsive.min_3.css
* About to connect() to eb.dev port 80 (#0)
*   Trying 127.0.0.1...
* connected
* Connected to eb.dev (127.0.0.1) port 80 (#0)
> GET /app_dev.php/css/min/base_bootstrap-responsive.min_3.css HTTP/1.1
> User-Agent: curl/7.27.0
> Host: eb.dev
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/5.4.13
< Set-Cookie: PHPSESSID=jskjr8gek71g7a7gh8os8fk2n4; path=/; HttpOnly
< Cache-Control: private, must-revalidate
< Date: Tue, 04 Mar 2014 15:13:38 GMT
< Expires: Tue, 04 Mar 2014 15:13:38 GMT
< Last-Modified: Thu, 27 Jun 2013 14:33:31 GMT
< ETag: "a9b196dccd5e89877ec5dadf9b3b3362"

Notice: Undefined variable: wildcardAccept in Negotiation/Negotiator.php on line 180

Using the following accept string:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8

and the following priorities:
array('application/json'),

I get the error:
Notice: Undefined variable: wildcardAccept in Negotiation/Negotiator.php on line 180

In addition, getBest()->getValue() gives me 'text/html', even though it's not in the priorities.

Language negotiator doesn't seem to work with IE and Edge

Internet Explorer and Edge send an HTTP_ACCEPT_LANGUAGE header equal to something like "fr-FR" that looks diferent from the format used by Chrome or Firefox, for example:
"fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"

With Internet Explorer and Edge, your lib returns null on this call:

$bestLanguage = $negotiator->getBest($_SERVER['HTTP_ACCEPT_LANGUAGE'], $languages);

You may try on your side with this example: http://benjamin-balet.info/tauch/
Although the outcome may differ depending on your configuration.

Regression in recent releases

Copy/paste from #92:

Hi @willdurand, @weierophinney! This PR and the new release (2.3) breaks our test suite on api-platform/core. See https://travis-ci.org/api-platform/core/jobs/228775063 for example.

I have this accept header: application/hal+json.
My priorities are:

[0] => "application/ld+json"
[1] => "application/hal+json"
[2] => "application/xml"
[3] => "text/xml"
[4] => "application/json"
[5] => "text/html"
Then, the AbstractNegotiator::getBest() method now returns application/ld+json instead of application/hal+json before.

It looks like something is not working properly here, nop? Thanks.

(Ping @dunglas)

Content type application/json selected incorrectly using Microsoft Edge headers

Hi all,

This was a interesting case encountered using the Microsoft Edge (41.16299.15.0) browser, which sends the following headers by default.

Accept: text/html, application/xhtml+xml, image/jxr, */*

When the priorities are prioritised with application/json, text/html; then the Content Negotiation will prefer the application/json response type; this should clearly prefer text/html based on the headers send by Edge.

I worked around the issue by changing the priority from application/json, text/html => text/html, application/json. But I thought it a good idea to flag this issue.

I've written some tests which expose this behaviour, the following is the (invalid) 'passing' test.

array(
    array(new Accept('text/html'), new Accept('application/xhtml+xml'), new Accept('image/jxr'), new Accept('*/*')),
    array(new Accept('application/json'), new Accept('text/html')),
    array(
        new Match(1.0, 0, 0),   // application/json
        new Match(1.0, 110, 1), // text/html
        new Match(1.0, 0, 1),   // */*
    )
)

This seems to be caused by the index, but I am not exactly sure what the correct fix would be, hense the lack of a PR. My best guess is that application/xhtml+xml should not match application/json in any way, as they are radically different.

change case of parameter keys causes failed match

The sanitize() function is lower casing the priority parameters, which are normally case sensitive. This causes the check in FormatNegotiator::getBest() to fail.

<?
include('vendor/autoload.php');

use \Negotiation\FormatNegotiator;

$neg = new FormatNegotiator();

$type = 'text/html; charset=UTF-8;';
$best = $neg->getBest($type, array($type));
var_dump($best);

[Question] Can you explain further your use of "format" vs. "media type"?

Ever since finding your excellent little library, I've been trying to trace down through the various RFCs and other documentation why you might use "format" instead of "media type". It appears as though formats in practical usage would mostly correspond to a single media type with the "format" in those cases being a helpful shorthand. However, it also appears you're using "format" as a parent term to group multiple similar media types together (e.g. application/javascript + text/javascript).

Were these format groupings purely derived from Symfony's usage, or is there some external resource you were looking at to create them? And are there any best practices for when to use format vs. media-type? Should I as an application developer default to using your libraries format negotiator to avoid having to encode the media type associations you're making my own codebase?

InvalidMediaType exception

Hi.
From the google spiders, I get the following Accept header:
text/html, image/gif, image/jpeg, *; q=0.2, */*; q=0.2
And this generate the following exception:

Negotiation\\Exception\\InvalidMediaType(code: 0):  at /vendor/willdurand/negotiation/src/Negotiation/Accept.php:20)

I think the problem is the first * that should be */*. Maybe this library should treat * as equivalent to */*?

Negotiator::getBest() return type hint?

To get offline source inspection (phan, Php Storm, etc.) currently I have to manually type-hint the return-type of getBest() inline - for example:

            $negotiator = new Negotiator();

            /** @var Accept $result */
            $result = $negotiator->getBest(
                $request->getHeaderLine("Accept"),
                ["application/json", "text/html"]
            );

            if ($result->getValue() === "application/json") {
                // ...
            }

Without the @var annotation, the if-statement will fail inspection, because the return-type of getBest() was declared as AcceptHeader rather than Accept, which appears to be the actual return-type.

What's the purpose of the empty AcceptHeader interface?

Any way to add formats??

Do you have any way to add new format??

I saw (code below) in FormatNegotiator but i don't see anything about adding something to this??

    protected $formats = array(
        'html' => array('text/html', 'application/xhtml+xml'),
        'txt'  => array('text/plain'),
        'js'   => array('application/javascript', 'application/x-javascript', 'text/javascript'),
        'css'  => array('text/css'),
        'json' => array('application/json', 'application/x-json'),
        'xml'  => array('text/xml', 'application/xml', 'application/x-xml'),
        'rdf'  => array('application/rdf+xml'),
        'atom' => array('application/atom+xml'),
        'rss'  => array('application/rss+xml'),
    );

I found on Symfony Docs something about adding mime type but yours (hardcoded) seem to pass over and block this features

Priorities could reflect resource availability ?

I am not sure about this. In my day-to-day use case, this applies to me (I might be biased toward this).

As of today, when a accept header is matched against priorities, the matching header is returned as the best. The following code snippet makes it happen

if ($this->matchPriorities($accept, $priorities)) {
    return $accept;
}

I priorities can be seen as what the application is able to provide, the matched priority should be returned.

For instance, if my Accept-Language is fr-FR, en;q=8.0 and the server priorities are en_US and de_DE, then en_US is the best language that can be negotiated because it matches the user agent's accept header and the representation known to the application (priorities).

In this case, if en is returned, the negotiator's caller object would have to remap the result against language availability in context (in my case: available translations).

What do you think ?

E_WARNING: array_map(): An error occurred while invoking the map callback

After deploying the negotiation library as part of FriendsOfSymfony/FOSRestBundle version 2.0.0 we started to get

E_WARNING: array_map(): An error occurred while invoking the map callback

from Negotiation\AbstractNegotiator::getBest() (27 or 28). I assume this happens because Negotiation\Accept::__construct() may throw an exception when constructed with an invalid value. This in turn seems to trigger PHP bug #55416 causing a warning to be emitted.

I don't know which exact value seems to cause this problem as the issue is not reproducible and only happens sparsely on our live servers - most often with some Android phones.

Can this part be rewritten somehow to circumvent PHP bug #55416? Or can the logic be implemented a little bit more tolerant to weird values coming in from web clients?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.