﻿tests/Negotiation/Tests/EncodingNegotiatorTest.php                                                  0000755                 00000004560 00000000000 0016374 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\EncodingNegotiator;

class EncodingNegotiatorTest extends TestCase
{

    /**
     * @var EncodingNegotiator
     */
    private $negotiator;

    protected function setUp(): void
    {
        $this->negotiator = new EncodingNegotiator();
    }

    public function testGetBestReturnsNullWithUnmatchedHeader()
    {
        $this->assertNull($this->negotiator->getBest('foo, bar, yo', array('baz')));
    }

    /**
     * @dataProvider dataProviderForTestGetBest
     */
    public function testGetBest($accept, $priorities, $expected)
    {
        $accept = $this->negotiator->getBest($accept, $priorities);

        if (null === $accept) {
            $this->assertNull($expected);
        } else {
            $this->assertInstanceOf('Negotiation\AcceptEncoding', $accept);
            $this->assertEquals($expected, $accept->getValue());
        }
    }

    public static function dataProviderForTestGetBest()
    {
        return array(
            array('gzip;q=1.0, identity; q=0.5, *;q=0', array('identity'), 'identity'),
            array('gzip;q=0.5, identity; q=0.5, *;q=0.7', array('bzip', 'foo'), 'bzip'),
            array('gzip;q=0.7, identity; q=0.5, *;q=0.7', array('gzip', 'foo'), 'gzip'),
            # Quality of source factors
            array('gzip;q=0.7,identity', array('identity;q=0.5', 'gzip;q=0.9'), 'gzip;q=0.9'),
        );
    }

    public function testGetBestRespectsQualityOfSource()
    {
        $accept = $this->negotiator->getBest('gzip;q=0.7,identity', array('identity;q=0.5', 'gzip;q=0.9'));
        $this->assertInstanceOf('Negotiation\AcceptEncoding', $accept);
        $this->assertEquals('gzip', $accept->getType());
    }

    /**
     * @dataProvider dataProviderForTestParseAcceptHeader
     */
    public function testParseAcceptHeader($header, $expected)
    {
        $accepts = $this->call_private_method('Negotiation\Negotiator', 'parseHeader', $this->negotiator, array($header));

        $this->assertSame($expected, $accepts);
    }

    public static function dataProviderForTestParseAcceptHeader()
    {
        return array(
            array('gzip,deflate,sdch', array('gzip', 'deflate', 'sdch')),
            array("gzip, deflate\t,sdch", array('gzip', 'deflate', 'sdch')),
            array('gzip;q=1.0, identity; q=0.5, *;q=0', array('gzip;q=1.0', 'identity; q=0.5', '*;q=0')),
        );
    }
}
                                                                                                                                                tests/Negotiation/Tests/CharsetNegotiatorTest.php                                                   0000755                 00000010711 00000000000 0016232 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\CharsetNegotiator;

class CharsetNegotiatorTest extends TestCase
{

    /**
     * @var CharsetNegotiator
     */
    private $negotiator;

    protected function setUp(): void
    {
        $this->negotiator = new CharsetNegotiator();
    }

    public function testGetBestReturnsNullWithUnmatchedHeader()
    {
        $this->assertNull($this->negotiator->getBest('foo, bar, yo', array('baz')));
    }

    /**
     * 'bu' has the highest quality rating, but is non-existent,
     * so we expect the next highest rated 'fr' content to be returned.
     *
     * See: http://svn.apache.org/repos/asf/httpd/test/framework/trunk/t/modules/negotiation.t
     */
    public function testGetBestIgnoresNonExistentContent()
    {
        $acceptCharset = 'en; q=0.1, fr; q=0.4, bu; q=1.0';
        $accept        = $this->negotiator->getBest($acceptCharset, array('en', 'fr'));

        $this->assertInstanceOf('Negotiation\AcceptCharset', $accept);
        $this->assertEquals('fr', $accept->getValue());
    }

    /**
     * @dataProvider dataProviderForTestGetBest
     */
    public function testGetBest($accept, $priorities, $expected)
    {
        if (is_null($expected))
            $this->expectException('Negotiation\Exception\InvalidArgument');

        $accept = $this->negotiator->getBest($accept, $priorities);
        if (null === $accept) {
            $this->assertNull($expected);
        } else {
            $this->assertInstanceOf('Negotiation\AcceptCharset', $accept);
            $this->assertSame($expected, $accept->getValue());
        }
    }

    public static function dataProviderForTestGetBest()
    {
        $pearCharset  = 'ISO-8859-1, Big5;q=0.6,utf-8;q=0.7, *;q=0.5';
        $pearCharset2 = 'ISO-8859-1, Big5;q=0.6,utf-8;q=0.7';

        return array(
            array($pearCharset, array( 'utf-8', 'big5', 'iso-8859-1', 'shift-jis',), 'iso-8859-1'),
            array($pearCharset, array( 'utf-8', 'big5', 'shift-jis',), 'utf-8'),
            array($pearCharset, array( 'Big5', 'shift-jis',), 'Big5'),
            array($pearCharset, array( 'shift-jis',), 'shift-jis'),
            array($pearCharset2, array( 'utf-8', 'big5', 'iso-8859-1', 'shift-jis',), 'iso-8859-1'),
            array($pearCharset2, array( 'utf-8', 'big5', 'shift-jis',), 'utf-8'),
            array($pearCharset2, array( 'Big5', 'shift-jis',), 'Big5'),
            array('utf-8;q=0.6,iso-8859-5;q=0.9', array( 'iso-8859-5', 'utf-8',), 'iso-8859-5'),
            array('', array( 'iso-8859-5', 'utf-8',), null),
            array('en, *;q=0.9', array('fr'), 'fr'),
            # Quality of source factors
            array($pearCharset, array('iso-8859-1;q=0.5', 'utf-8', 'utf-16;q=1.0'), 'utf-8'),
            array($pearCharset, array('iso-8859-1;q=0.8', 'utf-8', 'utf-16;q=1.0'), 'iso-8859-1;q=0.8'),
        );
    }

    public function testGetBestRespectsPriorities()
    {
        $accept = $this->negotiator->getBest('foo, bar, yo', array('yo'));

        $this->assertInstanceOf('Negotiation\AcceptCharset', $accept);
        $this->assertEquals('yo', $accept->getValue());
    }

    public function testGetBestDoesNotMatchPriorities()
    {
        $acceptCharset = 'en, de';
        $priorities           = array('fr');

        $this->assertNull($this->negotiator->getBest($acceptCharset, $priorities));
    }

    public function testGetBestRespectsQualityOfSource()
    {
        $accept = $this->negotiator->getBest('utf-8;q=0.5,iso-8859-1', array('iso-8859-1;q=0.3', 'utf-8;q=0.9', 'utf-16;q=1.0'));
        $this->assertInstanceOf('Negotiation\AcceptCharset', $accept);
        $this->assertEquals('utf-8', $accept->getType());
    }

    /**
     * @dataProvider dataProviderForTestParseHeader
     */
    public function testParseHeader($header, $expected)
    {
        $accepts = $this->call_private_method('Negotiation\CharsetNegotiator', 'parseHeader', $this->negotiator, array($header));

        $this->assertSame($expected, $accepts);
    }

    public static function dataProviderForTestParseHeader()
    {
        return array(
            array('*;q=0.3,ISO-8859-1,utf-8;q=0.7', array('*;q=0.3', 'ISO-8859-1', 'utf-8;q=0.7')),
            array('*;q=0.3,ISO-8859-1;q=0.7,utf-8;q=0.7', array('*;q=0.3', 'ISO-8859-1;q=0.7', 'utf-8;q=0.7')),
            array('*;q=0.3,utf-8;q=0.7,ISO-8859-1;q=0.7', array('*;q=0.3', 'utf-8;q=0.7', 'ISO-8859-1;q=0.7')),
            array('iso-8859-5, unicode-1-1;q=0.8', array('iso-8859-5', 'unicode-1-1;q=0.8')),
        );
    }
}
                                                       tests/Negotiation/Tests/NegotiatorTest.php                                                          0000755                 00000030707 00000000000 0014727 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\Exception\InvalidArgument;
use Negotiation\Exception\InvalidMediaType;
use Negotiation\Negotiator;
use Negotiation\Accept;
use Negotiation\AcceptMatch;

class NegotiatorTest extends TestCase
{

    /**
     * @var Negotiator
     */
    private $negotiator;

    protected function setUp(): void
    {
        $this->negotiator = new Negotiator();
    }

    /**
     * @dataProvider dataProviderForTestGetBest
     */
    public function testGetBest($header, $priorities, $expected)
    {
        try {
            $acceptHeader = $this->negotiator->getBest($header, $priorities);
        } catch (\Exception $e) {
            $this->assertEquals($expected, $e);

            return;
        }

        if ($acceptHeader === null) {
            $this->assertNull($expected);

            return;
        }

        $this->assertInstanceOf('Negotiation\Accept', $acceptHeader);

        $this->assertSame($expected[0], $acceptHeader->getType());
        $this->assertSame($expected[1], $acceptHeader->getParameters());
    }

    public static function dataProviderForTestGetBest()
    {
        $pearAcceptHeader = 'text/html,application/xhtml+xml,application/xml;q=0.9,text/*;q=0.7,*/*,image/gif; q=0.8, image/jpeg; q=0.6, image/*';
        $rfcHeader = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5';

        return array(
            # exceptions
            array('/qwer', array('f/g'), null),
            array('/qwer,f/g', array('f/g'), array('f/g', array())),
            array('foo/bar', array('/qwer'), new InvalidMediaType()),
            array('', array('foo/bar'), new InvalidArgument('The header string should not be empty.')),
            array('*/*', array(), new InvalidArgument('A set of server priorities should be given.')),

            # See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
            array($rfcHeader, array('text/html;level=1'), array('text/html', array('level' => '1'))),
            array($rfcHeader, array('text/html'), array('text/html', array())),
            array($rfcHeader, array('text/plain'), array('text/plain', array())),
            array($rfcHeader, array('image/jpeg',), array('image/jpeg', array())),
            array($rfcHeader, array('text/html;level=2'), array('text/html', array('level' => '2'))),
            array($rfcHeader, array('text/html;level=3'), array('text/html', array( 'level' => '3'))),

            array('text/*;q=0.7, text/html;q=0.3, */*;q=0.5, image/png;q=0.4', array('text/html', 'image/png'), array('image/png', array())),
            array('image/png;q=0.1, text/plain, audio/ogg;q=0.9', array('image/png', 'text/plain', 'audio/ogg'), array('text/plain', array())),
            array('image/png, text/plain, audio/ogg', array('baz/asdf'), null),
            array('image/png, text/plain, audio/ogg', array('audio/ogg'), array('audio/ogg', array())),
            array('image/png, text/plain, audio/ogg', array('YO/SuP'), null),
            array('text/html; charset=UTF-8, application/pdf', array('text/html; charset=UTF-8'), array('text/html', array('charset' => 'UTF-8'))),
            array('text/html; charset=UTF-8, application/pdf', array('text/html'), null),
            array('text/html, application/pdf', array('text/html; charset=UTF-8'), array('text/html', array('charset' => 'UTF-8'))),
            # PEAR HTTP2 tests - have been altered from original!
            array($pearAcceptHeader, array('image/gif', 'image/png', 'application/xhtml+xml', 'application/xml', 'text/html', 'image/jpeg', 'text/plain',), array('image/png', array())),
            array($pearAcceptHeader, array('image/gif', 'application/xhtml+xml', 'application/xml', 'image/jpeg', 'text/plain',), array('application/xhtml+xml', array())),
            array($pearAcceptHeader, array('image/gif', 'application/xml', 'image/jpeg', 'text/plain',), array('application/xml', array())),
            array($pearAcceptHeader, array('image/gif', 'image/jpeg', 'text/plain'), array('image/gif', array())),
            array($pearAcceptHeader, array('text/plain', 'image/png', 'image/jpeg'), array('image/png', array())),
            array($pearAcceptHeader, array('image/jpeg', 'image/gif',), array('image/gif', array())),
            array($pearAcceptHeader, array('image/png',), array('image/png', array())),
            array($pearAcceptHeader, array('audio/midi',), array('audio/midi', array())),
            array('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', array( 'application/rss+xml'), array('application/rss+xml', array())),
            # LWS / case sensitivity
            array('text/* ; q=0.3, TEXT/html ;Q=0.7, text/html ; level=1, texT/Html ;leVel = 2 ;q=0.4, */* ; q=0.5', array( 'text/html; level=2'), array('text/html', array( 'level' => '2'))),
            array('text/* ; q=0.3, text/html;Q=0.7, text/html ;level=1, text/html; level=2;q=0.4, */*;q=0.5', array( 'text/HTML; level=3'), array('text/html', array( 'level' => '3'))),
            # Incompatible
            array('text/html', array( 'application/rss'), null),
            # IE8 Accept header
            array('image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, */*', array( 'text/html', 'application/xhtml+xml'), array('text/html', array())),
            # Quality of source factors
            array($rfcHeader, array('text/html;q=0.4', 'text/plain'), array('text/plain', array())),
            # Wildcard "plus" parts (e.g., application/vnd.api+json)
            array('application/vnd.api+json', array('application/json', 'application/*+json'), array('application/*+json', array())),
            array('application/json;q=0.7, application/*+json;q=0.7', array('application/hal+json', 'application/problem+json'), array('application/hal+json', array())),
            array('application/json;q=0.7, application/problem+*;q=0.7', array('application/hal+xml', 'application/problem+xml'), array('application/problem+xml', array())),
            array($pearAcceptHeader, array('application/*+xml'), array('application/*+xml', array())),
            # @see https://github.com/willdurand/Negotiation/issues/93
            array('application/hal+json', array('application/ld+json', 'application/hal+json', 'application/xml', 'text/xml', 'application/json', 'text/html'), array('application/hal+json', array())),
        );
    }

    /**
     * @dataProvider dataProviderForTestGetOrderedElements
     */
    public function testGetOrderedElements($header, $expected)
    {
        try {
            $elements = $this->negotiator->getOrderedElements($header);
        } catch (\Exception $e) {
            $this->assertEquals($expected, $e);

            return;
        }

        if (empty($elements)) {
            $this->assertNull($expected);

            return;
        }

        $this->assertInstanceOf('Negotiation\Accept', $elements[0]);

        foreach ($expected as $key => $item) {
            $this->assertSame($item, $elements[$key]->getValue());
        }
    }

    public static function dataProviderForTestGetOrderedElements()
    {
        return array(
            // error cases
            array('', new InvalidArgument('The header string should not be empty.')),
            array('/qwer', null),

            // first one wins as no quality modifiers
            array('text/html, text/xml', array('text/html', 'text/xml')),

            // ordered by quality modifier
            array(
                'text/html;q=0.3, text/html;q=0.7',
                array('text/html;q=0.7', 'text/html;q=0.3')
            ),
            // ordered by quality modifier - the one with no modifier wins, level not taken into account
            array(
                'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5',
                array('text/html;level=1', 'text/html;q=0.7', '*/*;q=0.5', 'text/html;level=2;q=0.4', 'text/*;q=0.3')
            ),
        );
    }

    public function testGetBestRespectsQualityOfSource()
    {
        $accept = $this->negotiator->getBest('text/html,text/*;q=0.7', array('text/html;q=0.5', 'text/plain;q=0.9'));
        $this->assertInstanceOf('Negotiation\Accept', $accept);
        $this->assertEquals('text/plain', $accept->getType());
    }

    public function testGetBestInvalidMediaType()
    {
        $this->expectException(\Negotiation\Exception\InvalidMediaType::class);
        $header = 'sdlfkj20ff; wdf';
        $priorities = array('foo/qwer');

        $this->negotiator->getBest($header, $priorities, true);
    }

    /**
     * @dataProvider dataProviderForTestParseHeader
     */
    public function testParseHeader($header, $expected)
    {
        $accepts = $this->call_private_method('Negotiation\Negotiator', 'parseHeader', $this->negotiator, array($header));

        $this->assertSame($expected, $accepts);
    }

    public static function dataProviderForTestParseHeader()
    {
        return array(
            array('text/html ;   q=0.9', array('text/html ;   q=0.9')),
            array('text/html,application/xhtml+xml', array('text/html', 'application/xhtml+xml')),
            array(',,text/html;q=0.8 , , ', array('text/html;q=0.8')),
            array('text/html;charset=utf-8; q=0.8', array('text/html;charset=utf-8; q=0.8')),
            array('text/html; foo="bar"; q=0.8 ', array('text/html; foo="bar"; q=0.8')),
            array('text/html; foo="bar"; qwer="asdf", image/png', array('text/html; foo="bar"; qwer="asdf"', "image/png")),
            array('text/html ; quoted_comma="a,b  ,c,",application/xml;q=0.9,*/*;charset=utf-8; q=0.8', array('text/html ; quoted_comma="a,b  ,c,"', 'application/xml;q=0.9', '*/*;charset=utf-8; q=0.8')),
            array('text/html, application/json;q=0.8, text/csv;q=0.7', array('text/html', 'application/json;q=0.8', 'text/csv;q=0.7'))
        );
    }

    /**
     * @dataProvider dataProviderForTestFindMatches
     */
    public function testFindMatches($headerParts, $priorities, $expected)
    {
        $neg = new Negotiator();

        $matches = $this->call_private_method('Negotiation\Negotiator', 'findMatches', $neg, array($headerParts, $priorities));

        $this->assertEquals($expected, $matches);
    }

    public static function dataProviderForTestFindMatches()
    {
        return array(
            array(
                array(new Accept('text/html; charset=UTF-8'), new Accept('image/png; foo=bar; q=0.7'), new Accept('*/*; foo=bar; q=0.4')),
                array(new Accept('text/html; charset=UTF-8'), new Accept('image/png; foo=bar'), new Accept('application/pdf')),
                array(
                    new AcceptMatch(1.0, 111, 0),
                    new AcceptMatch(0.7, 111, 1),
                    new AcceptMatch(0.4, 1,   1),
                )
            ),
            array(
                array(new Accept('text/html'), new Accept('image/*; q=0.7')),
                array(new Accept('text/html; asfd=qwer'), new Accept('image/png'), new Accept('application/pdf')),
                array(
                    new AcceptMatch(1.0, 110, 0),
                    new AcceptMatch(0.7, 100, 1),
                )
            ),
            array( # https://tools.ietf.org/html/rfc7231#section-5.3.2
                array(new Accept('text/*; q=0.3'), new Accept('text/html; q=0.7'), new Accept('text/html; level=1'), new Accept('text/html; level=2; q=0.4'), new Accept('*/*; q=0.5')),
                array(new Accept('text/html; level=1'), new Accept('text/html'), new Accept('text/plain'), new Accept('image/jpeg'), new Accept('text/html; level=2'), new Accept('text/html; level=3')),
                array(
                    new AcceptMatch(0.3,    100,    0),
                    new AcceptMatch(0.7,    110,    0),
                    new AcceptMatch(1.0,    111,    0),
                    new AcceptMatch(0.5,      0,    0),
                    new AcceptMatch(0.3,    100,    1),
                    new AcceptMatch(0.7,    110,    1),
                    new AcceptMatch(0.5,      0,    1),
                    new AcceptMatch(0.3,    100,    2),
                    new AcceptMatch(0.5,      0,    2),
                    new AcceptMatch(0.5,      0,    3),
                    new AcceptMatch(0.3,    100,    4),
                    new AcceptMatch(0.7,    110,    4),
                    new AcceptMatch(0.4,    111,    4),
                    new AcceptMatch(0.5,      0,    4),
                    new AcceptMatch(0.3,    100,    5),
                    new AcceptMatch(0.7,    110,    5),
                    new AcceptMatch(0.5,      0,    5),
                )
            )
        );
    }
}
                                                         tests/Negotiation/Tests/AcceptLanguageTest.php                                                      0000755                 00000002267 00000000000 0015457 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\AcceptLanguage;

class AcceptLanguageTest extends TestCase
{

    /**
     * @dataProvider dataProviderForGetType
     */
    public function testGetType($header, $expected)
    {
        $accept = new AcceptLanguage($header);
        $actual = $accept->getType();
        $this->assertEquals($expected, $actual);
    }

    public static function dataProviderForGetType()
    {
        return array(
           array('en;q=0.7', 'en'),
           array('en-GB;q=0.8', 'en-gb'),
           array('da', 'da'),
           array('en-gb;q=0.8', 'en-gb'),
           array('es;q=0.7', 'es'),
           array('fr ; q= 0.1', 'fr'),
           array('', null),
           array(null, null),
       );
    }

    /**
     * @dataProvider dataProviderForGetValue
     */
    public function testGetValue($header, $expected)
    {
        $accept = new AcceptLanguage($header);
        $actual = $accept->getValue();
        $this->assertEquals($expected, $actual);

    }

    public static function dataProviderForGetValue()
    {
        return array(
           array('en;q=0.7', 'en;q=0.7'),
           array('en-GB;q=0.8', 'en-GB;q=0.8'),
        );
    }
}
                                                                                                                                                                                                                                                                                                                                         tests/Negotiation/Tests/BaseAcceptTest.php                                                          0000755                 00000004215 00000000000 0014601 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\BaseAccept;

class BaseAcceptTest extends TestCase
{
    /**
     * @dataProvider dataProviderForParseParameters
     */
    public function testParseParameters($value, $expected)
    {
        $accept     = new DummyAccept($value);
        $parameters = $accept->getParameters();

        // TODO: hack-ish... this is needed because logic in BaseAccept
        //constructor drops the quality from the parameter set.
        if (false !== strpos($value, 'q')) {
            $parameters['q'] = $accept->getQuality();
        }

        $this->assertCount(count($expected), $parameters);

        foreach ($expected as $key => $value) {
            $this->assertArrayHasKey($key, $parameters);
            $this->assertEquals($value, $parameters[$key]);
        }
    }

    public static function dataProviderForParseParameters()
    {
        return array(
            array(
                'application/json ;q=1.0; level=2;foo= bar',
                array(
                    'q' => 1.0,
                    'level' => 2,
                    'foo'   => 'bar',
                ),
            ),
            array(
                'application/json ;q = 1.0; level = 2;     FOO  = bAr',
                array(
                    'q' => 1.0,
                    'level' => 2,
                    'foo'   => 'bAr',
                ),
            ),
            array(
                'application/json;q=1.0',
                array(
                    'q' => 1.0,
                ),
            ),
            array(
                'application/json;foo',
                array(),
            ),
        );
    }

    /**
     * @dataProvider dataProviderBuildParametersString
     */

    public function testBuildParametersString($value, $expected)
    {
        $accept = new DummyAccept($value);

        $this->assertEquals($expected, $accept->getNormalizedValue());
    }

    public static function dataProviderBuildParametersString()
    {
        return array(
            array('media/type; xxx = 1.0;level=2;foo=bar', 'media/type; foo=bar; level=2; xxx=1.0'),
        );
    }
}

class DummyAccept extends BaseAccept
{
}
                                                                                                                                                                                                                                                                                                                                                                                   tests/Negotiation/Tests/TestCase.php                                                                0000755                 00000000577 00000000000 0013471 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use PHPUnit\Framework\TestCase as PHPUnitTestCase;

abstract class TestCase extends PHPUnitTestCase
{
    protected function call_private_method($class, $method, $object, $params)
    {
        $method = new \ReflectionMethod($class, $method);

        $method->setAccessible(true);

        return $method->invokeArgs($object, $params);
    }
}
                                                                                                                                 tests/Negotiation/Tests/AcceptTest.php                                                              0000755                 00000005177 00000000000 0014016 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\Accept;

class AcceptTest extends TestCase
{
    public function testGetParameter()
    {
        $accept = new Accept('foo/bar; q=1; hello=world');

        $this->assertTrue($accept->hasParameter('hello'));
        $this->assertEquals('world', $accept->getParameter('hello'));
        $this->assertFalse($accept->hasParameter('unknown'));
        $this->assertNull($accept->getParameter('unknown'));
        $this->assertFalse($accept->getParameter('unknown', false));
        $this->assertSame('world', $accept->getParameter('hello', 'goodbye'));
    }

    /**
     * @dataProvider dataProviderForTestGetNormalizedValue
     */
    public function testGetNormalizedValue($header, $expected)
    {
        $accept = new Accept($header);
        $actual = $accept->getNormalizedValue();
        $this->assertEquals($expected, $actual);
    }

    public static function dataProviderForTestGetNormalizedValue()
    {
        return array(
            array('text/html; z=y; a=b; c=d', 'text/html; a=b; c=d; z=y'),
            array('application/pdf; q=1; param=p',  'application/pdf; param=p')
        );
    }

    /**
     * @dataProvider dataProviderForGetType
     */
    public function testGetType($header, $expected)
    {
        $accept = new Accept($header);
        $actual = $accept->getType();
        $this->assertEquals($expected, $actual);
    }

    public static function dataProviderForGetType()
    {
        return array(
            array('text/html;hello=world', 'text/html'),
            array('application/pdf', 'application/pdf'),
            array('application/xhtml+xml;q=0.9', 'application/xhtml+xml'),
            array('text/plain; q=0.5', 'text/plain'),
            array('text/html;level=2;q=0.4', 'text/html'),
            array('text/html ; level = 2   ; q = 0.4', 'text/html'),
            array('text/*', 'text/*'),
            array('text/* ;q=1 ;level=2', 'text/*'),
            array('*/*', '*/*'),
            array('*', '*/*'),
            array('*/* ; param=555', '*/*'),
            array('* ; param=555', '*/*'),
            array('TEXT/hTmL;leVel=2; Q=0.4', 'text/html'),
        );
    }

    /**
     * @dataProvider dataProviderForGetValue
     */
    public function testGetValue($header, $expected)
    {
        $accept = new Accept($header);
        $actual = $accept->getValue();
        $this->assertEquals($expected, $actual);

    }

    public static function dataProviderForGetValue()
    {
        return array(
            array('text/html;hello=world  ;q=0.5', 'text/html;hello=world  ;q=0.5'),
            array('application/pdf', 'application/pdf'),
        );
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                 tests/Negotiation/Tests/LanguageNegotiatorTest.php                                                  0000755                 00000006762 00000000000 0016377 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\Exception\InvalidArgument;
use Negotiation\LanguageNegotiator;

class LanguageNegotiatorTest extends TestCase
{

    /**
     * @var LanguageNegotiator
     */
    private $negotiator;

    protected function setUp(): void
    {
        $this->negotiator = new LanguageNegotiator();
    }

    /**
     * @dataProvider dataProviderForTestGetBest
     */
    public function testGetBest($accept, $priorities, $expected)
    {
        try {
            $accept = $this->negotiator->getBest($accept, $priorities);

            if (null === $accept) {
                $this->assertNull($expected);
            } else {
                $this->assertInstanceOf('Negotiation\AcceptLanguage', $accept);
                $this->assertEquals($expected, $accept->getValue());
            }
        } catch (\Exception $e) {
            $this->assertEquals($expected, $e);
        }
    }

    public static function dataProviderForTestGetBest()
    {
        return array(
            array('en, de', array('fr'), null),
            array('foo, bar, yo', array('baz', 'biz'), null),
            array('fr-FR, en;q=0.8', array('en-US', 'de-DE'), 'en-US'),
            array('en, *;q=0.9', array('fr'), 'fr'),
            array('foo, bar, yo', array('yo'), 'yo'),
            array('en; q=0.1, fr; q=0.4, bu; q=1.0', array('en', 'fr'), 'fr'),
            array('en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2', array('en', 'fu'), 'fu'),
            array('', array('en', 'fu'), new InvalidArgument('The header string should not be empty.')),
            array('fr, zh-Hans-CN;q=0.3', array('fr'), 'fr'),
            # Quality of source factors
            array('en;q=0.5,de', array('de;q=0.3', 'en;q=0.9'), 'en;q=0.9'),
            # Generic fallback
            array('fr-FR, en-US;q=0.8', array('fr'), 'fr'),
            array('fr-FR, en-US;q=0.8', array('fr', 'en-US'), 'fr'),
            array('fr-FR, en-US;q=0.8', array('fr-CA', 'en'), 'en'),
        );
    }

    public function testGetBestRespectsQualityOfSource()
    {
        $accept = $this->negotiator->getBest('en;q=0.5,de', array('de;q=0.3', 'en;q=0.9'));
        $this->assertInstanceOf('Negotiation\AcceptLanguage', $accept);
        $this->assertEquals('en', $accept->getType());
    }

    /**
     * @dataProvider dataProviderForTestParseHeader
     */
    public function testParseHeader($header, $expected)
    {
        $accepts = $this->call_private_method('Negotiation\Negotiator', 'parseHeader', $this->negotiator, array($header));

        $this->assertSame($expected, $accepts);
    }

    public static function dataProviderForTestParseHeader()
    {
        return array(
            array('en; q=0.1, fr; q=0.4, bu; q=1.0', array('en; q=0.1', 'fr; q=0.4', 'bu; q=1.0')),
            array('en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2', array('en; q=0.1', 'fr; q=0.4', 'fu; q=0.9', 'de; q=0.2')),
        );
    }

    /**
     * Given a accept header containing specific languages (here 'en-US', 'fr-FR')
     *  And priorities containing a generic version of that language
     * Then the best language is mapped to the generic one here 'fr'
     */
    public function testSpecificLanguageAreMappedToGeneric()
    {
        $acceptLanguageHeader = 'fr-FR, en-US;q=0.8';
        $priorities           = array('fr');

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

        $this->assertInstanceOf('Negotiation\AcceptHeader', $acceptHeader);
        $this->assertEquals('fr', $acceptHeader->getValue());
    }
}
              tests/Negotiation/Tests/MatchTest.php                                                               0000755                 00000003363 00000000000 0013646 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Tests;

use Negotiation\AcceptMatch;

class MatchTest extends TestCase
{
    /**
     * @dataProvider dataProviderForTestCompare
     */
    public function testCompare($match1, $match2, $expected)
    {
        $this->assertEquals($expected, AcceptMatch::compare($match1, $match2));
    }

    public static function dataProviderForTestCompare()
    {
        return array(
            array(new AcceptMatch(1.0, 110, 1), new AcceptMatch(1.0, 111, 1),    0),
            array(new AcceptMatch(0.1, 10,  1), new AcceptMatch(0.1,  10, 2),   -1),
            array(new AcceptMatch(0.5, 110, 5), new AcceptMatch(0.5,  11, 4),    1),
            array(new AcceptMatch(0.4, 110, 1), new AcceptMatch(0.6, 111, 3),    1),
            array(new AcceptMatch(0.6, 110, 1), new AcceptMatch(0.4, 111, 3),   -1),
        );
    }

    /**
     * @dataProvider dataProviderForTestReduce
     */
    public function testReduce($carry, $match, $expected)
    {
        $this->assertEquals($expected, AcceptMatch::reduce($carry, $match));
    }

    public static function dataProviderForTestReduce()
    {
        return array(
            array(
                array(1 => new AcceptMatch(1.0, 10, 1)),
                new AcceptMatch(0.5, 111, 1),
                array(1 => new AcceptMatch(0.5, 111, 1)),
            ),
            array(
                array(1 => new AcceptMatch(1.0, 110, 1)),
                new AcceptMatch(0.5, 11, 1),
                array(1 => new AcceptMatch(1.0, 110, 1)),
            ),
            array(
                array(0 => new AcceptMatch(1.0, 10, 1)),
                new AcceptMatch(0.5, 111, 1),
                array(0 => new AcceptMatch(1.0, 10, 1), 1 => new AcceptMatch(0.5, 111, 1)),
            ),
        );
    }
}
                                                                                                                                                                                                                                                                             tests/bootstrap.php                                                                                 0000755                 00000000534 00000000000 0010402 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

if (! ($loader = @include __DIR__ . '/../vendor/autoload.php')) {
    die(<<<EOT
You need to install the project dependencies using Composer:
$ wget http://getcomposer.org/composer.phar
OR
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install --dev
$ phpunit
EOT
    );
}

$loader->add('Negotiation\Tests', __DIR__);
                                                                                                                                                                    README.md                                                                                           0000755                 00000012340 00000000000 0005767 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       Negotiation
===========

[![GitHub Actions](https://github.com/willdurand/Negotiation/workflows/ci/badge.svg)](https://github.com/willdurand/Negotiation/actions?query=workflow%3A%22ci%22+branch%3Amaster)
[![Total
Downloads](https://poser.pugx.org/willdurand/Negotiation/downloads.png)](https://packagist.org/packages/willdurand/Negotiation)
[![Latest Stable
Version](https://poser.pugx.org/willdurand/Negotiation/v/stable.png)](https://packagist.org/packages/willdurand/Negotiation)

**Negotiation** is a standalone library without any dependencies that allows you
to implement [content
negotiation](https://tools.ietf.org/html/rfc7231#section-5.3) in your
application, whatever framework you use.  This library is based on [RFC
7231](https://tools.ietf.org/html/rfc7231). 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](https://github.com/willdurand/Negotiation/blob/1.x/README.md#usage).
>
> Documentation for version **2.x** is available here: [Negotiation 2.x
> documentation](https://github.com/willdurand/Negotiation/blob/2.x/README.md#usage).


Installation
------------

The recommended way to install Negotiation is through
[Composer](http://getcomposer.org/):

```bash
$ composer require willdurand/negotiation
```


Usage Examples
--------------

### Media Type Negotiation

``` php
$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
<?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
<?php

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

The `EncodingNegotiator` returns an instance of `AcceptEncoding`.

### Charset Negotiation

``` php
<?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](http://semver.org/).

### End Of Life

#### 1.x

As of October 2016, [branch
`1.x`](https://github.com/willdurand/Negotiation/tree/1.x) is not supported
anymore, meaning major version `1` reached end of life. Last version is:
[1.5.0](https://github.com/willdurand/Negotiation/releases/tag/1.5.0).

#### 2.x

As of November 2020, [branch
`2.x`](https://github.com/willdurand/Negotiation/tree/2.x) is not supported
anymore, meaning major version `2` reached end of life. Last version is:
[2.3.1](https://github.com/willdurand/Negotiation/releases/tag/v2.3.1).

### Stable Version

#### 3.x (and `dev-master`)

Negotiation [3.0](https://github.com/willdurand/Negotiation/releases/tag/3.0.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](CONTRIBUTING.md) file.


Credits
-------

* Some parts of this library are inspired by:

    * [Symfony](http://github.com/symfony/symfony) framework;
    * [FOSRest](http://github.com/FriendsOfSymfony/FOSRest);
    * [PEAR HTTP2](https://github.com/pear/HTTP2).

* [William Durand](https://github.com/willdurand)
* [@neural-wetware](https://github.com/neural-wetware)


License
-------

Negotiation is released under the MIT License. See the bundled LICENSE file for
details.
                                                                                                                                                                                                                                                                                                src/Negotiation/AcceptMatch.php                                                                     0000755                 00000002256 00000000000 0012451 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

final class AcceptMatch
{
    /**
     * @var float
     */
    public $quality;

    /**
     * @var int
     */
    public $score;

    /**
     * @var int
     */
    public $index;

    public function __construct($quality, $score, $index)
    {
        $this->quality = $quality;
        $this->score   = $score;
        $this->index   = $index;
    }

    /**
     * @param AcceptMatch $a
     * @param AcceptMatch $b
     *
     * @return int
     */
    public static function compare(AcceptMatch $a, AcceptMatch $b)
    {
        if ($a->quality !== $b->quality) {
            return $a->quality > $b->quality ? -1 : 1;
        }

        if ($a->index !== $b->index) {
            return $a->index > $b->index ? 1 : -1;
        }

        return 0;
    }

    /**
     * @param array   $carry reduced array
     * @param AcceptMatch $match match to be reduced
     *
     * @return AcceptMatch[]
     */
    public static function reduce(array $carry, AcceptMatch $match)
    {
        if (!isset($carry[$match->index]) || $carry[$match->index]->score < $match->score) {
            $carry[$match->index] = $match;
        }

        return $carry;
    }
}
                                                                                                                                                                                                                                                                                                                                                  src/Negotiation/Exception/InvalidHeader.php                                                         0000755                 00000000160 00000000000 0014722 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Exception;

class InvalidHeader extends \RuntimeException implements Exception
{
}
                                                                                                                                                                                                                                                                                                                                                                                                                src/Negotiation/Exception/InvalidMediaType.php                                                      0000755                 00000000163 00000000000 0015416 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Exception;

class InvalidMediaType extends \RuntimeException implements Exception
{
}
                                                                                                                                                                                                                                                                                                                                                                                                             src/Negotiation/Exception/InvalidArgument.php                                                       0000755                 00000000172 00000000000 0015317 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Exception;

class InvalidArgument extends \InvalidArgumentException implements Exception
{
}
                                                                                                                                                                                                                                                                                                                                                                                                      src/Negotiation/Exception/InvalidLanguage.php                                                       0000755                 00000000162 00000000000 0015257 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Exception;

class InvalidLanguage extends \RuntimeException implements Exception
{
}
                                                                                                                                                                                                                                                                                                                                                                                                              src/Negotiation/Exception/Exception.php                                                             0000755                 00000000101 00000000000 0014154 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation\Exception;

interface Exception
{
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                               src/Negotiation/CharsetNegotiator.php                                                               0000755                 00000000340 00000000000 0013712 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

class CharsetNegotiator extends AbstractNegotiator
{
    /**
     * {@inheritdoc}
     */
    protected function acceptFactory($accept)
    {
        return new AcceptCharset($accept);
    }
}
                                                                                                                                                                                                                                                                                                src/Negotiation/LanguageNegotiator.php                                                              0000755                 00000002063 00000000000 0014050 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

class LanguageNegotiator extends AbstractNegotiator
{
    /**
     * {@inheritdoc}
     */
    protected function acceptFactory($accept)
    {
        return new AcceptLanguage($accept);
    }

    /**
     * {@inheritdoc}
     */
    protected function match(AcceptHeader $acceptLanguage, AcceptHeader $priority, $index)
    {
        if (!$acceptLanguage instanceof AcceptLanguage || !$priority instanceof AcceptLanguage) {
            return null;
        }

        $ab = $acceptLanguage->getBasePart();
        $pb = $priority->getBasePart();

        $as = $acceptLanguage->getSubPart();
        $ps = $priority->getSubPart();

        $baseEqual = !strcasecmp((string)$ab, (string)$pb);
        $subEqual  = !strcasecmp((string)$as, (string)$ps);

        if (($ab == '*' || $baseEqual) && ($as === null || $subEqual || null === $ps)) {
            $score = 10 * $baseEqual + $subEqual;

            return new AcceptMatch($acceptLanguage->getQuality() * $priority->getQuality(), $score, $index);
        }

        return null;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                             src/Negotiation/AcceptCharset.php                                                                   0000755                 00000000150 00000000000 0012775 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

final class AcceptCharset extends BaseAccept implements AcceptHeader
{
}
                                                                                                                                                                                                                                                                                                                                                                                                                        src/Negotiation/AbstractNegotiator.php                                                              0000755                 00000012000 00000000000 0014060 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

use Negotiation\Exception\InvalidArgument;
use Negotiation\Exception\InvalidHeader;

abstract class AbstractNegotiator
{
    /**
     * @param string $header     A string containing an `Accept|Accept-*` header.
     * @param array  $priorities A set of server priorities.
     *
     * @return AcceptHeader|null best matching type
     */
    public function getBest($header, array $priorities, $strict = false)
    {
        if (empty($priorities)) {
            throw new InvalidArgument('A set of server priorities should be given.');
        }

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

        // Once upon a time, two `array_map` calls were sitting there, but for
        // some reasons, they triggered `E_WARNING` time to time (because of
        // PHP bug [55416](https://bugs.php.net/bug.php?id=55416). Now, they
        // are gone.
        // See: https://github.com/willdurand/Negotiation/issues/81
        $acceptedHeaders = array();
        foreach ($this->parseHeader($header) as $h) {
            try {
                $acceptedHeaders[] = $this->acceptFactory($h);
            } catch (Exception\Exception $e) {
                if ($strict) {
                    throw $e;
                }
            }
        }
        $acceptedPriorities = array();
        foreach ($priorities as $p) {
            $acceptedPriorities[] = $this->acceptFactory($p);
        }
        $matches         = $this->findMatches($acceptedHeaders, $acceptedPriorities);
        $specificMatches = array_reduce($matches, 'Negotiation\AcceptMatch::reduce', []);

        usort($specificMatches, 'Negotiation\AcceptMatch::compare');

        $match = array_shift($specificMatches);

        return null === $match ? null : $acceptedPriorities[$match->index];
    }

    /**
     * @param string $header A string containing an `Accept|Accept-*` header.
     *
     * @return AcceptHeader[] An ordered list of accept header elements
     */
    public function getOrderedElements($header)
    {
        if (!$header) {
            throw new InvalidArgument('The header string should not be empty.');
        }

        $elements = array();
        $orderKeys = array();
        foreach ($this->parseHeader($header) as $key => $h) {
            try {
                $element = $this->acceptFactory($h);
                $elements[] = $element;
                $orderKeys[] = [$element->getQuality(), $key, $element->getValue()];
            } catch (Exception\Exception $e) {
                // silently skip in case of invalid headers coming in from a client
            }
        }

        // sort based on quality and then original order. This is necessary as
        // to ensure that the first in the list for two items with the same
        // quality stays in that order in both PHP5 and PHP7.
        uasort($orderKeys, function ($a, $b) {
            $qA = $a[0];
            $qB = $b[0];

            if ($qA == $qB) {
                return $a[1] <=> $b[1];
            }

            return ($qA > $qB) ? -1 : 1;
        });

        $orderedElements = [];
        foreach ($orderKeys as $key) {
            $orderedElements[] = $elements[$key[1]];
        }

        return $orderedElements;
    }

    /**
     * @param string $header accept header part or server priority
     *
     * @return AcceptHeader Parsed header object
     */
    abstract protected function acceptFactory($header);

    /**
     * @param AcceptHeader $header
     * @param AcceptHeader $priority
     * @param integer      $index
     *
     * @return AcceptMatch|null Headers matched
     */
    protected function match(AcceptHeader $header, AcceptHeader $priority, $index)
    {
        $ac = $header->getType();
        $pc = $priority->getType();

        $equal = !strcasecmp($ac, $pc);

        if ($equal || $ac === '*') {
            $score = 1 * $equal;

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

        return null;
    }

    /**
     * @param string $header A string that contains an `Accept*` header.
     *
     * @return AcceptHeader[]
     */
    private function parseHeader($header)
    {
        $res = preg_match_all('/(?:[^,"]*+(?:"[^"]*+")?)+[^,"]*+/', $header, $matches);

        if (!$res) {
            throw new InvalidHeader(sprintf('Failed to parse accept header: "%s"', $header));
        }

        return array_values(array_filter(array_map('trim', $matches[0])));
    }

    /**
     * @param AcceptHeader[] $headerParts
     * @param Priority[]     $priorities  Configured priorities
     *
     * @return AcceptMatch[] Headers matched
     */
    private function findMatches(array $headerParts, array $priorities)
    {
        $matches = [];
        foreach ($priorities as $index => $p) {
            foreach ($headerParts as $h) {
                if (null !== $match = $this->match($h, $p, $index)) {
                    $matches[] = $match;
                }
            }
        }

        return $matches;
    }
}
src/Negotiation/AcceptHeader.php                                                                    0000755                 00000000072 00000000000 0012577 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

interface AcceptHeader
{
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                      src/Negotiation/EncodingNegotiator.php                                                              0000755                 00000000342 00000000000 0014051 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

class EncodingNegotiator extends AbstractNegotiator
{
    /**
     * {@inheritdoc}
     */
    protected function acceptFactory($accept)
    {
        return new AcceptEncoding($accept);
    }
}
                                                                                                                                                                                                                                                                                              src/Negotiation/BaseAccept.php                                                                      0000755                 00000005715 00000000000 0012272 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

abstract class BaseAccept
{
    /**
     * @var float
     */
    private $quality = 1.0;

    /**
     * @var string
     */
    private $normalized;

    /**
     * @var string
     */
    private $value;

    /**
     * @var array
     */
    private $parameters;

    /**
     * @var string
     */
    protected $type;

    /**
     * @param string $value
     */
    public function __construct($value)
    {
        list($type, $parameters) = $this->parseParameters($value);

        if (isset($parameters['q'])) {
            $this->quality = (float) $parameters['q'];
            unset($parameters['q']);
        }

        $type = trim(strtolower($type));

        $this->value      = $value;
        $this->normalized = $type . ($parameters ? "; " . $this->buildParametersString($parameters) : '');
        $this->type       = $type;
        $this->parameters = $parameters;
    }

    /**
     * @return string
     */
    public function getNormalizedValue()
    {
        return $this->normalized;
    }

    /**
     * @return string
     */
    public function getValue()
    {
        return $this->value;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return float
     */
    public function getQuality()
    {
        return $this->quality;
    }

    /**
     * @return array
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * @param string $key
     * @param mixed  $default
     *
     * @return string|null
     */
    public function getParameter($key, $default = null)
    {
        return isset($this->parameters[$key]) ? $this->parameters[$key] : $default;
    }

    /**
     * @param string $key
     *
     * @return boolean
     */
    public function hasParameter($key)
    {
        return isset($this->parameters[$key]);
    }

    /**
     *
     * @param  string|null $acceptPart
     * @return array
     */
    private function parseParameters($acceptPart)
    {
        if ($acceptPart === null) {
            return ['', []];
        }

        $parts = explode(';', $acceptPart);
        $type  = array_shift($parts);

        $parameters = [];
        foreach ($parts as $part) {
            $part = explode('=', $part);

            if (2 !== count($part)) {
                continue; // TODO: throw exception here?
            }

            $key = strtolower(trim($part[0])); // TODO: technically not allowed space around "=". throw exception?
            $parameters[$key] = trim($part[1], ' "');
        }

        return [ $type, $parameters ];
    }

    /**
     * @param string $parameters
     *
     * @return string
     */
    private function buildParametersString($parameters)
    {
        $parts = [];

        ksort($parameters);
        foreach ($parameters as $key => $val) {
            $parts[] = sprintf('%s=%s', $key, $val);
        }

        return implode('; ', $parts);
    }
}
                                                   src/Negotiation/AcceptEncoding.php                                                                  0000755                 00000000151 00000000000 0013133 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

final class AcceptEncoding extends BaseAccept implements AcceptHeader
{
}
                                                                                                                                                                                                                                                                                                                                                                                                                       src/Negotiation/Accept.php                                                                          0000755                 00000001471 00000000000 0011472 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

use Negotiation\Exception\InvalidMediaType;

final class Accept extends BaseAccept implements AcceptHeader
{
    private $basePart;

    private $subPart;

    public function __construct($value)
    {
        parent::__construct($value);

        if ($this->type === '*') {
            $this->type = '*/*';
        }

        $parts = explode('/', $this->type);

        if (count($parts) !== 2 || !$parts[0] || !$parts[1]) {
            throw new InvalidMediaType();
        }

        $this->basePart = $parts[0];
        $this->subPart  = $parts[1];
    }

    /**
     * @return string
     */
    public function getSubPart()
    {
        return $this->subPart;
    }

    /**
     * @return string
     */
    public function getBasePart()
    {
        return $this->basePart;
    }
}
                                                                                                                                                                                                       src/Negotiation/Negotiator.php                                                                      0000755                 00000005501 00000000000 0012404 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

class Negotiator extends AbstractNegotiator
{
    /**
     * {@inheritdoc}
     */
    protected function acceptFactory($accept)
    {
        return new Accept($accept);
    }

    /**
     * {@inheritdoc}
     */
    protected function match(AcceptHeader $accept, AcceptHeader $priority, $index)
    {
        if (!$accept instanceof Accept || !$priority instanceof Accept) {
            return null;
        }

        $acceptBase = $accept->getBasePart();
        $priorityBase = $priority->getBasePart();

        $acceptSub = $accept->getSubPart();
        $prioritySub = $priority->getSubPart();

        $intersection = array_intersect_assoc($accept->getParameters(), $priority->getParameters());

        $baseEqual = !strcasecmp($acceptBase, $priorityBase);
        $subEqual  = !strcasecmp($acceptSub, $prioritySub);

        if (($acceptBase === '*' || $baseEqual)
            && ($acceptSub === '*' || $subEqual)
            && count($intersection) === count($accept->getParameters())
        ) {
            $score = 100 * $baseEqual + 10 * $subEqual + count($intersection);

            return new AcceptMatch($accept->getQuality() * $priority->getQuality(), $score, $index);
        }

        if (!strstr($acceptSub, '+') || !strstr($prioritySub, '+')) {
            return null;
        }

        // Handle "+" segment wildcards
        list($acceptSub, $acceptPlus) = $this->splitSubPart($acceptSub);
        list($prioritySub, $priorityPlus) = $this->splitSubPart($prioritySub);

        // If no wildcards in either the subtype or + segment, do nothing.
        if (!($acceptBase === '*' || $baseEqual)
            || !($acceptSub === '*' || $prioritySub === '*' || $acceptPlus === '*' || $priorityPlus === '*')
        ) {
            return null;
        }

        $subEqual  = !strcasecmp($acceptSub, $prioritySub);
        $plusEqual = !strcasecmp($acceptPlus, $priorityPlus);

        if (($acceptSub === '*' || $prioritySub === '*' || $subEqual)
            && ($acceptPlus === '*' || $priorityPlus === '*' || $plusEqual)
            && count($intersection) === count($accept->getParameters())
        ) {
            $score = 100 * $baseEqual + 10 * $subEqual + $plusEqual + count($intersection);

            return new AcceptMatch($accept->getQuality() * $priority->getQuality(), $score, $index);
        }

        return null;
    }

    /**
     * Split a subpart into the subpart and "plus" part.
     *
     * For media-types of the form "application/vnd.example+json", matching
     * should allow wildcards for either the portion before the "+" or
     * after. This method splits the subpart to allow such matching.
     */
    protected function splitSubPart($subPart)
    {
        if (!strstr($subPart, '+')) {
            return [$subPart, ''];
        }

        return explode('+', $subPart, 2);
    }
}
                                                                                                                                                                                               src/Negotiation/AcceptLanguage.php                                                                  0000755                 00000002047 00000000000 0013136 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace Negotiation;

use Negotiation\Exception\InvalidLanguage;

final class AcceptLanguage extends BaseAccept implements AcceptHeader
{
    private $language;
    private $script;
    private $region;

    public function __construct($value)
    {
        parent::__construct($value);

        $parts = explode('-', $this->type);

        if (2 === count($parts)) {
            $this->language = $parts[0];
            $this->region   = $parts[1];
        } elseif (1 === count($parts)) {
            $this->language = $parts[0];
        } elseif (3 === count($parts)) {
            $this->language = $parts[0];
            $this->script   = $parts[1];
            $this->region   = $parts[2];
        } else {
            // TODO: this part is never reached...
            throw new InvalidLanguage();
        }
    }

    /**
     * @return string
     */
    public function getSubPart()
    {
        return $this->region;
    }

    /**
     * @return string
     */
    public function getBasePart()
    {
        return $this->language;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         .github/workflows/ci.yaml                                                                           0000755                 00000001220 00000000000 0011357 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       name: ci

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  phpunit:
    runs-on: "ubuntu-20.04"

    strategy:
      fail-fast: false
      matrix:
        php-version:
          - "7.4"
          - "8.0"
          - "8.1"

    steps:
      - uses: actions/checkout@v2

      - name: "Install PHP ${{ matrix.php-version }}"
        uses: "shivammathur/setup-php@v2"
        with:
          php-version: "${{ matrix.php-version }}"
          coverage: "pcov"

      - name: "Install dependencies with Composer"
        uses: "ramsey/composer-install@v1"

      - name: "Run PHPUnit"
        run: "vendor/bin/simple-phpunit --coverage-text"
                                                                                                                                                                                                                                                                                                                                                                                composer.json                                                                                       0000755                 00000001440 00000000000 0007231 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       {
    "name": "willdurand/negotiation",
    "description": "Content Negotiation tools for PHP provided as a standalone library.",
    "keywords": [ "content", "negotiation", "format", "accept", "header" ],
    "license": "MIT",
    "homepage": "http://williamdurand.fr/Negotiation/",
    "authors": [
        {
            "name": "William Durand",
            "email": "will+git@drnd.me"
        }
    ],
    "require": {
        "php": ">=7.1.0"
    },
    "autoload": {
        "psr-4": { "Negotiation\\": "src/Negotiation" }
    },
    "autoload-dev": {
        "psr-4": { "Negotiation\\Tests\\": "tests/Negotiation/Tests" }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "3.0-dev"
        }
    },
    "require-dev": {
        "symfony/phpunit-bridge": "^5.0"
    }
}
                                                                                                                                                                                                                                LICENSE                                                                                             0000755                 00000002060 00000000000 0005513 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       Copyright (c) William Durand <will+git@drnd.me>

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.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                CONTRIBUTING.md                                                                                     0000755                 00000002302 00000000000 0006736 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       Contributing
============

First of all, **thank you** for contributing, **you are awesome**!

Here are a few rules to follow in order to ease code reviews, and discussions before
maintainers accept and merge your work.

You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
[PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).

You MUST run the test suite.

You MUST write (or update) unit tests.

You SHOULD write documentation.

Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request.

One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).

Also, while creating your Pull Request on GitHub, you MUST write a description
which gives the context and/or explains why you are creating it.

Thank you!
                                                                                                                                                                                                                                                                                                                              .gitignore                                                                                          0000755                 00000000055 00000000000 0006500 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /vendor/
composer.lock
.phpunit.result.cache
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   phpunit.xml.dist                                                                                    0000755                 00000001355 00000000000 0007667 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
    backupGlobals="false"
    backupStaticAttributes="false"
    colors="true"
    convertErrorsToExceptions="true"
    convertNoticesToExceptions="true"
    convertWarningsToExceptions="true"
    processIsolation="false"
    stopOnFailure="false"
    bootstrap="tests/bootstrap.php"
    >
    <testsuites>
        <testsuite name="Negotiation Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory>./src/Negotiation/</directory>
        </include>
    </coverage>
</phpunit>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                