File manager - Edit - /home/premiey/www/wp-includes/images/media/php-http.tar
Back
multipart-stream-builder/src/MultipartStreamBuilder.php 0000666 00000016730 15165515606 0017473 0 ustar 00 <?php namespace AmeliaHttp\Message\MultipartStream; use AmeliaHttp\Discovery\StreamFactoryDiscovery; use AmeliaHttp\Message\StreamFactory; use AmeliaPsr\Http\Message\StreamInterface; /** * Build your own Multipart stream. A Multipart stream is a collection of streams separated with a $bounary. This * class helps you to create a Multipart stream with stream implementations from any PSR7 library. * * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class MultipartStreamBuilder { /** * @var StreamFactory */ private $streamFactory; /** * @var MimetypeHelper */ private $mimetypeHelper; /** * @var string */ private $boundary; /** * @var array Element where each Element is an array with keys ['contents', 'headers', 'filename'] */ private $data = []; /** * @param StreamFactory|null $streamFactory */ public function __construct(StreamFactory $streamFactory = null) { $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find(); } /** * Add a resource to the Multipart Stream. * * @param string $name the formpost name * @param string|resource|StreamInterface $resource * @param array $options { * * @var array $headers additional headers ['header-name' => 'header-value'] * @var string $filename * } * * @return MultipartStreamBuilder */ public function addResource($name, $resource, array $options = []) { $stream = $this->streamFactory->createStream($resource); // validate options['headers'] exists if (!isset($options['headers'])) { $options['headers'] = []; } // Try to add filename if it is missing if (empty($options['filename'])) { $options['filename'] = null; $uri = $stream->getMetadata('uri'); if (substr($uri, 0, 6) !== 'php://') { $options['filename'] = $uri; } } $this->prepareHeaders($name, $stream, $options['filename'], $options['headers']); $this->data[] = ['contents' => $stream, 'headers' => $options['headers'], 'filename' => $options['filename']]; return $this; } /** * Build the stream. * * @return StreamInterface */ public function build() { $streams = ''; foreach ($this->data as $data) { // Add start and headers $streams .= "--{$this->getBoundary()}\r\n". $this->getHeaders($data['headers'])."\r\n"; // Convert the stream to string /* @var $contentStream StreamInterface */ $contentStream = $data['contents']; if ($contentStream->isSeekable()) { $streams .= $contentStream->__toString(); } else { $streams .= $contentStream->getContents(); } $streams .= "\r\n"; } // Append end $streams .= "--{$this->getBoundary()}--\r\n"; return $this->streamFactory->createStream($streams); } /** * Add extra headers if they are missing. * * @param string $name * @param StreamInterface $stream * @param string $filename * @param array &$headers */ private function prepareHeaders($name, StreamInterface $stream, $filename, array &$headers) { $hasFilename = $filename === '0' || $filename; // Set a default content-disposition header if one was not provided if (!$this->hasHeader($headers, 'content-disposition')) { $headers['Content-Disposition'] = sprintf('form-data; name="%s"', $name); if ($hasFilename) { $headers['Content-Disposition'] .= sprintf('; filename="%s"', $this->basename($filename)); } } // Set a default content-length header if one was not provided if (!$this->hasHeader($headers, 'content-length')) { if ($length = $stream->getSize()) { $headers['Content-Length'] = (string) $length; } } // Set a default Content-Type if one was not provided if (!$this->hasHeader($headers, 'content-type') && $hasFilename) { if ($type = $this->getMimetypeHelper()->getMimetypeFromFilename($filename)) { $headers['Content-Type'] = $type; } } } /** * Get the headers formatted for the HTTP message. * * @param array $headers * * @return string */ private function getHeaders(array $headers) { $str = ''; foreach ($headers as $key => $value) { $str .= sprintf("%s: %s\r\n", $key, $value); } return $str; } /** * Check if header exist. * * @param array $headers * @param string $key case insensitive * * @return bool */ private function hasHeader(array $headers, $key) { $lowercaseHeader = strtolower($key); foreach ($headers as $k => $v) { if (strtolower($k) === $lowercaseHeader) { return true; } } return false; } /** * Get the boundary that separates the streams. * * @return string */ public function getBoundary() { if ($this->boundary === null) { $this->boundary = uniqid('', true); } return $this->boundary; } /** * @param string $boundary * * @return MultipartStreamBuilder */ public function setBoundary($boundary) { $this->boundary = $boundary; return $this; } /** * @return MimetypeHelper */ private function getMimetypeHelper() { if ($this->mimetypeHelper === null) { $this->mimetypeHelper = new ApacheMimetypeHelper(); } return $this->mimetypeHelper; } /** * If you have custom file extension you may overwrite the default MimetypeHelper with your own. * * @param MimetypeHelper $mimetypeHelper * * @return MultipartStreamBuilder */ public function setMimetypeHelper(MimetypeHelper $mimetypeHelper) { $this->mimetypeHelper = $mimetypeHelper; return $this; } /** * Reset and clear all stored data. This allows you to use builder for a subsequent request. * * @return MultipartStreamBuilder */ public function reset() { $this->data = []; $this->boundary = null; return $this; } /** * Gets the filename from a given path. * * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character. * * @author Drupal 8.2 * * @param string $path * * @return string */ private function basename($path) { $separators = '/'; if (DIRECTORY_SEPARATOR != '/') { // For Windows OS add special separator. $separators .= DIRECTORY_SEPARATOR; } // Remove right-most slashes when $path points to directory. $path = rtrim($path, $separators); // Returns the trailing part of the $path starting after one of the directory separators. $filename = preg_match('@[^'.preg_quote($separators, '@').']+$@', $path, $matches) ? $matches[0] : ''; return $filename; } } multipart-stream-builder/src/ApacheMimetypeHelper.php 0000666 00000012244 15165515606 0017056 0 ustar 00 <?php namespace AmeliaHttp\Message\MultipartStream; /** * This class helps to find the proper mime types. The source of this file is taken * from Guzzle. * * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class ApacheMimetypeHelper implements MimetypeHelper { /** * {@inheritdoc} * * @see http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types */ public function getMimetypeFromFilename($filename) { return $this->getMimetypeFromExtension(pathinfo($filename, PATHINFO_EXTENSION)); } /** * {@inheritdoc} * * @see http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types */ public function getMimetypeFromExtension($extension) { static $mimetypes = [ '7z' => 'application/x-7z-compressed', 'aac' => 'audio/x-aac', 'ai' => 'application/postscript', 'aif' => 'audio/x-aiff', 'asc' => 'text/plain', 'asf' => 'video/x-ms-asf', 'atom' => 'application/atom+xml', 'avi' => 'video/x-msvideo', 'bmp' => 'image/bmp', 'bz2' => 'application/x-bzip2', 'cer' => 'application/pkix-cert', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'css' => 'text/css', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'deb' => 'application/x-debian-package', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dvi' => 'application/x-dvi', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'etx' => 'text/x-setext', 'flac' => 'audio/flac', 'flv' => 'video/x-flv', 'gif' => 'image/gif', 'gz' => 'application/gzip', 'htm' => 'text/html', 'html' => 'text/html', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ini' => 'text/plain', 'iso' => 'application/x-iso9660-image', 'jar' => 'application/java-archive', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'js' => 'text/javascript', 'json' => 'application/json', 'latex' => 'application/x-latex', 'log' => 'text/plain', 'm4a' => 'audio/mp4', 'm4v' => 'video/mp4', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mov' => 'video/quicktime', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4v' => 'video/mp4', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'oga' => 'audio/ogg', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'pbm' => 'image/x-portable-bitmap', 'pdf' => 'application/pdf', 'pgm' => 'image/x-portable-graymap', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'ppm' => 'image/x-portable-pixmap', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ps' => 'application/postscript', 'qt' => 'video/quicktime', 'rar' => 'application/x-rar-compressed', 'ras' => 'image/x-cmu-raster', 'rss' => 'application/rss+xml', 'rtf' => 'application/rtf', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'svg' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', 'tar' => 'application/x-tar', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'torrent' => 'application/x-bittorrent', 'ttf' => 'application/x-font-ttf', 'txt' => 'text/plain', 'wav' => 'audio/x-wav', 'webm' => 'video/webm', 'wma' => 'audio/x-ms-wma', 'wmv' => 'video/x-ms-wmv', 'woff' => 'application/x-font-woff', 'wsdl' => 'application/wsdl+xml', 'xbm' => 'image/x-xbitmap', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xml' => 'application/xml', 'xpm' => 'image/x-xpixmap', 'xwd' => 'image/x-xwindowdump', 'yaml' => 'text/yaml', 'yml' => 'text/yaml', 'zip' => 'application/zip', // Non-Apache standard 'pkpass' => 'application/vnd.apple.pkpass', 'msg' => 'application/vnd.ms-outlook', ]; $extension = strtolower($extension); return isset($mimetypes[$extension]) ? $mimetypes[$extension] : null; } } multipart-stream-builder/src/CustomMimetypeHelper.php 0000666 00000002237 15165515606 0017150 0 ustar 00 <?php namespace AmeliaHttp\Message\MultipartStream; /** * Let you add your own mimetypes. The mimetype lookup will fallback on the ApacheMimeTypeHelper. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class CustomMimetypeHelper extends ApacheMimetypeHelper { /** * @var array */ private $mimetypes = []; /** * @param array $mimetypes should be of type extension => mimetype */ public function __construct(array $mimetypes = []) { $this->mimetypes = $mimetypes; } /** * @param string $extension * @param string $mimetype * * @return $this */ public function addMimetype($extension, $mimetype) { $this->mimetypes[$extension] = $mimetype; return $this; } /** * {@inheritdoc} * * Check if we have any defined mimetypes and of not fallback to ApacheMimetypeHelper */ public function getMimetypeFromExtension($extension) { $extension = strtolower($extension); return isset($this->mimetypes[$extension]) ? $this->mimetypes[$extension] : parent::getMimetypeFromExtension($extension); } } multipart-stream-builder/src/MimetypeHelper.php 0000666 00000001070 15165515606 0015747 0 ustar 00 <?php namespace AmeliaHttp\Message\MultipartStream; /** * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ interface MimetypeHelper { /** * Determines the mimetype of a file by looking at its extension. * * @param string $filename * * @return null|string */ public function getMimetypeFromFilename($filename); /** * Maps a file extensions to a mimetype. * * @param string $extension The file extension * * @return string|null */ public function getMimetypeFromExtension($extension); } multipart-stream-builder/CHANGELOG.md 0000666 00000002017 15165515606 0013331 0 ustar 00 # Change Log ## 1.0.0 - 2017-05-21 No changes from 0.2.0. ## 0.2.0 - 2017-02-20 You may do a BC update to version 0.2.0 if you are sure that you are not adding multiple resources with the same name to the Builder. ### Fixed - Make sure one can add resources with same name without overwrite. ## 0.1.6 - 2017-02-16 ### Fixed - Performance improvements by avoid using `uniqid()`. ## 0.1.5 - 2017-02-14 ### Fixed - Support for non-readable streams. This fix was needed because flaws in Guzzle, Zend and Slims implementations of PSR-7. ## 0.1.4 - 2016-12-31 ### Added - Added support for resetting the builder ## 0.1.3 - 2016-12-22 ### Added - Added `CustomMimetypeHelper` to allow you to configure custom mimetypes. ### Changed - Using regular expression instead of `basename($filename)` because basename is depending on locale. ## 0.1.2 - 2016-08-31 ### Added - Support for Outlook msg files. ## 0.1.1 - 2016-08-10 ### Added - Support for Apple passbook. ## 0.1.0 - 2016-07-19 ### Added - Initial release multipart-stream-builder/composer.json 0000666 00000002267 15165515606 0014251 0 ustar 00 { "name": "php-http/multipart-stream-builder", "description": "A builder class that help you create a multipart stream", "license": "MIT", "keywords": ["http", "factory", "message", "stream", "multipart stream"], "homepage": "http://php-http.org", "authors": [ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com" } ], "require": { "php": "^5.5 || ^7.0", "psr/http-message": "^1.0", "php-http/message-factory": "^1.0.2", "php-http/discovery": "^1.0" }, "require-dev": { "phpunit/phpunit": "^4.8 || ^5.4", "php-http/message": "^1.5", "zendframework/zend-diactoros": "^1.3.5" }, "autoload": { "psr-4": { "Http\\Message\\MultipartStream\\": "src/" } }, "autoload-dev": { "psr-4": { "tests\\Http\\Message\\MultipartStream\\": "tests/" } }, "scripts": { "test": "vendor/bin/phpunit", "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" }, "extra": { "branch-alias": { "dev-master": "0.3-dev" } } } multipart-stream-builder/README.md 0000666 00000003101 15165515606 0012772 0 ustar 00 # PSR-7 Multipart Stream Builder [](https://github.com/php-http/multipart-stream-builder/releases) [](https://travis-ci.org/php-http/multipart-stream-builder) [](https://scrutinizer-ci.com/g/php-http/multipart-stream-builder) [](https://scrutinizer-ci.com/g/php-http/multipart-stream-builder) [](https://packagist.org/packages/php-http/multipart-stream-builder) **A builder for Multipart PSR-7 Streams. The builder create streams independenly form any PSR-7 implementation.** ## Install Via Composer ``` bash $ composer require php-http/multipart-stream-builder ``` ## Documentation Please see the [official documentation](http://php-http.readthedocs.org/en/latest/components/multipart-stream-builder.html). ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. multipart-stream-builder/appveyor.yml 0000666 00000002366 15165515606 0014117 0 ustar 00 build: false platform: - x86 - x64 clone_folder: c:\projects\php-http\multipart-stream-builder cache: - c:\tools\php -> appveyor.yml init: - SET PATH=c:\php;%PATH% - SET COMPOSER_NO_INTERACTION=1 - SET PHP=1 install: - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - cd c:\php - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-7.0.0-nts-Win32-VC14-x86.zip - IF %PHP%==1 7z x php-7.0.0-nts-Win32-VC14-x86.zip -y >nul - IF %PHP%==1 del /Q *.zip - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat - IF %PHP%==1 copy /Y php.ini-development php.ini - IF %PHP%==1 echo max_execution_time=1200 >> php.ini - IF %PHP%==1 echo date.timezone="UTC" >> php.ini - IF %PHP%==1 echo extension_dir=ext >> php.ini - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini - appveyor DownloadFile https://getcomposer.org/composer.phar - cd c:\projects\php-http\multipart-stream-builder - mkdir %APPDATA%\Composer - composer update --prefer-dist --no-progress --ansi test_script: - cd c:\projects\php-http\multipart-stream-builder - vendor\bin\phpunit.bat --verbose multipart-stream-builder/LICENSE 0000666 00000002065 15165515606 0012530 0 ustar 00 Copyright (c) 2015 PHP HTTP Team <team@php-http.org> 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. curl-client/README.md 0000666 00000003015 15165515606 0010261 0 ustar 00 # Curl client for PHP HTTP [](https://github.com/php-http/curl-client/releases) [](LICENSE) [](https://travis-ci.org/php-http/curl-client) [](https://scrutinizer-ci.com/g/php-http/curl-client) [](https://scrutinizer-ci.com/g/php-http/curl-client) [](https://packagist.org/packages/php-http/curl-client) The cURL client use the cURL PHP extension which must be activated in your `php.ini`. ## Install Via Composer ``` bash $ composer require php-http/curl-client ``` ## Documentation Please see the [official documentation](http://docs.php-http.org/en/latest/clients/curl-client.html). ## Testing ``` bash $ composer test ``` ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. curl-client/LICENSE 0000666 00000002065 15165515606 0010013 0 ustar 00 Copyright (c) 2015 PHP HTTP Team <team@php-http.org> 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. curl-client/puli.json 0000666 00000017375 15165515606 0010664 0 ustar 00 { "version": "1.0", "name": "php-http/curl-client", "bindings": { "98239b8b-103b-4f47-94c7-4cba49a05a1f": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Client\\Curl\\Client", "type": "Http\\Client\\HttpAsyncClient" }, "a6a79968-2aa5-427c-bbe1-a581d9a48321": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Client\\Curl\\Client", "type": "Http\\Client\\HttpClient" } }, "config": { "bootstrap-file": "vendor/autoload.php" }, "packages": { "clue/stream-filter": { "install-path": "vendor/clue/stream-filter", "installer": "composer", "env": "dev" }, "doctrine/instantiator": { "install-path": "vendor/doctrine/instantiator", "installer": "composer", "env": "dev" }, "guzzlehttp/psr7": { "install-path": "vendor/guzzlehttp/psr7", "installer": "composer", "env": "dev" }, "justinrainbow/json-schema": { "install-path": "vendor/justinrainbow/json-schema", "installer": "composer", "env": "dev" }, "paragonie/random_compat": { "install-path": "vendor/paragonie/random_compat", "installer": "composer", "env": "dev" }, "php-http/adapter-integration-tests": { "install-path": "vendor/php-http/adapter-integration-tests", "installer": "composer", "env": "dev" }, "php-http/discovery": { "install-path": "vendor/php-http/discovery", "installer": "composer", "env": "dev" }, "php-http/httplug": { "install-path": "vendor/php-http/httplug", "installer": "composer" }, "php-http/message": { "install-path": "vendor/php-http/message", "installer": "composer", "env": "dev" }, "php-http/message-factory": { "install-path": "vendor/php-http/message-factory", "installer": "composer" }, "php-http/promise": { "install-path": "vendor/php-http/promise", "installer": "composer" }, "phpdocumentor/reflection-docblock": { "install-path": "vendor/phpdocumentor/reflection-docblock", "installer": "composer", "env": "dev" }, "phpspec/prophecy": { "install-path": "vendor/phpspec/prophecy", "installer": "composer", "env": "dev" }, "phpunit/php-code-coverage": { "install-path": "vendor/phpunit/php-code-coverage", "installer": "composer", "env": "dev" }, "phpunit/php-file-iterator": { "install-path": "vendor/phpunit/php-file-iterator", "installer": "composer", "env": "dev" }, "phpunit/php-text-template": { "install-path": "vendor/phpunit/php-text-template", "installer": "composer", "env": "dev" }, "phpunit/php-timer": { "install-path": "vendor/phpunit/php-timer", "installer": "composer", "env": "dev" }, "phpunit/php-token-stream": { "install-path": "vendor/phpunit/php-token-stream", "installer": "composer", "env": "dev" }, "phpunit/phpunit": { "install-path": "vendor/phpunit/phpunit", "installer": "composer", "env": "dev" }, "phpunit/phpunit-mock-objects": { "install-path": "vendor/phpunit/phpunit-mock-objects", "installer": "composer", "env": "dev" }, "psr/http-message": { "install-path": "vendor/psr/http-message", "installer": "composer" }, "psr/log": { "install-path": "vendor/psr/log", "installer": "composer", "env": "dev" }, "puli/composer-plugin": { "install-path": "vendor/puli/composer-plugin", "installer": "composer", "env": "dev" }, "puli/discovery": { "install-path": "vendor/puli/discovery", "installer": "composer", "env": "dev" }, "puli/repository": { "install-path": "vendor/puli/repository", "installer": "composer", "env": "dev" }, "puli/url-generator": { "install-path": "vendor/puli/url-generator", "installer": "composer", "env": "dev" }, "ramsey/uuid": { "install-path": "vendor/ramsey/uuid", "installer": "composer", "env": "dev" }, "sebastian/comparator": { "install-path": "vendor/sebastian/comparator", "installer": "composer", "env": "dev" }, "sebastian/diff": { "install-path": "vendor/sebastian/diff", "installer": "composer", "env": "dev" }, "sebastian/environment": { "install-path": "vendor/sebastian/environment", "installer": "composer", "env": "dev" }, "sebastian/exporter": { "install-path": "vendor/sebastian/exporter", "installer": "composer", "env": "dev" }, "sebastian/global-state": { "install-path": "vendor/sebastian/global-state", "installer": "composer", "env": "dev" }, "sebastian/recursion-context": { "install-path": "vendor/sebastian/recursion-context", "installer": "composer", "env": "dev" }, "sebastian/version": { "install-path": "vendor/sebastian/version", "installer": "composer", "env": "dev" }, "seld/jsonlint": { "install-path": "vendor/seld/jsonlint", "installer": "composer", "env": "dev" }, "symfony/filesystem": { "install-path": "vendor/symfony/filesystem", "installer": "composer", "env": "dev" }, "symfony/process": { "install-path": "vendor/symfony/process", "installer": "composer", "env": "dev" }, "symfony/yaml": { "install-path": "vendor/symfony/yaml", "installer": "composer", "env": "dev" }, "th3n3rd/cartesian-product": { "install-path": "vendor/th3n3rd/cartesian-product", "installer": "composer", "env": "dev" }, "webmozart/assert": { "install-path": "vendor/webmozart/assert", "installer": "composer", "env": "dev" }, "webmozart/expression": { "install-path": "vendor/webmozart/expression", "installer": "composer", "env": "dev" }, "webmozart/glob": { "install-path": "vendor/webmozart/glob", "installer": "composer", "env": "dev" }, "webmozart/json": { "install-path": "vendor/webmozart/json", "installer": "composer", "env": "dev" }, "webmozart/path-util": { "install-path": "vendor/webmozart/path-util", "installer": "composer", "env": "dev" }, "zendframework/zend-diactoros": { "install-path": "vendor/zendframework/zend-diactoros", "installer": "composer", "env": "dev" } } } curl-client/composer.json 0000666 00000002515 15165515606 0011530 0 ustar 00 { "name": "php-http/curl-client", "description": "cURL client for PHP-HTTP", "license": "MIT", "keywords": ["http", "curl"], "homepage": "http://php-http.org", "authors": [ { "name": "Михаил Красильников", "email": "m.krasilnikov@yandex.ru" } ], "prefer-stable": true, "minimum-stability": "beta", "config": { "bin-dir": "vendor/bin" }, "require": { "php": "^5.5 || ^7.0", "ext-curl": "*", "php-http/httplug": "^1.0", "php-http/message-factory": "^1.0.2", "php-http/message": "^1.2", "php-http/discovery": "^1.0" }, "require-dev": { "guzzlehttp/psr7": "^1.0", "php-http/client-integration-tests": "^0.6", "phpunit/phpunit": "^4.8.27", "zendframework/zend-diactoros": "^1.0" }, "autoload": { "psr-4": { "Http\\Client\\Curl\\": "src/" } }, "autoload-dev": { "psr-4": { "Http\\Client\\Curl\\Tests\\": "tests/" } }, "provide": { "php-http/client-implementation": "1.0", "php-http/async-client-implementation": "1.0" }, "scripts": { "test": "vendor/bin/phpunit", "test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml" } } curl-client/src/ResponseBuilder.php 0000666 00000000740 15165515606 0013411 0 ustar 00 <?php namespace AmeliaHttp\Client\Curl; use AmeliaHttp\Message\Builder\ResponseBuilder as OriginalResponseBuilder; use AmeliaPsr\Http\Message\ResponseInterface; /** * Extended response builder. */ class ResponseBuilder extends OriginalResponseBuilder { /** * Replace response with a new instance. * * @param ResponseInterface $response */ public function setResponse(ResponseInterface $response) { $this->response = $response; } } curl-client/src/Client.php 0000666 00000027534 15165515606 0011534 0 ustar 00 <?php namespace AmeliaHttp\Client\Curl; use AmeliaHttp\Client\Exception; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Discovery\MessageFactoryDiscovery; use AmeliaHttp\Discovery\StreamFactoryDiscovery; use AmeliaHttp\Message\MessageFactory; use AmeliaHttp\Message\StreamFactory; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * PSR-7 compatible cURL based HTTP client. * * @license http://opensource.org/licenses/MIT MIT * @author Михаил Красильников <m.krasilnikov@yandex.ru> * @author Blake Williams <github@shabbyrobe.org> * * @api * * @since 1.0 */ class Client implements HttpClient, HttpAsyncClient { /** * cURL options. * * @var array */ private $options; /** * PSR-7 message factory. * * @var MessageFactory */ private $messageFactory; /** * PSR-7 stream factory. * * @var StreamFactory */ private $streamFactory; /** * cURL synchronous requests handle. * * @var resource|null */ private $handle = null; /** * Simultaneous requests runner. * * @var MultiRunner|null */ private $multiRunner = null; /** * Create new client. * * @param MessageFactory|null $messageFactory HTTP Message factory * @param StreamFactory|null $streamFactory HTTP Stream factory * @param array $options cURL options (see http://php.net/curl_setopt) * * @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed * * @since 1.0 */ public function __construct( MessageFactory $messageFactory = null, StreamFactory $streamFactory = null, array $options = [] ) { $this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find(); $this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find(); $this->options = $options; } /** * Release resources if still active. */ public function __destruct() { if (is_resource($this->handle)) { curl_close($this->handle); } } /** * Sends a PSR-7 request. * * @param RequestInterface $request * * @return ResponseInterface * * @throws \Http\Client\Exception\NetworkException In case of network problems * @throws \Http\Client\Exception\RequestException On invalid request * @throws \InvalidArgumentException For invalid header names or values * @throws \RuntimeException If creating the body stream fails * * @since 1.6 \UnexpectedValueException replaced with RequestException * @since 1.6 Throw NetworkException on network errors * @since 1.0 */ public function sendRequest(RequestInterface $request) { $responseBuilder = $this->createResponseBuilder(); $options = $this->createCurlOptions($request, $responseBuilder); if (is_resource($this->handle)) { curl_reset($this->handle); } else { $this->handle = curl_init(); } curl_setopt_array($this->handle, $options); curl_exec($this->handle); $errno = curl_errno($this->handle); switch ($errno) { case CURLE_OK: // All OK, no actions needed. break; case CURLE_COULDNT_RESOLVE_PROXY: case CURLE_COULDNT_RESOLVE_HOST: case CURLE_COULDNT_CONNECT: case CURLE_OPERATION_TIMEOUTED: case CURLE_SSL_CONNECT_ERROR: throw new Exception\NetworkException(curl_error($this->handle), $request); default: throw new Exception\RequestException(curl_error($this->handle), $request); } $response = $responseBuilder->getResponse(); $response->getBody()->seek(0); return $response; } /** * Sends a PSR-7 request in an asynchronous way. * * @param RequestInterface $request * * @return Promise * * @throws \Http\Client\Exception\RequestException On invalid request * @throws \InvalidArgumentException For invalid header names or values * @throws \RuntimeException If creating the body stream fails * * @since 1.6 \UnexpectedValueException replaced with RequestException * @since 1.0 */ public function sendAsyncRequest(RequestInterface $request) { if (!$this->multiRunner instanceof MultiRunner) { $this->multiRunner = new MultiRunner(); } $handle = curl_init(); $responseBuilder = $this->createResponseBuilder(); $options = $this->createCurlOptions($request, $responseBuilder); curl_setopt_array($handle, $options); $core = new PromiseCore($request, $handle, $responseBuilder); $promise = new CurlPromise($core, $this->multiRunner); $this->multiRunner->add($core); return $promise; } /** * Generates cURL options. * * @param RequestInterface $request * @param ResponseBuilder $responseBuilder * * @throws \Http\Client\Exception\RequestException On invalid request * @throws \InvalidArgumentException For invalid header names or values * @throws \RuntimeException if can not read body * * @return array */ private function createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder) { $options = $this->options; $options[CURLOPT_HEADER] = false; $options[CURLOPT_RETURNTRANSFER] = false; $options[CURLOPT_FOLLOWLOCATION] = false; try { $options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion()); } catch (\UnexpectedValueException $e) { throw new Exception\RequestException($e->getMessage(), $request); } $options[CURLOPT_URL] = (string) $request->getUri(); $options = $this->addRequestBodyOptions($request, $options); $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); if ($request->getUri()->getUserInfo()) { $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo(); } $options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) { $str = trim($data); if ('' !== $str) { if (strpos(strtolower($str), 'http/') === 0) { $responseBuilder->setStatus($str)->getResponse(); } else { $responseBuilder->addHeader($str); } } return strlen($data); }; $options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) { return $responseBuilder->getResponse()->getBody()->write($data); }; return $options; } /** * Return cURL constant for specified HTTP version. * * @param string $requestVersion * * @throws \UnexpectedValueException if unsupported version requested * * @return int */ private function getProtocolVersion($requestVersion) { switch ($requestVersion) { case '1.0': return CURL_HTTP_VERSION_1_0; case '1.1': return CURL_HTTP_VERSION_1_1; case '2.0': if (defined('CURL_HTTP_VERSION_2_0')) { return CURL_HTTP_VERSION_2_0; } throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support'); } return CURL_HTTP_VERSION_NONE; } /** * Add request body related cURL options. * * @param RequestInterface $request * @param array $options * * @return array */ private function addRequestBodyOptions(RequestInterface $request, array $options) { /* * Some HTTP methods cannot have payload: * * - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or * CURLOPT_POSTFIELDS. * - HEAD — cURL treats HEAD as GET request with a same restrictions. * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request. */ if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) { $body = $request->getBody(); $bodySize = $body->getSize(); if ($bodySize !== 0) { if ($body->isSeekable()) { $body->rewind(); } // Message has non empty body. if (null === $bodySize || $bodySize > 1024 * 1024) { // Avoid full loading large or unknown size body into memory $options[CURLOPT_UPLOAD] = true; if (null !== $bodySize) { $options[CURLOPT_INFILESIZE] = $bodySize; } $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { return $body->read($length); }; } else { // Small body can be loaded into memory $options[CURLOPT_POSTFIELDS] = (string) $body; } } } if ($request->getMethod() === 'HEAD') { // This will set HTTP method to "HEAD". $options[CURLOPT_NOBODY] = true; } elseif ($request->getMethod() !== 'GET') { // GET is a default method. Other methods should be specified explicitly. $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); } return $options; } /** * Create headers array for CURLOPT_HTTPHEADER. * * @param RequestInterface $request * @param array $options cURL options * * @return string[] */ private function createHeaders(RequestInterface $request, array $options) { $curlHeaders = []; $headers = $request->getHeaders(); foreach ($headers as $name => $values) { $header = strtolower($name); if ('expect' === $header) { // curl-client does not support "Expect-Continue", so dropping "expect" headers continue; } if ('content-length' === $header) { if (array_key_exists(CURLOPT_POSTFIELDS, $options)) { // Small body content length can be calculated here. $values = [strlen($options[CURLOPT_POSTFIELDS])]; } elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) { // Else if there is no body, forcing "Content-length" to 0 $values = [0]; } } foreach ($values as $value) { $curlHeaders[] = $name.': '.$value; } } /* * curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default. * We can not suppress it, but we can set it to empty. */ $curlHeaders[] = 'Expect:'; return $curlHeaders; } /** * Create new ResponseBuilder instance. * * @return ResponseBuilder * * @throws \RuntimeException If creating the stream from $body fails */ private function createResponseBuilder() { try { $body = $this->streamFactory->createStream(fopen('php://temp', 'w+b')); } catch (\InvalidArgumentException $e) { throw new \RuntimeException('Can not create "php://temp" stream.'); } $response = $this->messageFactory->createResponse(200, null, [], $body); return new ResponseBuilder($response); } } curl-client/src/PromiseCore.php 0000666 00000013522 15165515606 0012535 0 ustar 00 <?php namespace AmeliaHttp\Client\Curl; use AmeliaHttp\Client\Exception; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Shared promises core. * * @license http://opensource.org/licenses/MIT MIT * @author Михаил Красильников <m.krasilnikov@yandex.ru> */ class PromiseCore { /** * HTTP request. * * @var RequestInterface */ private $request; /** * cURL handle. * * @var resource */ private $handle; /** * Response builder. * * @var ResponseBuilder */ private $responseBuilder; /** * Promise state. * * @var string */ private $state; /** * Exception. * * @var Exception|null */ private $exception = null; /** * Functions to call when a response will be available. * * @var callable[] */ private $onFulfilled = []; /** * Functions to call when an error happens. * * @var callable[] */ private $onRejected = []; /** * Create shared core. * * @param RequestInterface $request HTTP request. * @param resource $handle cURL handle. * @param ResponseBuilder $responseBuilder Response builder. * * @throws \InvalidArgumentException If $handle is not a cURL resource. */ public function __construct( RequestInterface $request, $handle, ResponseBuilder $responseBuilder ) { if (!is_resource($handle)) { throw new \InvalidArgumentException( sprintf( 'Parameter $handle expected to be a cURL resource, %s given', gettype($handle) ) ); } if (get_resource_type($handle) !== 'curl') { throw new \InvalidArgumentException( sprintf( 'Parameter $handle expected to be a cURL resource, %s resource given', get_resource_type($handle) ) ); } $this->request = $request; $this->handle = $handle; $this->responseBuilder = $responseBuilder; $this->state = Promise::PENDING; } /** * Add on fulfilled callback. * * @param callable $callback */ public function addOnFulfilled(callable $callback) { if ($this->getState() === Promise::PENDING) { $this->onFulfilled[] = $callback; } elseif ($this->getState() === Promise::FULFILLED) { $response = call_user_func($callback, $this->responseBuilder->getResponse()); if ($response instanceof ResponseInterface) { $this->responseBuilder->setResponse($response); } } } /** * Add on rejected callback. * * @param callable $callback */ public function addOnRejected(callable $callback) { if ($this->getState() === Promise::PENDING) { $this->onRejected[] = $callback; } elseif ($this->getState() === Promise::REJECTED) { $this->exception = call_user_func($callback, $this->exception); } } /** * Return cURL handle. * * @return resource */ public function getHandle() { return $this->handle; } /** * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. * * @return string */ public function getState() { return $this->state; } /** * Return request. * * @return RequestInterface */ public function getRequest() { return $this->request; } /** * Return the value of the promise (fulfilled). * * @return ResponseInterface Response Object only when the Promise is fulfilled */ public function getResponse() { return $this->responseBuilder->getResponse(); } /** * Get the reason why the promise was rejected. * * If the exception is an instance of AmeliaHttp\Client\Exception\HttpException it will contain * the response object with the status code and the http reason. * * @return Exception Exception Object only when the Promise is rejected * * @throws \LogicException When the promise is not rejected */ public function getException() { if (null === $this->exception) { throw new \LogicException('Promise is not rejected'); } return $this->exception; } /** * Fulfill promise. */ public function fulfill() { $this->state = Promise::FULFILLED; $response = $this->responseBuilder->getResponse(); try { $response->getBody()->seek(0); } catch (\RuntimeException $e) { $exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e); $this->reject($exception); return; } while (count($this->onFulfilled) > 0) { $callback = array_shift($this->onFulfilled); $response = call_user_func($callback, $response); } if ($response instanceof ResponseInterface) { $this->responseBuilder->setResponse($response); } } /** * Reject promise. * * @param Exception $exception Reject reason */ public function reject(Exception $exception) { $this->exception = $exception; $this->state = Promise::REJECTED; while (count($this->onRejected) > 0) { $callback = array_shift($this->onRejected); try { $exception = call_user_func($callback, $this->exception); $this->exception = $exception; } catch (Exception $exception) { $this->exception = $exception; } } } } curl-client/src/CurlPromise.php 0000666 00000006304 15165515606 0012552 0 ustar 00 <?php namespace AmeliaHttp\Client\Curl; use AmeliaHttp\Promise\Promise; /** * Promise represents a response that may not be available yet, but will be resolved at some point * in future. It acts like a proxy to the actual response. * * This interface is an extension of the promises/a+ specification https://promisesaplus.com/ * Value is replaced by an object where its class implement a AmeliaPsr\Http\Message\RequestInterface. * Reason is replaced by an object where its class implement a AmeliaHttp\Client\Exception. * * @license http://opensource.org/licenses/MIT MIT * @author Михаил Красильников <m.krasilnikov@yandex.ru> */ class CurlPromise implements Promise { /** * Shared promise core. * * @var PromiseCore */ private $core; /** * Requests runner. * * @var MultiRunner */ private $runner; /** * Create new promise. * * @param PromiseCore $core Shared promise core * @param MultiRunner $runner Simultaneous requests runner */ public function __construct(PromiseCore $core, MultiRunner $runner) { $this->core = $core; $this->runner = $runner; } /** * Add behavior for when the promise is resolved or rejected. * * If you do not care about one of the cases, you can set the corresponding callable to null * The callback will be called when the response or exception arrived and never more than once. * * @param callable $onFulfilled Called when a response will be available * @param callable $onRejected Called when an error happens. * * You must always return the Response in the interface or throw an Exception * * @return Promise Always returns a new promise which is resolved with value of the executed * callback (onFulfilled / onRejected) */ public function then(callable $onFulfilled = null, callable $onRejected = null) { if ($onFulfilled) { $this->core->addOnFulfilled($onFulfilled); } if ($onRejected) { $this->core->addOnRejected($onRejected); } return new self($this->core, $this->runner); } /** * Get the state of the promise, one of PENDING, FULFILLED or REJECTED. * * @return string */ public function getState() { return $this->core->getState(); } /** * Wait for the promise to be fulfilled or rejected. * * When this method returns, the request has been resolved and the appropriate callable has terminated. * * When called with the unwrap option * * @param bool $unwrap Whether to return resolved value / throw reason or not * * @return \AmeliaPsr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false * * @throws \Http\Client\Exception The rejection reason */ public function wait($unwrap = true) { $this->runner->wait($this->core); if ($unwrap) { if ($this->core->getState() === self::REJECTED) { throw $this->core->getException(); } return $this->core->getResponse(); } return null; } } curl-client/src/MultiRunner.php 0000666 00000006155 15165515606 0012576 0 ustar 00 <?php namespace AmeliaHttp\Client\Curl; use AmeliaHttp\Client\Exception\RequestException; /** * Simultaneous requests runner. * * @license http://opensource.org/licenses/MIT MIT * @author Михаил Красильников <m.krasilnikov@yandex.ru> */ class MultiRunner { /** * cURL multi handle. * * @var resource|null */ private $multiHandle = null; /** * Awaiting cores. * * @var PromiseCore[] */ private $cores = []; /** * Release resources if still active. */ public function __destruct() { if (is_resource($this->multiHandle)) { curl_multi_close($this->multiHandle); } } /** * Add promise to runner. * * @param PromiseCore $core */ public function add(PromiseCore $core) { foreach ($this->cores as $existed) { if ($existed === $core) { return; } } $this->cores[] = $core; if (null === $this->multiHandle) { $this->multiHandle = curl_multi_init(); } curl_multi_add_handle($this->multiHandle, $core->getHandle()); } /** * Remove promise from runner. * * @param PromiseCore $core */ public function remove(PromiseCore $core) { foreach ($this->cores as $index => $existed) { if ($existed === $core) { curl_multi_remove_handle($this->multiHandle, $core->getHandle()); unset($this->cores[$index]); return; } } } /** * Wait for request(s) to be completed. * * @param PromiseCore|null $targetCore */ public function wait(PromiseCore $targetCore = null) { do { $status = curl_multi_exec($this->multiHandle, $active); $info = curl_multi_info_read($this->multiHandle); if (false !== $info) { $core = $this->findCoreByHandle($info['handle']); if (null === $core) { // We have no promise for this handle. Drop it. curl_multi_remove_handle($this->multiHandle, $info['handle']); continue; } if (CURLE_OK === $info['result']) { $core->fulfill(); } else { $error = curl_error($core->getHandle()); $core->reject(new RequestException($error, $core->getRequest())); } $this->remove($core); // This is a promise we are waited for. So exiting wait(). if ($core === $targetCore) { return; } } } while ($status === CURLM_CALL_MULTI_PERFORM || $active); } /** * Find core by handle. * * @param resource $handle * * @return PromiseCore|null */ private function findCoreByHandle($handle) { foreach ($this->cores as $core) { if ($core->getHandle() === $handle) { return $core; } } return null; } } curl-client/.styleci.yml 0000666 00000000057 15165515606 0011262 0 ustar 00 preset: psr2 finder: path: - "src" curl-client/CHANGELOG.md 0000666 00000005046 15165515606 0010621 0 ustar 00 # Change Log ## Unreleased ## 1.7.1 - 2018-03-36 ### Fixed - #36: Failure evaluating code: is_resource($handle) (string assertions are deprecated in PHP 7.2) ## 1.7 - 2017-02-09 ### Changed - #30: Make sure we rewind streams ## 1.6.2 - 2017-01-02 ### Fixed - #29: Request not using CURLOPT_POSTFIELDS have content-length set to ### Changed - Use binary mode to create response body stream. ## 1.6.1 - 2016-11-11 ### Fixed - #27: ErrorPlugin and sendAsyncRequest() incompatibility ## 1.6 - 2016-09-12 ### Changed - `Client::sendRequest` now throws `Http\Client\Exception\NetworkException` on network errors. - `\UnexpectedValueException` replaced with `Http\Client\Exception\RequestException` in `Client::sendRequest` and `Client::sendAsyncRequest` ## 1.5.1 - 2016-08-29 ### Fixed - #26: Combining CurlClient with StopwatchPlugin causes Promise onRejected handler to never be invoked. ## 1.5 - 2016-08-03 ### Changed - Request body can be send with any method except GET, HEAD and TRACE. - #25: Make discovery a hard dependency. ## 1.4.2 - 2016-06-14 ### Added - #23: "php-http/async-client-implementation" added to "provide" section. ## 1.4.1 - 2016-05-30 ### Fixed - #22: Cannot create the client using `HttpClientDiscovery`. ## 1.4 - 2016-03-30 ### Changed - #20: Minimize memory usage when reading large response body. ## 1.3 - 2016-03-14 ### Fixed - #18: Invalid "Expect" header. ### Removed - #13: Remove HeaderParser. ## 1.2 - 2016-03-09 ### Added - #16: Make sure discovery can find the curl client ### Fixed - #15: "Out of memory" sending large files. ## 1.1.0 - 2016-01-29 ### Changed - Switch to php-http/message 1.0. ## 1.0.0 - 2016-01-28 First stable release. ## 0.7.0 - 2016-01-26 ### Changed - Migrate from `php-http/discovery` and `php-http/utils` to `php-http/message`. ## 0.6.0 - 2016-01-12 ### Changed - Root namespace changed from `Http\Curl` to `Http\Client\Curl`. - Main client class name renamed from `CurlHttpClient` to `Client`. - Minimum required [php-http/discovery](https://packagist.org/packages/php-http/discovery) version changed to 0.5. ## 0.5.0 - 2015-12-18 ### Changed - Compatibility with php-http/httplug 1.0 beta - Switch to php-http/discovery 0.4 ## 0.4.0 - 2015-12-16 ### Changed - Switch to php-http/message-factory 1.0 ## 0.3.1 - 2015-12-14 ### Changed - Requirements fixed. ## 0.3.0 - 2015-11-24 ### Changed - Use cURL constants as options keys. ## 0.2.0 - 2015-11-17 ### Added - HttpAsyncClient support. ## 0.1.0 - 2015-11-11 ### Added - Initial release curl-client/.php_cs 0000666 00000000323 15165515606 0010256 0 ustar 00 <?php return Symfony\CS\Config\Config::create() ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) ->fixers([]) ->finder( Symfony\CS\Finder\DefaultFinder::create()->in(__DIR__ . '/src') ) ; client-common/CHANGELOG.md 0000666 00000011024 15165515606 0011135 0 ustar 00 # Change Log ## 1.11.0 - 2021-07-11 ### Changed - Backported from version 2: AddPathPlugin: Do not add the prefix if the URL already has the same prefix. ## 1.10.0 - 2019-11-18 ### Added - Support for Symfony 5 ## 1.9.1 - 2019-02-02 ### Added - Updated type hints in doc blocks. ## 1.9.0 - 2019-01-03 ### Added - Support for PSR-18 clients - Added traits `VersionBridgePlugin` and `VersionBridgeClient` to help plugins and clients to support both 1.x and 2.x version of `php-http/client-common` and `php-http/httplug`. ### Changed - [RetryPlugin] Renamed the configuration options for the exception retry callback from `decider` to `exception_decider` and `delay` to `exception_delay`. The old names still work but are deprecated. ## 1.8.2 - 2018-12-14 ### Changed - When multiple cookies exist, a single header with all cookies is sent as per RFC 6265 Section 5.4 - AddPathPlugin will now trim of ending slashes in paths ## 1.8.1 - 2018-10-09 ### Fixed - Reverted change to RetryPlugin so it again waits when retrying to avoid "can only throw objects" error. ## 1.8.0 - 2018-09-21 ### Added - Add an option on ErrorPlugin to only throw exception on response with 5XX status code. ### Changed - AddPathPlugin no longer add prefix multiple times if a request is restarted - it now only adds the prefix if that request chain has not yet passed through the AddPathPlugin - RetryPlugin no longer wait for retried requests and use a deferred promise instead ### Fixed - Decoder plugin will now remove header when there is no more encoding, instead of setting to an empty array ## 1.7.0 - 2017-11-30 ### Added - Symfony 4 support ### Changed - Strict comparison in DecoderPlugin ## 1.6.0 - 2017-10-16 ### Added - Add HttpClientPool client to leverage load balancing and fallback mechanism [see the documentation](http://docs.php-http.org/en/latest/components/client-common.html) for more details. - `PluginClientFactory` to create `PluginClient` instances. - Added new option 'delay' for `RetryPlugin`. - Added new option 'decider' for `RetryPlugin`. - Supports more cookie date formats in the Cookie Plugin ### Changed - The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like: ```php $plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) { return 0; }); ``` ### Deprecated - The `debug_plugins` option for `PluginClient` is deprecated and will be removed in 2.0. Use the decorator design pattern instead like in [ProfilePlugin](https://github.com/php-http/HttplugBundle/blob/de33f9c14252f22093a5ec7d84f17535ab31a384/Collector/ProfilePlugin.php). ## 1.5.0 - 2017-03-30 ### Added - `QueryDefaultsPlugin` to add default query parameters. ## 1.4.2 - 2017-03-18 ### Deprecated - `DecoderPlugin` does not longer claim to support `compress` content encoding ### Fixed - `CookiePlugin` allows main domain cookies to be sent/stored for subdomains - `DecoderPlugin` uses the right `FilteredStream` to handle `deflate` content encoding ## 1.4.1 - 2017-02-20 ### Fixed - Cast return value of `StreamInterface::getSize` to string in `ContentLengthPlugin` ## 1.4.0 - 2016-11-04 ### Added - Add Path plugin - Base URI plugin that combines Add Host and Add Path plugins ## 1.3.0 - 2016-10-16 ### Changed - Fix Emulated Trait to use Http based promise which respect the HttpAsyncClient interface - Require Httplug 1.1 where we use HTTP specific promises. - RedirectPlugin: use the full URL instead of the URI to properly keep track of redirects - Add AddPathPlugin for API URLs with base path - Add BaseUriPlugin that combines AddHostPlugin and AddPathPlugin ## 1.2.1 - 2016-07-26 ### Changed - AddHostPlugin also sets the port if specified ## 1.2.0 - 2016-07-14 ### Added - Suggest separate plugins in composer.json - Introduced `debug_plugins` option for `PluginClient` ## 1.1.0 - 2016-05-04 ### Added - Add a flexible http client providing both contract, and only emulating what's necessary - HTTP Client Router: route requests to underlying clients - Plugin client and core plugins moved here from `php-http/plugins` ### Deprecated - Extending client classes, they will be made final in version 2.0 ## 1.0.0 - 2016-01-27 ### Changed - Remove useless interface in BatchException ## 0.2.0 - 2016-01-12 ### Changed - Updated package files - Updated HTTPlug to RC1 ## 0.1.1 - 2015-12-26 ### Added - Emulated clients ## 0.1.0 - 2015-12-25 ### Added - Batch client from utils - Methods client from utils - Emulators and decorators from client-tools client-common/README.md 0000666 00000003414 15165515606 0010607 0 ustar 00 # HTTP Client Common [](https://github.com/php-http/client-common/releases) [](LICENSE) [](https://travis-ci.org/php-http/client-common) [](https://scrutinizer-ci.com/g/php-http/client-common) [](https://scrutinizer-ci.com/g/php-http/client-common) [](https://packagist.org/packages/php-http/client-common) **Common HTTP Client implementations and tools for HTTPlug.** ## Install Via Composer ``` bash $ composer require php-http/client-common ``` ## Usage This package provides common tools for HTTP Clients: - BatchClient to handle sending requests in parallel - A convenience client with HTTP method names as class methods - Emulator, decorator layers for sync/async clients ## Documentation Please see the [official documentation](http://docs.php-http.org/en/latest/components/client-common.html). ## Testing ``` bash $ composer test ``` ## Contributing Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. client-common/src/FlexibleHttpClient.php 0000666 00000002521 15165515606 0014357 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; /** * A flexible http client, which implements both interface and will emulate * one contract, the other, or none at all depending on the injected client contract. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class FlexibleHttpClient implements HttpClient, HttpAsyncClient { use HttpClientDecorator; use HttpAsyncClientDecorator; /** * @param HttpClient|HttpAsyncClient|ClientInterface $client */ public function __construct($client) { if (!($client instanceof HttpClient) && !($client instanceof HttpAsyncClient) && !($client instanceof ClientInterface)) { throw new \LogicException('Client must be an instance of AmeliaHttp\\Client\\HttpClient or AmeliaHttp\\Client\\HttpAsyncClient'); } $this->httpClient = $client; $this->httpAsyncClient = $client; if (!($this->httpClient instanceof HttpClient) && !($client instanceof ClientInterface)) { $this->httpClient = new EmulatedHttpClient($this->httpClient); } if (!($this->httpAsyncClient instanceof HttpAsyncClient)) { $this->httpAsyncClient = new EmulatedHttpAsyncClient($this->httpAsyncClient); } } } client-common/src/HttpClientDecorator.php 0000666 00000001103 15165515606 0014542 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; /** * Decorates an HTTP Client. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait HttpClientDecorator { /** * @var HttpClient|ClientInterface */ protected $httpClient; /** * {@inheritdoc} * * @see HttpClient::sendRequest */ public function sendRequest(RequestInterface $request) { return $this->httpClient->sendRequest($request); } } client-common/src/Exception/HttpClientNotFoundException.php 0000666 00000000433 15165515606 0020176 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\TransferException; /** * Thrown when a http client cannot be chosen in a pool. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class HttpClientNotFoundException extends TransferException { } client-common/src/Exception/LoopException.php 0000666 00000000414 15165515606 0015353 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\RequestException; /** * Thrown when the Plugin Client detects an endless loop. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class LoopException extends RequestException { } client-common/src/Exception/ClientErrorException.php 0000666 00000000401 15165515606 0016666 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\HttpException; /** * Thrown when there is a client error (4xx). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class ClientErrorException extends HttpException { } client-common/src/Exception/CircularRedirectionException.php 0000666 00000000414 15165515606 0020376 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\HttpException; /** * Thrown when circular redirection is detected. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class CircularRedirectionException extends HttpException { } client-common/src/Exception/MultipleRedirectionException.php 0000666 00000000402 15165515606 0020422 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\HttpException; /** * Redirect location cannot be chosen. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class MultipleRedirectionException extends HttpException { } client-common/src/Exception/ServerErrorException.php 0000666 00000000401 15165515606 0016716 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\HttpException; /** * Thrown when there is a server error (5xx). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class ServerErrorException extends HttpException { } client-common/src/Exception/BatchException.php 0000666 00000001540 15165515606 0015464 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Exception; use AmeliaHttp\Client\Exception\TransferException; use AmeliaHttp\Client\Common\BatchResult; /** * This exception is thrown when HttpClient::sendRequests led to at least one failure. * * It gives access to a BatchResult with the request-exception and request-response pairs. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class BatchException extends TransferException { /** * @var BatchResult */ private $result; /** * @param BatchResult $result */ public function __construct(BatchResult $result) { $this->result = $result; } /** * Returns the BatchResult that contains all responses and exceptions. * * @return BatchResult */ public function getResult() { return $this->result; } } client-common/src/Plugin/HeaderSetPlugin.php 0000666 00000001641 15165515606 0015111 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * Set headers on the request. * * If the header does not exist it wil be set, if the header already exists it will be replaced. * * @author Soufiane Ghzal <sghzal@gmail.com> */ final class HeaderSetPlugin implements Plugin { /** * @var array */ private $headers = []; /** * @param array $headers Hashmap of header name to header value */ public function __construct(array $headers) { $this->headers = $headers; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { foreach ($this->headers as $header => $headerValue) { $request = $request->withHeader($header, $headerValue); } return $next($request); } } client-common/src/Plugin/ErrorPlugin.php 0000666 00000005315 15165515606 0014340 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Exception\ClientErrorException; use AmeliaHttp\Client\Common\Exception\ServerErrorException; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Throw exception when the response of a request is not acceptable. * * Status codes 400-499 lead to a ClientErrorException, status 500-599 to a ServerErrorException. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class ErrorPlugin implements Plugin { /** * @var bool Whether this plugin should only throw 5XX Exceptions (default to false). * * If set to true 4XX Responses code will never throw an exception */ private $onlyServerException; /** * @param array $config { * * @var bool only_server_exception Whether this plugin should only throw 5XX Exceptions (default to false). * } */ public function __construct(array $config = []) { $resolver = new OptionsResolver(); $resolver->setDefaults([ 'only_server_exception' => false, ]); $resolver->setAllowedTypes('only_server_exception', 'bool'); $options = $resolver->resolve($config); $this->onlyServerException = $options['only_server_exception']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $promise = $next($request); return $promise->then(function (ResponseInterface $response) use ($request) { return $this->transformResponseToException($request, $response); }); } /** * Transform response to an error if possible. * * @param RequestInterface $request Request of the call * @param ResponseInterface $response Response of the call * * @throws ClientErrorException If response status code is a 4xx * @throws ServerErrorException If response status code is a 5xx * * @return ResponseInterface If status code is not in 4xx or 5xx return response */ protected function transformResponseToException(RequestInterface $request, ResponseInterface $response) { if (!$this->onlyServerException && $response->getStatusCode() >= 400 && $response->getStatusCode() < 500) { throw new ClientErrorException($response->getReasonPhrase(), $request, $response); } if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) { throw new ServerErrorException($response->getReasonPhrase(), $request, $response); } return $response; } } client-common/src/Plugin/HeaderRemovePlugin.php 0000666 00000001566 15165515606 0015621 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * Removes headers from the request. * * @author Soufiane Ghzal <sghzal@gmail.com> */ final class HeaderRemovePlugin implements Plugin { /** * @var array */ private $headers = []; /** * @param array $headers List of header names to remove from the request */ public function __construct(array $headers) { $this->headers = $headers; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { foreach ($this->headers as $header) { if ($request->hasHeader($header)) { $request = $request->withoutHeader($header); } } return $next($request); } } client-common/src/Plugin/RetryPlugin.php 0000666 00000011444 15165515606 0014354 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Retry the request if an exception is thrown. * * By default will retry only one time. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class RetryPlugin implements Plugin { /** * Number of retry before sending an exception. * * @var int */ private $retry; /** * @var callable */ private $exceptionDelay; /** * @var callable */ private $exceptionDecider; /** * Store the retry counter for each request. * * @var array */ private $retryStorage = []; /** * @param array $config { * * @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up. * @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried. * @var callable $exception_delay A callback that gets a request, an exception and the number of retries and returns how many microseconds we should wait before trying again. * } */ public function __construct(array $config = []) { if (array_key_exists('decider', $config)) { if (array_key_exists('exception_decider', $config)) { throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options'); } trigger_error('The "decider" option has been deprecated in favour of "exception_decider"', E_USER_DEPRECATED); $config['exception_decider'] = $config['decider']; unset($config['decider']); } if (array_key_exists('delay', $config)) { if (array_key_exists('exception_delay', $config)) { throw new \InvalidArgumentException('Do not set both the old "delay" and new "exception_delay" options'); } trigger_error('The "delay" option has been deprecated in favour of "exception_delay"', E_USER_DEPRECATED); $config['exception_delay'] = $config['delay']; unset($config['delay']); } $resolver = new OptionsResolver(); $resolver->setDefaults([ 'retries' => 1, 'exception_decider' => function (RequestInterface $request, Exception $e) { return true; }, 'exception_delay' => __CLASS__.'::defaultDelay', ]); $resolver->setAllowedTypes('retries', 'int'); $resolver->setAllowedTypes('exception_decider', 'callable'); $resolver->setAllowedTypes('exception_delay', 'callable'); $options = $resolver->resolve($config); $this->retry = $options['retries']; $this->exceptionDecider = $options['exception_decider']; $this->exceptionDelay = $options['exception_delay']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $chainIdentifier = spl_object_hash((object) $first); return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) { if (array_key_exists($chainIdentifier, $this->retryStorage)) { unset($this->retryStorage[$chainIdentifier]); } return $response; }, function (Exception $exception) use ($request, $next, $first, $chainIdentifier) { if (!array_key_exists($chainIdentifier, $this->retryStorage)) { $this->retryStorage[$chainIdentifier] = 0; } if ($this->retryStorage[$chainIdentifier] >= $this->retry) { unset($this->retryStorage[$chainIdentifier]); throw $exception; } if (!call_user_func($this->exceptionDecider, $request, $exception)) { throw $exception; } $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]); usleep($time); // Retry in synchrone ++$this->retryStorage[$chainIdentifier]; $promise = $this->handleRequest($request, $next, $first); return $promise->wait(); }); } /** * @param RequestInterface $request * @param Exception $e * @param int $retries The number of retries we made before. First time this get called it will be 0. * * @return int */ public static function defaultDelay(RequestInterface $request, Exception $e, $retries) { return pow(2, $retries) * 500000; } } client-common/src/Plugin/AuthenticationPlugin.php 0000666 00000001515 15165515606 0016224 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Send an authenticated request. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class AuthenticationPlugin implements Plugin { /** * @var Authentication An authentication system */ private $authentication; /** * @param Authentication $authentication */ public function __construct(Authentication $authentication) { $this->authentication = $authentication; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $request = $this->authentication->authenticate($request); return $next($request); } } client-common/src/Plugin/DecoderPlugin.php 0000666 00000010753 15165515606 0014616 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Message\Encoding; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; use AmeliaPsr\Http\Message\StreamInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Allow to decode response body with a chunk, deflate, compress or gzip encoding. * * If zlib is not installed, only chunked encoding can be handled. * * If Content-Encoding is not disabled, the plugin will add an Accept-Encoding header for the encoding methods it supports. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class DecoderPlugin implements Plugin { /** * @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true). * * If set to false only the Transfer-Encoding header will be used */ private $useContentEncoding; /** * @param array $config { * * @var bool $use_content_encoding Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true). * } */ public function __construct(array $config = []) { $resolver = new OptionsResolver(); $resolver->setDefaults([ 'use_content_encoding' => true, ]); $resolver->setAllowedTypes('use_content_encoding', 'bool'); $options = $resolver->resolve($config); $this->useContentEncoding = $options['use_content_encoding']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $encodings = extension_loaded('zlib') ? ['gzip', 'deflate'] : ['identity']; if ($this->useContentEncoding) { $request = $request->withHeader('Accept-Encoding', $encodings); } $encodings[] = 'chunked'; $request = $request->withHeader('TE', $encodings); return $next($request)->then(function (ResponseInterface $response) { return $this->decodeResponse($response); }); } /** * Decode a response body given its Transfer-Encoding or Content-Encoding value. * * @param ResponseInterface $response Response to decode * * @return ResponseInterface New response decoded */ private function decodeResponse(ResponseInterface $response) { $response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response); if ($this->useContentEncoding) { $response = $this->decodeOnEncodingHeader('Content-Encoding', $response); } return $response; } /** * Decode a response on a specific header (content encoding or transfer encoding mainly). * * @param string $headerName Name of the header * @param ResponseInterface $response Response * * @return ResponseInterface A new instance of the response decoded */ private function decodeOnEncodingHeader($headerName, ResponseInterface $response) { if ($response->hasHeader($headerName)) { $encodings = $response->getHeader($headerName); $newEncodings = []; while ($encoding = array_pop($encodings)) { $stream = $this->decorateStream($encoding, $response->getBody()); if (false === $stream) { array_unshift($newEncodings, $encoding); continue; } $response = $response->withBody($stream); } if (\count($newEncodings) > 0) { $response = $response->withHeader($headerName, $newEncodings); } else { $response = $response->withoutHeader($headerName); } } return $response; } /** * Decorate a stream given an encoding. * * @param string $encoding * @param StreamInterface $stream * * @return StreamInterface|false A new stream interface or false if encoding is not supported */ private function decorateStream($encoding, StreamInterface $stream) { if ('chunked' === strtolower($encoding)) { return new Encoding\DechunkStream($stream); } if ('deflate' === strtolower($encoding)) { return new Encoding\DecompressStream($stream); } if ('gzip' === strtolower($encoding)) { return new Encoding\GzipDecodeStream($stream); } return false; } } client-common/src/Plugin/Journal.php 0000666 00000001525 15165515606 0013501 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Records history of HTTP calls. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ interface Journal { /** * Record a successful call. * * @param RequestInterface $request Request use to make the call * @param ResponseInterface $response Response returned by the call */ public function addSuccess(RequestInterface $request, ResponseInterface $response); /** * Record a failed call. * * @param RequestInterface $request Request use to make the call * @param Exception $exception Exception returned by the call */ public function addFailure(RequestInterface $request, Exception $exception); } client-common/src/Plugin/CookiePlugin.php 0000666 00000011642 15165515606 0014460 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Exception\TransferException; use AmeliaHttp\Message\Cookie; use AmeliaHttp\Message\CookieJar; use AmeliaHttp\Message\CookieUtil; use AmeliaHttp\Message\Exception\UnexpectedValueException; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Handle request cookies. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class CookiePlugin implements Plugin { /** * Cookie storage. * * @var CookieJar */ private $cookieJar; /** * @param CookieJar $cookieJar */ public function __construct(CookieJar $cookieJar) { $this->cookieJar = $cookieJar; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $cookies = []; foreach ($this->cookieJar->getCookies() as $cookie) { if ($cookie->isExpired()) { continue; } if (!$cookie->matchDomain($request->getUri()->getHost())) { continue; } if (!$cookie->matchPath($request->getUri()->getPath())) { continue; } if ($cookie->isSecure() && ('https' !== $request->getUri()->getScheme())) { continue; } $cookies[] = sprintf('%s=%s', $cookie->getName(), $cookie->getValue()); } if (!empty($cookies)) { $request = $request->withAddedHeader('Cookie', implode('; ', array_unique($cookies))); } return $next($request)->then(function (ResponseInterface $response) use ($request) { if ($response->hasHeader('Set-Cookie')) { $setCookies = $response->getHeader('Set-Cookie'); foreach ($setCookies as $setCookie) { $cookie = $this->createCookie($request, $setCookie); // Cookie invalid do not use it if (null === $cookie) { continue; } // Restrict setting cookie from another domain if (!preg_match("/\.{$cookie->getDomain()}$/", '.'.$request->getUri()->getHost())) { continue; } $this->cookieJar->addCookie($cookie); } } return $response; }); } /** * Creates a cookie from a string. * * @param RequestInterface $request * @param $setCookie * * @return Cookie|null * * @throws TransferException */ private function createCookie(RequestInterface $request, $setCookie) { $parts = array_map('trim', explode(';', $setCookie)); if (empty($parts) || !strpos($parts[0], '=')) { return; } list($name, $cookieValue) = $this->createValueKey(array_shift($parts)); $maxAge = null; $expires = null; $domain = $request->getUri()->getHost(); $path = $request->getUri()->getPath(); $secure = false; $httpOnly = false; // Add the cookie pieces into the parsed data array foreach ($parts as $part) { list($key, $value) = $this->createValueKey($part); switch (strtolower($key)) { case 'expires': try { $expires = CookieUtil::parseDate($value); } catch (UnexpectedValueException $e) { throw new TransferException( sprintf( 'Cookie header `%s` expires value `%s` could not be converted to date', $name, $value ), 0, $e ); } break; case 'max-age': $maxAge = (int) $value; break; case 'domain': $domain = $value; break; case 'path': $path = $value; break; case 'secure': $secure = true; break; case 'httponly': $httpOnly = true; break; } } return new Cookie($name, $cookieValue, $maxAge, $domain, $path, $secure, $httpOnly, $expires); } /** * Separates key/value pair from cookie. * * @param $part * * @return array */ private function createValueKey($part) { $parts = explode('=', $part, 2); $key = trim($parts[0]); $value = isset($parts[1]) ? trim($parts[1]) : true; return [$key, $value]; } } client-common/src/Plugin/ContentLengthPlugin.php 0000666 00000002142 15165515606 0016016 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Message\Encoding\ChunkStream; use AmeliaPsr\Http\Message\RequestInterface; /** * Allow to set the correct content length header on the request or to transfer it as a chunk if not possible. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class ContentLengthPlugin implements Plugin { /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { if (!$request->hasHeader('Content-Length')) { $stream = $request->getBody(); // Cannot determine the size so we use a chunk stream if (null === $stream->getSize()) { $stream = new ChunkStream($stream); $request = $request->withBody($stream); $request = $request->withAddedHeader('Transfer-Encoding', 'chunked'); } else { $request = $request->withHeader('Content-Length', (string) $stream->getSize()); } } return $next($request); } } client-common/src/Plugin/AddHostPlugin.php 0000666 00000003645 15165515606 0014601 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\UriInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Add schema, host and port to a request. Can be set to overwrite the schema and host if desired. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class AddHostPlugin implements Plugin { /** * @var UriInterface */ private $host; /** * @var bool */ private $replace; /** * @param UriInterface $host * @param array $config { * * @var bool $replace True will replace all hosts, false will only add host when none is specified. * } */ public function __construct(UriInterface $host, array $config = []) { if ('' === $host->getHost()) { throw new \LogicException('Host can not be empty'); } $this->host = $host; $resolver = new OptionsResolver(); $this->configureOptions($resolver); $options = $resolver->resolve($config); $this->replace = $options['replace']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { if ($this->replace || '' === $request->getUri()->getHost()) { $uri = $request->getUri() ->withHost($this->host->getHost()) ->withScheme($this->host->getScheme()) ->withPort($this->host->getPort()) ; $request = $request->withUri($uri); } return $next($request); } /** * @param OptionsResolver $resolver */ private function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'replace' => false, ]); $resolver->setAllowedTypes('replace', 'bool'); } } client-common/src/Plugin/HeaderAppendPlugin.php 0000666 00000002067 15165515606 0015570 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * Append headers to the request. * * If the header already exists the value will be appended to the current value. * * This only makes sense for headers that can have multiple values like 'Forwarded' * * @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields * * @author Soufiane Ghzal <sghzal@gmail.com> */ final class HeaderAppendPlugin implements Plugin { /** * @var array */ private $headers = []; /** * @param array $headers Hashmap of header name to header value */ public function __construct(array $headers) { $this->headers = $headers; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { foreach ($this->headers as $header => $headerValue) { $request = $request->withAddedHeader($header, $headerValue); } return $next($request); } } client-common/src/Plugin/BaseUriPlugin.php 0000666 00000002664 15165515606 0014605 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\UriInterface; /** * Combines the AddHostPlugin and AddPathPlugin. * * @author Sullivan Senechal <soullivaneuh@gmail.com> */ final class BaseUriPlugin implements Plugin { /** * @var AddHostPlugin */ private $addHostPlugin; /** * @var AddPathPlugin|null */ private $addPathPlugin = null; /** * @param UriInterface $uri Has to contain a host name and cans have a path. * @param array $hostConfig Config for AddHostPlugin. @see AddHostPlugin::configureOptions */ public function __construct(UriInterface $uri, array $hostConfig = []) { $this->addHostPlugin = new AddHostPlugin($uri, $hostConfig); if (rtrim($uri->getPath(), '/')) { $this->addPathPlugin = new AddPathPlugin($uri); } } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $addHostNext = function (RequestInterface $request) use ($next, $first) { return $this->addHostPlugin->handleRequest($request, $next, $first); }; if ($this->addPathPlugin) { return $this->addPathPlugin->handleRequest($request, $addHostNext, $first); } return $addHostNext($request); } } client-common/src/Plugin/RequestMatcherPlugin.php 0000666 00000002143 15165515606 0016177 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Message\RequestMatcher; use AmeliaPsr\Http\Message\RequestInterface; /** * Apply a delegated plugin based on a request match. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class RequestMatcherPlugin implements Plugin { /** * @var RequestMatcher */ private $requestMatcher; /** * @var Plugin */ private $delegatedPlugin; /** * @param RequestMatcher $requestMatcher * @param Plugin $delegatedPlugin */ public function __construct(RequestMatcher $requestMatcher, Plugin $delegatedPlugin) { $this->requestMatcher = $requestMatcher; $this->delegatedPlugin = $delegatedPlugin; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { if ($this->requestMatcher->matches($request)) { return $this->delegatedPlugin->handleRequest($request, $next, $first); } return $next($request); } } client-common/src/Plugin/QueryDefaultsPlugin.php 0000666 00000002230 15165515606 0016035 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * Set query to default value if it does not exist. * * If a given query parameter already exists the value wont be replaced and the request wont be changed. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class QueryDefaultsPlugin implements Plugin { /** * @var array */ private $queryParams = []; /** * @param array $queryParams Hashmap of query name to query value. Names and values must not be url encoded as * this plugin will encode them */ public function __construct(array $queryParams) { $this->queryParams = $queryParams; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $uri = $request->getUri(); parse_str($uri->getQuery(), $query); $query += $this->queryParams; $request = $request->withUri( $uri->withQuery(http_build_query($query)) ); return $next($request); } } client-common/src/Plugin/HeaderDefaultsPlugin.php 0000666 00000001776 15165515606 0016136 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * Set header to default value if it does not exist. * * If a given header already exists the value wont be replaced and the request wont be changed. * * @author Soufiane Ghzal <sghzal@gmail.com> */ final class HeaderDefaultsPlugin implements Plugin { /** * @var array */ private $headers = []; /** * @param array $headers Hashmap of header name to header value */ public function __construct(array $headers) { $this->headers = $headers; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { foreach ($this->headers as $header => $headerValue) { if (!$request->hasHeader($header)) { $request = $request->withHeader($header, $headerValue); } } return $next($request); } } client-common/src/Plugin/HistoryPlugin.php 0000666 00000002174 15165515606 0014710 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Record HTTP calls. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class HistoryPlugin implements Plugin { /** * Journal use to store request / responses / exception. * * @var Journal */ private $journal; /** * @param Journal $journal */ public function __construct(Journal $journal) { $this->journal = $journal; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $journal = $this->journal; return $next($request)->then(function (ResponseInterface $response) use ($request, $journal) { $journal->addSuccess($request, $response); return $response; }, function (Exception $exception) use ($request, $journal) { $journal->addFailure($request, $exception); throw $exception; }); } } client-common/src/Plugin/ContentTypePlugin.php 0000666 00000006417 15165515606 0015527 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\StreamInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Allow to set the correct content type header on the request automatically only if it is not set. * * @author Karim Pinchon <karim.pinchon@gmail.com> */ final class ContentTypePlugin implements Plugin { /** * Allow to disable the content type detection when stream is too large (as it can consume a lot of resource). * * @var bool * * true skip the content type detection * false detect the content type (default value) */ protected $skipDetection; /** * Determine the size stream limit for which the detection as to be skipped (default to 16Mb). * * @var int */ protected $sizeLimit; /** * @param array $config { * * @var bool $skip_detection True skip detection if stream size is bigger than $size_limit. * @var int $size_limit size stream limit for which the detection as to be skipped. * } */ public function __construct(array $config = []) { $resolver = new OptionsResolver(); $resolver->setDefaults([ 'skip_detection' => false, 'size_limit' => 16000000, ]); $resolver->setAllowedTypes('skip_detection', 'bool'); $resolver->setAllowedTypes('size_limit', 'int'); $options = $resolver->resolve($config); $this->skipDetection = $options['skip_detection']; $this->sizeLimit = $options['size_limit']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { if (!$request->hasHeader('Content-Type')) { $stream = $request->getBody(); $streamSize = $stream->getSize(); if (!$stream->isSeekable()) { return $next($request); } if (0 === $streamSize) { return $next($request); } if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) { return $next($request); } if ($this->isJson($stream)) { $request = $request->withHeader('Content-Type', 'application/json'); return $next($request); } if ($this->isXml($stream)) { $request = $request->withHeader('Content-Type', 'application/xml'); return $next($request); } } return $next($request); } /** * @param $stream StreamInterface * * @return bool */ private function isJson($stream) { $stream->rewind(); json_decode($stream->getContents()); return JSON_ERROR_NONE === json_last_error(); } /** * @param $stream StreamInterface * * @return \SimpleXMLElement|false */ private function isXml($stream) { $stream->rewind(); $previousValue = libxml_use_internal_errors(true); $isXml = simplexml_load_string($stream->getContents()); libxml_use_internal_errors($previousValue); return $isXml; } } client-common/src/Plugin/AddPathPlugin.php 0000666 00000005426 15165515606 0014557 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\UriInterface; /** * Prepend a base path to the request URI. Useful for base API URLs like http://domain.com/api. * * @author Sullivan Senechal <soullivaneuh@gmail.com> */ final class AddPathPlugin implements Plugin { /** * @var UriInterface */ private $uri; /** * Stores identifiers of the already altered requests. * * @var array */ private $alteredRequests = []; /** * @param UriInterface $uri */ public function __construct(UriInterface $uri) { if ('' === $uri->getPath()) { throw new \LogicException('URI path cannot be empty'); } if ('/' === substr($uri->getPath(), -1)) { $uri = $uri->withPath(rtrim($uri->getPath(), '/')); } $this->uri = $uri; } /** * Adds a prefix in the beginning of the URL's path. * * The prefix is not added if that prefix is already on the URL's path. This will fail on the edge * case of the prefix being repeated, for example if `https://example.com/api/api/foo` is a valid * URL on the server and the configured prefix is `/api`. * * We looked at other solutions, but they are all much more complicated, while still having edge * cases: * - Doing an spl_object_hash on `$first` will lead to collisions over time because over time the * hash can collide. * - Have the PluginClient provide a magic header to identify the request chain and only apply * this plugin once. * * There are 2 reasons for the AddPathPlugin to be executed twice on the same request: * - A plugin can restart the chain by calling `$first`, e.g. redirect * - A plugin can call `$next` more than once, e.g. retry * * Depending on the scenario, the path should or should not be added. E.g. `$first` could * be called after a redirect response from the server. The server likely already has the * correct path. * * No solution fits all use cases. This implementation will work fine for the common use cases. * If you have a specific situation where this is not the right thing, you can build a custom plugin * that does exactly what you need. * * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { $prepend = $this->uri->getPath(); $path = $request->getUri()->getPath(); if (substr($path, 0, strlen($prepend)) !== $prepend) { $request = $request->withUri($request->getUri() ->withPath($prepend.$path) ); } return $next($request); } } client-common/src/Plugin/VersionBridgePlugin.php 0000666 00000001155 15165515606 0016007 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaPsr\Http\Message\RequestInterface; /** * A plugin that helps you migrate from php-http/client-common 1.x to 2.x. This * will also help you to support PHP5 at the same time you support 2.x. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ trait VersionBridgePlugin { abstract protected function doHandleRequest(RequestInterface $request, callable $next, callable $first); public function handleRequest(RequestInterface $request, callable $next, callable $first) { return $this->doHandleRequest($request, $next, $first); } } client-common/src/Plugin/RedirectPlugin.php 0000666 00000021644 15165515606 0015013 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Common\Exception\CircularRedirectionException; use AmeliaHttp\Client\Common\Exception\MultipleRedirectionException; use AmeliaHttp\Client\Common\Plugin; use AmeliaHttp\Client\Exception\HttpException; use AmeliaPsr\Http\Message\MessageInterface; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; use AmeliaPsr\Http\Message\UriInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * Follow redirections. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class RedirectPlugin implements Plugin { /** * Rule on how to redirect, change method for the new request. * * @var array */ protected $redirectCodes = [ 300 => [ 'switch' => [ 'unless' => ['GET', 'HEAD'], 'to' => 'GET', ], 'multiple' => true, 'permanent' => false, ], 301 => [ 'switch' => [ 'unless' => ['GET', 'HEAD'], 'to' => 'GET', ], 'multiple' => false, 'permanent' => true, ], 302 => [ 'switch' => [ 'unless' => ['GET', 'HEAD'], 'to' => 'GET', ], 'multiple' => false, 'permanent' => false, ], 303 => [ 'switch' => [ 'unless' => ['GET', 'HEAD'], 'to' => 'GET', ], 'multiple' => false, 'permanent' => false, ], 307 => [ 'switch' => false, 'multiple' => false, 'permanent' => false, ], 308 => [ 'switch' => false, 'multiple' => false, 'permanent' => true, ], ]; /** * Determine how header should be preserved from old request. * * @var bool|array * * true will keep all previous headers (default value) * false will ditch all previous headers * string[] will keep only headers with the specified names */ protected $preserveHeader; /** * Store all previous redirect from 301 / 308 status code. * * @var array */ protected $redirectStorage = []; /** * Whether the location header must be directly used for a multiple redirection status code (300). * * @var bool */ protected $useDefaultForMultiple; /** * @var array */ protected $circularDetection = []; /** * @param array $config { * * @var bool|string[] $preserve_header True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep * @var bool $use_default_for_multiple Whether the location header must be directly used for a multiple redirection status code (300). * } */ public function __construct(array $config = []) { $resolver = new OptionsResolver(); $resolver->setDefaults([ 'preserve_header' => true, 'use_default_for_multiple' => true, ]); $resolver->setAllowedTypes('preserve_header', ['bool', 'array']); $resolver->setAllowedTypes('use_default_for_multiple', 'bool'); $resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) { if (is_bool($value) && false === $value) { return []; } return $value; }); $options = $resolver->resolve($config); $this->preserveHeader = $options['preserve_header']; $this->useDefaultForMultiple = $options['use_default_for_multiple']; } /** * {@inheritdoc} */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { // Check in storage if (array_key_exists((string) $request->getUri(), $this->redirectStorage)) { $uri = $this->redirectStorage[(string) $request->getUri()]['uri']; $statusCode = $this->redirectStorage[(string) $request->getUri()]['status']; $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode); return $first($redirectRequest); } return $next($request)->then(function (ResponseInterface $response) use ($request, $first) { $statusCode = $response->getStatusCode(); if (!array_key_exists($statusCode, $this->redirectCodes)) { return $response; } $uri = $this->createUri($response, $request); $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode); $chainIdentifier = spl_object_hash((object) $first); if (!array_key_exists($chainIdentifier, $this->circularDetection)) { $this->circularDetection[$chainIdentifier] = []; } $this->circularDetection[$chainIdentifier][] = (string) $request->getUri(); if (in_array((string) $redirectRequest->getUri(), $this->circularDetection[$chainIdentifier])) { throw new CircularRedirectionException('Circular redirection detected', $request, $response); } if ($this->redirectCodes[$statusCode]['permanent']) { $this->redirectStorage[(string) $request->getUri()] = [ 'uri' => $uri, 'status' => $statusCode, ]; } // Call redirect request in synchrone $redirectPromise = $first($redirectRequest); return $redirectPromise->wait(); }); } /** * Builds the redirect request. * * @param RequestInterface $request Original request * @param UriInterface $uri New uri * @param int $statusCode Status code from the redirect response * * @return MessageInterface|RequestInterface */ protected function buildRedirectRequest(RequestInterface $request, UriInterface $uri, $statusCode) { $request = $request->withUri($uri); if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($request->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) { $request = $request->withMethod($this->redirectCodes[$statusCode]['switch']['to']); } if (is_array($this->preserveHeader)) { $headers = array_keys($request->getHeaders()); foreach ($headers as $name) { if (!in_array($name, $this->preserveHeader)) { $request = $request->withoutHeader($name); } } } return $request; } /** * Creates a new Uri from the old request and the location header. * * @param ResponseInterface $response The redirect response * @param RequestInterface $request The original request * * @throws HttpException If location header is not usable (missing or incorrect) * @throws MultipleRedirectionException If a 300 status code is received and default location cannot be resolved (doesn't use the location header or not present) * * @return UriInterface */ private function createUri(ResponseInterface $response, RequestInterface $request) { if ($this->redirectCodes[$response->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$response->hasHeader('Location'))) { throw new MultipleRedirectionException('Cannot choose a redirection', $request, $response); } if (!$response->hasHeader('Location')) { throw new HttpException('Redirect status code, but no location header present in the response', $request, $response); } $location = $response->getHeaderLine('Location'); $parsedLocation = parse_url($location); if (false === $parsedLocation) { throw new HttpException(sprintf('Location %s could not be parsed', $location), $request, $response); } $uri = $request->getUri(); if (array_key_exists('scheme', $parsedLocation)) { $uri = $uri->withScheme($parsedLocation['scheme']); } if (array_key_exists('host', $parsedLocation)) { $uri = $uri->withHost($parsedLocation['host']); } if (array_key_exists('port', $parsedLocation)) { $uri = $uri->withPort($parsedLocation['port']); } if (array_key_exists('path', $parsedLocation)) { $uri = $uri->withPath($parsedLocation['path']); } if (array_key_exists('query', $parsedLocation)) { $uri = $uri->withQuery($parsedLocation['query']); } else { $uri = $uri->withQuery(''); } if (array_key_exists('fragment', $parsedLocation)) { $uri = $uri->withFragment($parsedLocation['fragment']); } else { $uri = $uri->withFragment(''); } return $uri; } } client-common/src/HttpAsyncClientEmulator.php 0000666 00000001464 15165515606 0015420 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception; use AmeliaHttp\Client\Promise; use AmeliaPsr\Http\Message\RequestInterface; /** * Emulates an HTTP Async Client in an HTTP Client. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait HttpAsyncClientEmulator { /** * {@inheritdoc} * * @see HttpClient::sendRequest */ abstract public function sendRequest(RequestInterface $request); /** * {@inheritdoc} * * @see HttpAsyncClient::sendAsyncRequest */ public function sendAsyncRequest(RequestInterface $request) { try { return new Promise\HttpFulfilledPromise($this->sendRequest($request)); } catch (Exception $e) { return new Promise\HttpRejectedPromise($e); } } } client-common/src/Plugin.php 0000666 00000002072 15165515606 0012065 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\RequestInterface; /** * A plugin is a middleware to transform the request and/or the response. * * The plugin can: * - break the chain and return a response * - dispatch the request to the next middleware * - restart the request * * @author Joel Wurtz <joel.wurtz@gmail.com> */ interface Plugin { /** * Handle the request and return the response coming from the next callable. * * @see http://docs.php-http.org/en/latest/plugins/build-your-own.html * * @param RequestInterface $request * @param callable $next Next middleware in the chain, the request is passed as the first argument * @param callable $first First middleware in the chain, used to to restart a request * * @return Promise Resolves a PSR-7 Response or fails with an AmeliaHttp\Client\Exception (The same as HttpAsyncClient). */ public function handleRequest(RequestInterface $request, callable $next, callable $first); } client-common/src/Deferred.php 0000666 00000006050 15165515606 0012347 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\ResponseInterface; /** * A deferred allow to return a promise which has not been resolved yet. */ class Deferred implements Promise { private $value; private $failure; private $state; private $waitCallback; private $onFulfilledCallbacks; private $onRejectedCallbacks; public function __construct(callable $waitCallback) { $this->waitCallback = $waitCallback; $this->state = Promise::PENDING; $this->onFulfilledCallbacks = []; $this->onRejectedCallbacks = []; } /** * {@inheritdoc} */ public function then(callable $onFulfilled = null, callable $onRejected = null) { $deferred = new self($this->waitCallback); $this->onFulfilledCallbacks[] = function (ResponseInterface $response) use ($onFulfilled, $deferred) { try { if (null !== $onFulfilled) { $response = $onFulfilled($response); } $deferred->resolve($response); } catch (Exception $exception) { $deferred->reject($exception); } }; $this->onRejectedCallbacks[] = function (Exception $exception) use ($onRejected, $deferred) { try { if (null !== $onRejected) { $response = $onRejected($exception); $deferred->resolve($response); return; } $deferred->reject($exception); } catch (Exception $newException) { $deferred->reject($newException); } }; return $deferred; } /** * {@inheritdoc} */ public function getState() { return $this->state; } /** * Resolve this deferred with a Response. */ public function resolve(ResponseInterface $response) { if (self::PENDING !== $this->state) { return; } $this->value = $response; $this->state = self::FULFILLED; foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) { $onFulfilledCallback($response); } } /** * Reject this deferred with an Exception. */ public function reject(Exception $exception) { if (self::PENDING !== $this->state) { return; } $this->failure = $exception; $this->state = self::REJECTED; foreach ($this->onRejectedCallbacks as $onRejectedCallback) { $onRejectedCallback($exception); } } /** * {@inheritdoc} */ public function wait($unwrap = true) { if (self::PENDING === $this->state) { $callback = $this->waitCallback; $callback(); } if (!$unwrap) { return; } if (self::FULFILLED === $this->state) { return $this->value; } throw $this->failure; } } client-common/src/PluginClient.php 0000666 00000013107 15165515606 0013225 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Common\Exception\LoopException; use AmeliaHttp\Client\Exception as HttplugException; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Client\Promise\HttpFulfilledPromise; use AmeliaHttp\Client\Promise\HttpRejectedPromise; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; use Symfony\Component\OptionsResolver\OptionsResolver; /** * The client managing plugins and providing a decorator around HTTP Clients. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class PluginClient implements HttpClient, HttpAsyncClient { /** * An HTTP async client. * * @var HttpAsyncClient */ private $client; /** * The plugin chain. * * @var Plugin[] */ private $plugins; /** * A list of options. * * @var array */ private $options; /** * @param HttpClient|HttpAsyncClient|ClientInterface $client * @param Plugin[] $plugins * @param array $options { * * @var int $max_restarts * @var Plugin[] $debug_plugins an array of plugins that are injected between each normal plugin * } * * @throws \RuntimeException if client is not an instance of HttpClient or HttpAsyncClient */ public function __construct($client, array $plugins = [], array $options = []) { if ($client instanceof HttpAsyncClient) { $this->client = $client; } elseif ($client instanceof HttpClient || $client instanceof ClientInterface) { $this->client = new EmulatedHttpAsyncClient($client); } else { throw new \RuntimeException('Client must be an instance of AmeliaHttp\\Client\\HttpClient or AmeliaHttp\\Client\\HttpAsyncClient'); } $this->plugins = $plugins; $this->options = $this->configure($options); } /** * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { // If we don't have an http client, use the async call if (!($this->client instanceof HttpClient)) { return $this->sendAsyncRequest($request)->wait(); } // Else we want to use the synchronous call of the underlying client, and not the async one in the case // we have both an async and sync call $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { try { return new HttpFulfilledPromise($this->client->sendRequest($request)); } catch (HttplugException $exception) { return new HttpRejectedPromise($exception); } }); return $pluginChain($request)->wait(); } /** * {@inheritdoc} */ public function sendAsyncRequest(RequestInterface $request) { $pluginChain = $this->createPluginChain($this->plugins, function (RequestInterface $request) { return $this->client->sendAsyncRequest($request); }); return $pluginChain($request); } /** * Configure the plugin client. * * @param array $options * * @return array */ private function configure(array $options = []) { if (isset($options['debug_plugins'])) { @trigger_error('The "debug_plugins" option is deprecated since 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); } $resolver = new OptionsResolver(); $resolver->setDefaults([ 'max_restarts' => 10, 'debug_plugins' => [], ]); $resolver ->setAllowedTypes('debug_plugins', 'array') ->setAllowedValues('debug_plugins', function (array $plugins) { foreach ($plugins as $plugin) { // Make sure each object passed with the `debug_plugins` is an instance of Plugin. if (!$plugin instanceof Plugin) { return false; } } return true; }); return $resolver->resolve($options); } /** * Create the plugin chain. * * @param Plugin[] $pluginList A list of plugins * @param callable $clientCallable Callable making the HTTP call * * @return callable */ private function createPluginChain($pluginList, callable $clientCallable) { $firstCallable = $lastCallable = $clientCallable; /* * Inject debug plugins between each plugin. */ $pluginListWithDebug = $this->options['debug_plugins']; foreach ($pluginList as $plugin) { $pluginListWithDebug[] = $plugin; $pluginListWithDebug = array_merge($pluginListWithDebug, $this->options['debug_plugins']); } while ($plugin = array_pop($pluginListWithDebug)) { $lastCallable = function (RequestInterface $request) use ($plugin, $lastCallable, &$firstCallable) { return $plugin->handleRequest($request, $lastCallable, $firstCallable); }; $firstCallable = $lastCallable; } $firstCalls = 0; $firstCallable = function (RequestInterface $request) use ($lastCallable, &$firstCalls) { if ($firstCalls > $this->options['max_restarts']) { throw new LoopException('Too many restarts in plugin client', $request); } ++$firstCalls; return $lastCallable($request); }; return $firstCallable; } } client-common/src/PluginClientFactory.php 0000666 00000003567 15165515606 0014566 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; /** * Factory to create PluginClient instances. Using this factory instead of calling PluginClient constructor will enable * the Symfony profiling without any configuration. * * @author Fabien Bourigault <bourigaultfabien@gmail.com> */ final class PluginClientFactory { /** * @var callable */ private static $factory; /** * Set the factory to use. * The callable to provide must have the same arguments and return type as PluginClientFactory::createClient. * This is used by the HTTPlugBundle to provide a better Symfony integration. * Unlike the createClient method, this one is static to allow zero configuration profiling by hooking into early * application execution. * * @internal * * @param callable $factory */ public static function setFactory(callable $factory) { static::$factory = $factory; } /** * @param HttpClient|HttpAsyncClient|ClientInterface $client * @param Plugin[] $plugins * @param array $options { * * @var string $client_name to give client a name which may be used when displaying client information like in * the HTTPlugBundle profiler. * } * * @see PluginClient constructor for PluginClient specific $options. * * @return PluginClient */ public function createClient($client, array $plugins = [], array $options = []) { if (static::$factory) { $factory = static::$factory; return $factory($client, $plugins, $options); } unset($options['client_name']); return new PluginClient($client, $plugins, $options); } } client-common/src/HttpClientPool.php 0000666 00000003102 15165515606 0013532 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Common\Exception\HttpClientNotFoundException; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; /** * A http client pool allows to send requests on a pool of different http client using a specific strategy (least used, * round robin, ...). */ abstract class HttpClientPool implements HttpAsyncClient, HttpClient { /** * @var HttpClientPoolItem[] */ protected $clientPool = []; /** * Add a client to the pool. * * @param HttpClient|HttpAsyncClient|HttpClientPoolItem|ClientInterface $client */ public function addHttpClient($client) { if (!$client instanceof HttpClientPoolItem) { $client = new HttpClientPoolItem($client); } $this->clientPool[] = $client; } /** * Return an http client given a specific strategy. * * @throws HttpClientNotFoundException When no http client has been found into the pool * * @return HttpClientPoolItem Return a http client that can do both sync or async */ abstract protected function chooseHttpClient(); /** * {@inheritdoc} */ public function sendAsyncRequest(RequestInterface $request) { return $this->chooseHttpClient()->sendAsyncRequest($request); } /** * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { return $this->chooseHttpClient()->sendRequest($request); } } client-common/src/HttpClientPool/RoundRobinClientPool.php 0000666 00000002245 15165515606 0017613 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\HttpClientPool; use AmeliaHttp\Client\Common\Exception\HttpClientNotFoundException; use AmeliaHttp\Client\Common\HttpClientPool; /** * RoundRobinClientPool will choose the next client in the pool. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class RoundRobinClientPool extends HttpClientPool { /** * {@inheritdoc} */ protected function chooseHttpClient() { $last = current($this->clientPool); do { $client = next($this->clientPool); if (false === $client) { $client = reset($this->clientPool); if (false === $client) { throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); } } // Case when there is only one and the last one has been disabled if ($last === $client && $client->isDisabled()) { throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool'); } } while ($client->isDisabled()); return $client; } } client-common/src/HttpClientPool/RandomClientPool.php 0000666 00000001561 15165515606 0016752 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\HttpClientPool; use AmeliaHttp\Client\Common\Exception\HttpClientNotFoundException; use AmeliaHttp\Client\Common\HttpClientPool; use AmeliaHttp\Client\Common\HttpClientPoolItem; /** * RoundRobinClientPool will choose the next client in the pool. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class RandomClientPool extends HttpClientPool { /** * {@inheritdoc} */ protected function chooseHttpClient() { $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); }); if (0 === count($clientPool)) { throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); } return $clientPool[array_rand($clientPool)]; } } client-common/src/HttpClientPool/LeastUsedClientPool.php 0000666 00000002505 15165515606 0017422 0 ustar 00 <?php namespace AmeliaHttp\Client\Common\HttpClientPool; use AmeliaHttp\Client\Common\Exception\HttpClientNotFoundException; use AmeliaHttp\Client\Common\HttpClientPool; use AmeliaHttp\Client\Common\HttpClientPoolItem; /** * LeastUsedClientPool will choose the client with the less current request in the pool. * * This strategy is only useful when doing async request * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class LeastUsedClientPool extends HttpClientPool { /** * {@inheritdoc} */ protected function chooseHttpClient() { $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) { return !$clientPoolItem->isDisabled(); }); if (0 === count($clientPool)) { throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool'); } usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) { if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) { return 0; } if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) { return -1; } return 1; }); return reset($clientPool); } } client-common/src/EmulatedHttpAsyncClient.php 0000666 00000001547 15165515606 0015372 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; /** * Emulates an async HTTP client. * * This should be replaced by an anonymous class in PHP 7. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient { use HttpAsyncClientEmulator; use HttpClientDecorator; /** * @param HttpClient|ClientInterface $httpClient */ public function __construct($httpClient) { if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) { throw new \LogicException('Client must be an instance of AmeliaHttp\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); } $this->httpClient = $httpClient; } } client-common/src/EmulatedHttpClient.php 0000666 00000001127 15165515606 0014366 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; /** * Emulates an HTTP client. * * This should be replaced by an anonymous class in PHP 7. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class EmulatedHttpClient implements HttpClient, HttpAsyncClient { use HttpAsyncClientDecorator; use HttpClientEmulator; /** * @param HttpAsyncClient $httpAsyncClient */ public function __construct(HttpAsyncClient $httpAsyncClient) { $this->httpAsyncClient = $httpAsyncClient; } } client-common/src/BatchResult.php 0000666 00000010422 15165515606 0013045 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Responses and exceptions returned from parallel request execution. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class BatchResult { /** * @var \SplObjectStorage */ private $responses; /** * @var \SplObjectStorage */ private $exceptions; public function __construct() { $this->responses = new \SplObjectStorage(); $this->exceptions = new \SplObjectStorage(); } /** * Checks if there are any successful responses at all. * * @return bool */ public function hasResponses() { return $this->responses->count() > 0; } /** * Returns all successful responses. * * @return ResponseInterface[] */ public function getResponses() { $responses = []; foreach ($this->responses as $request) { $responses[] = $this->responses[$request]; } return $responses; } /** * Checks if there is a successful response for a request. * * @param RequestInterface $request * * @return bool */ public function isSuccessful(RequestInterface $request) { return $this->responses->contains($request); } /** * Returns the response for a successful request. * * @param RequestInterface $request * * @return ResponseInterface * * @throws \UnexpectedValueException If request was not part of the batch or failed */ public function getResponseFor(RequestInterface $request) { try { return $this->responses[$request]; } catch (\UnexpectedValueException $e) { throw new \UnexpectedValueException('Request not found', $e->getCode(), $e); } } /** * Adds a response in an immutable way. * * @param RequestInterface $request * @param ResponseInterface $response * * @return BatchResult the new BatchResult with this request-response pair added to it */ public function addResponse(RequestInterface $request, ResponseInterface $response) { $new = clone $this; $new->responses->attach($request, $response); return $new; } /** * Checks if there are any unsuccessful requests at all. * * @return bool */ public function hasExceptions() { return $this->exceptions->count() > 0; } /** * Returns all exceptions for the unsuccessful requests. * * @return Exception[] */ public function getExceptions() { $exceptions = []; foreach ($this->exceptions as $request) { $exceptions[] = $this->exceptions[$request]; } return $exceptions; } /** * Checks if there is an exception for a request, meaning the request failed. * * @param RequestInterface $request * * @return bool */ public function isFailed(RequestInterface $request) { return $this->exceptions->contains($request); } /** * Returns the exception for a failed request. * * @param RequestInterface $request * * @return Exception * * @throws \UnexpectedValueException If request was not part of the batch or was successful */ public function getExceptionFor(RequestInterface $request) { try { return $this->exceptions[$request]; } catch (\UnexpectedValueException $e) { throw new \UnexpectedValueException('Request not found', $e->getCode(), $e); } } /** * Adds an exception in an immutable way. * * @param RequestInterface $request * @param Exception $exception * * @return BatchResult the new BatchResult with this request-exception pair added to it */ public function addException(RequestInterface $request, Exception $exception) { $new = clone $this; $new->exceptions->attach($request, $exception); return $new; } public function __clone() { $this->responses = clone $this->responses; $this->exceptions = clone $this->exceptions; } } client-common/src/HttpClientPoolItem.php 0000666 00000010554 15165515606 0014362 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaHttp\Client\Exception; /** * A HttpClientPoolItem represent a HttpClient inside a Pool. * * It is disabled when a request failed and can be reenable after a certain number of seconds * It also keep tracks of the current number of request the client is currently sending (only usable for async method) * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class HttpClientPoolItem implements HttpClient, HttpAsyncClient { /** * @var int Number of request this client is currently sending */ private $sendingRequestCount = 0; /** * @var \DateTime|null Time when this client has been disabled or null if enable */ private $disabledAt; /** * @var int|null Number of seconds after this client is reenable, by default null: never reenable this client */ private $reenableAfter; /** * @var FlexibleHttpClient A http client responding to async and sync request */ private $client; /** * @param HttpClient|HttpAsyncClient|ClientInterface $client * @param null|int $reenableAfter Number of seconds after this client is reenable */ public function __construct($client, $reenableAfter = null) { $this->client = new FlexibleHttpClient($client); $this->reenableAfter = $reenableAfter; } /** * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { if ($this->isDisabled()) { throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); } try { $this->incrementRequestCount(); $response = $this->client->sendRequest($request); $this->decrementRequestCount(); } catch (Exception $e) { $this->disable(); $this->decrementRequestCount(); throw $e; } return $response; } /** * {@inheritdoc} */ public function sendAsyncRequest(RequestInterface $request) { if ($this->isDisabled()) { throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request); } $this->incrementRequestCount(); return $this->client->sendAsyncRequest($request)->then(function ($response) { $this->decrementRequestCount(); return $response; }, function ($exception) { $this->disable(); $this->decrementRequestCount(); throw $exception; }); } /** * Whether this client is disabled or not. * * Will also reactivate this client if possible * * @internal * * @return bool */ public function isDisabled() { $disabledAt = $this->getDisabledAt(); if (null !== $this->reenableAfter && null !== $disabledAt) { // Reenable after a certain time $now = new \DateTime(); if (($now->getTimestamp() - $disabledAt->getTimestamp()) >= $this->reenableAfter) { $this->enable(); return false; } return true; } return null !== $disabledAt; } /** * Get current number of request that is send by the underlying http client. * * @internal * * @return int */ public function getSendingRequestCount() { return $this->sendingRequestCount; } /** * Return when this client has been disabled or null if it's enabled. * * @return \DateTime|null */ private function getDisabledAt() { return $this->disabledAt; } /** * Increment the request count. */ private function incrementRequestCount() { ++$this->sendingRequestCount; } /** * Decrement the request count. */ private function decrementRequestCount() { --$this->sendingRequestCount; } /** * Enable the current client. */ private function enable() { $this->disabledAt = null; } /** * Disable the current client. */ private function disable() { $this->disabledAt = new \DateTime('now'); } } client-common/src/HttpMethodsClient.php 0000666 00000012475 15165515606 0014241 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Message\RequestFactory; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; use AmeliaPsr\Http\Message\StreamInterface; use AmeliaPsr\Http\Message\UriInterface; /** * Convenience HTTP client that integrates the MessageFactory in order to send * requests in the following form:. * * $client * ->get('/foo') * ->post('/bar') * ; * * The client also exposes the sendRequest methods of the wrapped HttpClient. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * @author David Buchmann <mail@davidbu.ch> */ class HttpMethodsClient implements HttpClient { /** * @var HttpClient|ClientInterface */ private $httpClient; /** * @var RequestFactory */ private $requestFactory; /** * @param HttpClient|ClientInterface $httpClient The client to send requests with * @param RequestFactory $requestFactory The message factory to create requests */ public function __construct($httpClient, RequestFactory $requestFactory) { if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) { throw new \LogicException('Client must be an instance of AmeliaHttp\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); } $this->httpClient = $httpClient; $this->requestFactory = $requestFactory; } /** * Sends a GET request. * * @param string|UriInterface $uri * @param array $headers * * @throws Exception * * @return ResponseInterface */ public function get($uri, array $headers = []) { return $this->send('GET', $uri, $headers, null); } /** * Sends an HEAD request. * * @param string|UriInterface $uri * @param array $headers * * @throws Exception * * @return ResponseInterface */ public function head($uri, array $headers = []) { return $this->send('HEAD', $uri, $headers, null); } /** * Sends a TRACE request. * * @param string|UriInterface $uri * @param array $headers * * @throws Exception * * @return ResponseInterface */ public function trace($uri, array $headers = []) { return $this->send('TRACE', $uri, $headers, null); } /** * Sends a POST request. * * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function post($uri, array $headers = [], $body = null) { return $this->send('POST', $uri, $headers, $body); } /** * Sends a PUT request. * * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function put($uri, array $headers = [], $body = null) { return $this->send('PUT', $uri, $headers, $body); } /** * Sends a PATCH request. * * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function patch($uri, array $headers = [], $body = null) { return $this->send('PATCH', $uri, $headers, $body); } /** * Sends a DELETE request. * * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function delete($uri, array $headers = [], $body = null) { return $this->send('DELETE', $uri, $headers, $body); } /** * Sends an OPTIONS request. * * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function options($uri, array $headers = [], $body = null) { return $this->send('OPTIONS', $uri, $headers, $body); } /** * Sends a request with any HTTP method. * * @param string $method HTTP method to use * @param string|UriInterface $uri * @param array $headers * @param string|StreamInterface|null $body * * @throws Exception * * @return ResponseInterface */ public function send($method, $uri, array $headers = [], $body = null) { return $this->sendRequest($this->requestFactory->createRequest( $method, $uri, $headers, $body )); } /** * Forward to the underlying HttpClient. * * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { return $this->httpClient->sendRequest($request); } } client-common/src/HttpClientEmulator.php 0000666 00000001203 15165515606 0014411 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaPsr\Http\Message\RequestInterface; /** * Emulates an HTTP Client in an HTTP Async Client. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait HttpClientEmulator { /** * {@inheritdoc} * * @see HttpClient::sendRequest */ public function sendRequest(RequestInterface $request) { $promise = $this->sendAsyncRequest($request); return $promise->wait(); } /** * {@inheritdoc} * * @see HttpAsyncClient::sendAsyncRequest */ abstract public function sendAsyncRequest(RequestInterface $request); } client-common/src/BatchClient.php 0000666 00000004423 15165515606 0013011 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Client\Common\Exception\BatchException; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; /** * BatchClient allow to sends multiple request and retrieve a Batch Result. * * This implementation simply loops over the requests and uses sendRequest with each of them. * * @author Joel Wurtz <jwurtz@jolicode.com> */ class BatchClient implements HttpClient { /** * @var HttpClient|ClientInterface */ private $client; /** * @param HttpClient|ClientInterface $client */ public function __construct($client) { if (!($client instanceof HttpClient) && !($client instanceof ClientInterface)) { throw new \LogicException('Client must be an instance of AmeliaHttp\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface'); } $this->client = $client; } /** * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { return $this->client->sendRequest($request); } /** * Send several requests. * * You may not assume that the requests are executed in a particular order. If the order matters * for your application, use sendRequest sequentially. * * @param RequestInterface[] The requests to send * * @return BatchResult Containing one result per request * * @throws BatchException If one or more requests fails. The exception gives access to the * BatchResult with a map of request to result for success, request to * exception for failures */ public function sendRequests(array $requests) { $batchResult = new BatchResult(); foreach ($requests as $request) { try { $response = $this->sendRequest($request); $batchResult = $batchResult->addResponse($request, $response); } catch (Exception $e) { $batchResult = $batchResult->addException($request, $e); } } if ($batchResult->hasExceptions()) { throw new BatchException($batchResult); } return $batchResult; } } client-common/src/HttpAsyncClientDecorator.php 0000666 00000001101 15165515606 0015536 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaPsr\Http\Message\RequestInterface; /** * Decorates an HTTP Async Client. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait HttpAsyncClientDecorator { /** * @var HttpAsyncClient */ protected $httpAsyncClient; /** * {@inheritdoc} * * @see HttpAsyncClient::sendAsyncRequest */ public function sendAsyncRequest(RequestInterface $request) { return $this->httpAsyncClient->sendAsyncRequest($request); } } client-common/src/VersionBridgeClient.php 0000666 00000001011 15165515606 0014520 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaPsr\Http\Message\RequestInterface; /** * A client that helps you migrate from php-http/httplug 1.x to 2.x. This * will also help you to support PHP5 at the same time you support 2.x. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ trait VersionBridgeClient { abstract protected function doSendRequest(RequestInterface $request); public function sendRequest(RequestInterface $request) { return $this->doSendRequest($request); } } client-common/src/HttpClientRouter.php 0000666 00000003576 15165515606 0014120 0 ustar 00 <?php namespace AmeliaHttp\Client\Common; use AmeliaHttp\Client\Exception\RequestException; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Message\RequestMatcher; use Psr\Http\Client\ClientInterface; use AmeliaPsr\Http\Message\RequestInterface; /** * Route a request to a specific client in the stack based using a RequestMatcher. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class HttpClientRouter implements HttpClient, HttpAsyncClient { /** * @var array */ private $clients = []; /** * {@inheritdoc} */ public function sendRequest(RequestInterface $request) { $client = $this->chooseHttpClient($request); return $client->sendRequest($request); } /** * {@inheritdoc} */ public function sendAsyncRequest(RequestInterface $request) { $client = $this->chooseHttpClient($request); return $client->sendAsyncRequest($request); } /** * Add a client to the router. * * @param HttpClient|HttpAsyncClient|ClientInterface $client * @param RequestMatcher $requestMatcher */ public function addClient($client, RequestMatcher $requestMatcher) { $this->clients[] = [ 'matcher' => $requestMatcher, 'client' => new FlexibleHttpClient($client), ]; } /** * Choose an HTTP client given a specific request. * * @param RequestInterface $request * * @return HttpClient|HttpAsyncClient|ClientInterface */ protected function chooseHttpClient(RequestInterface $request) { foreach ($this->clients as $client) { if ($client['matcher']->matches($request)) { return $client['client']; } } throw new RequestException('No client found for the specified request', $request); } } client-common/LICENSE 0000666 00000002072 15165515606 0010334 0 ustar 00 Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org> 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. client-common/composer.json 0000666 00000002335 15165515606 0012053 0 ustar 00 { "name": "php-http/client-common", "description": "Common HTTP Client implementations and tools for HTTPlug", "license": "MIT", "keywords": ["http", "client", "httplug", "common"], "homepage": "http://httplug.io", "authors": [ { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require": { "php": "^5.4 || ^7.0", "php-http/httplug": "^1.1", "php-http/message-factory": "^1.0", "php-http/message": "^1.6", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" }, "require-dev": { "phpspec/phpspec": "^2.5 || ^3.4 || ^4.2", "guzzlehttp/psr7": "^1.4" }, "suggest": { "php-http/logger-plugin": "PSR-3 Logger plugin", "php-http/cache-plugin": "PSR-6 Cache plugin", "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" }, "autoload": { "psr-4": { "Http\\Client\\Common\\": "src/" } }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" }, "extra": { "branch-alias": { "dev-master": "1.10.x-dev" } } } promise/composer.json 0000666 00000001524 15165515606 0010764 0 ustar 00 { "name": "php-http/promise", "description": "Promise used for asynchronous HTTP requests", "license": "MIT", "keywords": ["promise"], "homepage": "http://httplug.io", "authors": [ { "name": "Joel Wurtz", "email": "joel.wurtz@gmail.com" }, { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require-dev": { "phpspec/phpspec": "^2.4", "henrikbjorn/phpspec-code-coverage" : "^1.0" }, "autoload": { "psr-4": { "Http\\Promise\\": "src/" } }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.yml.ci" }, "extra": { "branch-alias": { "dev-master": "1.1-dev" } } } promise/CHANGELOG.md 0000666 00000000611 15165515606 0010047 0 ustar 00 # Change Log ## 1.0.0 - 2016-01-26 ### Removed - PSR-7 dependency ## 1.0.0-RC1 - 2016-01-12 ### Added - Tests for full coverage ## Changed - Updated package files - Clarified wait method behavior - Contributing guide moved to the documentation ## 0.1.1 - 2015-12-24 ## Added - Fulfilled and Rejected promise implementations ## 0.1.0 - 2015-12-13 ## Added - Promise interface promise/LICENSE 0000666 00000002072 15165515606 0007246 0 ustar 00 Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org> 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. promise/src/FulfilledPromise.php 0000666 00000001750 15165515606 0013010 0 ustar 00 <?php namespace AmeliaHttp\Promise; /** * A promise already fulfilled. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class FulfilledPromise implements Promise { /** * @var mixed */ private $result; /** * @param $result */ public function __construct($result) { $this->result = $result; } /** * {@inheritdoc} */ public function then(callable $onFulfilled = null, callable $onRejected = null) { if (null === $onFulfilled) { return $this; } try { return new self($onFulfilled($this->result)); } catch (\Exception $e) { return new RejectedPromise($e); } } /** * {@inheritdoc} */ public function getState() { return Promise::FULFILLED; } /** * {@inheritdoc} */ public function wait($unwrap = true) { if ($unwrap) { return $this->result; } } } promise/src/RejectedPromise.php 0000666 00000002013 15165515606 0012620 0 ustar 00 <?php namespace AmeliaHttp\Promise; /** * A rejected promise. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class RejectedPromise implements Promise { /** * @var \Exception */ private $exception; /** * @param \Exception $exception */ public function __construct(\Exception $exception) { $this->exception = $exception; } /** * {@inheritdoc} */ public function then(callable $onFulfilled = null, callable $onRejected = null) { if (null === $onRejected) { return $this; } try { return new FulfilledPromise($onRejected($this->exception)); } catch (\Exception $e) { return new self($e); } } /** * {@inheritdoc} */ public function getState() { return Promise::REJECTED; } /** * {@inheritdoc} */ public function wait($unwrap = true) { if ($unwrap) { throw $this->exception; } } } promise/src/Promise.php 0000666 00000004271 15165515606 0011162 0 ustar 00 <?php namespace AmeliaHttp\Promise; /** * Promise represents a value that may not be available yet, but will be resolved at some point in future. * It acts like a proxy to the actual value. * * This interface is an extension of the promises/a+ specification. * * @see https://promisesaplus.com/ * * @author Joel Wurtz <joel.wurtz@gmail.com> * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface Promise { /** * Promise has not been fulfilled or rejected. */ const PENDING = 'pending'; /** * Promise has been fulfilled. */ const FULFILLED = 'fulfilled'; /** * Promise has been rejected. */ const REJECTED = 'rejected'; /** * Adds behavior for when the promise is resolved or rejected (response will be available, or error happens). * * If you do not care about one of the cases, you can set the corresponding callable to null * The callback will be called when the value arrived and never more than once. * * @param callable $onFulfilled Called when a response will be available. * @param callable $onRejected Called when an exception occurs. * * @return Promise A new resolved promise with value of the executed callback (onFulfilled / onRejected). */ public function then(callable $onFulfilled = null, callable $onRejected = null); /** * Returns the state of the promise, one of PENDING, FULFILLED or REJECTED. * * @return string */ public function getState(); /** * Wait for the promise to be fulfilled or rejected. * * When this method returns, the request has been resolved and if callables have been * specified, the appropriate one has terminated. * * When $unwrap is true (the default), the response is returned, or the exception thrown * on failure. Otherwise, nothing is returned or thrown. * * @param bool $unwrap Whether to return resolved value / throw reason or not * * @return mixed Resolved value, null if $unwrap is set to false * * @throws \Exception The rejection reason if $unwrap is set to true and the request failed. */ public function wait($unwrap = true); } promise/README.md 0000666 00000003067 15165515606 0007525 0 ustar 00 # Promise [](https://github.com/php-http/promise/releases) [](LICENSE) [](https://travis-ci.org/php-http/promise) [](https://scrutinizer-ci.com/g/php-http/promise) [](https://scrutinizer-ci.com/g/php-http/promise) [](https://packagist.org/packages/php-http/promise) **Promise used for asynchronous HTTP requests.** **Note:** This will eventually be removed/deprecated and replaced with the upcoming Promise PSR. ## Install Via Composer ``` bash $ composer require php-http/promise ``` ## Documentation Please see the [official documentation](http://docs.php-http.org). ## Testing ``` bash $ composer test ``` ## Contributing Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). ## Security If you discover any security related issues, please contact us at [security@httplug.io](mailto:security@httplug.io) or [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. discovery/composer.json 0000666 00000002707 15165515606 0011321 0 ustar 00 { "name": "php-http/discovery", "description": "Finds installed HTTPlug implementations and PSR-7 message factories", "license": "MIT", "keywords": ["http", "discovery", "client", "adapter", "message", "factory", "psr7"], "homepage": "http://php-http.org", "authors": [ { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require": { "php": "^5.5 || ^7.0" }, "require-dev": { "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", "puli/composer-plugin": "1.0.0-beta10", "phpspec/phpspec": "^2.4" }, "suggest": { "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details.", "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories" }, "autoload": { "psr-4": { "Http\\Discovery\\": "src/" } }, "autoload-dev": { "psr-4": { "spec\\Http\\Discovery\\": "spec/" } }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" }, "extra": { "branch-alias": { "dev-master": "1.5-dev" } }, "conflict": { "nyholm/psr7": "<1.0" }, "prefer-stable": true, "minimum-stability": "beta" } discovery/CHANGELOG.md 0000666 00000010062 15165515606 0010401 0 ustar 00 # Change Log ## 1.6.1 - 2019-02-23 ### Fixed - MockClientStrategy also provides the mock client when requesting an async client ## 1.6.0 - 2019-01-23 ### Added - Support for PSR-17 factories - Support for PSR-18 clients ## 1.5.2 - 2018-12-31 Corrected mistakes in 1.5.1. The different between 1.5.2 and 1.5.0 is that we removed some PHP 7 code. https://github.com/php-http/discovery/compare/1.5.0...1.5.2 ## 1.5.1 - 2018-12-31 This version added new features by mistake. These are reverted in 1.5.2. Do not use 1.5.1. ### Fixed - Removed PHP 7 code ## 1.5.0 - 2018-12-30 ### Added - Support for `nyholm/psr7` version 1.0. - `ClassDiscovery::safeClassExists` which will help Magento users. - Support for HTTPlug 2.0 - Support for Buzz 1.0 - Better error message when nothing found by introducing a new exception: `NoCandidateFoundException`. ### Fixed - Fixed condition evaluation, it should stop after first invalid condition. ## 1.4.0 - 2018-02-06 ### Added - Discovery support for nyholm/psr7 ## 1.3.0 - 2017-08-03 ### Added - Discovery support for CakePHP adapter - Discovery support for Zend adapter - Discovery support for Artax adapter ## 1.2.1 - 2017-03-02 ### Fixed - Fixed minor issue with `MockClientStrategy`, also added more tests. ## 1.2.0 - 2017-02-12 ### Added - MockClientStrategy class. ## 1.1.1 - 2016-11-27 ### Changed - Made exception messages clearer. `StrategyUnavailableException` is no longer the previous exception to `DiscoveryFailedException`. - `CommonClassesStrategy` is using `self` instead of `static`. Using `static` makes no sense when `CommonClassesStrategy` is final. ## 1.1.0 - 2016-10-20 ### Added - Discovery support for Slim Framework factories ## 1.0.0 - 2016-07-18 ### Added - Added back `Http\Discovery\NotFoundException` to preserve BC with 0.8 version. You may upgrade from 0.8.x and 0.9.x to 1.0.0 without any BC breaks. - Added interface `Http\Discovery\Exception` which is implemented by all our exceptions ### Changed - Puli strategy renamed to Puli Beta strategy to prevent incompatibility with a future Puli stable ### Deprecated - For BC reasons, the old `Http\Discovery\NotFoundException` (extending the new exception) will be thrown until version 2.0 ## 0.9.1 - 2016-06-28 ### Changed - Dropping PHP 5.4 support because we use the ::class constant. ## 0.9.0 - 2016-06-25 ### Added - Discovery strategies to find classes ### Changed - [Puli](http://puli.io) made optional - Improved exceptions - **[BC] `NotFoundException` moved to `Http\Discovery\Exception\NotFoundException`** ## 0.8.0 - 2016-02-11 ### Changed - Puli composer plugin must be installed separately ## 0.7.0 - 2016-01-15 ### Added - Temporary puli.phar (Beta 10) executable ### Changed - Updated HTTPlug dependencies - Updated Puli dependencies - Local configuration to make tests passing ### Removed - Puli CLI dependency ## 0.6.4 - 2016-01-07 ### Fixed - Puli [not working](https://twitter.com/PuliPHP/status/685132540588507137) with the latest json-schema ## 0.6.3 - 2016-01-04 ### Changed - Adjust Puli dependencies ## 0.6.2 - 2016-01-04 ### Changed - Make Puli CLI a requirement ## 0.6.1 - 2016-01-03 ### Changed - More flexible Puli requirement ## 0.6.0 - 2015-12-30 ### Changed - Use [Puli](http://puli.io) for discovery - Improved exception messages ## 0.5.0 - 2015-12-25 ### Changed - Updated message factory dependency (php-http/message) ## 0.4.0 - 2015-12-17 ### Added - Array condition evaluation in the Class Discovery ### Removed - Message factories (moved to php-http/utils) ## 0.3.0 - 2015-11-18 ### Added - HTTP Async Client Discovery - Stream factories ### Changed - Discoveries and Factories are final - Message and Uri factories have the type in their names - Diactoros Message factory uses Stream factory internally ### Fixed - Improved docblocks for API documentation generation ## 0.2.0 - 2015-10-31 ### Changed - Renamed AdapterDiscovery to ClientDiscovery ## 0.1.1 - 2015-06-13 ### Fixed - Bad HTTP Adapter class name for Guzzle 5 ## 0.1.0 - 2015-06-12 ### Added - Initial release discovery/LICENSE 0000666 00000002072 15165515606 0007577 0 ustar 00 Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org> 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. discovery/src/NotFoundException.php 0000666 00000000600 15165515606 0013500 0 ustar 00 <?php namespace AmeliaHttp\Discovery; /** * Thrown when a discovery does not find any matches. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * * @deprecated since since version 1.0, and will be removed in 2.0. Use {@link \Http\Discovery\Exception\NotFoundException} instead. */ final class NotFoundException extends \Http\Discovery\Exception\NotFoundException { } discovery/src/StreamFactoryDiscovery.php 0000666 00000002020 15165515606 0014536 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use AmeliaHttp\Message\StreamFactory; /** * Finds a Stream Factory. * * @author Михаил Красильников <m.krasilnikov@yandex.ru> * * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. */ final class StreamFactoryDiscovery extends ClassDiscovery { /** * Finds a Stream Factory. * * @return StreamFactory * * @throws Exception\NotFoundException */ public static function find() { try { $streamFactory = static::findOneByType(StreamFactory::class); } catch (DiscoveryFailedException $e) { throw new NotFoundException( 'No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e ); } return static::instantiateClass($streamFactory); } } discovery/src/Psr18ClientDiscovery.php 0000666 00000001661 15165515606 0014041 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use Psr\Http\Client\ClientInterface; /** * Finds a PSR-18 HTTP Client. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class Psr18ClientDiscovery extends ClassDiscovery { /** * Finds a PSR-18 HTTP Client. * * @return ClientInterface * * @throws Exception\NotFoundException */ public static function find() { try { $client = static::findOneByType(ClientInterface::class); } catch (DiscoveryFailedException $e) { throw new \Http\Discovery\Exception\NotFoundException( 'No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e ); } return static::instantiateClass($client); } } discovery/src/Exception/NoCandidateFoundException.php 0000666 00000002320 15165515606 0017050 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; use AmeliaHttp\Discovery\Exception; /** * When we have used a strategy but no candidates provided by that strategy could be used. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class NoCandidateFoundException extends \Exception implements Exception { /** * @param string $strategy * @param array $candidates */ public function __construct($strategy, array $candidates) { $classes = array_map( function ($a) { return $a['class']; }, $candidates ); $message = sprintf( 'No valid candidate found using strategy "%s". We tested the following candidates: %s.', $strategy, implode(', ', array_map([$this, 'stringify'], $classes)) ); parent::__construct($message); } private function stringify($mixed) { if (is_string($mixed)) { return $mixed; } if (is_array($mixed) && 2 === count($mixed)) { return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]); } return is_object($mixed) ? get_class($mixed) : gettype($mixed); } } discovery/src/Exception/PuliUnavailableException.php 0000666 00000000356 15165515606 0016767 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; /** * Thrown when we can't use Puli for discovery. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class PuliUnavailableException extends StrategyUnavailableException { } discovery/src/Exception/DiscoveryFailedException.php 0000666 00000002253 15165515606 0016764 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; use AmeliaHttp\Discovery\Exception; /** * Thrown when all discovery strategies fails to find a resource. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class DiscoveryFailedException extends \Exception implements Exception { /** * @var \Exception[] */ private $exceptions; /** * @param string $message * @param \Exception[] $exceptions */ public function __construct($message, array $exceptions = []) { $this->exceptions = $exceptions; parent::__construct($message); } /** * @param \Exception[] $exceptions */ public static function create($exceptions) { $message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors'; foreach ($exceptions as $e) { $message .= "\n - ".$e->getMessage(); } $message .= "\n\n"; return new self($message, $exceptions); } /** * @return \Exception[] */ public function getExceptions() { return $this->exceptions; } } discovery/src/Exception/StrategyUnavailableException.php 0000666 00000000562 15165515606 0017657 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; use AmeliaHttp\Discovery\Exception; /** * This exception is thrown when we cannot use a discovery strategy. This is *not* thrown when * the discovery fails to find a class. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class StrategyUnavailableException extends \RuntimeException implements Exception { } discovery/src/Exception/ClassInstantiationFailedException.php 0000666 00000000443 15165515606 0020626 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; use AmeliaHttp\Discovery\Exception; /** * Thrown when a class fails to instantiate. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class ClassInstantiationFailedException extends \RuntimeException implements Exception { } discovery/src/Exception/NotFoundException.php 0000666 00000000546 15165515606 0015447 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Exception; use AmeliaHttp\Discovery\Exception; /** * Thrown when a discovery does not find any matches. * * @final do NOT extend this class, not final for BC reasons * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ /*final */class NotFoundException extends \RuntimeException implements Exception { } discovery/src/Strategy/DiscoveryStrategy.php 0000666 00000001162 15165515606 0015365 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Strategy; use AmeliaHttp\Discovery\Exception\StrategyUnavailableException; /** * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ interface DiscoveryStrategy { /** * Find a resource of a specific type. * * @param string $type * * @return array The return value is always an array with zero or more elements. Each * element is an array with two keys ['class' => string, 'condition' => mixed]. * * @throws StrategyUnavailableException if we cannot use this strategy. */ public static function getCandidates($type); } discovery/src/Strategy/PuliBetaStrategy.php 0000666 00000004362 15165515606 0015130 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Strategy; use AmeliaHttp\Discovery\ClassDiscovery; use AmeliaHttp\Discovery\Exception\PuliUnavailableException; use Puli\Discovery\Api\Discovery; use Puli\GeneratedPuliFactory; /** * Find candidates using Puli. * * @internal * @final * * @author David de Boer <david@ddeboer.nl> * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class PuliBetaStrategy implements DiscoveryStrategy { /** * @var GeneratedPuliFactory */ protected static $puliFactory; /** * @var Discovery */ protected static $puliDiscovery; /** * @return GeneratedPuliFactory * * @throws PuliUnavailableException */ private static function getPuliFactory() { if (null === self::$puliFactory) { if (!defined('PULI_FACTORY_CLASS')) { throw new PuliUnavailableException('Puli Factory is not available'); } $puliFactoryClass = PULI_FACTORY_CLASS; if (!ClassDiscovery::safeClassExists($puliFactoryClass)) { throw new PuliUnavailableException('Puli Factory class does not exist'); } self::$puliFactory = new $puliFactoryClass(); } return self::$puliFactory; } /** * Returns the Puli discovery layer. * * @return Discovery * * @throws PuliUnavailableException */ private static function getPuliDiscovery() { if (!isset(self::$puliDiscovery)) { $factory = self::getPuliFactory(); $repository = $factory->createRepository(); self::$puliDiscovery = $factory->createDiscovery($repository); } return self::$puliDiscovery; } /** * {@inheritdoc} */ public static function getCandidates($type) { $returnData = []; $bindings = self::getPuliDiscovery()->findBindings($type); foreach ($bindings as $binding) { $condition = true; if ($binding->hasParameterValue('depends')) { $condition = $binding->getParameterValue('depends'); } $returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition]; } return $returnData; } } discovery/src/Strategy/CommonClassesStrategy.php 0000666 00000012302 15165515606 0016162 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Strategy; use AmeliaGuzzleHttp\Psr7\Request as GuzzleRequest; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Discovery\MessageFactoryDiscovery; use AmeliaHttp\Message\MessageFactory; use AmeliaHttp\Message\MessageFactory\GuzzleMessageFactory; use AmeliaHttp\Message\StreamFactory; use AmeliaHttp\Message\StreamFactory\GuzzleStreamFactory; use AmeliaHttp\Message\UriFactory; use AmeliaHttp\Message\UriFactory\GuzzleUriFactory; use AmeliaHttp\Message\MessageFactory\DiactorosMessageFactory; use AmeliaHttp\Message\StreamFactory\DiactorosStreamFactory; use AmeliaHttp\Message\UriFactory\DiactorosUriFactory; use Psr\Http\Client\ClientInterface as Psr18Client; use Zend\Diactoros\Request as DiactorosRequest; use AmeliaHttp\Message\MessageFactory\SlimMessageFactory; use AmeliaHttp\Message\StreamFactory\SlimStreamFactory; use AmeliaHttp\Message\UriFactory\SlimUriFactory; use Slim\Http\Request as SlimRequest; use AmeliaHttp\Adapter\Guzzle6\Client as Guzzle6; use AmeliaHttp\Adapter\Guzzle5\Client as Guzzle5; use AmeliaHttp\Client\Curl\Client as Curl; use AmeliaHttp\Client\Socket\Client as Socket; use AmeliaHttp\Adapter\React\Client as React; use AmeliaHttp\Adapter\Buzz\Client as Buzz; use AmeliaHttp\Adapter\Cake\Client as Cake; use AmeliaHttp\Adapter\Zend\Client as Zend; use AmeliaHttp\Adapter\Artax\Client as Artax; use Nyholm\Psr7\Factory\HttplugFactory as NyholmHttplugFactory; /** * @internal * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class CommonClassesStrategy implements DiscoveryStrategy { /** * @var array */ private static $classes = [ MessageFactory::class => [ ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]], ['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]], ['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]], ], StreamFactory::class => [ ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]], ['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]], ['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]], ], UriFactory::class => [ ['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]], ['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]], ['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]], ], HttpAsyncClient::class => [ ['class' => Guzzle6::class, 'condition' => Guzzle6::class], ['class' => Curl::class, 'condition' => Curl::class], ['class' => React::class, 'condition' => React::class], ], HttpClient::class => [ ['class' => Guzzle6::class, 'condition' => Guzzle6::class], ['class' => Guzzle5::class, 'condition' => Guzzle5::class], ['class' => Curl::class, 'condition' => Curl::class], ['class' => Socket::class, 'condition' => Socket::class], ['class' => Buzz::class, 'condition' => Buzz::class], ['class' => React::class, 'condition' => React::class], ['class' => Cake::class, 'condition' => Cake::class], ['class' => Zend::class, 'condition' => Zend::class], ['class' => Artax::class, 'condition' => Artax::class], [ 'class' => [self::class, 'buzzInstantiate'], 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], ], ], Psr18Client::class => [ [ 'class' => [self::class, 'buzzInstantiate'], 'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class], ], ], ]; /** * {@inheritdoc} */ public static function getCandidates($type) { if (Psr18Client::class === $type) { $candidates = self::$classes[PSR18Client::class]; // HTTPlug 2.0 clients implements PSR18Client too. foreach (self::$classes[HttpClient::class] as $c) { if (is_subclass_of($c['class'], Psr18Client::class)) { $candidates[] = $c; } } return $candidates; } if (isset(self::$classes[$type])) { return self::$classes[$type]; } return []; } public static function buzzInstantiate() { return new \Buzz\Client\FileGetContents(MessageFactoryDiscovery::find()); } } discovery/src/Strategy/MockClientStrategy.php 0000666 00000001175 15165515606 0015452 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Strategy; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Mock\Client as Mock; /** * Find the Mock client. * * @author Sam Rapaport <me@samrapdev.com> */ final class MockClientStrategy implements DiscoveryStrategy { /** * {@inheritdoc} */ public static function getCandidates($type) { switch ($type) { case HttpClient::class: case HttpAsyncClient::class: return [['class' => Mock::class, 'condition' => Mock::class]]; default: return []; } } } discovery/src/Strategy/CommonPsr17ClassesStrategy.php 0000666 00000005325 15165515606 0017026 0 ustar 00 <?php namespace AmeliaHttp\Discovery\Strategy; use AmeliaPsr\Http\Message\RequestFactoryInterface; use AmeliaPsr\Http\Message\ResponseFactoryInterface; use AmeliaPsr\Http\Message\ServerRequestFactoryInterface; use AmeliaPsr\Http\Message\StreamFactoryInterface; use AmeliaPsr\Http\Message\UploadedFileFactoryInterface; use AmeliaPsr\Http\Message\UriFactoryInterface; /** * @internal * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class CommonPsr17ClassesStrategy implements DiscoveryStrategy { /** * @var array */ private static $classes = [ RequestFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\RequestFactory', 'Http\Factory\Diactoros\RequestFactory', 'Http\Factory\Guzzle\RequestFactory', 'Http\Factory\Slim\RequestFactory', ], ResponseFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\ResponseFactory', 'Http\Factory\Diactoros\ResponseFactory', 'Http\Factory\Guzzle\ResponseFactory', 'Http\Factory\Slim\ResponseFactory', ], ServerRequestFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\ServerRequestFactory', 'Http\Factory\Diactoros\ServerRequestFactory', 'Http\Factory\Guzzle\ServerRequestFactory', 'Http\Factory\Slim\ServerRequestFactory', ], StreamFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\StreamFactory', 'Http\Factory\Diactoros\StreamFactory', 'Http\Factory\Guzzle\StreamFactory', 'Http\Factory\Slim\StreamFactory', ], UploadedFileFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\UploadedFileFactory', 'Http\Factory\Diactoros\UploadedFileFactory', 'Http\Factory\Guzzle\UploadedFileFactory', 'Http\Factory\Slim\UploadedFileFactory', ], UriFactoryInterface::class => [ 'Nyholm\Psr7\Factory\Psr17Factory', 'Zend\Diactoros\UriFactory', 'Http\Factory\Diactoros\UriFactory', 'Http\Factory\Guzzle\UriFactory', 'Http\Factory\Slim\UriFactory', ], ]; /** * {@inheritdoc} */ public static function getCandidates($type) { $candidates = []; if (isset(self::$classes[$type])) { foreach (self::$classes[$type] as $class) { $candidates[] = ['class' => $class, 'condition' => [$class]]; } } return $candidates; } } discovery/src/Psr17FactoryDiscovery.php 0000666 00000007017 15165515606 0014232 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use AmeliaPsr\Http\Message\RequestFactoryInterface; use AmeliaPsr\Http\Message\ResponseFactoryInterface; use AmeliaPsr\Http\Message\ServerRequestFactoryInterface; use AmeliaPsr\Http\Message\StreamFactoryInterface; use AmeliaPsr\Http\Message\UploadedFileFactoryInterface; use AmeliaPsr\Http\Message\UriFactoryInterface; /** * Finds PSR-17 factories. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ final class Psr17FactoryDiscovery extends ClassDiscovery { private static function createException($type, Exception $e) { return new \Http\Discovery\Exception\NotFoundException( 'No PSR-17 '.$type.' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation', 0, $e ); } /** * @return RequestFactoryInterface * * @throws Exception\NotFoundException */ public static function findRequestFactory() { try { $messageFactory = static::findOneByType(RequestFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('request factory', $e); } return static::instantiateClass($messageFactory); } /** * @return ResponseFactoryInterface * * @throws Exception\NotFoundException */ public static function findResponseFactory() { try { $messageFactory = static::findOneByType(ResponseFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('response factory', $e); } return static::instantiateClass($messageFactory); } /** * @return ServerRequestFactoryInterface * * @throws Exception\NotFoundException */ public static function findServerRequestFactory() { try { $messageFactory = static::findOneByType(ServerRequestFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('server request factory', $e); } return static::instantiateClass($messageFactory); } /** * @return StreamFactoryInterface * * @throws Exception\NotFoundException */ public static function findStreamFactory() { try { $messageFactory = static::findOneByType(StreamFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('stream factory', $e); } return static::instantiateClass($messageFactory); } /** * @return UploadedFileFactoryInterface * * @throws Exception\NotFoundException */ public static function findUploadedFileFactory() { try { $messageFactory = static::findOneByType(UploadedFileFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('uploaded file factory', $e); } return static::instantiateClass($messageFactory); } /** * @return UriFactoryInterface * * @throws Exception\NotFoundException */ public static function findUrlFactory() { try { $messageFactory = static::findOneByType(UriFactoryInterface::class); } catch (DiscoveryFailedException $e) { throw self::createException('url factory', $e); } return static::instantiateClass($messageFactory); } } discovery/src/HttpClientDiscovery.php 0000666 00000001604 15165515606 0014040 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Client\HttpClient; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; /** * Finds an HTTP Client. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class HttpClientDiscovery extends ClassDiscovery { /** * Finds an HTTP Client. * * @return HttpClient * * @throws Exception\NotFoundException */ public static function find() { try { $client = static::findOneByType(HttpClient::class); } catch (DiscoveryFailedException $e) { throw new NotFoundException( 'No HTTPlug clients found. Make sure to install a package providing "php-http/client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e ); } return static::instantiateClass($client); } } discovery/src/Exception.php 0000666 00000000277 15165515606 0012035 0 ustar 00 <?php namespace AmeliaHttp\Discovery; /** * An interface implemented by all discovery related exceptions. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ interface Exception { } discovery/src/HttpAsyncClientDiscovery.php 0000666 00000001665 15165515606 0015045 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Client\HttpAsyncClient; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; /** * Finds an HTTP Asynchronous Client. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class HttpAsyncClientDiscovery extends ClassDiscovery { /** * Finds an HTTP Async Client. * * @return HttpAsyncClient * * @throws Exception\NotFoundException */ public static function find() { try { $asyncClient = static::findOneByType(HttpAsyncClient::class); } catch (DiscoveryFailedException $e) { throw new NotFoundException( 'No HTTPlug async clients found. Make sure to install a package providing "php-http/async-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e ); } return static::instantiateClass($asyncClient); } } discovery/src/ClassDiscovery.php 0000666 00000014201 15165515606 0013024 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\ClassInstantiationFailedException; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use AmeliaHttp\Discovery\Exception\NoCandidateFoundException; use AmeliaHttp\Discovery\Exception\StrategyUnavailableException; /** * Registry that based find results on class existence. * * @author David de Boer <david@ddeboer.nl> * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ abstract class ClassDiscovery { /** * A list of strategies to find classes. * * @var array */ private static $strategies = [ Strategy\PuliBetaStrategy::class, Strategy\CommonClassesStrategy::class, Strategy\CommonPsr17ClassesStrategy::class, ]; /** * Discovery cache to make the second time we use discovery faster. * * @var array */ private static $cache = []; /** * Finds a class. * * @param string $type * * @return string|\Closure * * @throws DiscoveryFailedException */ protected static function findOneByType($type) { // Look in the cache if (null !== ($class = self::getFromCache($type))) { return $class; } $exceptions = []; foreach (self::$strategies as $strategy) { try { $candidates = call_user_func($strategy.'::getCandidates', $type); } catch (StrategyUnavailableException $e) { $exceptions[] = $e; continue; } foreach ($candidates as $candidate) { if (isset($candidate['condition'])) { if (!self::evaluateCondition($candidate['condition'])) { continue; } } // save the result for later use self::storeInCache($type, $candidate); return $candidate['class']; } $exceptions[] = new NoCandidateFoundException($strategy, $candidates); } throw DiscoveryFailedException::create($exceptions); } /** * Get a value from cache. * * @param string $type * * @return string|null */ private static function getFromCache($type) { if (!isset(self::$cache[$type])) { return; } $candidate = self::$cache[$type]; if (isset($candidate['condition'])) { if (!self::evaluateCondition($candidate['condition'])) { return; } } return $candidate['class']; } /** * Store a value in cache. * * @param string $type * @param string $class */ private static function storeInCache($type, $class) { self::$cache[$type] = $class; } /** * Set new strategies and clear the cache. * * @param array $strategies string array of fully qualified class name to a DiscoveryStrategy */ public static function setStrategies(array $strategies) { self::$strategies = $strategies; self::clearCache(); } /** * Append a strategy at the end of the strategy queue. * * @param string $strategy Fully qualified class name to a DiscoveryStrategy */ public static function appendStrategy($strategy) { self::$strategies[] = $strategy; self::clearCache(); } /** * Prepend a strategy at the beginning of the strategy queue. * * @param string $strategy Fully qualified class name to a DiscoveryStrategy */ public static function prependStrategy($strategy) { array_unshift(self::$strategies, $strategy); self::clearCache(); } /** * Clear the cache. */ public static function clearCache() { self::$cache = []; } /** * Evaluates conditions to boolean. * * @param mixed $condition * * @return bool */ protected static function evaluateCondition($condition) { if (is_string($condition)) { // Should be extended for functions, extensions??? return self::safeClassExists($condition); } if (is_callable($condition)) { return (bool) $condition(); } if (is_bool($condition)) { return $condition; } if (is_array($condition)) { foreach ($condition as $c) { if (false === static::evaluateCondition($c)) { // Immediately stop execution if the condition is false return false; } } return true; } return false; } /** * Get an instance of the $class. * * @param string|\Closure $class A FQCN of a class or a closure that instantiate the class. * * @return object * * @throws ClassInstantiationFailedException */ protected static function instantiateClass($class) { try { if (is_string($class)) { return new $class(); } if (is_callable($class)) { return $class(); } } catch (\Exception $e) { throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e); } throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string'); } /** * We want to do a "safe" version of PHP's "class_exists" because Magento has a bug * (or they call it a "feature"). Magento is throwing an exception if you do class_exists() * on a class that ends with "Factory" and if that file does not exits. * * This function will catch all potential exceptions and make sure it returns a boolean. * * @param string $class * @param bool $autoload * * @return bool */ public static function safeClassExists($class) { try { return class_exists($class); } catch (\Exception $e) { return false; } } } discovery/src/MessageFactoryDiscovery.php 0000666 00000002007 15165515606 0014674 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use AmeliaHttp\Message\MessageFactory; /** * Finds a Message Factory. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. */ final class MessageFactoryDiscovery extends ClassDiscovery { /** * Finds a Message Factory. * * @return MessageFactory * * @throws Exception\NotFoundException */ public static function find() { try { $messageFactory = static::findOneByType(MessageFactory::class); } catch (DiscoveryFailedException $e) { throw new NotFoundException( 'No message factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e ); } return static::instantiateClass($messageFactory); } } discovery/src/UriFactoryDiscovery.php 0000666 00000001726 15165515606 0014056 0 ustar 00 <?php namespace AmeliaHttp\Discovery; use AmeliaHttp\Discovery\Exception\DiscoveryFailedException; use AmeliaHttp\Message\UriFactory; /** * Finds a URI Factory. * * @author David de Boer <david@ddeboer.nl> * * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery. */ final class UriFactoryDiscovery extends ClassDiscovery { /** * Finds a URI Factory. * * @return UriFactory * * @throws Exception\NotFoundException */ public static function find() { try { $uriFactory = static::findOneByType(UriFactory::class); } catch (DiscoveryFailedException $e) { throw new NotFoundException( 'No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e ); } return static::instantiateClass($uriFactory); } } discovery/README.md 0000666 00000002777 15165515606 0010065 0 ustar 00 # HTTPlug Discovery [](https://github.com/php-http/discovery/releases) [](LICENSE) [](https://travis-ci.org/php-http/discovery) [](https://scrutinizer-ci.com/g/php-http/discovery) [](https://scrutinizer-ci.com/g/php-http/discovery) [](https://packagist.org/packages/php-http/discovery) **Finds installed HTTPlug implementations and PSR-7 message factories.** ## Install Via Composer ``` bash $ composer require php-http/discovery ``` ## Documentation Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.html). ## Testing ``` bash $ composer test ``` ## Contributing Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. message/README.md 0000666 00000003506 15165515606 0007471 0 ustar 00 # HTTP Message [](https://github.com/php-http/message/releases) [](LICENSE) [](https://travis-ci.org/php-http/message) [](https://scrutinizer-ci.com/g/php-http/message) [](https://scrutinizer-ci.com/g/php-http/message) [](https://packagist.org/packages/php-http/message) **HTTP Message related tools.** ## Install Via Composer ``` bash $ composer require php-http/message ``` ## Intro This package contains various PSR-7 tools which might be useful in an HTTP workflow: - Authentication method implementations - Various Stream encoding tools - Message decorators - Message factory implementations for Guzzle PSR-7 and Diactoros - Cookie implementation - Request matchers ## Documentation Please see the [official documentation](http://docs.php-http.org/en/latest/message.html). ## Testing ``` bash $ composer test ``` ## Contributing Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). ## Credits Thanks to [Cuzzle](https://github.com/namshi/cuzzle) for inpiration for the `CurlCommandFormatter`. ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. message/puli.json 0000666 00000011306 15165515606 0010053 0 ustar 00 { "version": "1.0", "name": "php-http/message", "bindings": { "064d003d-78a1-48c4-8f3b-1f92ff25da69": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", "type": "Http\\Message\\MessageFactory", "parameters": { "depends": "Zend\\Diactoros\\Request" } }, "0836751e-6558-4d1b-8993-4a52012947c3": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", "type": "Http\\Message\\ResponseFactory" }, "1d127622-dc61-4bfa-b9da-d221548d72c3": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", "type": "Http\\Message\\RequestFactory" }, "2438c2d0-0658-441f-8855-ddaf0f87d54d": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", "type": "Http\\Message\\MessageFactory", "parameters": { "depends": "GuzzleHttp\\Psr7\\Request" } }, "253aa08c-d705-46e7-b1d2-e28c97eef792": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", "type": "Http\\Message\\RequestFactory", "parameters": { "depends": "GuzzleHttp\\Psr7\\Request" } }, "273a34f9-62f4-4ba1-9801-b1284d49ff89": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\StreamFactory\\GuzzleStreamFactory", "type": "Http\\Message\\StreamFactory", "parameters": { "depends": "GuzzleHttp\\Psr7\\Stream" } }, "304b83db-b594-4d83-ae75-1f633adf92f7": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\UriFactory\\GuzzleUriFactory", "type": "Http\\Message\\UriFactory", "parameters": { "depends": "GuzzleHttp\\Psr7\\Uri" } }, "3f4bc1cd-aa95-4702-9fa7-65408e471691": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\UriFactory\\DiactorosUriFactory", "type": "Http\\Message\\UriFactory", "parameters": { "depends": "Zend\\Diactoros\\Uri" } }, "4672a6ee-ad9e-4109-a5d1-b7d46f26c7a1": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\SlimMessageFactory", "type": "Http\\Message\\MessageFactory" }, "6234e947-d3bd-43eb-97d5-7f9e22e6bb1b": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", "type": "Http\\Message\\ResponseFactory", "parameters": { "depends": "Zend\\Diactoros\\Response" } }, "6a9ad6ce-d82c-470f-8e30-60f21d9d95bf": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\UriFactory\\SlimUriFactory", "type": "Http\\Message\\UriFactory" }, "72c2afa0-ea56-4d03-adb6-a9f241a8a734": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\StreamFactory\\SlimStreamFactory", "type": "Http\\Message\\StreamFactory" }, "95c1be8f-39fe-4abd-8351-92cb14379a75": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\StreamFactory\\DiactorosStreamFactory", "type": "Http\\Message\\StreamFactory", "parameters": { "depends": "Zend\\Diactoros\\Stream" } }, "a018af27-7590-4dcf-83a1-497f95604cd6": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory", "type": "Http\\Message\\ResponseFactory", "parameters": { "depends": "GuzzleHttp\\Psr7\\Response" } }, "c07955b1-de46-43db-923b-d07fae9382cb": { "_class": "Puli\\Discovery\\Binding\\ClassBinding", "class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory", "type": "Http\\Message\\RequestFactory", "parameters": { "depends": "Zend\\Diactoros\\Request" } } } } message/src/Formatter/SimpleFormatter.php 0000666 00000001736 15165515606 0014575 0 ustar 00 <?php namespace AmeliaHttp\Message\Formatter; use AmeliaHttp\Message\Formatter; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Normalize a request or a response into a string or an array. * * @author Joel Wurtz <joel.wurtz@gmail.com> * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class SimpleFormatter implements Formatter { /** * {@inheritdoc} */ public function formatRequest(RequestInterface $request) { return sprintf( '%s %s %s', $request->getMethod(), $request->getUri()->__toString(), $request->getProtocolVersion() ); } /** * {@inheritdoc} */ public function formatResponse(ResponseInterface $response) { return sprintf( '%s %s %s', $response->getStatusCode(), $response->getReasonPhrase(), $response->getProtocolVersion() ); } } message/src/Formatter/CurlCommandFormatter.php 0000666 00000005026 15165515606 0015544 0 ustar 00 <?php namespace AmeliaHttp\Message\Formatter; use AmeliaHttp\Message\Formatter; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * A formatter that prints a cURL command for HTTP requests. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class CurlCommandFormatter implements Formatter { /** * {@inheritdoc} */ public function formatRequest(RequestInterface $request) { $command = sprintf('curl %s', escapeshellarg((string) $request->getUri()->withFragment(''))); if ('1.0' === $request->getProtocolVersion()) { $command .= ' --http1.0'; } elseif ('2.0' === $request->getProtocolVersion()) { $command .= ' --http2'; } $method = strtoupper($request->getMethod()); if ('HEAD' === $method) { $command .= ' --head'; } elseif ('GET' !== $method) { $command .= ' --request '.$method; } $command .= $this->getHeadersAsCommandOptions($request); $body = $request->getBody(); if ($body->getSize() > 0) { if ($body->isSeekable()) { $data = $body->__toString(); $body->rewind(); if (preg_match('/[\x00-\x1F\x7F]/', $data)) { $data = '[binary stream omitted]'; } } else { $data = '[non-seekable stream omitted]'; } $escapedData = @escapeshellarg($data); if (empty($escapedData)) { $escapedData = 'We couldn\'t not escape the data properly'; } $command .= sprintf(' --data %s', $escapedData); } return $command; } /** * {@inheritdoc} */ public function formatResponse(ResponseInterface $response) { return ''; } /** * @param RequestInterface $request * * @return string */ private function getHeadersAsCommandOptions(RequestInterface $request) { $command = ''; foreach ($request->getHeaders() as $name => $values) { if ('host' === strtolower($name) && $values[0] === $request->getUri()->getHost()) { continue; } if ('user-agent' === strtolower($name)) { $command .= sprintf(' -A %s', escapeshellarg($values[0])); continue; } $command .= sprintf(' -H %s', escapeshellarg($name.': '.$request->getHeaderLine($name))); } return $command; } } message/src/Formatter/FullHttpMessageFormatter.php 0000666 00000004333 15165515606 0016407 0 ustar 00 <?php namespace AmeliaHttp\Message\Formatter; use AmeliaHttp\Message\Formatter; use AmeliaPsr\Http\Message\MessageInterface; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * A formatter that prints the complete HTTP message. * * @author Tobias Nyholm <tobias.nyholm@gmail.com> */ class FullHttpMessageFormatter implements Formatter { /** * The maximum length of the body. * * @var int */ private $maxBodyLength; /** * @param int $maxBodyLength */ public function __construct($maxBodyLength = 1000) { $this->maxBodyLength = $maxBodyLength; } /** * {@inheritdoc} */ public function formatRequest(RequestInterface $request) { $message = sprintf( "%s %s HTTP/%s\n", $request->getMethod(), $request->getRequestTarget(), $request->getProtocolVersion() ); foreach ($request->getHeaders() as $name => $values) { $message .= $name.': '.implode(', ', $values)."\n"; } return $this->addBody($request, $message); } /** * {@inheritdoc} */ public function formatResponse(ResponseInterface $response) { $message = sprintf( "HTTP/%s %s %s\n", $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase() ); foreach ($response->getHeaders() as $name => $values) { $message .= $name.': '.implode(', ', $values)."\n"; } return $this->addBody($response, $message); } /** * Add the message body if the stream is seekable. * * @param MessageInterface $request * @param string $message * * @return string */ private function addBody(MessageInterface $request, $message) { $stream = $request->getBody(); if (!$stream->isSeekable() || 0 === $this->maxBodyLength) { // Do not read the stream $message .= "\n"; } else { $message .= "\n".mb_substr($stream->__toString(), 0, $this->maxBodyLength); $stream->rewind(); } return $message; } } message/src/UriFactory/SlimUriFactory.php 0000666 00000001155 15165515606 0014513 0 ustar 00 <?php namespace AmeliaHttp\Message\UriFactory; use AmeliaHttp\Message\UriFactory; use AmeliaPsr\Http\Message\UriInterface; use Slim\Http\Uri; /** * Creates Slim 3 URI. * * @author Mika Tuupola <tuupola@appelsiini.net> */ final class SlimUriFactory implements UriFactory { /** * {@inheritdoc} */ public function createUri($uri) { if ($uri instanceof UriInterface) { return $uri; } if (is_string($uri)) { return Uri::createFromString($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); } } message/src/UriFactory/GuzzleUriFactory.php 0000666 00000000555 15165515606 0015072 0 ustar 00 <?php namespace AmeliaHttp\Message\UriFactory; use AmeliaGuzzleHttp\Psr7; use AmeliaHttp\Message\UriFactory; /** * Creates Guzzle URI. * * @author David de Boer <david@ddeboer.nl> */ final class GuzzleUriFactory implements UriFactory { /** * {@inheritdoc} */ public function createUri($uri) { return Psr7\uri_for($uri); } } message/src/UriFactory/DiactorosUriFactory.php 0000666 00000001142 15165515606 0015532 0 ustar 00 <?php namespace AmeliaHttp\Message\UriFactory; use AmeliaHttp\Message\UriFactory; use AmeliaPsr\Http\Message\UriInterface; use Zend\Diactoros\Uri; /** * Creates Diactoros URI. * * @author David de Boer <david@ddeboer.nl> */ final class DiactorosUriFactory implements UriFactory { /** * {@inheritdoc} */ public function createUri($uri) { if ($uri instanceof UriInterface) { return $uri; } elseif (is_string($uri)) { return new Uri($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); } } message/src/MessageFactory/DiactorosMessageFactory.php 0000666 00000002512 15165515606 0017206 0 ustar 00 <?php namespace AmeliaHttp\Message\MessageFactory; use AmeliaHttp\Message\StreamFactory\DiactorosStreamFactory; use AmeliaHttp\Message\MessageFactory; use Zend\Diactoros\Request; use Zend\Diactoros\Response; /** * Creates Diactoros messages. * * @author GeLo <geloen.eric@gmail.com> */ final class DiactorosMessageFactory implements MessageFactory { /** * @var DiactorosStreamFactory */ private $streamFactory; public function __construct() { $this->streamFactory = new DiactorosStreamFactory(); } /** * {@inheritdoc} */ public function createRequest( $method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return (new Request( $uri, $method, $this->streamFactory->createStream($body), $headers ))->withProtocolVersion($protocolVersion); } /** * {@inheritdoc} */ public function createResponse( $statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return (new Response( $this->streamFactory->createStream($body), $statusCode, $headers ))->withProtocolVersion($protocolVersion); } } message/src/MessageFactory/GuzzleMessageFactory.php 0000666 00000002047 15165515606 0016542 0 ustar 00 <?php namespace AmeliaHttp\Message\MessageFactory; use AmeliaGuzzleHttp\Psr7\Request; use AmeliaGuzzleHttp\Psr7\Response; use AmeliaHttp\Message\MessageFactory; /** * Creates Guzzle messages. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class GuzzleMessageFactory implements MessageFactory { /** * {@inheritdoc} */ public function createRequest( $method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return new Request( $method, $uri, $headers, $body, $protocolVersion ); } /** * {@inheritdoc} */ public function createResponse( $statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return new Response( $statusCode, $headers, $body, $protocolVersion, $reasonPhrase ); } } message/src/MessageFactory/SlimMessageFactory.php 0000666 00000003132 15165515606 0016162 0 ustar 00 <?php namespace AmeliaHttp\Message\MessageFactory; use AmeliaHttp\Message\StreamFactory\SlimStreamFactory; use AmeliaHttp\Message\UriFactory\SlimUriFactory; use AmeliaHttp\Message\MessageFactory; use Slim\Http\Request; use Slim\Http\Response; use Slim\Http\Headers; /** * Creates Slim 3 messages. * * @author Mika Tuupola <tuupola@appelsiini.net> */ final class SlimMessageFactory implements MessageFactory { /** * @var SlimStreamFactory */ private $streamFactory; /** * @var SlimUriFactory */ private $uriFactory; public function __construct() { $this->streamFactory = new SlimStreamFactory(); $this->uriFactory = new SlimUriFactory(); } /** * {@inheritdoc} */ public function createRequest( $method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return (new Request( $method, $this->uriFactory->createUri($uri), new Headers($headers), [], [], $this->streamFactory->createStream($body), [] ))->withProtocolVersion($protocolVersion); } /** * {@inheritdoc} */ public function createResponse( $statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $protocolVersion = '1.1' ) { return (new Response( $statusCode, new Headers($headers), $this->streamFactory->createStream($body) ))->withProtocolVersion($protocolVersion); } } message/src/RequestMatcher/CallbackRequestMatcher.php 0000666 00000001260 15165515606 0017012 0 ustar 00 <?php namespace AmeliaHttp\Message\RequestMatcher; use AmeliaHttp\Message\RequestMatcher; use AmeliaPsr\Http\Message\RequestInterface; /** * Match a request with a callback. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class CallbackRequestMatcher implements RequestMatcher { /** * @var callable */ private $callback; /** * @param callable $callback */ public function __construct(callable $callback) { $this->callback = $callback; } /** * {@inheritdoc} */ public function matches(RequestInterface $request) { return (bool) call_user_func($this->callback, $request); } } message/src/RequestMatcher/RequestMatcher.php 0000666 00000004112 15165515606 0015374 0 ustar 00 <?php namespace AmeliaHttp\Message\RequestMatcher; use AmeliaHttp\Message\RequestMatcher as RequestMatcherInterface; use AmeliaPsr\Http\Message\RequestInterface; /** * A port of the Symfony RequestMatcher for PSR-7. * * @author Fabien Potencier <fabien@symfony.com> * @author Joel Wurtz <joel.wurtz@gmail.com> */ final class RequestMatcher implements RequestMatcherInterface { /** * @var string */ private $path; /** * @var string */ private $host; /** * @var array */ private $methods = []; /** * @var string[] */ private $schemes = []; /** * The regular expressions used for path or host must be specified without delimiter. * You do not need to escape the forward slash / to match it. * * @param string|null $path Regular expression for the path * @param string|null $host Regular expression for the hostname * @param string|string[]|null $methods Method or list of methods to match * @param string|string[]|null $schemes Scheme or list of schemes to match (e.g. http or https) */ public function __construct($path = null, $host = null, $methods = [], $schemes = []) { $this->path = $path; $this->host = $host; $this->methods = array_map('strtoupper', (array) $methods); $this->schemes = array_map('strtolower', (array) $schemes); } /** * {@inheritdoc} * * @api */ public function matches(RequestInterface $request) { if ($this->schemes && !in_array($request->getUri()->getScheme(), $this->schemes)) { return false; } if ($this->methods && !in_array($request->getMethod(), $this->methods)) { return false; } if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getUri()->getPath()))) { return false; } if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getUri()->getHost())) { return false; } return true; } } message/src/RequestMatcher/RegexRequestMatcher.php 0000666 00000001754 15165515606 0016400 0 ustar 00 <?php namespace AmeliaHttp\Message\RequestMatcher; use AmeliaHttp\Message\RequestMatcher; use AmeliaPsr\Http\Message\RequestInterface; @trigger_error('The '.__NAMESPACE__.'\RegexRequestMatcher class is deprecated since version 1.2 and will be removed in 2.0. Use AmeliaHttp\Message\RequestMatcher\RequestMatcher instead.', E_USER_DEPRECATED); /** * Match a request with a regex on the uri. * * @author Joel Wurtz <joel.wurtz@gmail.com> * * @deprecated since version 1.2 and will be removed in 2.0. Use {@link RequestMatcher} instead. */ final class RegexRequestMatcher implements RequestMatcher { /** * Matching regex. * * @var string */ private $regex; /** * @param string $regex */ public function __construct($regex) { $this->regex = $regex; } /** * {@inheritdoc} */ public function matches(RequestInterface $request) { return (bool) preg_match($this->regex, (string) $request->getUri()); } } message/src/Cookie.php 0000666 00000026112 15165515606 0010721 0 ustar 00 <?php namespace AmeliaHttp\Message; /** * Cookie Value Object. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * * @see http://tools.ietf.org/search/rfc6265 */ final class Cookie { /** * @var string */ private $name; /** * @var string|null */ private $value; /** * @var int|null */ private $maxAge; /** * @var string|null */ private $domain; /** * @var string */ private $path; /** * @var bool */ private $secure; /** * @var bool */ private $httpOnly; /** * Expires attribute is HTTP 1.0 only and should be avoided. * * @var \DateTime|null */ private $expires; /** * @param string $name * @param string|null $value * @param int|null $maxAge * @param string|null $domain * @param string|null $path * @param bool $secure * @param bool $httpOnly * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. * * @throws \InvalidArgumentException If name, value or max age is not valid. */ public function __construct( $name, $value = null, $maxAge = null, $domain = null, $path = null, $secure = false, $httpOnly = false, \DateTime $expires = null ) { $this->validateName($name); $this->validateValue($value); $this->validateMaxAge($maxAge); $this->name = $name; $this->value = $value; $this->maxAge = $maxAge; $this->expires = $expires; $this->domain = $this->normalizeDomain($domain); $this->path = $this->normalizePath($path); $this->secure = (bool) $secure; $this->httpOnly = (bool) $httpOnly; } /** * Creates a new cookie without any attribute validation. * * @param string $name * @param string|null $value * @param int $maxAge * @param string|null $domain * @param string|null $path * @param bool $secure * @param bool $httpOnly * @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided. */ public static function createWithoutValidation( $name, $value = null, $maxAge = null, $domain = null, $path = null, $secure = false, $httpOnly = false, \DateTime $expires = null ) { $cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires); $cookie->name = $name; $cookie->value = $value; $cookie->maxAge = $maxAge; return $cookie; } /** * Returns the name. * * @return string */ public function getName() { return $this->name; } /** * Returns the value. * * @return string|null */ public function getValue() { return $this->value; } /** * Checks if there is a value. * * @return bool */ public function hasValue() { return isset($this->value); } /** * Sets the value. * * @param string|null $value * * @return Cookie */ public function withValue($value) { $this->validateValue($value); $new = clone $this; $new->value = $value; return $new; } /** * Returns the max age. * * @return int|null */ public function getMaxAge() { return $this->maxAge; } /** * Checks if there is a max age. * * @return bool */ public function hasMaxAge() { return isset($this->maxAge); } /** * Sets the max age. * * @param int|null $maxAge * * @return Cookie */ public function withMaxAge($maxAge) { $this->validateMaxAge($maxAge); $new = clone $this; $new->maxAge = $maxAge; return $new; } /** * Returns the expiration time. * * @return \DateTime|null */ public function getExpires() { return $this->expires; } /** * Checks if there is an expiration time. * * @return bool */ public function hasExpires() { return isset($this->expires); } /** * Sets the expires. * * @param \DateTime|null $expires * * @return Cookie */ public function withExpires(\DateTime $expires = null) { $new = clone $this; $new->expires = $expires; return $new; } /** * Checks if the cookie is expired. * * @return bool */ public function isExpired() { return isset($this->expires) and $this->expires < new \DateTime(); } /** * Returns the domain. * * @return string|null */ public function getDomain() { return $this->domain; } /** * Checks if there is a domain. * * @return bool */ public function hasDomain() { return isset($this->domain); } /** * Sets the domain. * * @param string|null $domain * * @return Cookie */ public function withDomain($domain) { $new = clone $this; $new->domain = $this->normalizeDomain($domain); return $new; } /** * Checks whether this cookie is meant for this domain. * * @see http://tools.ietf.org/html/rfc6265#section-5.1.3 * * @param string $domain * * @return bool */ public function matchDomain($domain) { // Domain is not set or exact match if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) { return true; } // Domain is not an IP address if (filter_var($domain, FILTER_VALIDATE_IP)) { return false; } return (bool) preg_match(sprintf('/\b%s$/i', preg_quote($this->domain)), $domain); } /** * Returns the path. * * @return string */ public function getPath() { return $this->path; } /** * Sets the path. * * @param string|null $path * * @return Cookie */ public function withPath($path) { $new = clone $this; $new->path = $this->normalizePath($path); return $new; } /** * Checks whether this cookie is meant for this path. * * @see http://tools.ietf.org/html/rfc6265#section-5.1.4 * * @param string $path * * @return bool */ public function matchPath($path) { return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/')); } /** * Checks whether this cookie may only be sent over HTTPS. * * @return bool */ public function isSecure() { return $this->secure; } /** * Sets whether this cookie should only be sent over HTTPS. * * @param bool $secure * * @return Cookie */ public function withSecure($secure) { $new = clone $this; $new->secure = (bool) $secure; return $new; } /** * Check whether this cookie may not be accessed through Javascript. * * @return bool */ public function isHttpOnly() { return $this->httpOnly; } /** * Sets whether this cookie may not be accessed through Javascript. * * @param bool $httpOnly * * @return Cookie */ public function withHttpOnly($httpOnly) { $new = clone $this; $new->httpOnly = (bool) $httpOnly; return $new; } /** * Checks if this cookie represents the same cookie as $cookie. * * This does not compare the values, only name, domain and path. * * @param Cookie $cookie * * @return bool */ public function match(self $cookie) { return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path; } /** * Validates cookie attributes. * * @return bool */ public function isValid() { try { $this->validateName($this->name); $this->validateValue($this->value); $this->validateMaxAge($this->maxAge); } catch (\InvalidArgumentException $e) { return false; } return true; } /** * Validates the name attribute. * * @see http://tools.ietf.org/search/rfc2616#section-2.2 * * @param string $name * * @throws \InvalidArgumentException If the name is empty or contains invalid characters. */ private function validateName($name) { if (strlen($name) < 1) { throw new \InvalidArgumentException('The name cannot be empty'); } // Name attribute is a token as per spec in RFC 2616 if (preg_match('/[\x00-\x20\x22\x28-\x29\x2C\x2F\x3A-\x40\x5B-\x5D\x7B\x7D\x7F]/', $name)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); } } /** * Validates a value. * * @see http://tools.ietf.org/html/rfc6265#section-4.1.1 * * @param string|null $value * * @throws \InvalidArgumentException If the value contains invalid characters. */ private function validateValue($value) { if (isset($value)) { if (preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) { throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value)); } } } /** * Validates a Max-Age attribute. * * @param int|null $maxAge * * @throws \InvalidArgumentException If the Max-Age is not an empty or integer value. */ private function validateMaxAge($maxAge) { if (isset($maxAge)) { if (!is_int($maxAge)) { throw new \InvalidArgumentException('Max-Age must be integer'); } } } /** * Remove the leading '.' and lowercase the domain as per spec in RFC 6265. * * @see http://tools.ietf.org/html/rfc6265#section-4.1.2.3 * @see http://tools.ietf.org/html/rfc6265#section-5.1.3 * @see http://tools.ietf.org/html/rfc6265#section-5.2.3 * * @param string|null $domain * * @return string */ private function normalizeDomain($domain) { if (isset($domain)) { $domain = ltrim(strtolower($domain), '.'); } return $domain; } /** * Processes path as per spec in RFC 6265. * * @see http://tools.ietf.org/html/rfc6265#section-5.1.4 * @see http://tools.ietf.org/html/rfc6265#section-5.2.4 * * @param string|null $path * * @return string */ private function normalizePath($path) { $path = rtrim($path, '/'); if (empty($path) or '/' !== substr($path, 0, 1)) { $path = '/'; } return $path; } } message/src/Builder/ResponseBuilder.php 0000666 00000007567 15165515606 0014220 0 ustar 00 <?php namespace AmeliaHttp\Message\Builder; use AmeliaPsr\Http\Message\ResponseInterface; /** * Fills response object with values. */ class ResponseBuilder { /** * The response to be built. * * @var ResponseInterface */ protected $response; /** * Create builder for the given response. * * @param ResponseInterface $response */ public function __construct(ResponseInterface $response) { $this->response = $response; } /** * Return response. * * @return ResponseInterface */ public function getResponse() { return $this->response; } /** * Add headers represented by an array of header lines. * * @param string[] $headers Response headers as array of header lines. * * @return $this * * @throws \UnexpectedValueException For invalid header values. * @throws \InvalidArgumentException For invalid status code arguments. */ public function setHeadersFromArray(array $headers) { $status = array_shift($headers); $this->setStatus($status); foreach ($headers as $headerLine) { $headerLine = trim($headerLine); if ('' === $headerLine) { continue; } $this->addHeader($headerLine); } return $this; } /** * Add headers represented by a single string. * * @param string $headers Response headers as single string. * * @return $this * * @throws \InvalidArgumentException if $headers is not a string on object with __toString() * @throws \UnexpectedValueException For invalid header values. */ public function setHeadersFromString($headers) { if (!(is_string($headers) || (is_object($headers) && method_exists($headers, '__toString'))) ) { throw new \InvalidArgumentException( sprintf( '%s expects parameter 1 to be a string, %s given', __METHOD__, is_object($headers) ? get_class($headers) : gettype($headers) ) ); } $this->setHeadersFromArray(explode("\r\n", $headers)); return $this; } /** * Set response status from a status string. * * @param string $statusLine Response status as a string. * * @return $this * * @throws \InvalidArgumentException For invalid status line. */ public function setStatus($statusLine) { $parts = explode(' ', $statusLine, 3); if (count($parts) < 2 || 0 !== strpos(strtolower($parts[0]), 'http/')) { throw new \InvalidArgumentException( sprintf('"%s" is not a valid HTTP status line', $statusLine) ); } $reasonPhrase = count($parts) > 2 ? $parts[2] : ''; $this->response = $this->response ->withStatus((int) $parts[1], $reasonPhrase) ->withProtocolVersion(substr($parts[0], 5)); return $this; } /** * Add header represented by a string. * * @param string $headerLine Response header as a string. * * @return $this * * @throws \InvalidArgumentException For invalid header names or values. */ public function addHeader($headerLine) { $parts = explode(':', $headerLine, 2); if (2 !== count($parts)) { throw new \InvalidArgumentException( sprintf('"%s" is not a valid HTTP header line', $headerLine) ); } $name = trim($parts[0]); $value = trim($parts[1]); if ($this->response->hasHeader($name)) { $this->response = $this->response->withAddedHeader($name, $value); } else { $this->response = $this->response->withHeader($name, $value); } return $this; } } message/src/Authentication/QueryParam.php 0000666 00000002214 15165515606 0014552 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request by adding parameters to its query. * * Note: Although in some cases it can be useful, we do not recommend using query parameters for authentication. * Credentials in the URL is generally unsafe as they are not encrypted, anyone can see them. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class QueryParam implements Authentication { /** * @var array */ private $params = []; /** * @param array $params */ public function __construct(array $params) { $this->params = $params; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { $uri = $request->getUri(); $query = $uri->getQuery(); $params = []; parse_str($query, $params); $params = array_merge($params, $this->params); $query = http_build_query($params, "", '&'); $uri = $uri->withQuery($query); return $request->withUri($uri); } } message/src/Authentication/AutoBasicAuth.php 0000666 00000002235 15165515606 0015163 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request using Basic Auth based on credentials in the URI. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class AutoBasicAuth implements Authentication { /** * Whether user info should be removed from the URI. * * @var bool */ private $shouldRemoveUserInfo; /** * @param bool|true $shouldRremoveUserInfo */ public function __construct($shouldRremoveUserInfo = true) { $this->shouldRemoveUserInfo = (bool) $shouldRremoveUserInfo; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { $uri = $request->getUri(); $userInfo = $uri->getUserInfo(); if (!empty($userInfo)) { if ($this->shouldRemoveUserInfo) { $request = $request->withUri($uri->withUserInfo('')); } $request = $request->withHeader('Authorization', sprintf('Basic %s', base64_encode($userInfo))); } return $request; } } message/src/Authentication/RequestConditional.php 0000666 00000002136 15165515606 0016303 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\RequestMatcher; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request if the request is matching the given request matcher. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class RequestConditional implements Authentication { /** * @var RequestMatcher */ private $requestMatcher; /** * @var Authentication */ private $authentication; /** * @param RequestMatcher $requestMatcher * @param Authentication $authentication */ public function __construct(RequestMatcher $requestMatcher, Authentication $authentication) { $this->requestMatcher = $requestMatcher; $this->authentication = $authentication; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { if ($this->requestMatcher->matches($request)) { return $this->authentication->authenticate($request); } return $request; } } message/src/Authentication/Chain.php 0000666 00000002302 15165515606 0013504 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request with a multiple authentication methods. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class Chain implements Authentication { /** * @var Authentication[] */ private $authenticationChain = []; /** * @param Authentication[] $authenticationChain */ public function __construct(array $authenticationChain = []) { foreach ($authenticationChain as $authentication) { if (!$authentication instanceof Authentication) { throw new \InvalidArgumentException( 'Members of the authentication chain must be of type AmeliaHttp\Message\Authentication' ); } } $this->authenticationChain = $authenticationChain; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { foreach ($this->authenticationChain as $authentication) { $request = $authentication->authenticate($request); } return $request; } } message/src/Authentication/Bearer.php 0000666 00000001311 15165515606 0013661 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request using a token. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class Bearer implements Authentication { /** * @var string */ private $token; /** * @param string $token */ public function __construct($token) { $this->token = $token; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { $header = sprintf('Bearer %s', $this->token); return $request->withHeader('Authorization', $header); } } message/src/Authentication/BasicAuth.php 0000666 00000001634 15165515606 0014334 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request using Basic Auth. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class BasicAuth implements Authentication { /** * @var string */ private $username; /** * @var string */ private $password; /** * @param string $username * @param string $password */ public function __construct($username, $password) { $this->username = $username; $this->password = $password; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { $header = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $this->username, $this->password))); return $request->withHeader('Authorization', $header); } } message/src/Authentication/Matching.php 0000666 00000003741 15165515606 0014224 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\RequestMatcher\CallbackRequestMatcher; use AmeliaPsr\Http\Message\RequestInterface; @trigger_error('The '.__NAMESPACE__.'\Matching class is deprecated since version 1.2 and will be removed in 2.0. Use AmeliaHttp\Message\Authentication\RequestConditional instead.', E_USER_DEPRECATED); /** * Authenticate a PSR-7 Request if the request is matching. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * * @deprecated since since version 1.2, and will be removed in 2.0. Use {@link RequestConditional} instead. */ final class Matching implements Authentication { /** * @var Authentication */ private $authentication; /** * @var CallbackRequestMatcher */ private $matcher; /** * @param Authentication $authentication * @param callable|null $matcher */ public function __construct(Authentication $authentication, callable $matcher = null) { if (is_null($matcher)) { $matcher = function () { return true; }; } $this->authentication = $authentication; $this->matcher = new CallbackRequestMatcher($matcher); } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { if ($this->matcher->matches($request)) { return $this->authentication->authenticate($request); } return $request; } /** * Creates a matching authentication for an URL. * * @param Authentication $authentication * @param string $url * * @return self */ public static function createUrlMatcher(Authentication $authentication, $url) { $matcher = function (RequestInterface $request) use ($url) { return preg_match($url, $request->getRequestTarget()); }; return new static($authentication, $matcher); } } message/src/Authentication/Wsse.php 0000666 00000002474 15165515606 0013415 0 ustar 00 <?php namespace AmeliaHttp\Message\Authentication; use AmeliaHttp\Message\Authentication; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request using WSSE. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class Wsse implements Authentication { /** * @var string */ private $username; /** * @var string */ private $password; /** * @param string $username * @param string $password */ public function __construct($username, $password) { $this->username = $username; $this->password = $password; } /** * {@inheritdoc} */ public function authenticate(RequestInterface $request) { // TODO: generate better nonce? $nonce = substr(md5(uniqid(uniqid().'_', true)), 0, 16); $created = date('c'); $digest = base64_encode(sha1(base64_decode($nonce).$created.$this->password, true)); $wsse = sprintf( 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"', $this->username, $digest, $nonce, $created ); return $request ->withHeader('Authorization', 'WSSE profile="UsernameToken"') ->withHeader('X-WSSE', $wsse) ; } } message/src/Exception.php 0000666 00000000212 15165515606 0011437 0 ustar 00 <?php namespace AmeliaHttp\Message; /** * An interface implemented by all HTTP message related exceptions. */ interface Exception { } message/src/Stream/BufferedStream.php 0000666 00000014361 15165515606 0013644 0 ustar 00 <?php namespace AmeliaHttp\Message\Stream; use AmeliaPsr\Http\Message\StreamInterface; /** * Decorator to make any stream seekable. * * Internally it buffers an existing StreamInterface into a php://temp resource (or memory). By default it will use * 2 megabytes of memory before writing to a temporary disk file. * * Due to this, very large stream can suffer performance issue (i/o slowdown). */ class BufferedStream implements StreamInterface { /** @var resource The buffered resource used to seek previous data */ private $resource; /** @var int size of the stream if available */ private $size; /** @var StreamInterface The underlying stream decorated by this class */ private $stream; /** @var int How many bytes were written */ private $written = 0; /** * @param StreamInterface $stream Decorated stream * @param bool $useFileBuffer Whether to use a file buffer (write to a file, if data exceed a certain size) * by default, set this to false to only use memory * @param int $memoryBuffer In conjunction with using file buffer, limit (in bytes) from which it begins to buffer * the data in a file */ public function __construct(StreamInterface $stream, $useFileBuffer = true, $memoryBuffer = 2097152) { $this->stream = $stream; $this->size = $stream->getSize(); if ($useFileBuffer) { $this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+'); } else { $this->resource = fopen('php://memory', 'rw+'); } if (false === $this->resource) { throw new \RuntimeException('Cannot create a resource over temp or memory implementation'); } } /** * {@inheritdoc} */ public function __toString() { try { $this->rewind(); return $this->getContents(); } catch (\Throwable $throwable) { return ''; } catch (\Exception $exception) { // Layer to be BC with PHP 5, remove this when we only support PHP 7+ return ''; } } /** * {@inheritdoc} */ public function close() { if (null === $this->resource) { throw new \RuntimeException('Cannot close on a detached stream'); } $this->stream->close(); fclose($this->resource); } /** * {@inheritdoc} */ public function detach() { if (null === $this->resource) { return; } // Force reading the remaining data of the stream $this->getContents(); $resource = $this->resource; $this->stream->close(); $this->stream = null; $this->resource = null; return $resource; } /** * {@inheritdoc} */ public function getSize() { if (null === $this->resource) { return; } if (null === $this->size && $this->stream->eof()) { return $this->written; } return $this->size; } /** * {@inheritdoc} */ public function tell() { if (null === $this->resource) { throw new \RuntimeException('Cannot tell on a detached stream'); } return ftell($this->resource); } /** * {@inheritdoc} */ public function eof() { if (null === $this->resource) { throw new \RuntimeException('Cannot call eof on a detached stream'); } // We are at the end only when both our resource and underlying stream are at eof return $this->stream->eof() && (ftell($this->resource) === $this->written); } /** * {@inheritdoc} */ public function isSeekable() { return null !== $this->resource; } /** * {@inheritdoc} */ public function seek($offset, $whence = SEEK_SET) { if (null === $this->resource) { throw new \RuntimeException('Cannot seek on a detached stream'); } fseek($this->resource, $offset, $whence); } /** * {@inheritdoc} */ public function rewind() { if (null === $this->resource) { throw new \RuntimeException('Cannot rewind on a detached stream'); } rewind($this->resource); } /** * {@inheritdoc} */ public function isWritable() { return false; } /** * {@inheritdoc} */ public function write($string) { throw new \RuntimeException('Cannot write on this stream'); } /** * {@inheritdoc} */ public function isReadable() { return null !== $this->resource; } /** * {@inheritdoc} */ public function read($length) { if (null === $this->resource) { throw new \RuntimeException('Cannot read on a detached stream'); } $read = ''; // First read from the resource if (ftell($this->resource) !== $this->written) { $read = fread($this->resource, $length); } $bytesRead = strlen($read); if ($bytesRead < $length) { $streamRead = $this->stream->read($length - $bytesRead); // Write on the underlying stream what we read $this->written += fwrite($this->resource, $streamRead); $read .= $streamRead; } return $read; } /** * {@inheritdoc} */ public function getContents() { if (null === $this->resource) { throw new \RuntimeException('Cannot read on a detached stream'); } $read = ''; while (!$this->eof()) { $read .= $this->read(8192); } return $read; } /** * {@inheritdoc} */ public function getMetadata($key = null) { if (null === $this->resource) { if (null === $key) { return []; } return; } $metadata = stream_get_meta_data($this->resource); if (null === $key) { return $metadata; } if (!array_key_exists($key, $metadata)) { return; } return $metadata[$key]; } } message/src/CookieUtil.php 0000666 00000002623 15165515606 0011560 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaHttp\Message\Exception\UnexpectedValueException; final class CookieUtil { /** * Handles dates as defined by RFC 2616 section 3.3.1, and also some other * non-standard, but common formats. * * @var array */ private static $dateFormats = [ 'D, d M y H:i:s T', 'D, d M Y H:i:s T', 'D, d-M-y H:i:s T', 'D, d-M-Y H:i:s T', 'D, d-m-y H:i:s T', 'D, d-m-Y H:i:s T', 'D M j G:i:s Y', 'D M d H:i:s Y T', ]; /** * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/BrowserKit/Cookie.php * * @param string $dateValue * * @return \DateTime * * @throws UnexpectedValueException if we cannot parse the cookie date string. */ public static function parseDate($dateValue) { foreach (self::$dateFormats as $dateFormat) { if (false !== $date = \DateTime::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) { return $date; } } // attempt a fallback for unusual formatting if (false !== $date = date_create($dateValue, new \DateTimeZone('GMT'))) { return $date; } throw new UnexpectedValueException(sprintf( 'Unparseable cookie date string "%s"', $dateValue )); } } message/src/Encoding/CompressStream.php 0000666 00000002013 15165515606 0014177 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream compress (RFC 1950). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class CompressStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { if (!extension_loaded('zlib')) { throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } parent::__construct($stream, ['window' => 15, 'level' => $level]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.deflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.inflate'; } } message/src/Encoding/InflateStream.php 0000666 00000002013 15165515606 0013766 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream inflate (RFC 1951). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class InflateStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { if (!extension_loaded('zlib')) { throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } parent::__construct($stream, ['window' => -15]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15, 'level' => $level]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.inflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.deflate'; } } message/src/Encoding/DeflateStream.php 0000666 00000001565 15165515606 0013763 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream deflate (RFC 1951). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class DeflateStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { parent::__construct($stream, ['window' => -15, 'level' => $level]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.deflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.inflate'; } } message/src/Encoding/GzipDecodeStream.php 0000666 00000002042 15165515606 0014423 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream for decoding from gzip format (RFC 1952). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class GzipDecodeStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { if (!extension_loaded('zlib')) { throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } parent::__construct($stream, ['window' => 31]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31, 'level' => $level]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.inflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.deflate'; } } message/src/Encoding/DecompressStream.php 0000666 00000002017 15165515606 0014514 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream decompress (RFC 1950). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class DecompressStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { if (!extension_loaded('zlib')) { throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } parent::__construct($stream, ['window' => 15]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15, 'level' => $level]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.inflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.deflate'; } } message/src/Encoding/GzipEncodeStream.php 0000666 00000002040 15165515606 0014433 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaPsr\Http\Message\StreamInterface; /** * Stream for encoding to gzip format (RFC 1952). * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class GzipEncodeStream extends FilteredStream { /** * @param StreamInterface $stream * @param int $level */ public function __construct(StreamInterface $stream, $level = -1) { if (!extension_loaded('zlib')) { throw new \RuntimeException('The zlib extension must be enabled to use this stream'); } parent::__construct($stream, ['window' => 31, 'level' => $level]); // @deprecated will be removed in 2.0 $this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31]); } /** * {@inheritdoc} */ protected function readFilter() { return 'zlib.deflate'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'zlib.inflate'; } } message/src/Encoding/ChunkStream.php 0000666 00000001145 15165515606 0013461 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; /** * Transform a regular stream into a chunked one. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class ChunkStream extends FilteredStream { /** * {@inheritdoc} */ protected function readFilter() { return 'chunk'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'dechunk'; } /** * {@inheritdoc} */ protected function fill() { parent::fill(); if ($this->stream->eof()) { $this->buffer .= "0\r\n\r\n"; } } } message/src/Encoding/FilteredStream.php 0000666 00000013315 15165515606 0014151 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; use AmeliaClue\StreamFilter as Filter; use AmeliaHttp\Message\Decorator\StreamDecorator; use AmeliaPsr\Http\Message\StreamInterface; /** * A filtered stream has a filter for filtering output and a filter for filtering input made to a underlying stream. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ abstract class FilteredStream implements StreamInterface { const BUFFER_SIZE = 8192; use StreamDecorator { rewind as private doRewind; seek as private doSeek; } /** * @var callable */ protected $readFilterCallback; /** * @var resource * * @deprecated since version 1.5, will be removed in 2.0 */ protected $readFilter; /** * @var callable * * @deprecated since version 1.5, will be removed in 2.0 */ protected $writeFilterCallback; /** * @var resource * * @deprecated since version 1.5, will be removed in 2.0 */ protected $writeFilter; /** * Internal buffer. * * @var string */ protected $buffer = ''; /** * @param StreamInterface $stream * @param mixed|null $readFilterOptions * @param mixed|null $writeFilterOptions deprecated since 1.5, will be removed in 2.0 */ public function __construct(StreamInterface $stream, $readFilterOptions = null, $writeFilterOptions = null) { if (null !== $readFilterOptions) { $this->readFilterCallback = Filter\fun($this->readFilter(), $readFilterOptions); } else { $this->readFilterCallback = Filter\fun($this->readFilter()); } if (null !== $writeFilterOptions) { $this->writeFilterCallback = Filter\fun($this->writeFilter(), $writeFilterOptions); @trigger_error('The $writeFilterOptions argument is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); } else { $this->writeFilterCallback = Filter\fun($this->writeFilter()); } $this->stream = $stream; } /** * {@inheritdoc} */ public function read($length) { if (strlen($this->buffer) >= $length) { $read = substr($this->buffer, 0, $length); $this->buffer = substr($this->buffer, $length); return $read; } if ($this->stream->eof()) { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } $read = $this->buffer; $this->buffer = ''; $this->fill(); return $read.$this->read($length - strlen($read)); } /** * {@inheritdoc} */ public function eof() { return $this->stream->eof() && '' === $this->buffer; } /** * Buffer is filled by reading underlying stream. * * Callback is reading once more even if the stream is ended. * This allow to get last data in the PHP buffer otherwise this * bug is present : https://bugs.php.net/bug.php?id=48725 */ protected function fill() { $readFilterCallback = $this->readFilterCallback; $this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE)); if ($this->stream->eof()) { $this->buffer .= $readFilterCallback(); } } /** * {@inheritdoc} */ public function getContents() { $buffer = ''; while (!$this->eof()) { $buf = $this->read(self::BUFFER_SIZE); // Using a loose equality here to match on '' and false. if (null == $buf) { break; } $buffer .= $buf; } return $buffer; } /** * Always returns null because we can't tell the size of a stream when we filter. */ public function getSize() { return null; } /** * {@inheritdoc} */ public function __toString() { return $this->getContents(); } /** * Filtered streams are not seekable. * * We would need to buffer and process everything to allow seeking. */ public function isSeekable() { return false; } /** * {@inheritdoc} */ public function rewind() { @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); $this->doRewind(); } /** * {@inheritdoc} */ public function seek($offset, $whence = SEEK_SET) { @trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED); $this->doSeek($offset, $whence); } /** * Returns the read filter name. * * @return string * * @deprecated since version 1.5, will be removed in 2.0 */ public function getReadFilter() { @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); return $this->readFilter(); } /** * Returns the write filter name. * * @return string */ abstract protected function readFilter(); /** * Returns the write filter name. * * @return string * * @deprecated since version 1.5, will be removed in 2.0 */ public function getWriteFilter() { @trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED); return $this->writeFilter(); } /** * Returns the write filter name. * * @return string */ abstract protected function writeFilter(); } message/src/Encoding/Filter/Chunk.php 0000666 00000001406 15165515606 0013532 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding\Filter; /** * Userland implementation of the chunk stream filter. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class Chunk extends \php_user_filter { /** * {@inheritdoc} */ public function filter($in, $out, &$consumed, $closing) { while ($bucket = stream_bucket_make_writeable($in)) { $lenbucket = stream_bucket_new($this->stream, dechex($bucket->datalen)."\r\n"); stream_bucket_append($out, $lenbucket); $consumed += $bucket->datalen; stream_bucket_append($out, $bucket); $lenbucket = stream_bucket_new($this->stream, "\r\n"); stream_bucket_append($out, $lenbucket); } return PSFS_PASS_ON; } } message/src/Encoding/DechunkStream.php 0000666 00000000702 15165515606 0013770 0 ustar 00 <?php namespace AmeliaHttp\Message\Encoding; /** * Decorate a stream which is chunked. * * Allow to decode a chunked stream * * @author Joel Wurtz <joel.wurtz@gmail.com> */ class DechunkStream extends FilteredStream { /** * {@inheritdoc} */ protected function readFilter() { return 'dechunk'; } /** * {@inheritdoc} */ protected function writeFilter() { return 'chunk'; } } message/src/RequestMatcher.php 0000666 00000001255 15165515606 0012445 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\RequestInterface; /** * Match a request. * * PSR-7 equivalent of Symfony's RequestMatcher * * @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/RequestMatcherInterface.php * * @author Joel Wurtz <joel.wurtz@gmail.com> */ interface RequestMatcher { /** * Decides whether the rule(s) implemented by the strategy matches the supplied request. * * @param RequestInterface $request The PSR7 request to check for a match * * @return bool true if the request matches, false otherwise */ public function matches(RequestInterface $request); } message/src/filters.php 0000666 00000000262 15165515606 0011156 0 ustar 00 <?php // Register chunk filter if not found if (!array_key_exists('chunk', stream_get_filters())) { stream_filter_register('chunk', 'Http\Message\Encoding\Filter\Chunk'); } message/src/Formatter.php 0000666 00000001201 15165515606 0011443 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Formats a request and/or a response as a string. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface Formatter { /** * Formats a request. * * @param RequestInterface $request * * @return string */ public function formatRequest(RequestInterface $request); /** * Formats a response. * * @param ResponseInterface $response * * @return string */ public function formatResponse(ResponseInterface $response); } message/src/Decorator/RequestDecorator.php 0000666 00000003162 15165515606 0014725 0 ustar 00 <?php namespace AmeliaHttp\Message\Decorator; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\UriInterface; /** * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait RequestDecorator { use MessageDecorator { getMessage as getRequest; } /** * Exchanges the underlying request with another. * * @param RequestInterface $request * * @return self */ public function withRequest(RequestInterface $request) { $new = clone $this; $new->message = $request; return $new; } /** * {@inheritdoc} */ public function getRequestTarget() { return $this->message->getRequestTarget(); } /** * {@inheritdoc} */ public function withRequestTarget($requestTarget) { $new = clone $this; $new->message = $this->message->withRequestTarget($requestTarget); return $new; } /** * {@inheritdoc} */ public function getMethod() { return $this->message->getMethod(); } /** * {@inheritdoc} */ public function withMethod($method) { $new = clone $this; $new->message = $this->message->withMethod($method); return $new; } /** * {@inheritdoc} */ public function getUri() { return $this->message->getUri(); } /** * {@inheritdoc} */ public function withUri(UriInterface $uri, $preserveHost = false) { $new = clone $this; $new->message = $this->message->withUri($uri, $preserveHost); return $new; } } message/src/Decorator/ResponseDecorator.php 0000666 00000002053 15165515606 0015071 0 ustar 00 <?php namespace AmeliaHttp\Message\Decorator; use AmeliaPsr\Http\Message\ResponseInterface; /** * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait ResponseDecorator { use MessageDecorator { getMessage as getResponse; } /** * Exchanges the underlying response with another. * * @param ResponseInterface $response * * @return self */ public function withResponse(ResponseInterface $response) { $new = clone $this; $new->message = $response; return $new; } /** * {@inheritdoc} */ public function getStatusCode() { return $this->message->getStatusCode(); } /** * {@inheritdoc} */ public function withStatus($code, $reasonPhrase = '') { $new = clone $this; $new->message = $this->message->withStatus($code, $reasonPhrase); return $new; } /** * {@inheritdoc} */ public function getReasonPhrase() { return $this->message->getReasonPhrase(); } } message/src/Decorator/MessageDecorator.php 0000666 00000004713 15165515606 0014664 0 ustar 00 <?php namespace AmeliaHttp\Message\Decorator; use AmeliaPsr\Http\Message\MessageInterface; use AmeliaPsr\Http\Message\StreamInterface; /** * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait MessageDecorator { /** * @var MessageInterface */ private $message; /** * Returns the decorated message. * * Since the underlying Message is immutable as well * exposing it is not an issue, because it's state cannot be altered * * @return MessageInterface */ public function getMessage() { return $this->message; } /** * {@inheritdoc} */ public function getProtocolVersion() { return $this->message->getProtocolVersion(); } /** * {@inheritdoc} */ public function withProtocolVersion($version) { $new = clone $this; $new->message = $this->message->withProtocolVersion($version); return $new; } /** * {@inheritdoc} */ public function getHeaders() { return $this->message->getHeaders(); } /** * {@inheritdoc} */ public function hasHeader($header) { return $this->message->hasHeader($header); } /** * {@inheritdoc} */ public function getHeader($header) { return $this->message->getHeader($header); } /** * {@inheritdoc} */ public function getHeaderLine($header) { return $this->message->getHeaderLine($header); } /** * {@inheritdoc} */ public function withHeader($header, $value) { $new = clone $this; $new->message = $this->message->withHeader($header, $value); return $new; } /** * {@inheritdoc} */ public function withAddedHeader($header, $value) { $new = clone $this; $new->message = $this->message->withAddedHeader($header, $value); return $new; } /** * {@inheritdoc} */ public function withoutHeader($header) { $new = clone $this; $new->message = $this->message->withoutHeader($header); return $new; } /** * {@inheritdoc} */ public function getBody() { return $this->message->getBody(); } /** * {@inheritdoc} */ public function withBody(StreamInterface $body) { $new = clone $this; $new->message = $this->message->withBody($body); return $new; } } message/src/Decorator/StreamDecorator.php 0000666 00000004157 15165515606 0014535 0 ustar 00 <?php namespace AmeliaHttp\Message\Decorator; use AmeliaPsr\Http\Message\StreamInterface; /** * Decorates a stream. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ trait StreamDecorator { /** * @var StreamInterface */ protected $stream; /** * {@inheritdoc} */ public function __toString() { return $this->stream->__toString(); } /** * {@inheritdoc} */ public function close() { $this->stream->close(); } /** * {@inheritdoc} */ public function detach() { return $this->stream->detach(); } /** * {@inheritdoc} */ public function getSize() { return $this->stream->getSize(); } /** * {@inheritdoc} */ public function tell() { return $this->stream->tell(); } /** * {@inheritdoc} */ public function eof() { return $this->stream->eof(); } /** * {@inheritdoc} */ public function isSeekable() { return $this->stream->isSeekable(); } /** * {@inheritdoc} */ public function seek($offset, $whence = SEEK_SET) { $this->stream->seek($offset, $whence); } /** * {@inheritdoc} */ public function rewind() { $this->stream->rewind(); } /** * {@inheritdoc} */ public function isWritable() { return $this->stream->isWritable(); } /** * {@inheritdoc} */ public function write($string) { return $this->stream->write($string); } /** * {@inheritdoc} */ public function isReadable() { return $this->stream->isReadable(); } /** * {@inheritdoc} */ public function read($length) { return $this->stream->read($length); } /** * {@inheritdoc} */ public function getContents() { return $this->stream->getContents(); } /** * {@inheritdoc} */ public function getMetadata($key = null) { return $this->stream->getMetadata($key); } } message/src/StreamFactory/SlimStreamFactory.php 0000666 00000001416 15165515606 0015703 0 ustar 00 <?php namespace AmeliaHttp\Message\StreamFactory; use AmeliaHttp\Message\StreamFactory; use AmeliaPsr\Http\Message\StreamInterface; use Slim\Http\Stream; /** * Creates Slim 3 streams. * * @author Mika Tuupola <tuupola@appelsiini.net> */ final class SlimStreamFactory implements StreamFactory { /** * {@inheritdoc} */ public function createStream($body = null) { if ($body instanceof StreamInterface) { return $body; } if (is_resource($body)) { return new Stream($body); } $resource = fopen('php://memory', 'r+'); $stream = new Stream($resource); if (null !== $body && '' !== $body) { $stream->write((string) $body); } return $stream; } } message/src/StreamFactory/DiactorosStreamFactory.php 0000666 00000001417 15165515606 0016727 0 ustar 00 <?php namespace AmeliaHttp\Message\StreamFactory; use AmeliaHttp\Message\StreamFactory; use AmeliaPsr\Http\Message\StreamInterface; use Zend\Diactoros\Stream; /** * Creates Diactoros streams. * * @author Михаил Красильников <m.krasilnikov@yandex.ru> */ final class DiactorosStreamFactory implements StreamFactory { /** * {@inheritdoc} */ public function createStream($body = null) { if ($body instanceof StreamInterface) { return $body; } if (is_resource($body)) { return new Stream($body); } $stream = new Stream('php://memory', 'rw'); if (null !== $body && '' !== $body) { $stream->write((string) $body); } return $stream; } } message/src/StreamFactory/GuzzleStreamFactory.php 0000666 00000000642 15165515606 0016257 0 ustar 00 <?php namespace AmeliaHttp\Message\StreamFactory; use AmeliaHttp\Message\StreamFactory; /** * Creates Guzzle streams. * * @author Михаил Красильников <m.krasilnikov@yandex.ru> */ final class GuzzleStreamFactory implements StreamFactory { /** * {@inheritdoc} */ public function createStream($body = null) { return \AmeliaGuzzleHttp\Psr7\stream_for($body); } } message/src/CookieJar.php 0000666 00000010760 15165515606 0011360 0 ustar 00 <?php namespace AmeliaHttp\Message; /** * Cookie Jar holds a set of Cookies. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ final class CookieJar implements \Countable, \IteratorAggregate { /** * @var \SplObjectStorage */ protected $cookies; public function __construct() { $this->cookies = new \SplObjectStorage(); } /** * Checks if there is a cookie. * * @param Cookie $cookie * * @return bool */ public function hasCookie(Cookie $cookie) { return $this->cookies->contains($cookie); } /** * Adds a cookie. * * @param Cookie $cookie */ public function addCookie(Cookie $cookie) { if (!$this->hasCookie($cookie)) { $cookies = $this->getMatchingCookies($cookie); foreach ($cookies as $matchingCookie) { if ($cookie->getValue() !== $matchingCookie->getValue() || $cookie->getMaxAge() > $matchingCookie->getMaxAge()) { $this->removeCookie($matchingCookie); continue; } } if ($cookie->hasValue()) { $this->cookies->attach($cookie); } } } /** * Removes a cookie. * * @param Cookie $cookie */ public function removeCookie(Cookie $cookie) { $this->cookies->detach($cookie); } /** * Returns the cookies. * * @return Cookie[] */ public function getCookies() { $match = function ($matchCookie) { return true; }; return $this->findMatchingCookies($match); } /** * Returns all matching cookies. * * @param Cookie $cookie * * @return Cookie[] */ public function getMatchingCookies(Cookie $cookie) { $match = function ($matchCookie) use ($cookie) { return $matchCookie->match($cookie); }; return $this->findMatchingCookies($match); } /** * Finds matching cookies based on a callable. * * @param callable $match * * @return Cookie[] */ protected function findMatchingCookies(callable $match) { $cookies = []; foreach ($this->cookies as $cookie) { if ($match($cookie)) { $cookies[] = $cookie; } } return $cookies; } /** * Checks if there are cookies. * * @return bool */ public function hasCookies() { return $this->cookies->count() > 0; } /** * Sets the cookies and removes any previous one. * * @param Cookie[] $cookies */ public function setCookies(array $cookies) { $this->clear(); $this->addCookies($cookies); } /** * Adds some cookies. * * @param Cookie[] $cookies */ public function addCookies(array $cookies) { foreach ($cookies as $cookie) { $this->addCookie($cookie); } } /** * Removes some cookies. * * @param Cookie[] $cookies */ public function removeCookies(array $cookies) { foreach ($cookies as $cookie) { $this->removeCookie($cookie); } } /** * Removes cookies which match the given parameters. * * Null means that parameter should not be matched * * @param string|null $name * @param string|null $domain * @param string|null $path */ public function removeMatchingCookies($name = null, $domain = null, $path = null) { $match = function ($cookie) use ($name, $domain, $path) { $match = true; if (isset($name)) { $match = $match && ($cookie->getName() === $name); } if (isset($domain)) { $match = $match && $cookie->matchDomain($domain); } if (isset($path)) { $match = $match && $cookie->matchPath($path); } return $match; }; $cookies = $this->findMatchingCookies($match); $this->removeCookies($cookies); } /** * Removes all cookies. */ public function clear() { $this->cookies = new \SplObjectStorage(); } /** * {@inheritdoc} */ public function count() { return $this->cookies->count(); } /** * {@inheritdoc} */ public function getIterator() { return clone $this->cookies; } } message/src/Authentication.php 0000666 00000000631 15165515606 0012465 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\RequestInterface; /** * Authenticate a PSR-7 Request. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface Authentication { /** * Authenticates a request. * * @param RequestInterface $request * * @return RequestInterface */ public function authenticate(RequestInterface $request); } message/src/Exception/UnexpectedValueException.php 0000666 00000000263 15165515606 0016425 0 ustar 00 <?php namespace AmeliaHttp\Message\Exception; use AmeliaHttp\Message\Exception; final class UnexpectedValueException extends \UnexpectedValueException implements Exception { } message/apigen.neon 0000666 00000000106 15165515606 0010327 0 ustar 00 source: - src/ destination: build/api/ templateTheme: bootstrap message/composer.json 0000666 00000003266 15165515606 0010737 0 ustar 00 { "name": "php-http/message", "description": "HTTP Message related tools", "license": "MIT", "keywords": ["message", "http", "psr-7"], "homepage": "http://php-http.org", "authors": [ { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require": { "php": "^5.4 || ^7.0", "psr/http-message": "^1.0", "php-http/message-factory": "^1.0.2", "clue/stream-filter": "^1.4" }, "provide": { "php-http/message-factory-implementation": "1.0" }, "require-dev": { "zendframework/zend-diactoros": "^1.0", "guzzlehttp/psr7": "^1.0", "ext-zlib": "*", "phpspec/phpspec": "^2.4", "henrikbjorn/phpspec-code-coverage" : "^1.0", "coduo/phpspec-data-provider-extension": "^1.0", "akeneo/phpspec-skip-example-extension": "^1.0", "slim/slim": "^3.0" }, "suggest": { "zendframework/zend-diactoros": "Used with Diactoros Factories", "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", "slim/slim": "Used with Slim Framework PSR-7 implementation", "ext-zlib": "Used with compressor/decompressor streams" }, "autoload": { "psr-4": { "Http\\Message\\": "src/" }, "files": [ "src/filters.php" ] }, "autoload-dev": { "psr-4": { "spec\\Http\\Message\\": "spec/" } }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" }, "extra": { "branch-alias": { "dev-master": "1.6-dev" } } } message/LICENSE 0000666 00000002072 15165515606 0007214 0 ustar 00 Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org> 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. message/CHANGELOG.md 0000666 00000011720 15165515606 0010020 0 ustar 00 # Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased ## [1.7.2] - 2018-10-30 ### Fixed - FilteredStream uses `@trigger_error` instead of throwing exceptions to not break careless users. You still need to fix your stream code to respect `isSeekable`. Seeking does not work as expected, and we will add exceptions in version 2. ## [1.7.1] - 2018-10-29 ### Fixed - FilteredStream is not actually seekable ## [1.7.0] - 2018-08-15 ### Fixed - Fix CurlCommandFormatter for binary request payloads - Fix QueryParam authentication to assemble proper URL regardless of PHP `arg_separator.output` directive - Do not pass `null` parameters to `Clue\StreamFilter\fun` ### Changed - Dropped tests on HHVM ## [1.6.0] - 2017-07-05 ### Added - CookieUtil::parseDate to create a date from cookie date string ### Fixed - Fix curl command of CurlFormatter when there is an user-agent header ## [1.5.0] - 2017-02-14 ### Added - Check for empty string in Stream factories - Cookie::createWithoutValidation Static constructor to create a cookie. Will not perform any attribute validation during instantiation. - Cookie::isValid Method to check if cookie attributes are valid. ### Fixed - FilteredStream::getSize returns null because the contents size is unknown. - Stream factories does not rewinds streams. The previous behavior was not coherent between factories and inputs. ### Deprecated - FilteredStream::getReadFilter The read filter is internal and should never be used by consuming code. - FilteredStream::getWriteFilter We did not implement writing to the streams at all. And if we do, the filter is an internal information and should not be used by consuming code. ## [1.4.1] - 2016-12-16 ### Fixed - Cookie::matchPath Cookie with root path (`/`) will not match sub path (e.g. `/cookie`). ## [1.4.0] - 2016-10-20 ### Added - Message, stream and URI factories for [Slim Framework](https://github.com/slimphp/Slim) - BufferedStream that allow you to decorate a non-seekable stream with a seekable one. - cUrlFormatter to be able to redo the request with a cURL command ## [1.3.1] - 2016-07-15 ### Fixed - FullHttpMessageFormatter will not read from streams that you cannot rewind (non-seekable) - FullHttpMessageFormatter will not read from the stream if $maxBodyLength is zero - FullHttpMessageFormatter rewinds streams after they are read ## [1.3.0] - 2016-07-14 ### Added - FullHttpMessageFormatter to include headers and body in the formatted message ### Fixed - #41: Response builder broke header value ## [1.2.0] - 2016-03-29 ### Added - The RequestMatcher is built after the Symfony RequestMatcher and separates scheme, host and path expressions and provides an option to filter on the method - New RequestConditional authentication method using request matchers - Add automatic basic auth info detection based on the URL ### Changed - Improved ResponseBuilder ### Deprecated - RegexRequestMatcher, use RequestMatcher instead - Matching authenitcation method, use RequestConditional instead ## [1.1.0] - 2016-02-25 ### Added - Add a request matcher interface and regex implementation - Add a callback request matcher implementation - Add a ResponseBuilder, to create PSR7 Response from a string ### Fixed - Fix casting string on a FilteredStream not filtering the output ## [1.0.0] - 2016-01-27 ## [0.2.0] - 2015-12-29 ### Added - Autoregistration of stream filters using Composer autoload - Cookie - [Apigen](http://www.apigen.org/) configuration ## [0.1.2] - 2015-12-26 ### Added - Request and response factory bindings ### Fixed - Chunk filter namespace in Dechunk stream ## [0.1.1] - 2015-12-25 ### Added - Formatter ## 0.1.0 - 2015-12-24 ### Added - Authentication - Encoding - Message decorator - Message factory (Guzzle, Diactoros) [Unreleased]: https://github.com/php-http/message/compare/v1.7.1...HEAD [1.7.1]: https://github.com/php-http/message/compare/1.7.0...v1.7.1 [1.7.0]: https://github.com/php-http/message/compare/1.6.0...1.7.0 [1.6.0]: https://github.com/php-http/message/compare/1.5.0...1.6.0 [1.5.0]: https://github.com/php-http/message/compare/v1.4.1...1.5.0 [1.4.1]: https://github.com/php-http/message/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/php-http/message/compare/v1.3.1...v1.4.0 [1.3.1]: https://github.com/php-http/message/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/php-http/message/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/php-http/message/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/php-http/message/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/php-http/message/compare/0.2.0...v1.0.0 [0.2.0]: https://github.com/php-http/message/compare/v0.1.2...0.2.0 [0.1.2]: https://github.com/php-http/message/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/php-http/message/compare/v0.1.0...v0.1.1 message-factory/src/StreamFactory.php 0000666 00000001073 15165515606 0013737 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\StreamInterface; /** * Factory for PSR-7 Stream. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface StreamFactory { /** * Creates a new PSR-7 stream. * * @param string|resource|StreamInterface|null $body * * @return StreamInterface * * @throws \InvalidArgumentException If the stream body is invalid. * @throws \RuntimeException If creating the stream from $body fails. */ public function createStream($body = null); } message-factory/src/UriFactory.php 0000666 00000000726 15165515606 0013247 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\UriInterface; /** * Factory for PSR-7 URI. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface UriFactory { /** * Creates an PSR-7 URI. * * @param string|UriInterface $uri * * @return UriInterface * * @throws \InvalidArgumentException If the $uri argument can not be converted into a valid URI. */ public function createUri($uri); } message-factory/src/RequestFactory.php 0000666 00000001523 15165515606 0014134 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\UriInterface; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\StreamInterface; /** * Factory for PSR-7 Request. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface RequestFactory { /** * Creates a new PSR-7 request. * * @param string $method * @param string|UriInterface $uri * @param array $headers * @param resource|string|StreamInterface|null $body * @param string $protocolVersion * * @return RequestInterface */ public function createRequest( $method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1' ); } message-factory/src/ResponseFactory.php 0000666 00000001652 15165515606 0014305 0 ustar 00 <?php namespace AmeliaHttp\Message; use AmeliaPsr\Http\Message\ResponseInterface; use AmeliaPsr\Http\Message\StreamInterface; /** * Factory for PSR-7 Response. * * This factory contract can be reused in Message and Server Message factories. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface ResponseFactory { /** * Creates a new PSR-7 response. * * @param int $statusCode * @param string|null $reasonPhrase * @param array $headers * @param resource|string|StreamInterface|null $body * @param string $protocolVersion * * @return ResponseInterface */ public function createResponse( $statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $protocolVersion = '1.1' ); } message-factory/src/MessageFactory.php 0000666 00000000332 15165515606 0014065 0 ustar 00 <?php namespace AmeliaHttp\Message; /** * Factory for PSR-7 Request and Response. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface MessageFactory extends RequestFactory, ResponseFactory { } message-factory/CHANGELOG.md 0000666 00000001602 15165515606 0011463 0 ustar 00 # Change Log ## 1.0.2 - 2015-12-19 ### Added - Request and Response factory binding types to Puli ## 1.0.1 - 2015-12-17 ### Added - Puli configuration and binding types ## 1.0.0 - 2015-12-15 ### Added - Response Factory in order to be reused in Message and Server Message factories - Request Factory ### Changed - Message Factory extends Request and Response factories ## 1.0.0-RC1 - 2015-12-14 ### Added - CS check ### Changed - RuntimeException is thrown when the StreamFactory cannot write to the underlying stream ## 0.3.0 - 2015-11-16 ### Removed - Client Context Factory - Factory Awares and Templates ## 0.2.0 - 2015-11-16 ### Changed - Reordered the parameters when creating a message to have the protocol last, as its the least likely to need to be changed. ## 0.1.0 - 2015-06-01 ### Added - Initial release ### Changed - Helpers are renamed to templates message-factory/LICENSE 0000666 00000002065 15165515606 0010663 0 ustar 00 Copyright (c) 2015 PHP HTTP Team <team@php-http.org> 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. message-factory/composer.json 0000666 00000001174 15165515606 0012400 0 ustar 00 { "name": "php-http/message-factory", "description": "Factory interfaces for PSR-7 HTTP Message", "license": "MIT", "keywords": ["http", "factory", "message", "stream", "uri"], "homepage": "http://php-http.org", "authors": [ { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require": { "php": ">=5.4", "psr/http-message": "^1.0" }, "autoload": { "psr-4": { "Http\\Message\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } } } message-factory/puli.json 0000666 00000002571 15165515606 0011524 0 ustar 00 { "version": "1.0", "binding-types": { "Http\\Message\\MessageFactory": { "description": "PSR-7 Message Factory", "parameters": { "depends": { "description": "Optional class dependency which can be checked by consumers" } } }, "Http\\Message\\RequestFactory": { "parameters": { "depends": { "description": "Optional class dependency which can be checked by consumers" } } }, "Http\\Message\\ResponseFactory": { "parameters": { "depends": { "description": "Optional class dependency which can be checked by consumers" } } }, "Http\\Message\\StreamFactory": { "description": "PSR-7 Stream Factory", "parameters": { "depends": { "description": "Optional class dependency which can be checked by consumers" } } }, "Http\\Message\\UriFactory": { "description": "PSR-7 URI Factory", "parameters": { "depends": { "description": "Optional class dependency which can be checked by consumers" } } } } } message-factory/README.md 0000666 00000002017 15165515606 0011132 0 ustar 00 # PSR-7 Message Factory [](https://github.com/php-http/message-factory/releases) [](LICENSE) [](https://packagist.org/packages/php-http/message-factory) **Factory interfaces for PSR-7 HTTP Message.** ## Install Via Composer ``` bash $ composer require php-http/message-factory ``` ## Documentation Please see the [official documentation](http://php-http.readthedocs.org/en/latest/message-factory/). ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. httplug/CHANGELOG.md 0000666 00000002077 15165515606 0010070 0 ustar 00 # Change Log ## 1.1.0 - 2016-08-31 - Added HttpFulfilledPromise and HttpRejectedPromise which respect the HttpAsyncClient interface ## 1.0.0 - 2016-01-26 ### Removed - Stability configuration from composer ## 1.0.0-RC1 - 2016-01-12 ### Changed - Updated package files - Updated promise dependency to RC1 ## 1.0.0-beta - 2015-12-17 ### Added - Puli configuration and binding types ### Changed - Exception concept ## 1.0.0-alpha3 - 2015-12-13 ### Changed - Async client does not throw exceptions ### Removed - Promise interface moved to its own repository: [php-http/promise](https://github.com/php-http/promise) ## 1.0.0-alpha2 - 2015-11-16 ### Added - Async client and Promise interface ## 1.0.0-alpha - 2015-10-26 ### Added - Better domain exceptions. ### Changed - Purpose of the library: general HTTP CLient abstraction. ### Removed - Request options: they should be configured at construction time. - Multiple request sending: should be done asynchronously using Async Client. - `getName` method ## 0.1.0 - 2015-06-03 ### Added - Initial release httplug/src/HttpAsyncClient.php 0000666 00000001362 15165515606 0012627 0 ustar 00 <?php namespace AmeliaHttp\Client; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\RequestInterface; /** * Sends a PSR-7 Request in an asynchronous way by returning a Promise. * * @author Joel Wurtz <joel.wurtz@gmail.com> */ interface HttpAsyncClient { /** * Sends a PSR-7 request in an asynchronous way. * * Exceptions related to processing the request are available from the returned Promise. * * @param RequestInterface $request * * @return Promise Resolves a PSR-7 Response or fails with an AmeliaHttp\Client\Exception. * * @throws \Exception If processing the request is impossible (eg. bad configuration). */ public function sendAsyncRequest(RequestInterface $request); } httplug/src/Exception/TransferException.php 0000666 00000000423 15165515606 0015151 0 ustar 00 <?php namespace AmeliaHttp\Client\Exception; use AmeliaHttp\Client\Exception; /** * Base exception for transfer related exceptions. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class TransferException extends \RuntimeException implements Exception { } httplug/src/Exception/NetworkException.php 0000666 00000000523 15165515606 0015017 0 ustar 00 <?php namespace AmeliaHttp\Client\Exception; /** * Thrown when the request cannot be completed because of network issues. * * There is no response object as this exception is thrown when no response has been received. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class NetworkException extends RequestException { } httplug/src/Exception/RequestException.php 0000666 00000001733 15165515606 0015022 0 ustar 00 <?php namespace AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; /** * Exception for when a request failed, providing access to the failed request. * * This could be due to an invalid request, or one of the extending exceptions * for network errors or HTTP error responses. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class RequestException extends TransferException { /** * @var RequestInterface */ private $request; /** * @param string $message * @param RequestInterface $request * @param \Exception|null $previous */ public function __construct($message, RequestInterface $request, \Exception $previous = null) { $this->request = $request; parent::__construct($message, 0, $previous); } /** * Returns the request. * * @return RequestInterface */ public function getRequest() { return $this->request; } } httplug/src/Exception/HttpException.php 0000666 00000003606 15165515606 0014312 0 ustar 00 <?php namespace AmeliaHttp\Client\Exception; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Thrown when a response was received but the request itself failed. * * In addition to the request, this exception always provides access to the response object. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ class HttpException extends RequestException { /** * @var ResponseInterface */ protected $response; /** * @param string $message * @param RequestInterface $request * @param ResponseInterface $response * @param \Exception|null $previous */ public function __construct( $message, RequestInterface $request, ResponseInterface $response, \Exception $previous = null ) { parent::__construct($message, $request, $previous); $this->response = $response; $this->code = $response->getStatusCode(); } /** * Returns the response. * * @return ResponseInterface */ public function getResponse() { return $this->response; } /** * Factory method to create a new exception with a normalized error message. * * @param RequestInterface $request * @param ResponseInterface $response * @param \Exception|null $previous * * @return HttpException */ public static function create( RequestInterface $request, ResponseInterface $response, \Exception $previous = null ) { $message = sprintf( '[url] %s [http method] %s [status code] %s [reason phrase] %s', $request->getRequestTarget(), $request->getMethod(), $response->getStatusCode(), $response->getReasonPhrase() ); return new self($message, $request, $response, $previous); } } httplug/src/HttpClient.php 0000666 00000001361 15165515606 0011630 0 ustar 00 <?php namespace AmeliaHttp\Client; use AmeliaPsr\Http\Message\RequestInterface; use AmeliaPsr\Http\Message\ResponseInterface; /** * Sends a PSR-7 Request and returns a PSR-7 response. * * @author GeLo <geloen.eric@gmail.com> * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> * @author David Buchmann <mail@davidbu.ch> */ interface HttpClient { /** * Sends a PSR-7 request. * * @param RequestInterface $request * * @return ResponseInterface * * @throws \Http\Client\Exception If an error happens during processing the request. * @throws \Exception If processing the request is impossible (eg. bad configuration). */ public function sendRequest(RequestInterface $request); } httplug/src/Exception.php 0000666 00000000307 15165515606 0011507 0 ustar 00 <?php namespace AmeliaHttp\Client; /** * Every HTTP Client related Exception must implement this interface. * * @author Márk Sági-Kazár <mark.sagikazar@gmail.com> */ interface Exception { } httplug/src/Promise/HttpRejectedPromise.php 0000666 00000002011 15165515606 0015105 0 ustar 00 <?php namespace AmeliaHttp\Client\Promise; use AmeliaHttp\Client\Exception; use AmeliaHttp\Promise\Promise; final class HttpRejectedPromise implements Promise { /** * @var Exception */ private $exception; /** * @param Exception $exception */ public function __construct(Exception $exception) { $this->exception = $exception; } /** * {@inheritdoc} */ public function then(callable $onFulfilled = null, callable $onRejected = null) { if (null === $onRejected) { return $this; } try { return new HttpFulfilledPromise($onRejected($this->exception)); } catch (Exception $e) { return new self($e); } } /** * {@inheritdoc} */ public function getState() { return Promise::REJECTED; } /** * {@inheritdoc} */ public function wait($unwrap = true) { if ($unwrap) { throw $this->exception; } } } httplug/src/Promise/HttpFulfilledPromise.php 0000666 00000002114 15165515606 0015272 0 ustar 00 <?php namespace AmeliaHttp\Client\Promise; use AmeliaHttp\Client\Exception; use AmeliaHttp\Promise\Promise; use AmeliaPsr\Http\Message\ResponseInterface; final class HttpFulfilledPromise implements Promise { /** * @var ResponseInterface */ private $response; /** * @param ResponseInterface $response */ public function __construct(ResponseInterface $response) { $this->response = $response; } /** * {@inheritdoc} */ public function then(callable $onFulfilled = null, callable $onRejected = null) { if (null === $onFulfilled) { return $this; } try { return new self($onFulfilled($this->response)); } catch (Exception $e) { return new HttpRejectedPromise($e); } } /** * {@inheritdoc} */ public function getState() { return Promise::FULFILLED; } /** * {@inheritdoc} */ public function wait($unwrap = true) { if ($unwrap) { return $this->response; } } } httplug/composer.json 0000666 00000001724 15165515606 0010777 0 ustar 00 { "name": "php-http/httplug", "description": "HTTPlug, the HTTP client abstraction for PHP", "license": "MIT", "keywords": ["http", "client"], "homepage": "http://httplug.io", "authors": [ { "name": "Eric GELOEN", "email": "geloen.eric@gmail.com" }, { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com" } ], "require": { "php": ">=5.4", "psr/http-message": "^1.0", "php-http/promise": "^1.0" }, "require-dev": { "phpspec/phpspec": "^2.4", "henrikbjorn/phpspec-code-coverage" : "^1.0" }, "autoload": { "psr-4": { "Http\\Client\\": "src/" } }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" }, "extra": { "branch-alias": { "dev-master": "1.1-dev" } } } httplug/puli.json 0000666 00000000425 15165515606 0010116 0 ustar 00 { "version": "1.0", "name": "php-http/httplug", "binding-types": { "Http\\Client\\HttpAsyncClient": { "description": "Async HTTP Client" }, "Http\\Client\\HttpClient": { "description": "HTTP Client" } } } httplug/LICENSE 0000666 00000002166 15165515606 0007263 0 ustar 00 Copyright (c) 2014-2015 Eric GELOEN <geloen.eric@gmail.com> Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org> 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. httplug/README.md 0000666 00000003606 15165515606 0007535 0 ustar 00 # HTTPlug [](https://github.com/php-http/httplug/releases) [](LICENSE) [](https://travis-ci.org/php-http/httplug) [](https://scrutinizer-ci.com/g/php-http/httplug) [](https://scrutinizer-ci.com/g/php-http/httplug) [](https://packagist.org/packages/php-http/httplug) [](http://slack.httplug.io) [](mailto:team@httplug.io) **HTTPlug, the HTTP client abstraction for PHP.** ## Install Via Composer ``` bash $ composer require php-http/httplug ``` ## Intro This is the contract package for HTTP Client. Use it to create HTTP Clients which are interoperable and compatible with [PSR-7](http://www.php-fig.org/psr/psr-7/). This library is the official successor of the [ivory http adapter](https://github.com/egeloen/ivory-http-adapter). ## Documentation Please see the [official documentation](http://docs.php-http.org). ## Testing ``` bash $ composer test ``` ## Contributing Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). ## Security If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org). ## License The MIT License (MIT). Please see [License File](LICENSE) for more information.
| ver. 1.4 |
Github
|
.
| PHP 5.4.45 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings