.travis.yml000064400000002604151742725260006672 0ustar00language: php php: - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 - hhvm - nightly env: global: - TEST_COMMAND="composer test" before_script: - curl --version - pear config-set php_ini ~/.phpenv/versions/`php -r 'echo phpversion();'`/etc/php.ini || echo 'Error modifying PEAR' - pecl install uri_template || echo 'Error installing uri_template' # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction - ~/.nvm/nvm.sh install v0.6.14 - ~/.nvm/nvm.sh run v0.6.14 script: $TEST_COMMAND matrix: allow_failures: - php: hhvm - php: nightly fast_finish: true include: - php: 5.4 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" before_deploy: - make package deploy: provider: releases api_key: secure: UpypqlYgsU68QT/x40YzhHXvzWjFwCNo9d+G8KAdm7U9+blFfcWhV1aMdzugvPMl6woXgvJj7qHq5tAL4v6oswCORhpSBfLgOQVFaica5LiHsvWlAedOhxGmnJqMTwuepjBCxXhs3+I8Kof1n4oUL9gKytXjOVCX/f7XU1HiinU= file: - build/artifacts/guzzle.phar - build/artifacts/guzzle.zip on: repo: guzzle/guzzle tags: true all_branches: true php: 5.4 CHANGELOG.md000064400000175154151742725260006405 0ustar00# CHANGELOG ## 5.3.3 - 2018-07-31 * Fix bug parsing 0 epoch expiry times * Fix PHP 7.3 compatibility ## 5.3.2 - 2018-01-15 * Improve tests * Fix react promise dependency * Fix PHP 7 compatibility ## 5.3.1 - 2016-07-18 * Address HTTP_PROXY security vulnerability, CVE-2016-5385: https://httpoxy.org/ * Event name fix: https://github.com/guzzle/guzzle/commit/fcae91ff31de41e312fe113ec3acbcda31b2622e * Response header case sensitivity fix: https://github.com/guzzle/guzzle/commit/043eeadf20ee40ddc6712faee4d3957a91f2b041 ## 5.3.0 - 2015-05-19 * Mock now supports `save_to` * Marked `AbstractRequestEvent::getTransaction()` as public. * Fixed a bug in which multiple headers using different casing would overwrite previous headers in the associative array. * Added `Utils::getDefaultHandler()` * Marked `GuzzleHttp\Client::getDefaultUserAgent` as deprecated. * URL scheme is now always lowercased. ## 5.2.0 - 2015-01-27 * Added `AppliesHeadersInterface` to make applying headers to a request based on the body more generic and not specific to `PostBodyInterface`. * Reduced the number of stack frames needed to send requests. * Nested futures are now resolved in the client rather than the RequestFsm * Finishing state transitions is now handled in the RequestFsm rather than the RingBridge. * Added a guard in the Pool class to not use recursion for request retries. ## 5.1.0 - 2014-12-19 * Pool class no longer uses recursion when a request is intercepted. * The size of a Pool can now be dynamically adjusted using a callback. See https://github.com/guzzle/guzzle/pull/943. * Setting a request option to `null` when creating a request with a client will ensure that the option is not set. This allows you to overwrite default request options on a per-request basis. See https://github.com/guzzle/guzzle/pull/937. * Added the ability to limit which protocols are allowed for redirects by specifying a `protocols` array in the `allow_redirects` request option. * Nested futures due to retries are now resolved when waiting for synchronous responses. See https://github.com/guzzle/guzzle/pull/947. * `"0"` is now an allowed URI path. See https://github.com/guzzle/guzzle/pull/935. * `Query` no longer typehints on the `$query` argument in the constructor, allowing for strings and arrays. * Exceptions thrown in the `end` event are now correctly wrapped with Guzzle specific exceptions if necessary. ## 5.0.3 - 2014-11-03 This change updates query strings so that they are treated as un-encoded values by default where the value represents an un-encoded value to send over the wire. A Query object then encodes the value before sending over the wire. This means that even value query string values (e.g., ":") are url encoded. This makes the Query class match PHP's http_build_query function. However, if you want to send requests over the wire using valid query string characters that do not need to be encoded, then you can provide a string to Url::setQuery() and pass true as the second argument to specify that the query string is a raw string that should not be parsed or encoded (unless a call to getQuery() is subsequently made, forcing the query-string to be converted into a Query object). ## 5.0.2 - 2014-10-30 * Added a trailing `\r\n` to multipart/form-data payloads. See https://github.com/guzzle/guzzle/pull/871 * Added a `GuzzleHttp\Pool::send()` convenience method to match the docs. * Status codes are now returned as integers. See https://github.com/guzzle/guzzle/issues/881 * No longer overwriting an existing `application/x-www-form-urlencoded` header when sending POST requests, allowing for customized headers. See https://github.com/guzzle/guzzle/issues/877 * Improved path URL serialization. * No longer double percent-encoding characters in the path or query string if they are already encoded. * Now properly encoding the supplied path to a URL object, instead of only encoding ' ' and '?'. * Note: This has been changed in 5.0.3 to now encode query string values by default unless the `rawString` argument is provided when setting the query string on a URL: Now allowing many more characters to be present in the query string without being percent encoded. See http://tools.ietf.org/html/rfc3986#appendix-A ## 5.0.1 - 2014-10-16 Bugfix release. * Fixed an issue where connection errors still returned response object in error and end events event though the response is unusable. This has been corrected so that a response is not returned in the `getResponse` method of these events if the response did not complete. https://github.com/guzzle/guzzle/issues/867 * Fixed an issue where transfer statistics were not being populated in the RingBridge. https://github.com/guzzle/guzzle/issues/866 ## 5.0.0 - 2014-10-12 Adding support for non-blocking responses and some minor API cleanup. ### New Features * Added support for non-blocking responses based on `guzzlehttp/guzzle-ring`. * Added a public API for creating a default HTTP adapter. * Updated the redirect plugin to be non-blocking so that redirects are sent concurrently. Other plugins like this can now be updated to be non-blocking. * Added a "progress" event so that you can get upload and download progress events. * Added `GuzzleHttp\Pool` which implements FutureInterface and transfers requests concurrently using a capped pool size as efficiently as possible. * Added `hasListeners()` to EmitterInterface. * Removed `GuzzleHttp\ClientInterface::sendAll` and marked `GuzzleHttp\Client::sendAll` as deprecated (it's still there, just not the recommended way). ### Breaking changes The breaking changes in this release are relatively minor. The biggest thing to look out for is that request and response objects no longer implement fluent interfaces. * Removed the fluent interfaces (i.e., `return $this`) from requests, responses, `GuzzleHttp\Collection`, `GuzzleHttp\Url`, `GuzzleHttp\Query`, `GuzzleHttp\Post\PostBody`, and `GuzzleHttp\Cookie\SetCookie`. This blog post provides a good outline of why I did this: http://ocramius.github.io/blog/fluent-interfaces-are-evil/. This also makes the Guzzle message interfaces compatible with the current PSR-7 message proposal. * Removed "functions.php", so that Guzzle is truly PSR-4 compliant. Except for the HTTP request functions from function.php, these functions are now implemented in `GuzzleHttp\Utils` using camelCase. `GuzzleHttp\json_decode` moved to `GuzzleHttp\Utils::jsonDecode`. `GuzzleHttp\get_path` moved to `GuzzleHttp\Utils::getPath`. `GuzzleHttp\set_path` moved to `GuzzleHttp\Utils::setPath`. `GuzzleHttp\batch` should now be `GuzzleHttp\Pool::batch`, which returns an `objectStorage`. Using functions.php caused problems for many users: they aren't PSR-4 compliant, require an explicit include, and needed an if-guard to ensure that the functions are not declared multiple times. * Rewrote adapter layer. * Removing all classes from `GuzzleHttp\Adapter`, these are now implemented as callables that are stored in `GuzzleHttp\Ring\Client`. * Removed the concept of "parallel adapters". Sending requests serially or concurrently is now handled using a single adapter. * Moved `GuzzleHttp\Adapter\Transaction` to `GuzzleHttp\Transaction`. The Transaction object now exposes the request, response, and client as public properties. The getters and setters have been removed. * Removed the "headers" event. This event was only useful for changing the body a response once the headers of the response were known. You can implement a similar behavior in a number of ways. One example might be to use a FnStream that has access to the transaction being sent. For example, when the first byte is written, you could check if the response headers match your expectations, and if so, change the actual stream body that is being written to. * Removed the `asArray` parameter from `GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header value as an array, then use the newly added `getHeaderAsArray()` method of `MessageInterface`. This change makes the Guzzle interfaces compatible with the PSR-7 interfaces. * `GuzzleHttp\Message\MessageFactory` no longer allows subclasses to add custom request options using double-dispatch (this was an implementation detail). Instead, you should now provide an associative array to the constructor which is a mapping of the request option name mapping to a function that applies the option value to a request. * Removed the concept of "throwImmediately" from exceptions and error events. This control mechanism was used to stop a transfer of concurrent requests from completing. This can now be handled by throwing the exception or by cancelling a pool of requests or each outstanding future request individually. * Updated to "GuzzleHttp\Streams" 3.0. * `GuzzleHttp\Stream\StreamInterface::getContents()` no longer accepts a `maxLen` parameter. This update makes the Guzzle streams project compatible with the current PSR-7 proposal. * `GuzzleHttp\Stream\Stream::__construct`, `GuzzleHttp\Stream\Stream::factory`, and `GuzzleHttp\Stream\Utils::create` no longer accept a size in the second argument. They now accept an associative array of options, including the "size" key and "metadata" key which can be used to provide custom metadata. ## 4.2.2 - 2014-09-08 * Fixed a memory leak in the CurlAdapter when reusing cURL handles. * No longer using `request_fulluri` in stream adapter proxies. * Relative redirects are now based on the last response, not the first response. ## 4.2.1 - 2014-08-19 * Ensuring that the StreamAdapter does not always add a Content-Type header * Adding automated github releases with a phar and zip ## 4.2.0 - 2014-08-17 * Now merging in default options using a case-insensitive comparison. Closes https://github.com/guzzle/guzzle/issues/767 * Added the ability to automatically decode `Content-Encoding` response bodies using the `decode_content` request option. This is set to `true` by default to decode the response body if it comes over the wire with a `Content-Encoding`. Set this value to `false` to disable decoding the response content, and pass a string to provide a request `Accept-Encoding` header and turn on automatic response decoding. This feature now allows you to pass an `Accept-Encoding` header in the headers of a request but still disable automatic response decoding. Closes https://github.com/guzzle/guzzle/issues/764 * Added the ability to throw an exception immediately when transferring requests in parallel. Closes https://github.com/guzzle/guzzle/issues/760 * Updating guzzlehttp/streams dependency to ~2.1 * No longer utilizing the now deprecated namespaced methods from the stream package. ## 4.1.8 - 2014-08-14 * Fixed an issue in the CurlFactory that caused setting the `stream=false` request option to throw an exception. See: https://github.com/guzzle/guzzle/issues/769 * TransactionIterator now calls rewind on the inner iterator. See: https://github.com/guzzle/guzzle/pull/765 * You can now set the `Content-Type` header to `multipart/form-data` when creating POST requests to force multipart bodies. See https://github.com/guzzle/guzzle/issues/768 ## 4.1.7 - 2014-08-07 * Fixed an error in the HistoryPlugin that caused the same request and response to be logged multiple times when an HTTP protocol error occurs. * Ensuring that cURL does not add a default Content-Type when no Content-Type has been supplied by the user. This prevents the adapter layer from modifying the request that is sent over the wire after any listeners may have already put the request in a desired state (e.g., signed the request). * Throwing an exception when you attempt to send requests that have the "stream" set to true in parallel using the MultiAdapter. * Only calling curl_multi_select when there are active cURL handles. This was previously changed and caused performance problems on some systems due to PHP always selecting until the maximum select timeout. * Fixed a bug where multipart/form-data POST fields were not correctly aggregated (e.g., values with "&"). ## 4.1.6 - 2014-08-03 * Added helper methods to make it easier to represent messages as strings, including getting the start line and getting headers as a string. ## 4.1.5 - 2014-08-02 * Automatically retrying cURL "Connection died, retrying a fresh connect" errors when possible. * cURL implementation cleanup * Allowing multiple event subscriber listeners to be registered per event by passing an array of arrays of listener configuration. ## 4.1.4 - 2014-07-22 * Fixed a bug that caused multi-part POST requests with more than one field to serialize incorrectly. * Paths can now be set to "0" * `ResponseInterface::xml` now accepts a `libxml_options` option and added a missing default argument that was required when parsing XML response bodies. * A `save_to` stream is now created lazily, which means that files are not created on disk unless a request succeeds. ## 4.1.3 - 2014-07-15 * Various fixes to multipart/form-data POST uploads * Wrapping function.php in an if-statement to ensure Guzzle can be used globally and in a Composer install * Fixed an issue with generating and merging in events to an event array * POST headers are only applied before sending a request to allow you to change the query aggregator used before uploading * Added much more robust query string parsing * Fixed various parsing and normalization issues with URLs * Fixing an issue where multi-valued headers were not being utilized correctly in the StreamAdapter ## 4.1.2 - 2014-06-18 * Added support for sending payloads with GET requests ## 4.1.1 - 2014-06-08 * Fixed an issue related to using custom message factory options in subclasses * Fixed an issue with nested form fields in a multi-part POST * Fixed an issue with using the `json` request option for POST requests * Added `ToArrayInterface` to `GuzzleHttp\Cookie\CookieJar` ## 4.1.0 - 2014-05-27 * Added a `json` request option to easily serialize JSON payloads. * Added a `GuzzleHttp\json_decode()` wrapper to safely parse JSON. * Added `setPort()` and `getPort()` to `GuzzleHttp\Message\RequestInterface`. * Added the ability to provide an emitter to a client in the client constructor. * Added the ability to persist a cookie session using $_SESSION. * Added a trait that can be used to add event listeners to an iterator. * Removed request method constants from RequestInterface. * Fixed warning when invalid request start-lines are received. * Updated MessageFactory to work with custom request option methods. * Updated cacert bundle to latest build. 4.0.2 (2014-04-16) ------------------ * Proxy requests using the StreamAdapter now properly use request_fulluri (#632) * Added the ability to set scalars as POST fields (#628) ## 4.0.1 - 2014-04-04 * The HTTP status code of a response is now set as the exception code of RequestException objects. * 303 redirects will now correctly switch from POST to GET requests. * The default parallel adapter of a client now correctly uses the MultiAdapter. * HasDataTrait now initializes the internal data array as an empty array so that the toArray() method always returns an array. ## 4.0.0 - 2014-03-29 * For more information on the 4.0 transition, see: http://mtdowling.com/blog/2014/03/15/guzzle-4-rc/ * For information on changes and upgrading, see: https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 * Added `GuzzleHttp\batch()` as a convenience function for sending requests in parallel without needing to write asynchronous code. * Restructured how events are added to `GuzzleHttp\ClientInterface::sendAll()`. You can now pass a callable or an array of associative arrays where each associative array contains the "fn", "priority", and "once" keys. ## 4.0.0.rc-2 - 2014-03-25 * Removed `getConfig()` and `setConfig()` from clients to avoid confusion around whether things like base_url, message_factory, etc. should be able to be retrieved or modified. * Added `getDefaultOption()` and `setDefaultOption()` to ClientInterface * functions.php functions were renamed using snake_case to match PHP idioms * Added support for `HTTP_PROXY`, `HTTPS_PROXY`, and `GUZZLE_CURL_SELECT_TIMEOUT` environment variables * Added the ability to specify custom `sendAll()` event priorities * Added the ability to specify custom stream context options to the stream adapter. * Added a functions.php function for `get_path()` and `set_path()` * CurlAdapter and MultiAdapter now use a callable to generate curl resources * MockAdapter now properly reads a body and emits a `headers` event * Updated Url class to check if a scheme and host are set before adding ":" and "//". This allows empty Url (e.g., "") to be serialized as "". * Parsing invalid XML no longer emits warnings * Curl classes now properly throw AdapterExceptions * Various performance optimizations * Streams are created with the faster `Stream\create()` function * Marked deprecation_proxy() as internal * Test server is now a collection of static methods on a class ## 4.0.0-rc.1 - 2014-03-15 * See https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 ## 3.8.1 - 2014-01-28 * Bug: Always using GET requests when redirecting from a 303 response * Bug: CURLOPT_SSL_VERIFYHOST is now correctly set to false when setting `$certificateAuthority` to false in `Guzzle\Http\ClientInterface::setSslVerification()` * Bug: RedirectPlugin now uses strict RFC 3986 compliance when combining a base URL with a relative URL * Bug: The body of a request can now be set to `"0"` * Sending PHP stream requests no longer forces `HTTP/1.0` * Adding more information to ExceptionCollection exceptions so that users have more context, including a stack trace of each sub-exception * Updated the `$ref` attribute in service descriptions to merge over any existing parameters of a schema (rather than clobbering everything). * Merging URLs will now use the query string object from the relative URL (thus allowing custom query aggregators) * Query strings are now parsed in a way that they do no convert empty keys with no value to have a dangling `=`. For example `foo&bar=baz` is now correctly parsed and recognized as `foo&bar=baz` rather than `foo=&bar=baz`. * Now properly escaping the regular expression delimiter when matching Cookie domains. * Network access is now disabled when loading XML documents ## 3.8.0 - 2013-12-05 * Added the ability to define a POST name for a file * JSON response parsing now properly walks additionalProperties * cURL error code 18 is now retried automatically in the BackoffPlugin * Fixed a cURL error when URLs contain fragments * Fixed an issue in the BackoffPlugin retry event where it was trying to access all exceptions as if they were CurlExceptions * CURLOPT_PROGRESS function fix for PHP 5.5 (69fcc1e) * Added the ability for Guzzle to work with older versions of cURL that do not support `CURLOPT_TIMEOUT_MS` * Fixed a bug that was encountered when parsing empty header parameters * UriTemplate now has a `setRegex()` method to match the docs * The `debug` request parameter now checks if it is truthy rather than if it exists * Setting the `debug` request parameter to true shows verbose cURL output instead of using the LogPlugin * Added the ability to combine URLs using strict RFC 3986 compliance * Command objects can now return the validation errors encountered by the command * Various fixes to cache revalidation (#437 and 29797e5) * Various fixes to the AsyncPlugin * Cleaned up build scripts ## 3.7.4 - 2013-10-02 * Bug fix: 0 is now an allowed value in a description parameter that has a default value (#430) * Bug fix: SchemaFormatter now returns an integer when formatting to a Unix timestamp (see https://github.com/aws/aws-sdk-php/issues/147) * Bug fix: Cleaned up and fixed URL dot segment removal to properly resolve internal dots * Minimum PHP version is now properly specified as 5.3.3 (up from 5.3.2) (#420) * Updated the bundled cacert.pem (#419) * OauthPlugin now supports adding authentication to headers or query string (#425) ## 3.7.3 - 2013-09-08 * Added the ability to get the exception associated with a request/command when using `MultiTransferException` and `CommandTransferException`. * Setting `additionalParameters` of a response to false is now honored when parsing responses with a service description * Schemas are only injected into response models when explicitly configured. * No longer guessing Content-Type based on the path of a request. Content-Type is now only guessed based on the path of an EntityBody. * Bug fix: ChunkedIterator can now properly chunk a \Traversable as well as an \Iterator. * Bug fix: FilterIterator now relies on `\Iterator` instead of `\Traversable`. * Bug fix: Gracefully handling malformed responses in RequestMediator::writeResponseBody() * Bug fix: Replaced call to canCache with canCacheRequest in the CallbackCanCacheStrategy of the CachePlugin * Bug fix: Visiting XML attributes first before visiting XML children when serializing requests * Bug fix: Properly parsing headers that contain commas contained in quotes * Bug fix: mimetype guessing based on a filename is now case-insensitive ## 3.7.2 - 2013-08-02 * Bug fix: Properly URL encoding paths when using the PHP-only version of the UriTemplate expander See https://github.com/guzzle/guzzle/issues/371 * Bug fix: Cookie domains are now matched correctly according to RFC 6265 See https://github.com/guzzle/guzzle/issues/377 * Bug fix: GET parameters are now used when calculating an OAuth signature * Bug fix: Fixed an issue with cache revalidation where the If-None-Match header was being double quoted * `Guzzle\Common\AbstractHasDispatcher::dispatch()` now returns the event that was dispatched * `Guzzle\Http\QueryString::factory()` now guesses the most appropriate query aggregator to used based on the input. See https://github.com/guzzle/guzzle/issues/379 * Added a way to add custom domain objects to service description parsing using the `operation.parse_class` event. See https://github.com/guzzle/guzzle/pull/380 * cURL multi cleanup and optimizations ## 3.7.1 - 2013-07-05 * Bug fix: Setting default options on a client now works * Bug fix: Setting options on HEAD requests now works. See #352 * Bug fix: Moving stream factory before send event to before building the stream. See #353 * Bug fix: Cookies no longer match on IP addresses per RFC 6265 * Bug fix: Correctly parsing header parameters that are in `<>` and quotes * Added `cert` and `ssl_key` as request options * `Host` header can now diverge from the host part of a URL if the header is set manually * `Guzzle\Service\Command\LocationVisitor\Request\XmlVisitor` was rewritten to change from using SimpleXML to XMLWriter * OAuth parameters are only added via the plugin if they aren't already set * Exceptions are now thrown when a URL cannot be parsed * Returning `false` if `Guzzle\Http\EntityBody::getContentMd5()` fails * Not setting a `Content-MD5` on a command if calculating the Content-MD5 fails via the CommandContentMd5Plugin ## 3.7.0 - 2013-06-10 * See UPGRADING.md for more information on how to upgrade. * Requests now support the ability to specify an array of $options when creating a request to more easily modify a request. You can pass a 'request.options' configuration setting to a client to apply default request options to every request created by a client (e.g. default query string variables, headers, curl options, etc.). * Added a static facade class that allows you to use Guzzle with static methods and mount the class to `\Guzzle`. See `Guzzle\Http\StaticClient::mount`. * Added `command.request_options` to `Guzzle\Service\Command\AbstractCommand` to pass request options to requests created by a command (e.g. custom headers, query string variables, timeout settings, etc.). * Stream size in `Guzzle\Stream\PhpStreamRequestFactory` will now be set if Content-Length is returned in the headers of a response * Added `Guzzle\Common\Collection::setPath($path, $value)` to set a value into an array using a nested key (e.g. `$collection->setPath('foo/baz/bar', 'test'); echo $collection['foo']['bar']['bar'];`) * ServiceBuilders now support storing and retrieving arbitrary data * CachePlugin can now purge all resources for a given URI * CachePlugin can automatically purge matching cached items when a non-idempotent request is sent to a resource * CachePlugin now uses the Vary header to determine if a resource is a cache hit * `Guzzle\Http\Message\Response` now implements `\Serializable` * Added `Guzzle\Cache\CacheAdapterFactory::fromCache()` to more easily create cache adapters * `Guzzle\Service\ClientInterface::execute()` now accepts an array, single command, or Traversable * Fixed a bug in `Guzzle\Http\Message\Header\Link::addLink()` * Better handling of calculating the size of a stream in `Guzzle\Stream\Stream` using fstat() and caching the size * `Guzzle\Common\Exception\ExceptionCollection` now creates a more readable exception message * Fixing BC break: Added back the MonologLogAdapter implementation rather than extending from PsrLog so that older Symfony users can still use the old version of Monolog. * Fixing BC break: Added the implementation back in for `Guzzle\Http\Message\AbstractMessage::getTokenizedHeader()`. Now triggering an E_USER_DEPRECATED warning when used. Use `$message->getHeader()->parseParams()`. * Several performance improvements to `Guzzle\Common\Collection` * Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: createRequest, head, delete, put, patch, post, options, prepareRequest * Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` * Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` * Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a resource, string, or EntityBody into the $options parameter to specify the download location of the response. * Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a default `array()` * Added `Guzzle\Stream\StreamInterface::isRepeatable` * Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or $client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))`. * Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use $client->getConfig()->getPath('request.options/headers')`. * Removed `Guzzle\Http\ClientInterface::expandTemplate()` * Removed `Guzzle\Http\ClientInterface::setRequestFactory()` * Removed `Guzzle\Http\ClientInterface::getCurlMulti()` * Removed `Guzzle\Http\Message\RequestInterface::canCache` * Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect` * Removed `Guzzle\Http\Message\RequestInterface::isRedirect` * Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. * You can now enable E_USER_DEPRECATED warnings to see if you are using a deprecated method by setting `Guzzle\Common\Version::$emitWarnings` to true. * Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use `$request->getResponseBody()->isRepeatable()` instead. * Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. * Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. * Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. * Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. * Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated * Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0 * Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use [request.options][params]. * Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. * Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use $client->getConfig()->getPath('request.options/headers')`. * Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. * Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. * Marked `Guzzle\Common\Collection::inject()` as deprecated. * Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest');` * CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a CacheStorageInterface. These two objects and interface will be removed in a future version. * Always setting X-cache headers on cached responses * Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin * `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface $request, Response $response);` * `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` * `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` * Added `CacheStorageInterface::purge($url)` * `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, CanCacheStrategyInterface $canCache = null)` * Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` ## 3.6.0 - 2013-05-29 * ServiceDescription now implements ToArrayInterface * Added command.hidden_params to blacklist certain headers from being treated as additionalParameters * Guzzle can now correctly parse incomplete URLs * Mixed casing of headers are now forced to be a single consistent casing across all values for that header. * Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution * Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). * Specific header implementations can be created for complex headers. When a message creates a header, it uses a HeaderFactory which can map specific headers to specific header classes. There is now a Link header and CacheControl header implementation. * Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate * Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() * Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in Guzzle\Http\Curl\RequestMediator * Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. * Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface * Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() * Removed Guzzle\Parser\ParserRegister::get(). Use getParser() * Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). * All response header helper functions return a string rather than mixing Header objects and strings inconsistently * Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle directly via interfaces * Removed the injecting of a request object onto a response object. The methods to get and set a request still exist but are a no-op until removed. * Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a `Guzzle\Service\Command\ArrayCommandInterface`. * Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response on a request while the request is still being transferred * The ability to case-insensitively search for header values * Guzzle\Http\Message\Header::hasExactHeader * Guzzle\Http\Message\Header::raw. Use getAll() * Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object instead. * `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess * Added the ability to cast Model objects to a string to view debug information. ## 3.5.0 - 2013-05-13 * Bug: Fixed a regression so that request responses are parsed only once per oncomplete event rather than multiple times * Bug: Better cleanup of one-time events across the board (when an event is meant to fire once, it will now remove itself from the EventDispatcher) * Bug: `Guzzle\Log\MessageFormatter` now properly writes "total_time" and "connect_time" values * Bug: Cloning an EntityEnclosingRequest now clones the EntityBody too * Bug: Fixed an undefined index error when parsing nested JSON responses with a sentAs parameter that reference a non-existent key * Bug: All __call() method arguments are now required (helps with mocking frameworks) * Deprecating Response::getRequest() and now using a shallow clone of a request object to remove a circular reference to help with refcount based garbage collection of resources created by sending a request * Deprecating ZF1 cache and log adapters. These will be removed in the next major version. * Deprecating `Response::getPreviousResponse()` (method signature still exists, but it'sdeprecated). Use the HistoryPlugin for a history. * Added a `responseBody` alias for the `response_body` location * Refactored internals to no longer rely on Response::getRequest() * HistoryPlugin can now be cast to a string * HistoryPlugin now logs transactions rather than requests and responses to more accurately keep track of the requests and responses that are sent over the wire * Added `getEffectiveUrl()` and `getRedirectCount()` to Response objects ## 3.4.3 - 2013-04-30 * Bug fix: Fixing bug introduced in 3.4.2 where redirect responses are duplicated on the final redirected response * Added a check to re-extract the temp cacert bundle from the phar before sending each request ## 3.4.2 - 2013-04-29 * Bug fix: Stream objects now work correctly with "a" and "a+" modes * Bug fix: Removing `Transfer-Encoding: chunked` header when a Content-Length is present * Bug fix: AsyncPlugin no longer forces HEAD requests * Bug fix: DateTime timezones are now properly handled when using the service description schema formatter * Bug fix: CachePlugin now properly handles stale-if-error directives when a request to the origin server fails * Setting a response on a request will write to the custom request body from the response body if one is specified * LogPlugin now writes to php://output when STDERR is undefined * Added the ability to set multiple POST files for the same key in a single call * application/x-www-form-urlencoded POSTs now use the utf-8 charset by default * Added the ability to queue CurlExceptions to the MockPlugin * Cleaned up how manual responses are queued on requests (removed "queued_response" and now using request.before_send) * Configuration loading now allows remote files ## 3.4.1 - 2013-04-16 * Large refactoring to how CurlMulti handles work. There is now a proxy that sits in front of a pool of CurlMulti handles. This greatly simplifies the implementation, fixes a couple bugs, and provides a small performance boost. * Exceptions are now properly grouped when sending requests in parallel * Redirects are now properly aggregated when a multi transaction fails * Redirects now set the response on the original object even in the event of a failure * Bug fix: Model names are now properly set even when using $refs * Added support for PHP 5.5's CurlFile to prevent warnings with the deprecated @ syntax * Added support for oauth_callback in OAuth signatures * Added support for oauth_verifier in OAuth signatures * Added support to attempt to retrieve a command first literally, then ucfirst, the with inflection ## 3.4.0 - 2013-04-11 * Bug fix: URLs are now resolved correctly based on http://tools.ietf.org/html/rfc3986#section-5.2. #289 * Bug fix: Absolute URLs with a path in a service description will now properly override the base URL. #289 * Bug fix: Parsing a query string with a single PHP array value will now result in an array. #263 * Bug fix: Better normalization of the User-Agent header to prevent duplicate headers. #264. * Bug fix: Added `number` type to service descriptions. * Bug fix: empty parameters are removed from an OAuth signature * Bug fix: Revalidating a cache entry prefers the Last-Modified over the Date header * Bug fix: Fixed "array to string" error when validating a union of types in a service description * Bug fix: Removed code that attempted to determine the size of a stream when data is written to the stream * Bug fix: Not including an `oauth_token` if the value is null in the OauthPlugin. * Bug fix: Now correctly aggregating successful requests and failed requests in CurlMulti when a redirect occurs. * The new default CURLOPT_TIMEOUT setting has been increased to 150 seconds so that Guzzle works on poor connections. * Added a feature to EntityEnclosingRequest::setBody() that will automatically set the Content-Type of the request if the Content-Type can be determined based on the entity body or the path of the request. * Added the ability to overwrite configuration settings in a client when grabbing a throwaway client from a builder. * Added support for a PSR-3 LogAdapter. * Added a `command.after_prepare` event * Added `oauth_callback` parameter to the OauthPlugin * Added the ability to create a custom stream class when using a stream factory * Added a CachingEntityBody decorator * Added support for `additionalParameters` in service descriptions to define how custom parameters are serialized. * The bundled SSL certificate is now provided in the phar file and extracted when running Guzzle from a phar. * You can now send any EntityEnclosingRequest with POST fields or POST files and cURL will handle creating bodies * POST requests using a custom entity body are now treated exactly like PUT requests but with a custom cURL method. This means that the redirect behavior of POST requests with custom bodies will not be the same as POST requests that use POST fields or files (the latter is only used when emulating a form POST in the browser). * Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest ## 3.3.1 - 2013-03-10 * Added the ability to create PHP streaming responses from HTTP requests * Bug fix: Running any filters when parsing response headers with service descriptions * Bug fix: OauthPlugin fixes to allow for multi-dimensional array signing, and sorting parameters before signing * Bug fix: Removed the adding of default empty arrays and false Booleans to responses in order to be consistent across response location visitors. * Bug fix: Removed the possibility of creating configuration files with circular dependencies * RequestFactory::create() now uses the key of a POST file when setting the POST file name * Added xmlAllowEmpty to serialize an XML body even if no XML specific parameters are set ## 3.3.0 - 2013-03-03 * A large number of performance optimizations have been made * Bug fix: Added 'wb' as a valid write mode for streams * Bug fix: `Guzzle\Http\Message\Response::json()` now allows scalar values to be returned * Bug fix: Fixed bug in `Guzzle\Http\Message\Response` where wrapping quotes were stripped from `getEtag()` * BC: Removed `Guzzle\Http\Utils` class * BC: Setting a service description on a client will no longer modify the client's command factories. * BC: Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' * BC: `Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getSteamType()` are no longer converted to lowercase * Operation parameter objects are now lazy loaded internally * Added ErrorResponsePlugin that can throw errors for responses defined in service description operations' errorResponses * Added support for instantiating responseType=class responseClass classes. Classes must implement `Guzzle\Service\Command\ResponseClassInterface` * Added support for additionalProperties for top-level parameters in responseType=model responseClasses. These additional properties also support locations and can be used to parse JSON responses where the outermost part of the JSON is an array * Added support for nested renaming of JSON models (rename sentAs to name) * CachePlugin * Added support for stale-if-error so that the CachePlugin can now serve stale content from the cache on error * Debug headers can now added to cached response in the CachePlugin ## 3.2.0 - 2013-02-14 * CurlMulti is no longer reused globally. A new multi object is created per-client. This helps to isolate clients. * URLs with no path no longer contain a "/" by default * Guzzle\Http\QueryString does no longer manages the leading "?". This is now handled in Guzzle\Http\Url. * BadResponseException no longer includes the full request and response message * Adding setData() to Guzzle\Service\Description\ServiceDescriptionInterface * Adding getResponseBody() to Guzzle\Http\Message\RequestInterface * Various updates to classes to use ServiceDescriptionInterface type hints rather than ServiceDescription * Header values can now be normalized into distinct values when multiple headers are combined with a comma separated list * xmlEncoding can now be customized for the XML declaration of a XML service description operation * Guzzle\Http\QueryString now uses Guzzle\Http\QueryAggregator\QueryAggregatorInterface objects to add custom value aggregation and no longer uses callbacks * The URL encoding implementation of Guzzle\Http\QueryString can now be customized * Bug fix: Filters were not always invoked for array service description parameters * Bug fix: Redirects now use a target response body rather than a temporary response body * Bug fix: The default exponential backoff BackoffPlugin was not giving when the request threshold was exceeded * Bug fix: Guzzle now takes the first found value when grabbing Cache-Control directives ## 3.1.2 - 2013-01-27 * Refactored how operation responses are parsed. Visitors now include a before() method responsible for parsing the response body. For example, the XmlVisitor now parses the XML response into an array in the before() method. * Fixed an issue where cURL would not automatically decompress responses when the Accept-Encoding header was sent * CURLOPT_SSL_VERIFYHOST is never set to 1 because it is deprecated (see 5e0ff2ef20f839e19d1eeb298f90ba3598784444) * Fixed a bug where redirect responses were not chained correctly using getPreviousResponse() * Setting default headers on a client after setting the user-agent will not erase the user-agent setting ## 3.1.1 - 2013-01-20 * Adding wildcard support to Guzzle\Common\Collection::getPath() * Adding alias support to ServiceBuilder configs * Adding Guzzle\Service\Resource\CompositeResourceIteratorFactory and cleaning up factory interface ## 3.1.0 - 2013-01-12 * BC: CurlException now extends from RequestException rather than BadResponseException * BC: Renamed Guzzle\Plugin\Cache\CanCacheStrategyInterface::canCache() to canCacheRequest() and added CanCacheResponse() * Added getData to ServiceDescriptionInterface * Added context array to RequestInterface::setState() * Bug: Removing hard dependency on the BackoffPlugin from Guzzle\Http * Bug: Adding required content-type when JSON request visitor adds JSON to a command * Bug: Fixing the serialization of a service description with custom data * Made it easier to deal with exceptions thrown when transferring commands or requests in parallel by providing an array of successful and failed responses * Moved getPath from Guzzle\Service\Resource\Model to Guzzle\Common\Collection * Added Guzzle\Http\IoEmittingEntityBody * Moved command filtration from validators to location visitors * Added `extends` attributes to service description parameters * Added getModels to ServiceDescriptionInterface ## 3.0.7 - 2012-12-19 * Fixing phar detection when forcing a cacert to system if null or true * Allowing filename to be passed to `Guzzle\Http\Message\Request::setResponseBody()` * Cleaning up `Guzzle\Common\Collection::inject` method * Adding a response_body location to service descriptions ## 3.0.6 - 2012-12-09 * CurlMulti performance improvements * Adding setErrorResponses() to Operation * composer.json tweaks ## 3.0.5 - 2012-11-18 * Bug: Fixing an infinite recursion bug caused from revalidating with the CachePlugin * Bug: Response body can now be a string containing "0" * Bug: Using Guzzle inside of a phar uses system by default but now allows for a custom cacert * Bug: QueryString::fromString now properly parses query string parameters that contain equal signs * Added support for XML attributes in service description responses * DefaultRequestSerializer now supports array URI parameter values for URI template expansion * Added better mimetype guessing to requests and post files ## 3.0.4 - 2012-11-11 * Bug: Fixed a bug when adding multiple cookies to a request to use the correct glue value * Bug: Cookies can now be added that have a name, domain, or value set to "0" * Bug: Using the system cacert bundle when using the Phar * Added json and xml methods to Response to make it easier to parse JSON and XML response data into data structures * Enhanced cookie jar de-duplication * Added the ability to enable strict cookie jars that throw exceptions when invalid cookies are added * Added setStream to StreamInterface to actually make it possible to implement custom rewind behavior for entity bodies * Added the ability to create any sort of hash for a stream rather than just an MD5 hash ## 3.0.3 - 2012-11-04 * Implementing redirects in PHP rather than cURL * Added PECL URI template extension and using as default parser if available * Bug: Fixed Content-Length parsing of Response factory * Adding rewind() method to entity bodies and streams. Allows for custom rewinding of non-repeatable streams. * Adding ToArrayInterface throughout library * Fixing OauthPlugin to create unique nonce values per request ## 3.0.2 - 2012-10-25 * Magic methods are enabled by default on clients * Magic methods return the result of a command * Service clients no longer require a base_url option in the factory * Bug: Fixed an issue with URI templates where null template variables were being expanded ## 3.0.1 - 2012-10-22 * Models can now be used like regular collection objects by calling filter, map, etc. * Models no longer require a Parameter structure or initial data in the constructor * Added a custom AppendIterator to get around a PHP bug with the `\AppendIterator` ## 3.0.0 - 2012-10-15 * Rewrote service description format to be based on Swagger * Now based on JSON schema * Added nested input structures and nested response models * Support for JSON and XML input and output models * Renamed `commands` to `operations` * Removed dot class notation * Removed custom types * Broke the project into smaller top-level namespaces to be more component friendly * Removed support for XML configs and descriptions. Use arrays or JSON files. * Removed the Validation component and Inspector * Moved all cookie code to Guzzle\Plugin\Cookie * Magic methods on a Guzzle\Service\Client now return the command un-executed. * Calling getResult() or getResponse() on a command will lazily execute the command if needed. * Now shipping with cURL's CA certs and using it by default * Added previousResponse() method to response objects * No longer sending Accept and Accept-Encoding headers on every request * Only sending an Expect header by default when a payload is greater than 1MB * Added/moved client options: * curl.blacklist to curl.option.blacklist * Added ssl.certificate_authority * Added a Guzzle\Iterator component * Moved plugins from Guzzle\Http\Plugin to Guzzle\Plugin * Added a more robust backoff retry strategy (replaced the ExponentialBackoffPlugin) * Added a more robust caching plugin * Added setBody to response objects * Updating LogPlugin to use a more flexible MessageFormatter * Added a completely revamped build process * Cleaning up Collection class and removing default values from the get method * Fixed ZF2 cache adapters ## 2.8.8 - 2012-10-15 * Bug: Fixed a cookie issue that caused dot prefixed domains to not match where popular browsers did ## 2.8.7 - 2012-09-30 * Bug: Fixed config file aliases for JSON includes * Bug: Fixed cookie bug on a request object by using CookieParser to parse cookies on requests * Bug: Removing the path to a file when sending a Content-Disposition header on a POST upload * Bug: Hardening request and response parsing to account for missing parts * Bug: Fixed PEAR packaging * Bug: Fixed Request::getInfo * Bug: Fixed cases where CURLM_CALL_MULTI_PERFORM return codes were causing curl transactions to fail * Adding the ability for the namespace Iterator factory to look in multiple directories * Added more getters/setters/removers from service descriptions * Added the ability to remove POST fields from OAuth signatures * OAuth plugin now supports 2-legged OAuth ## 2.8.6 - 2012-09-05 * Added the ability to modify and build service descriptions * Added the use of visitors to apply parameters to locations in service descriptions using the dynamic command * Added a `json` parameter location * Now allowing dot notation for classes in the CacheAdapterFactory * Using the union of two arrays rather than an array_merge when extending service builder services and service params * Ensuring that a service is a string before doing strpos() checks on it when substituting services for references in service builder config files. * Services defined in two different config files that include one another will by default replace the previously defined service, but you can now create services that extend themselves and merge their settings over the previous * The JsonLoader now supports aliasing filenames with different filenames. This allows you to alias something like '_default' with a default JSON configuration file. ## 2.8.5 - 2012-08-29 * Bug: Suppressed empty arrays from URI templates * Bug: Added the missing $options argument from ServiceDescription::factory to enable caching * Added support for HTTP responses that do not contain a reason phrase in the start-line * AbstractCommand commands are now invokable * Added a way to get the data used when signing an Oauth request before a request is sent ## 2.8.4 - 2012-08-15 * Bug: Custom delay time calculations are no longer ignored in the ExponentialBackoffPlugin * Added the ability to transfer entity bodies as a string rather than streamed. This gets around curl error 65. Set `body_as_string` in a request's curl options to enable. * Added a StreamInterface, EntityBodyInterface, and added ftell() to Guzzle\Common\Stream * Added an AbstractEntityBodyDecorator and a ReadLimitEntityBody decorator to transfer only a subset of a decorated stream * Stream and EntityBody objects will now return the file position to the previous position after a read required operation (e.g. getContentMd5()) * Added additional response status codes * Removed SSL information from the default User-Agent header * DELETE requests can now send an entity body * Added an EventDispatcher to the ExponentialBackoffPlugin and added an ExponentialBackoffLogger to log backoff retries * Added the ability of the MockPlugin to consume mocked request bodies * LogPlugin now exposes request and response objects in the extras array ## 2.8.3 - 2012-07-30 * Bug: Fixed a case where empty POST requests were sent as GET requests * Bug: Fixed a bug in ExponentialBackoffPlugin that caused fatal errors when retrying an EntityEnclosingRequest that does not have a body * Bug: Setting the response body of a request to null after completing a request, not when setting the state of a request to new * Added multiple inheritance to service description commands * Added an ApiCommandInterface and added `getParamNames()` and `hasParam()` * Removed the default 2mb size cutoff from the Md5ValidatorPlugin so that it now defaults to validating everything * Changed CurlMulti::perform to pass a smaller timeout to CurlMulti::executeHandles ## 2.8.2 - 2012-07-24 * Bug: Query string values set to 0 are no longer dropped from the query string * Bug: A Collection object is no longer created each time a call is made to `Guzzle\Service\Command\AbstractCommand::getRequestHeaders()` * Bug: `+` is now treated as an encoded space when parsing query strings * QueryString and Collection performance improvements * Allowing dot notation for class paths in filters attribute of a service descriptions ## 2.8.1 - 2012-07-16 * Loosening Event Dispatcher dependency * POST redirects can now be customized using CURLOPT_POSTREDIR ## 2.8.0 - 2012-07-15 * BC: Guzzle\Http\Query * Query strings with empty variables will always show an equal sign unless the variable is set to QueryString::BLANK (e.g. ?acl= vs ?acl) * Changed isEncodingValues() and isEncodingFields() to isUrlEncoding() * Changed setEncodeValues(bool) and setEncodeFields(bool) to useUrlEncoding(bool) * Changed the aggregation functions of QueryString to be static methods * Can now use fromString() with querystrings that have a leading ? * cURL configuration values can be specified in service descriptions using `curl.` prefixed parameters * Content-Length is set to 0 before emitting the request.before_send event when sending an empty request body * Cookies are no longer URL decoded by default * Bug: URI template variables set to null are no longer expanded ## 2.7.2 - 2012-07-02 * BC: Moving things to get ready for subtree splits. Moving Inflection into Common. Moving Guzzle\Http\Parser to Guzzle\Parser. * BC: Removing Guzzle\Common\Batch\Batch::count() and replacing it with isEmpty() * CachePlugin now allows for a custom request parameter function to check if a request can be cached * Bug fix: CachePlugin now only caches GET and HEAD requests by default * Bug fix: Using header glue when transferring headers over the wire * Allowing deeply nested arrays for composite variables in URI templates * Batch divisors can now return iterators or arrays ## 2.7.1 - 2012-06-26 * Minor patch to update version number in UA string * Updating build process ## 2.7.0 - 2012-06-25 * BC: Inflection classes moved to Guzzle\Inflection. No longer static methods. Can now inject custom inflectors into classes. * BC: Removed magic setX methods from commands * BC: Magic methods mapped to service description commands are now inflected in the command factory rather than the client __call() method * Verbose cURL options are no longer enabled by default. Set curl.debug to true on a client to enable. * Bug: Now allowing colons in a response start-line (e.g. HTTP/1.1 503 Service Unavailable: Back-end server is at capacity) * Guzzle\Service\Resource\ResourceIteratorApplyBatched now internally uses the Guzzle\Common\Batch namespace * Added Guzzle\Service\Plugin namespace and a PluginCollectionPlugin * Added the ability to set POST fields and files in a service description * Guzzle\Http\EntityBody::factory() now accepts objects with a __toString() method * Adding a command.before_prepare event to clients * Added BatchClosureTransfer and BatchClosureDivisor * BatchTransferException now includes references to the batch divisor and transfer strategies * Fixed some tests so that they pass more reliably * Added Guzzle\Common\Log\ArrayLogAdapter ## 2.6.6 - 2012-06-10 * BC: Removing Guzzle\Http\Plugin\BatchQueuePlugin * BC: Removing Guzzle\Service\Command\CommandSet * Adding generic batching system (replaces the batch queue plugin and command set) * Updating ZF cache and log adapters and now using ZF's composer repository * Bug: Setting the name of each ApiParam when creating through an ApiCommand * Adding result_type, result_doc, deprecated, and doc_url to service descriptions * Bug: Changed the default cookie header casing back to 'Cookie' ## 2.6.5 - 2012-06-03 * BC: Renaming Guzzle\Http\Message\RequestInterface::getResourceUri() to getResource() * BC: Removing unused AUTH_BASIC and AUTH_DIGEST constants from * BC: Guzzle\Http\Cookie is now used to manage Set-Cookie data, not Cookie data * BC: Renaming methods in the CookieJarInterface * Moving almost all cookie logic out of the CookiePlugin and into the Cookie or CookieJar implementations * Making the default glue for HTTP headers ';' instead of ',' * Adding a removeValue to Guzzle\Http\Message\Header * Adding getCookies() to request interface. * Making it easier to add event subscribers to HasDispatcherInterface classes. Can now directly call addSubscriber() ## 2.6.4 - 2012-05-30 * BC: Cleaning up how POST files are stored in EntityEnclosingRequest objects. Adding PostFile class. * BC: Moving ApiCommand specific functionality from the Inspector and on to the ApiCommand * Bug: Fixing magic method command calls on clients * Bug: Email constraint only validates strings * Bug: Aggregate POST fields when POST files are present in curl handle * Bug: Fixing default User-Agent header * Bug: Only appending or prepending parameters in commands if they are specified * Bug: Not requiring response reason phrases or status codes to match a predefined list of codes * Allowing the use of dot notation for class namespaces when using instance_of constraint * Added any_match validation constraint * Added an AsyncPlugin * Passing request object to the calculateWait method of the ExponentialBackoffPlugin * Allowing the result of a command object to be changed * Parsing location and type sub values when instantiating a service description rather than over and over at runtime ## 2.6.3 - 2012-05-23 * [BC] Guzzle\Common\FromConfigInterface no longer requires any config options. * [BC] Refactoring how POST files are stored on an EntityEnclosingRequest. They are now separate from POST fields. * You can now use an array of data when creating PUT request bodies in the request factory. * Removing the requirement that HTTPS requests needed a Cache-Control: public directive to be cacheable. * [Http] Adding support for Content-Type in multipart POST uploads per upload * [Http] Added support for uploading multiple files using the same name (foo[0], foo[1]) * Adding more POST data operations for easier manipulation of POST data. * You can now set empty POST fields. * The body of a request is only shown on EntityEnclosingRequest objects that do not use POST files. * Split the Guzzle\Service\Inspector::validateConfig method into two methods. One to initialize when a command is created, and one to validate. * CS updates ## 2.6.2 - 2012-05-19 * [Http] Better handling of nested scope requests in CurlMulti. Requests are now always prepares in the send() method rather than the addRequest() method. ## 2.6.1 - 2012-05-19 * [BC] Removing 'path' support in service descriptions. Use 'uri'. * [BC] Guzzle\Service\Inspector::parseDocBlock is now protected. Adding getApiParamsForClass() with cache. * [BC] Removing Guzzle\Common\NullObject. Use https://github.com/mtdowling/NullObject if you need it. * [BC] Removing Guzzle\Common\XmlElement. * All commands, both dynamic and concrete, have ApiCommand objects. * Adding a fix for CurlMulti so that if all of the connections encounter some sort of curl error, then the loop exits. * Adding checks to EntityEnclosingRequest so that empty POST files and fields are ignored. * Making the method signature of Guzzle\Service\Builder\ServiceBuilder::factory more flexible. ## 2.6.0 - 2012-05-15 * [BC] Moving Guzzle\Service\Builder to Guzzle\Service\Builder\ServiceBuilder * [BC] Executing a Command returns the result of the command rather than the command * [BC] Moving all HTTP parsing logic to Guzzle\Http\Parsers. Allows for faster C implementations if needed. * [BC] Changing the Guzzle\Http\Message\Response::setProtocol() method to accept a protocol and version in separate args. * [BC] Moving ResourceIterator* to Guzzle\Service\Resource * [BC] Completely refactored ResourceIterators to iterate over a cloned command object * [BC] Moved Guzzle\Http\UriTemplate to Guzzle\Http\Parser\UriTemplate\UriTemplate * [BC] Guzzle\Guzzle is now deprecated * Moving Guzzle\Common\Guzzle::inject to Guzzle\Common\Collection::inject * Adding Guzzle\Version class to give version information about Guzzle * Adding Guzzle\Http\Utils class to provide getDefaultUserAgent() and getHttpDate() * Adding Guzzle\Curl\CurlVersion to manage caching curl_version() data * ServiceDescription and ServiceBuilder are now cacheable using similar configs * Changing the format of XML and JSON service builder configs. Backwards compatible. * Cleaned up Cookie parsing * Trimming the default Guzzle User-Agent header * Adding a setOnComplete() method to Commands that is called when a command completes * Keeping track of requests that were mocked in the MockPlugin * Fixed a caching bug in the CacheAdapterFactory * Inspector objects can be injected into a Command object * Refactoring a lot of code and tests to be case insensitive when dealing with headers * Adding Guzzle\Http\Message\HeaderComparison for easy comparison of HTTP headers using a DSL * Adding the ability to set global option overrides to service builder configs * Adding the ability to include other service builder config files from within XML and JSON files * Moving the parseQuery method out of Url and on to QueryString::fromString() as a static factory method. ## 2.5.0 - 2012-05-08 * Major performance improvements * [BC] Simplifying Guzzle\Common\Collection. Please check to see if you are using features that are now deprecated. * [BC] Using a custom validation system that allows a flyweight implementation for much faster validation. No longer using Symfony2 Validation component. * [BC] No longer supporting "{{ }}" for injecting into command or UriTemplates. Use "{}" * Added the ability to passed parameters to all requests created by a client * Added callback functionality to the ExponentialBackoffPlugin * Using microtime in ExponentialBackoffPlugin to allow more granular backoff strategies. * Rewinding request stream bodies when retrying requests * Exception is thrown when JSON response body cannot be decoded * Added configurable magic method calls to clients and commands. This is off by default. * Fixed a defect that added a hash to every parsed URL part * Fixed duplicate none generation for OauthPlugin. * Emitting an event each time a client is generated by a ServiceBuilder * Using an ApiParams object instead of a Collection for parameters of an ApiCommand * cache.* request parameters should be renamed to params.cache.* * Added the ability to set arbitrary curl options on requests (disable_wire, progress, etc.). See CurlHandle. * Added the ability to disable type validation of service descriptions * ServiceDescriptions and ServiceBuilders are now Serializable LICENSE000064400000002134151742725260005564 0ustar00Copyright (c) 2011-2015 Michael Dowling, https://github.com/mtdowling 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. README.md000064400000004432151742725260006041 0ustar00Guzzle, PHP HTTP client and webservice framework ================================================ [![Build Status](https://secure.travis-ci.org/guzzle/guzzle.svg?branch=master)](http://travis-ci.org/guzzle/guzzle) Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services. - Manages things like persistent connections, represents query strings as collections, simplifies sending streaming POST requests with fields and files, and abstracts away the underlying HTTP transport layer. - Can send both synchronous and asynchronous requests using the same interface without requiring a dependency on a specific event loop. - Pluggable HTTP adapters allows Guzzle to integrate with any method you choose for sending HTTP requests over the wire (e.g., cURL, sockets, PHP's stream wrapper, non-blocking event loops like ReactPHP. - Guzzle makes it so that you no longer need to fool around with cURL options, stream contexts, or sockets. ```php $client = new GuzzleHttp\Client(); $response = $client->get('http://guzzlephp.org'); $res = $client->get('https://api.github.com/user', ['auth' => ['user', 'pass']]); echo $res->getStatusCode(); // "200" echo $res->getHeader('content-type'); // 'application/json; charset=utf8' echo $res->getBody(); // {"type":"User"...' var_export($res->json()); // Outputs the JSON decoded data // Send an asynchronous request. $req = $client->createRequest('GET', 'http://httpbin.org', ['future' => true]); $client->send($req)->then(function ($response) { echo 'I completed! ' . $response; }); ``` Get more information and answers with the [Documentation](http://guzzlephp.org/), [Forums](https://groups.google.com/forum/?hl=en#!forum/guzzle), and [Gitter](https://gitter.im/guzzle/guzzle). ### Installing via Composer The recommended way to install Guzzle is through [Composer](http://getcomposer.org). ```bash # Install Composer curl -sS https://getcomposer.org/installer | php ``` Next, run the Composer command to install the latest stable version of Guzzle: ```bash composer.phar require guzzlehttp/guzzle ``` After installing, you need to require Composer's autoloader: ```php require 'vendor/autoload.php'; ``` ### Documentation More information can be found in the online documentation at http://guzzlephp.org/. UPGRADING.md000064400000124241151742725260006425 0ustar00Guzzle Upgrade Guide ==================== 4.x to 5.0 ---------- ## Rewritten Adapter Layer Guzzle now uses [RingPHP](http://ringphp.readthedocs.org/en/latest) to send HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor is still supported, but it has now been renamed to `handler`. Instead of passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP `callable` that follows the RingPHP specification. ## Removed Fluent Interfaces [Fluent interfaces were removed](http://ocramius.github.io/blog/fluent-interfaces-are-evil) from the following classes: - `GuzzleHttp\Collection` - `GuzzleHttp\Url` - `GuzzleHttp\Query` - `GuzzleHttp\Post\PostBody` - `GuzzleHttp\Cookie\SetCookie` ## Removed functions.php Removed "functions.php", so that Guzzle is truly PSR-4 compliant. The following functions can be used as replacements. - `GuzzleHttp\json_decode` -> `GuzzleHttp\Utils::jsonDecode` - `GuzzleHttp\get_path` -> `GuzzleHttp\Utils::getPath` - `GuzzleHttp\Utils::setPath` -> `GuzzleHttp\set_path` - `GuzzleHttp\Pool::batch` -> `GuzzleHttp\batch`. This function is, however, deprecated in favor of using `GuzzleHttp\Pool::batch()`. The "procedural" global client has been removed with no replacement (e.g., `GuzzleHttp\get()`, `GuzzleHttp\post()`, etc.). Use a `GuzzleHttp\Client` object as a replacement. ## `throwImmediately` has been removed The concept of "throwImmediately" has been removed from exceptions and error events. This control mechanism was used to stop a transfer of concurrent requests from completing. This can now be handled by throwing the exception or by cancelling a pool of requests or each outstanding future request individually. ## headers event has been removed Removed the "headers" event. This event was only useful for changing the body a response once the headers of the response were known. You can implement a similar behavior in a number of ways. One example might be to use a FnStream that has access to the transaction being sent. For example, when the first byte is written, you could check if the response headers match your expectations, and if so, change the actual stream body that is being written to. ## Updates to HTTP Messages Removed the `asArray` parameter from `GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header value as an array, then use the newly added `getHeaderAsArray()` method of `MessageInterface`. This change makes the Guzzle interfaces compatible with the PSR-7 interfaces. 3.x to 4.0 ---------- ## Overarching changes: - Now requires PHP 5.4 or greater. - No longer requires cURL to send requests. - Guzzle no longer wraps every exception it throws. Only exceptions that are recoverable are now wrapped by Guzzle. - Various namespaces have been removed or renamed. - No longer requiring the Symfony EventDispatcher. A custom event dispatcher based on the Symfony EventDispatcher is now utilized in `GuzzleHttp\Event\EmitterInterface` (resulting in significant speed and functionality improvements). Changes per Guzzle 3.x namespace are described below. ## Batch The `Guzzle\Batch` namespace has been removed. This is best left to third-parties to implement on top of Guzzle's core HTTP library. ## Cache The `Guzzle\Cache` namespace has been removed. (Todo: No suitable replacement has been implemented yet, but hoping to utilize a PSR cache interface). ## Common - Removed all of the wrapped exceptions. It's better to use the standard PHP library for unrecoverable exceptions. - `FromConfigInterface` has been removed. - `Guzzle\Common\Version` has been removed. The VERSION constant can be found at `GuzzleHttp\ClientInterface::VERSION`. ### Collection - `getAll` has been removed. Use `toArray` to convert a collection to an array. - `inject` has been removed. - `keySearch` has been removed. - `getPath` no longer supports wildcard expressions. Use something better like JMESPath for this. - `setPath` now supports appending to an existing array via the `[]` notation. ### Events Guzzle no longer requires Symfony's EventDispatcher component. Guzzle now uses `GuzzleHttp\Event\Emitter`. - `Symfony\Component\EventDispatcher\EventDispatcherInterface` is replaced by `GuzzleHttp\Event\EmitterInterface`. - `Symfony\Component\EventDispatcher\EventDispatcher` is replaced by `GuzzleHttp\Event\Emitter`. - `Symfony\Component\EventDispatcher\Event` is replaced by `GuzzleHttp\Event\Event`, and Guzzle now has an EventInterface in `GuzzleHttp\Event\EventInterface`. - `AbstractHasDispatcher` has moved to a trait, `HasEmitterTrait`, and `HasDispatcherInterface` has moved to `HasEmitterInterface`. Retrieving the event emitter of a request, client, etc. now uses the `getEmitter` method rather than the `getDispatcher` method. #### Emitter - Use the `once()` method to add a listener that automatically removes itself the first time it is invoked. - Use the `listeners()` method to retrieve a list of event listeners rather than the `getListeners()` method. - Use `emit()` instead of `dispatch()` to emit an event from an emitter. - Use `attach()` instead of `addSubscriber()` and `detach()` instead of `removeSubscriber()`. ```php $mock = new Mock(); // 3.x $request->getEventDispatcher()->addSubscriber($mock); $request->getEventDispatcher()->removeSubscriber($mock); // 4.x $request->getEmitter()->attach($mock); $request->getEmitter()->detach($mock); ``` Use the `on()` method to add a listener rather than the `addListener()` method. ```php // 3.x $request->getEventDispatcher()->addListener('foo', function (Event $event) { /* ... */ } ); // 4.x $request->getEmitter()->on('foo', function (Event $event, $name) { /* ... */ } ); ``` ## Http ### General changes - The cacert.pem certificate has been moved to `src/cacert.pem`. - Added the concept of adapters that are used to transfer requests over the wire. - Simplified the event system. - Sending requests in parallel is still possible, but batching is no longer a concept of the HTTP layer. Instead, you must use the `complete` and `error` events to asynchronously manage parallel request transfers. - `Guzzle\Http\Url` has moved to `GuzzleHttp\Url`. - `Guzzle\Http\QueryString` has moved to `GuzzleHttp\Query`. - QueryAggregators have been rewritten so that they are simply callable functions. - `GuzzleHttp\StaticClient` has been removed. Use the functions provided in `functions.php` for an easy to use static client instance. - Exceptions in `GuzzleHttp\Exception` have been updated to all extend from `GuzzleHttp\Exception\TransferException`. ### Client Calling methods like `get()`, `post()`, `head()`, etc. no longer create and return a request, but rather creates a request, sends the request, and returns the response. ```php // 3.0 $request = $client->get('/'); $response = $request->send(); // 4.0 $response = $client->get('/'); // or, to mirror the previous behavior $request = $client->createRequest('GET', '/'); $response = $client->send($request); ``` `GuzzleHttp\ClientInterface` has changed. - The `send` method no longer accepts more than one request. Use `sendAll` to send multiple requests in parallel. - `setUserAgent()` has been removed. Use a default request option instead. You could, for example, do something like: `$client->setConfig('defaults/headers/User-Agent', 'Foo/Bar ' . $client::getDefaultUserAgent())`. - `setSslVerification()` has been removed. Use default request options instead, like `$client->setConfig('defaults/verify', true)`. `GuzzleHttp\Client` has changed. - The constructor now accepts only an associative array. You can include a `base_url` string or array to use a URI template as the base URL of a client. You can also specify a `defaults` key that is an associative array of default request options. You can pass an `adapter` to use a custom adapter, `batch_adapter` to use a custom adapter for sending requests in parallel, or a `message_factory` to change the factory used to create HTTP requests and responses. - The client no longer emits a `client.create_request` event. - Creating requests with a client no longer automatically utilize a URI template. You must pass an array into a creational method (e.g., `createRequest`, `get`, `put`, etc.) in order to expand a URI template. ### Messages Messages no longer have references to their counterparts (i.e., a request no longer has a reference to it's response, and a response no loger has a reference to its request). This association is now managed through a `GuzzleHttp\Adapter\TransactionInterface` object. You can get references to these transaction objects using request events that are emitted over the lifecycle of a request. #### Requests with a body - `GuzzleHttp\Message\EntityEnclosingRequest` and `GuzzleHttp\Message\EntityEnclosingRequestInterface` have been removed. The separation between requests that contain a body and requests that do not contain a body has been removed, and now `GuzzleHttp\Message\RequestInterface` handles both use cases. - Any method that previously accepts a `GuzzleHttp\Response` object now accept a `GuzzleHttp\Message\ResponseInterface`. - `GuzzleHttp\Message\RequestFactoryInterface` has been renamed to `GuzzleHttp\Message\MessageFactoryInterface`. This interface is used to create both requests and responses and is implemented in `GuzzleHttp\Message\MessageFactory`. - POST field and file methods have been removed from the request object. You must now use the methods made available to `GuzzleHttp\Post\PostBodyInterface` to control the format of a POST body. Requests that are created using a standard `GuzzleHttp\Message\MessageFactoryInterface` will automatically use a `GuzzleHttp\Post\PostBody` body if the body was passed as an array or if the method is POST and no body is provided. ```php $request = $client->createRequest('POST', '/'); $request->getBody()->setField('foo', 'bar'); $request->getBody()->addFile(new PostFile('file_key', fopen('/path/to/content', 'r'))); ``` #### Headers - `GuzzleHttp\Message\Header` has been removed. Header values are now simply represented by an array of values or as a string. Header values are returned as a string by default when retrieving a header value from a message. You can pass an optional argument of `true` to retrieve a header value as an array of strings instead of a single concatenated string. - `GuzzleHttp\PostFile` and `GuzzleHttp\PostFileInterface` have been moved to `GuzzleHttp\Post`. This interface has been simplified and now allows the addition of arbitrary headers. - Custom headers like `GuzzleHttp\Message\Header\Link` have been removed. Most of the custom headers are now handled separately in specific subscribers/plugins, and `GuzzleHttp\Message\HeaderValues::parseParams()` has been updated to properly handle headers that contain parameters (like the `Link` header). #### Responses - `GuzzleHttp\Message\Response::getInfo()` and `GuzzleHttp\Message\Response::setInfo()` have been removed. Use the event system to retrieve this type of information. - `GuzzleHttp\Message\Response::getRawHeaders()` has been removed. - `GuzzleHttp\Message\Response::getMessage()` has been removed. - `GuzzleHttp\Message\Response::calculateAge()` and other cache specific methods have moved to the CacheSubscriber. - Header specific helper functions like `getContentMd5()` have been removed. Just use `getHeader('Content-MD5')` instead. - `GuzzleHttp\Message\Response::setRequest()` and `GuzzleHttp\Message\Response::getRequest()` have been removed. Use the event system to work with request and response objects as a transaction. - `GuzzleHttp\Message\Response::getRedirectCount()` has been removed. Use the Redirect subscriber instead. - `GuzzleHttp\Message\Response::isSuccessful()` and other related methods have been removed. Use `getStatusCode()` instead. #### Streaming responses Streaming requests can now be created by a client directly, returning a `GuzzleHttp\Message\ResponseInterface` object that contains a body stream referencing an open PHP HTTP stream. ```php // 3.0 use Guzzle\Stream\PhpStreamRequestFactory; $request = $client->get('/'); $factory = new PhpStreamRequestFactory(); $stream = $factory->fromRequest($request); $data = $stream->read(1024); // 4.0 $response = $client->get('/', ['stream' => true]); // Read some data off of the stream in the response body $data = $response->getBody()->read(1024); ``` #### Redirects The `configureRedirects()` method has been removed in favor of a `allow_redirects` request option. ```php // Standard redirects with a default of a max of 5 redirects $request = $client->createRequest('GET', '/', ['allow_redirects' => true]); // Strict redirects with a custom number of redirects $request = $client->createRequest('GET', '/', [ 'allow_redirects' => ['max' => 5, 'strict' => true] ]); ``` #### EntityBody EntityBody interfaces and classes have been removed or moved to `GuzzleHttp\Stream`. All classes and interfaces that once required `GuzzleHttp\EntityBodyInterface` now require `GuzzleHttp\Stream\StreamInterface`. Creating a new body for a request no longer uses `GuzzleHttp\EntityBody::factory` but now uses `GuzzleHttp\Stream\Stream::factory` or even better: `GuzzleHttp\Stream\create()`. - `Guzzle\Http\EntityBodyInterface` is now `GuzzleHttp\Stream\StreamInterface` - `Guzzle\Http\EntityBody` is now `GuzzleHttp\Stream\Stream` - `Guzzle\Http\CachingEntityBody` is now `GuzzleHttp\Stream\CachingStream` - `Guzzle\Http\ReadLimitEntityBody` is now `GuzzleHttp\Stream\LimitStream` - `Guzzle\Http\IoEmittyinEntityBody` has been removed. #### Request lifecycle events Requests previously submitted a large number of requests. The number of events emitted over the lifecycle of a request has been significantly reduced to make it easier to understand how to extend the behavior of a request. All events emitted during the lifecycle of a request now emit a custom `GuzzleHttp\Event\EventInterface` object that contains context providing methods and a way in which to modify the transaction at that specific point in time (e.g., intercept the request and set a response on the transaction). - `request.before_send` has been renamed to `before` and now emits a `GuzzleHttp\Event\BeforeEvent` - `request.complete` has been renamed to `complete` and now emits a `GuzzleHttp\Event\CompleteEvent`. - `request.sent` has been removed. Use `complete`. - `request.success` has been removed. Use `complete`. - `error` is now an event that emits a `GuzzleHttp\Event\ErrorEvent`. - `request.exception` has been removed. Use `error`. - `request.receive.status_line` has been removed. - `curl.callback.progress` has been removed. Use a custom `StreamInterface` to maintain a status update. - `curl.callback.write` has been removed. Use a custom `StreamInterface` to intercept writes. - `curl.callback.read` has been removed. Use a custom `StreamInterface` to intercept reads. `headers` is a new event that is emitted after the response headers of a request have been received before the body of the response is downloaded. This event emits a `GuzzleHttp\Event\HeadersEvent`. You can intercept a request and inject a response using the `intercept()` event of a `GuzzleHttp\Event\BeforeEvent`, `GuzzleHttp\Event\CompleteEvent`, and `GuzzleHttp\Event\ErrorEvent` event. See: http://docs.guzzlephp.org/en/latest/events.html ## Inflection The `Guzzle\Inflection` namespace has been removed. This is not a core concern of Guzzle. ## Iterator The `Guzzle\Iterator` namespace has been removed. - `Guzzle\Iterator\AppendIterator`, `Guzzle\Iterator\ChunkedIterator`, and `Guzzle\Iterator\MethodProxyIterator` are nice, but not a core requirement of Guzzle itself. - `Guzzle\Iterator\FilterIterator` is no longer needed because an equivalent class is shipped with PHP 5.4. - `Guzzle\Iterator\MapIterator` is not really needed when using PHP 5.5 because it's easier to just wrap an iterator in a generator that maps values. For a replacement of these iterators, see https://github.com/nikic/iter ## Log The LogPlugin has moved to https://github.com/guzzle/log-subscriber. The `Guzzle\Log` namespace has been removed. Guzzle now relies on `Psr\Log\LoggerInterface` for all logging. The MessageFormatter class has been moved to `GuzzleHttp\Subscriber\Log\Formatter`. ## Parser The `Guzzle\Parser` namespace has been removed. This was previously used to make it possible to plug in custom parsers for cookies, messages, URI templates, and URLs; however, this level of complexity is not needed in Guzzle so it has been removed. - Cookie: Cookie parsing logic has been moved to `GuzzleHttp\Cookie\SetCookie::fromString`. - Message: Message parsing logic for both requests and responses has been moved to `GuzzleHttp\Message\MessageFactory::fromMessage`. Message parsing is only used in debugging or deserializing messages, so it doesn't make sense for Guzzle as a library to add this level of complexity to parsing messages. - UriTemplate: URI template parsing has been moved to `GuzzleHttp\UriTemplate`. The Guzzle library will automatically use the PECL URI template library if it is installed. - Url: URL parsing is now performed in `GuzzleHttp\Url::fromString` (previously it was `Guzzle\Http\Url::factory()`). If custom URL parsing is necessary, then developers are free to subclass `GuzzleHttp\Url`. ## Plugin The `Guzzle\Plugin` namespace has been renamed to `GuzzleHttp\Subscriber`. Several plugins are shipping with the core Guzzle library under this namespace. - `GuzzleHttp\Subscriber\Cookie`: Replaces the old CookiePlugin. Cookie jar code has moved to `GuzzleHttp\Cookie`. - `GuzzleHttp\Subscriber\History`: Replaces the old HistoryPlugin. - `GuzzleHttp\Subscriber\HttpError`: Throws errors when a bad HTTP response is received. - `GuzzleHttp\Subscriber\Mock`: Replaces the old MockPlugin. - `GuzzleHttp\Subscriber\Prepare`: Prepares the body of a request just before sending. This subscriber is attached to all requests by default. - `GuzzleHttp\Subscriber\Redirect`: Replaces the RedirectPlugin. The following plugins have been removed (third-parties are free to re-implement these if needed): - `GuzzleHttp\Plugin\Async` has been removed. - `GuzzleHttp\Plugin\CurlAuth` has been removed. - `GuzzleHttp\Plugin\ErrorResponse\ErrorResponsePlugin` has been removed. This functionality should instead be implemented with event listeners that occur after normal response parsing occurs in the guzzle/command package. The following plugins are not part of the core Guzzle package, but are provided in separate repositories: - `Guzzle\Http\Plugin\BackoffPlugin` has been rewritten to be muchs simpler to build custom retry policies using simple functions rather than various chained classes. See: https://github.com/guzzle/retry-subscriber - `Guzzle\Http\Plugin\Cache\CachePlugin` has moved to https://github.com/guzzle/cache-subscriber - `Guzzle\Http\Plugin\Log\LogPlugin` has moved to https://github.com/guzzle/log-subscriber - `Guzzle\Http\Plugin\Md5\Md5Plugin` has moved to https://github.com/guzzle/message-integrity-subscriber - `Guzzle\Http\Plugin\Mock\MockPlugin` has moved to `GuzzleHttp\Subscriber\MockSubscriber`. - `Guzzle\Http\Plugin\Oauth\OauthPlugin` has moved to https://github.com/guzzle/oauth-subscriber ## Service The service description layer of Guzzle has moved into two separate packages: - http://github.com/guzzle/command Provides a high level abstraction over web services by representing web service operations using commands. - http://github.com/guzzle/guzzle-services Provides an implementation of guzzle/command that provides request serialization and response parsing using Guzzle service descriptions. ## Stream Stream have moved to a separate package available at https://github.com/guzzle/streams. `Guzzle\Stream\StreamInterface` has been given a large update to cleanly take on the responsibilities of `Guzzle\Http\EntityBody` and `Guzzle\Http\EntityBodyInterface` now that they have been removed. The number of methods implemented by the `StreamInterface` has been drastically reduced to allow developers to more easily extend and decorate stream behavior. ## Removed methods from StreamInterface - `getStream` and `setStream` have been removed to better encapsulate streams. - `getMetadata` and `setMetadata` have been removed in favor of `GuzzleHttp\Stream\MetadataStreamInterface`. - `getWrapper`, `getWrapperData`, `getStreamType`, and `getUri` have all been removed. This data is accessible when using streams that implement `GuzzleHttp\Stream\MetadataStreamInterface`. - `rewind` has been removed. Use `seek(0)` for a similar behavior. ## Renamed methods - `detachStream` has been renamed to `detach`. - `feof` has been renamed to `eof`. - `ftell` has been renamed to `tell`. - `readLine` has moved from an instance method to a static class method of `GuzzleHttp\Stream\Stream`. ## Metadata streams `GuzzleHttp\Stream\MetadataStreamInterface` has been added to denote streams that contain additional metadata accessible via `getMetadata()`. `GuzzleHttp\Stream\StreamInterface::getMetadata` and `GuzzleHttp\Stream\StreamInterface::setMetadata` have been removed. ## StreamRequestFactory The entire concept of the StreamRequestFactory has been removed. The way this was used in Guzzle 3 broke the actual interface of sending streaming requests (instead of getting back a Response, you got a StreamInterface). Streeaming PHP requests are now implemented throught the `GuzzleHttp\Adapter\StreamAdapter`. 3.6 to 3.7 ---------- ### Deprecations - You can now enable E_USER_DEPRECATED warnings to see if you are using any deprecated methods.: ```php \Guzzle\Common\Version::$emitWarnings = true; ``` The following APIs and options have been marked as deprecated: - Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use `$request->getResponseBody()->isRepeatable()` instead. - Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. - Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. - Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. - Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. - Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated - Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. - Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. - Marked `Guzzle\Common\Collection::inject()` as deprecated. - Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` or `$client->setDefaultOption('auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` 3.7 introduces `request.options` as a parameter for a client configuration and as an optional argument to all creational request methods. When paired with a client's configuration settings, these options allow you to specify default settings for various aspects of a request. Because these options make other previous configuration options redundant, several configuration options and methods of a client and AbstractCommand have been deprecated. - Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use `$client->getDefaultOption('headers')`. - Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use `$client->setDefaultOption('headers/{header_name}', 'value')`. - Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use `$client->setDefaultOption('params/{param_name}', 'value')` - Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0 $command = $client->getCommand('foo', array( 'command.headers' => array('Test' => '123'), 'command.response_body' => '/path/to/file' )); // Should be changed to: $command = $client->getCommand('foo', array( 'command.request_options' => array( 'headers' => array('Test' => '123'), 'save_as' => '/path/to/file' ) )); ### Interface changes Additions and changes (you will need to update any implementations or subclasses you may have created): - Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: createRequest, head, delete, put, patch, post, options, prepareRequest - Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` - Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` - Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a resource, string, or EntityBody into the $options parameter to specify the download location of the response. - Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a default `array()` - Added `Guzzle\Stream\StreamInterface::isRepeatable` - Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. The following methods were removed from interfaces. All of these methods are still available in the concrete classes that implement them, but you should update your code to use alternative methods: - Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use `$client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or `$client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))` or `$client->setDefaultOption('headers/{header_name}', 'value')`. or `$client->setDefaultOption('headers', array('header_name' => 'value'))`. - Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use `$client->getConfig()->getPath('request.options/headers')`. - Removed `Guzzle\Http\ClientInterface::expandTemplate()`. This is an implementation detail. - Removed `Guzzle\Http\ClientInterface::setRequestFactory()`. This is an implementation detail. - Removed `Guzzle\Http\ClientInterface::getCurlMulti()`. This is a very specific implementation detail. - Removed `Guzzle\Http\Message\RequestInterface::canCache`. Use the CachePlugin. - Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect`. Use the HistoryPlugin. - Removed `Guzzle\Http\Message\RequestInterface::isRedirect`. Use the HistoryPlugin. ### Cache plugin breaking changes - CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a CacheStorageInterface. These two objects and interface will be removed in a future version. - Always setting X-cache headers on cached responses - Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin - `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface $request, Response $response);` - `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` - `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` - Added `CacheStorageInterface::purge($url)` - `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, CanCacheStrategyInterface $canCache = null)` - Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` 3.5 to 3.6 ---------- * Mixed casing of headers are now forced to be a single consistent casing across all values for that header. * Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution * Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). For example, setHeader() first removes the header using unset on a HeaderCollection and then calls addHeader(). Keeping the Host header and URL host in sync is now handled by overriding the addHeader method in Request. * Specific header implementations can be created for complex headers. When a message creates a header, it uses a HeaderFactory which can map specific headers to specific header classes. There is now a Link header and CacheControl header implementation. * Moved getLinks() from Response to just be used on a Link header object. If you previously relied on Guzzle\Http\Message\Header::raw(), then you will need to update your code to use the HeaderInterface (e.g. toArray(), getAll(), etc.). ### Interface changes * Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate * Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() * Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in Guzzle\Http\Curl\RequestMediator * Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. * Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface * Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() ### Removed deprecated functions * Removed Guzzle\Parser\ParserRegister::get(). Use getParser() * Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). ### Deprecations * The ability to case-insensitively search for header values * Guzzle\Http\Message\Header::hasExactHeader * Guzzle\Http\Message\Header::raw. Use getAll() * Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object instead. ### Other changes * All response header helper functions return a string rather than mixing Header objects and strings inconsistently * Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle directly via interfaces * Removed the injecting of a request object onto a response object. The methods to get and set a request still exist but are a no-op until removed. * Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a `Guzzle\Service\Command\ArrayCommandInterface`. * Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response on a request while the request is still being transferred * `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess 3.3 to 3.4 ---------- Base URLs of a client now follow the rules of http://tools.ietf.org/html/rfc3986#section-5.2.2 when merging URLs. 3.2 to 3.3 ---------- ### Response::getEtag() quote stripping removed `Guzzle\Http\Message\Response::getEtag()` no longer strips quotes around the ETag response header ### Removed `Guzzle\Http\Utils` The `Guzzle\Http\Utils` class was removed. This class was only used for testing. ### Stream wrapper and type `Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getStreamType()` are no longer converted to lowercase. ### curl.emit_io became emit_io Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' 3.1 to 3.2 ---------- ### CurlMulti is no longer reused globally Before 3.2, the same CurlMulti object was reused globally for each client. This can cause issue where plugins added to a single client can pollute requests dispatched from other clients. If you still wish to reuse the same CurlMulti object with each client, then you can add a listener to the ServiceBuilder's `service_builder.create_client` event to inject a custom CurlMulti object into each client as it is created. ```php $multi = new Guzzle\Http\Curl\CurlMulti(); $builder = Guzzle\Service\Builder\ServiceBuilder::factory('/path/to/config.json'); $builder->addListener('service_builder.create_client', function ($event) use ($multi) { $event['client']->setCurlMulti($multi); } }); ``` ### No default path URLs no longer have a default path value of '/' if no path was specified. Before: ```php $request = $client->get('http://www.foo.com'); echo $request->getUrl(); // >> http://www.foo.com/ ``` After: ```php $request = $client->get('http://www.foo.com'); echo $request->getUrl(); // >> http://www.foo.com ``` ### Less verbose BadResponseException The exception message for `Guzzle\Http\Exception\BadResponseException` no longer contains the full HTTP request and response information. You can, however, get access to the request and response object by calling `getRequest()` or `getResponse()` on the exception object. ### Query parameter aggregation Multi-valued query parameters are no longer aggregated using a callback function. `Guzzle\Http\Query` now has a setAggregator() method that accepts a `Guzzle\Http\QueryAggregator\QueryAggregatorInterface` object. This object is responsible for handling the aggregation of multi-valued query string variables into a flattened hash. 2.8 to 3.x ---------- ### Guzzle\Service\Inspector Change `\Guzzle\Service\Inspector::fromConfig` to `\Guzzle\Common\Collection::fromConfig` **Before** ```php use Guzzle\Service\Inspector; class YourClient extends \Guzzle\Service\Client { public static function factory($config = array()) { $default = array(); $required = array('base_url', 'username', 'api_key'); $config = Inspector::fromConfig($config, $default, $required); $client = new self( $config->get('base_url'), $config->get('username'), $config->get('api_key') ); $client->setConfig($config); $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); return $client; } ``` **After** ```php use Guzzle\Common\Collection; class YourClient extends \Guzzle\Service\Client { public static function factory($config = array()) { $default = array(); $required = array('base_url', 'username', 'api_key'); $config = Collection::fromConfig($config, $default, $required); $client = new self( $config->get('base_url'), $config->get('username'), $config->get('api_key') ); $client->setConfig($config); $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); return $client; } ``` ### Convert XML Service Descriptions to JSON **Before** ```xml Get a list of groups Uses a search query to get a list of groups Create a group Delete a group by ID Update a group ``` **After** ```json { "name": "Zendesk REST API v2", "apiVersion": "2012-12-31", "description":"Provides access to Zendesk views, groups, tickets, ticket fields, and users", "operations": { "list_groups": { "httpMethod":"GET", "uri": "groups.json", "summary": "Get a list of groups" }, "search_groups":{ "httpMethod":"GET", "uri": "search.json?query=\"{query} type:group\"", "summary": "Uses a search query to get a list of groups", "parameters":{ "query":{ "location": "uri", "description":"Zendesk Search Query", "type": "string", "required": true } } }, "create_group": { "httpMethod":"POST", "uri": "groups.json", "summary": "Create a group", "parameters":{ "data": { "type": "array", "location": "body", "description":"Group JSON", "filters": "json_encode", "required": true }, "Content-Type":{ "type": "string", "location":"header", "static": "application/json" } } }, "delete_group": { "httpMethod":"DELETE", "uri": "groups/{id}.json", "summary": "Delete a group", "parameters":{ "id":{ "location": "uri", "description":"Group to delete by ID", "type": "integer", "required": true } } }, "get_group": { "httpMethod":"GET", "uri": "groups/{id}.json", "summary": "Get a ticket", "parameters":{ "id":{ "location": "uri", "description":"Group to get by ID", "type": "integer", "required": true } } }, "update_group": { "httpMethod":"PUT", "uri": "groups/{id}.json", "summary": "Update a group", "parameters":{ "id": { "location": "uri", "description":"Group to update by ID", "type": "integer", "required": true }, "data": { "type": "array", "location": "body", "description":"Group JSON", "filters": "json_encode", "required": true }, "Content-Type":{ "type": "string", "location":"header", "static": "application/json" } } } } ``` ### Guzzle\Service\Description\ServiceDescription Commands are now called Operations **Before** ```php use Guzzle\Service\Description\ServiceDescription; $sd = new ServiceDescription(); $sd->getCommands(); // @returns ApiCommandInterface[] $sd->hasCommand($name); $sd->getCommand($name); // @returns ApiCommandInterface|null $sd->addCommand($command); // @param ApiCommandInterface $command ``` **After** ```php use Guzzle\Service\Description\ServiceDescription; $sd = new ServiceDescription(); $sd->getOperations(); // @returns OperationInterface[] $sd->hasOperation($name); $sd->getOperation($name); // @returns OperationInterface|null $sd->addOperation($operation); // @param OperationInterface $operation ``` ### Guzzle\Common\Inflection\Inflector Namespace is now `Guzzle\Inflection\Inflector` ### Guzzle\Http\Plugin Namespace is now `Guzzle\Plugin`. Many other changes occur within this namespace and are detailed in their own sections below. ### Guzzle\Http\Plugin\LogPlugin and Guzzle\Common\Log Now `Guzzle\Plugin\Log\LogPlugin` and `Guzzle\Log` respectively. **Before** ```php use Guzzle\Common\Log\ClosureLogAdapter; use Guzzle\Http\Plugin\LogPlugin; /** @var \Guzzle\Http\Client */ $client; // $verbosity is an integer indicating desired message verbosity level $client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $verbosity = LogPlugin::LOG_VERBOSE); ``` **After** ```php use Guzzle\Log\ClosureLogAdapter; use Guzzle\Log\MessageFormatter; use Guzzle\Plugin\Log\LogPlugin; /** @var \Guzzle\Http\Client */ $client; // $format is a string indicating desired message format -- @see MessageFormatter $client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $format = MessageFormatter::DEBUG_FORMAT); ``` ### Guzzle\Http\Plugin\CurlAuthPlugin Now `Guzzle\Plugin\CurlAuth\CurlAuthPlugin`. ### Guzzle\Http\Plugin\ExponentialBackoffPlugin Now `Guzzle\Plugin\Backoff\BackoffPlugin`, and other changes. **Before** ```php use Guzzle\Http\Plugin\ExponentialBackoffPlugin; $backoffPlugin = new ExponentialBackoffPlugin($maxRetries, array_merge( ExponentialBackoffPlugin::getDefaultFailureCodes(), array(429) )); $client->addSubscriber($backoffPlugin); ``` **After** ```php use Guzzle\Plugin\Backoff\BackoffPlugin; use Guzzle\Plugin\Backoff\HttpBackoffStrategy; // Use convenient factory method instead -- see implementation for ideas of what // you can do with chaining backoff strategies $backoffPlugin = BackoffPlugin::getExponentialBackoff($maxRetries, array_merge( HttpBackoffStrategy::getDefaultFailureCodes(), array(429) )); $client->addSubscriber($backoffPlugin); ``` ### Known Issues #### [BUG] Accept-Encoding header behavior changed unintentionally. (See #217) (Fixed in 09daeb8c666fb44499a0646d655a8ae36456575e) In version 2.8 setting the `Accept-Encoding` header would set the CURLOPT_ENCODING option, which permitted cURL to properly handle gzip/deflate compressed responses from the server. In versions affected by this bug this does not happen. See issue #217 for a workaround, or use a version containing the fix. composer.json000064400000001746151742725260007311 0ustar00{ "name": "guzzlehttp/guzzle", "type": "library", "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", "keywords": ["framework", "http", "rest", "web service", "curl", "client", "HTTP client"], "homepage": "http://guzzlephp.org/", "license": "MIT", "authors": [ { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" } ], "require": { "php": ">=5.4.0", "guzzlehttp/ringphp": "^1.1", "react/promise": "^2.2" }, "require-dev": { "ext-curl": "*", "phpunit/phpunit": "^4.0" }, "autoload": { "psr-4": { "GuzzleHttp\\": "src/" } }, "autoload-dev": { "psr-4": { "GuzzleHttp\\Tests\\": "tests/" } }, "scripts": { "test": "make test", "test-ci": "make coverage" } } src/BatchResults.php000064400000006311151742725260010463 0ustar00hash = $hash; } /** * Get the keys that are available on the batch result. * * @return array */ public function getKeys() { return iterator_to_array($this->hash); } /** * Gets a result from the container for the given object. When getting * results for a batch of requests, provide the request object. * * @param object $forObject Object to retrieve the result for. * * @return mixed|null */ public function getResult($forObject) { return isset($this->hash[$forObject]) ? $this->hash[$forObject] : null; } /** * Get an array of successful results. * * @return array */ public function getSuccessful() { $results = []; foreach ($this->hash as $key) { if (!($this->hash[$key] instanceof \Exception)) { $results[] = $this->hash[$key]; } } return $results; } /** * Get an array of failed results. * * @return array */ public function getFailures() { $results = []; foreach ($this->hash as $key) { if ($this->hash[$key] instanceof \Exception) { $results[] = $this->hash[$key]; } } return $results; } /** * Allows iteration over all batch result values. * * @return \ArrayIterator */ public function getIterator() { $results = []; foreach ($this->hash as $key) { $results[] = $this->hash[$key]; } return new \ArrayIterator($results); } /** * Counts the number of elements in the batch result. * * @return int */ public function count() { return count($this->hash); } /** * Checks if the batch contains a specific numerical array index. * * @param int $key Index to access * * @return bool */ public function offsetExists($key) { return $key < count($this->hash); } /** * Allows access of the batch using a numerical array index. * * @param int $key Index to access. * * @return mixed|null */ public function offsetGet($key) { $i = -1; foreach ($this->hash as $obj) { if ($key === ++$i) { return $this->hash[$obj]; } } return null; } public function offsetUnset($key) { throw new \RuntimeException('Not implemented'); } public function offsetSet($key, $value) { throw new \RuntimeException('Not implemented'); } } src/Client.php000064400000027430151742725260007303 0ustar00 [ * 'http://www.foo.com/{version}/', * ['version' => '123'] * ], * 'defaults' => [ * 'timeout' => 10, * 'allow_redirects' => false, * 'proxy' => '192.168.16.1:10' * ] * ]); * * @param array $config Client configuration settings * - base_url: Base URL of the client that is merged into relative URLs. * Can be a string or an array that contains a URI template followed * by an associative array of expansion variables to inject into the * URI template. * - handler: callable RingPHP handler used to transfer requests * - message_factory: Factory used to create request and response object * - defaults: Default request options to apply to each request * - emitter: Event emitter used for request events * - fsm: (internal use only) The request finite state machine. A * function that accepts a transaction and optional final state. The * function is responsible for transitioning a request through its * lifecycle events. */ public function __construct(array $config = []) { $this->configureBaseUrl($config); $this->configureDefaults($config); if (isset($config['emitter'])) { $this->emitter = $config['emitter']; } $this->messageFactory = isset($config['message_factory']) ? $config['message_factory'] : new MessageFactory(); if (isset($config['fsm'])) { $this->fsm = $config['fsm']; } else { if (isset($config['handler'])) { $handler = $config['handler']; } elseif (isset($config['adapter'])) { $handler = $config['adapter']; } else { $handler = Utils::getDefaultHandler(); } $this->fsm = new RequestFsm($handler, $this->messageFactory); } } public function getDefaultOption($keyOrPath = null) { return $keyOrPath === null ? $this->defaults : Utils::getPath($this->defaults, $keyOrPath); } public function setDefaultOption($keyOrPath, $value) { Utils::setPath($this->defaults, $keyOrPath, $value); } public function getBaseUrl() { return (string) $this->baseUrl; } public function createRequest($method, $url = null, array $options = []) { $options = $this->mergeDefaults($options); // Use a clone of the client's emitter $options['config']['emitter'] = clone $this->getEmitter(); $url = $url || (is_string($url) && strlen($url)) ? $this->buildUrl($url) : (string) $this->baseUrl; return $this->messageFactory->createRequest($method, $url, $options); } public function get($url = null, $options = []) { return $this->send($this->createRequest('GET', $url, $options)); } public function head($url = null, array $options = []) { return $this->send($this->createRequest('HEAD', $url, $options)); } public function delete($url = null, array $options = []) { return $this->send($this->createRequest('DELETE', $url, $options)); } public function put($url = null, array $options = []) { return $this->send($this->createRequest('PUT', $url, $options)); } public function patch($url = null, array $options = []) { return $this->send($this->createRequest('PATCH', $url, $options)); } public function post($url = null, array $options = []) { return $this->send($this->createRequest('POST', $url, $options)); } public function options($url = null, array $options = []) { return $this->send($this->createRequest('OPTIONS', $url, $options)); } public function send(RequestInterface $request) { $isFuture = $request->getConfig()->get('future'); $trans = new Transaction($this, $request, $isFuture); $fn = $this->fsm; try { $fn($trans); if ($isFuture) { // Turn the normal response into a future if needed. return $trans->response instanceof FutureInterface ? $trans->response : new FutureResponse(new FulfilledPromise($trans->response)); } // Resolve deep futures if this is not a future // transaction. This accounts for things like retries // that do not have an immediate side-effect. while ($trans->response instanceof FutureInterface) { $trans->response = $trans->response->wait(); } return $trans->response; } catch (\Exception $e) { if ($isFuture) { // Wrap the exception in a promise return new FutureResponse(new RejectedPromise($e)); } throw RequestException::wrapException($trans->request, $e); } catch (\TypeError $error) { $exception = new \Exception($error->getMessage(), $error->getCode(), $error); if ($isFuture) { // Wrap the exception in a promise return new FutureResponse(new RejectedPromise($exception)); } throw RequestException::wrapException($trans->request, $exception); } } /** * Get an array of default options to apply to the client * * @return array */ protected function getDefaultOptions() { $settings = [ 'allow_redirects' => true, 'exceptions' => true, 'decode_content' => true, 'verify' => true ]; // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. // We can only trust the HTTP_PROXY environment variable in a CLI // process due to the fact that PHP has no reliable mechanism to // get environment variables that start with "HTTP_". if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) { $settings['proxy']['http'] = getenv('HTTP_PROXY'); } if ($proxy = getenv('HTTPS_PROXY')) { $settings['proxy']['https'] = $proxy; } return $settings; } /** * Expand a URI template and inherit from the base URL if it's relative * * @param string|array $url URL or an array of the URI template to expand * followed by a hash of template varnames. * @return string * @throws \InvalidArgumentException */ private function buildUrl($url) { // URI template (absolute or relative) if (!is_array($url)) { return strpos($url, '://') ? (string) $url : (string) $this->baseUrl->combine($url); } if (!isset($url[1])) { throw new \InvalidArgumentException('You must provide a hash of ' . 'varname options in the second element of a URL array.'); } // Absolute URL if (strpos($url[0], '://')) { return Utils::uriTemplate($url[0], $url[1]); } // Combine the relative URL with the base URL return (string) $this->baseUrl->combine( Utils::uriTemplate($url[0], $url[1]) ); } private function configureBaseUrl(&$config) { if (!isset($config['base_url'])) { $this->baseUrl = new Url('', ''); } elseif (!is_array($config['base_url'])) { $this->baseUrl = Url::fromString($config['base_url']); } elseif (count($config['base_url']) < 2) { throw new \InvalidArgumentException('You must provide a hash of ' . 'varname options in the second element of a base_url array.'); } else { $this->baseUrl = Url::fromString( Utils::uriTemplate( $config['base_url'][0], $config['base_url'][1] ) ); $config['base_url'] = (string) $this->baseUrl; } } private function configureDefaults($config) { if (!isset($config['defaults'])) { $this->defaults = $this->getDefaultOptions(); } else { $this->defaults = array_replace( $this->getDefaultOptions(), $config['defaults'] ); } // Add the default user-agent header if (!isset($this->defaults['headers'])) { $this->defaults['headers'] = [ 'User-Agent' => Utils::getDefaultUserAgent() ]; } elseif (!Core::hasHeader($this->defaults, 'User-Agent')) { // Add the User-Agent header if one was not already set $this->defaults['headers']['User-Agent'] = Utils::getDefaultUserAgent(); } } /** * Merges default options into the array passed by reference. * * @param array $options Options to modify by reference * * @return array */ private function mergeDefaults($options) { $defaults = $this->defaults; // Case-insensitively merge in default headers if both defaults and // options have headers specified. if (!empty($defaults['headers']) && !empty($options['headers'])) { // Create a set of lowercased keys that are present. $lkeys = []; foreach (array_keys($options['headers']) as $k) { $lkeys[strtolower($k)] = true; } // Merge in lowercase default keys when not present in above set. foreach ($defaults['headers'] as $key => $value) { if (!isset($lkeys[strtolower($key)])) { $options['headers'][$key] = $value; } } // No longer need to merge in headers. unset($defaults['headers']); } $result = array_replace_recursive($defaults, $options); foreach ($options as $k => $v) { if ($v === null) { unset($result[$k]); } } return $result; } /** * @deprecated Use {@see GuzzleHttp\Pool} instead. * @see GuzzleHttp\Pool */ public function sendAll($requests, array $options = []) { Pool::send($this, $requests, $options); } /** * @deprecated Use GuzzleHttp\Utils::getDefaultHandler */ public static function getDefaultHandler() { return Utils::getDefaultHandler(); } /** * @deprecated Use GuzzleHttp\Utils::getDefaultUserAgent */ public static function getDefaultUserAgent() { return Utils::getDefaultUserAgent(); } } src/ClientInterface.php000064400000011604151742725260011120 0ustar00data = $data; } /** * Create a new collection from an array, validate the keys, and add default * values where missing * * @param array $config Configuration values to apply. * @param array $defaults Default parameters * @param array $required Required parameter names * * @return self * @throws \InvalidArgumentException if a parameter is missing */ public static function fromConfig( array $config = [], array $defaults = [], array $required = [] ) { $data = $config + $defaults; if ($missing = array_diff($required, array_keys($data))) { throw new \InvalidArgumentException( 'Config is missing the following keys: ' . implode(', ', $missing)); } return new self($data); } /** * Removes all key value pairs */ public function clear() { $this->data = []; } /** * Get a specific key value. * * @param string $key Key to retrieve. * * @return mixed|null Value of the key or NULL */ public function get($key) { return isset($this->data[$key]) ? $this->data[$key] : null; } /** * Set a key value pair * * @param string $key Key to set * @param mixed $value Value to set */ public function set($key, $value) { $this->data[$key] = $value; } /** * Add a value to a key. If a key of the same name has already been added, * the key value will be converted into an array and the new value will be * pushed to the end of the array. * * @param string $key Key to add * @param mixed $value Value to add to the key */ public function add($key, $value) { if (!array_key_exists($key, $this->data)) { $this->data[$key] = $value; } elseif (is_array($this->data[$key])) { $this->data[$key][] = $value; } else { $this->data[$key] = array($this->data[$key], $value); } } /** * Remove a specific key value pair * * @param string $key A key to remove */ public function remove($key) { unset($this->data[$key]); } /** * Get all keys in the collection * * @return array */ public function getKeys() { return array_keys($this->data); } /** * Returns whether or not the specified key is present. * * @param string $key The key for which to check the existence. * * @return bool */ public function hasKey($key) { return array_key_exists($key, $this->data); } /** * Checks if any keys contains a certain value * * @param string $value Value to search for * * @return mixed Returns the key if the value was found FALSE if the value * was not found. */ public function hasValue($value) { return array_search($value, $this->data, true); } /** * Replace the data of the object with the value of an array * * @param array $data Associative array of data */ public function replace(array $data) { $this->data = $data; } /** * Add and merge in a Collection or array of key value pair data. * * @param Collection|array $data Associative array of key value pair data */ public function merge($data) { foreach ($data as $key => $value) { $this->add($key, $value); } } /** * Overwrite key value pairs in this collection with all of the data from * an array or collection. * * @param array|\Traversable $data Values to override over this config */ public function overwriteWith($data) { if (is_array($data)) { $this->data = $data + $this->data; } elseif ($data instanceof Collection) { $this->data = $data->toArray() + $this->data; } else { foreach ($data as $key => $value) { $this->data[$key] = $value; } } } /** * Returns a Collection containing all the elements of the collection after * applying the callback function to each one. * * The callable should accept three arguments: * - (string) $key * - (string) $value * - (array) $context * * The callable must return a the altered or unaltered value. * * @param callable $closure Map function to apply * @param array $context Context to pass to the callable * * @return Collection */ public function map(callable $closure, array $context = []) { $collection = new static(); foreach ($this as $key => $value) { $collection[$key] = $closure($key, $value, $context); } return $collection; } /** * Iterates over each key value pair in the collection passing them to the * callable. If the callable returns true, the current value from input is * returned into the result Collection. * * The callable must accept two arguments: * - (string) $key * - (string) $value * * @param callable $closure Evaluation function * * @return Collection */ public function filter(callable $closure) { $collection = new static(); foreach ($this->data as $key => $value) { if ($closure($key, $value)) { $collection[$key] = $value; } } return $collection; } } src/Cookie/CookieJar.php000064400000016430151742725260011142 0ustar00strictMode = $strictMode; foreach ($cookieArray as $cookie) { if (!($cookie instanceof SetCookie)) { $cookie = new SetCookie($cookie); } $this->setCookie($cookie); } } /** * Create a new Cookie jar from an associative array and domain. * * @param array $cookies Cookies to create the jar from * @param string $domain Domain to set the cookies to * * @return self */ public static function fromArray(array $cookies, $domain) { $cookieJar = new self(); foreach ($cookies as $name => $value) { $cookieJar->setCookie(new SetCookie([ 'Domain' => $domain, 'Name' => $name, 'Value' => $value, 'Discard' => true ])); } return $cookieJar; } /** * Quote the cookie value if it is not already quoted and it contains * problematic characters. * * @param string $value Value that may or may not need to be quoted * * @return string */ public static function getCookieValue($value) { if (substr($value, 0, 1) !== '"' && substr($value, -1, 1) !== '"' && strpbrk($value, ';,') ) { $value = '"' . $value . '"'; } return $value; } public function toArray() { return array_map(function (SetCookie $cookie) { return $cookie->toArray(); }, $this->getIterator()->getArrayCopy()); } public function clear($domain = null, $path = null, $name = null) { if (!$domain) { $this->cookies = []; return; } elseif (!$path) { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain) { return !$cookie->matchesDomain($domain); } ); } elseif (!$name) { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain) { return !($cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } else { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain, $name) { return !($cookie->getName() == $name && $cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } } public function clearSessionCookies() { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) { return !$cookie->getDiscard() && $cookie->getExpires(); } ); } public function setCookie(SetCookie $cookie) { // Only allow cookies with set and valid domain, name, value $result = $cookie->validate(); if ($result !== true) { if ($this->strictMode) { throw new \RuntimeException('Invalid cookie: ' . $result); } else { $this->removeCookieIfEmpty($cookie); return false; } } // Resolve conflicts with previously set cookies foreach ($this->cookies as $i => $c) { // Two cookies are identical, when their path, and domain are // identical. if ($c->getPath() != $cookie->getPath() || $c->getDomain() != $cookie->getDomain() || $c->getName() != $cookie->getName() ) { continue; } // The previously set cookie is a discard cookie and this one is // not so allow the new cookie to be set if (!$cookie->getDiscard() && $c->getDiscard()) { unset($this->cookies[$i]); continue; } // If the new cookie's expiration is further into the future, then // replace the old cookie if ($cookie->getExpires() > $c->getExpires()) { unset($this->cookies[$i]); continue; } // If the value has changed, we better change it if ($cookie->getValue() !== $c->getValue()) { unset($this->cookies[$i]); continue; } // The cookie exists, so no need to continue return false; } $this->cookies[] = $cookie; return true; } public function count() { return count($this->cookies); } public function getIterator() { return new \ArrayIterator(array_values($this->cookies)); } public function extractCookies( RequestInterface $request, ResponseInterface $response ) { if ($cookieHeader = $response->getHeaderAsArray('Set-Cookie')) { foreach ($cookieHeader as $cookie) { $sc = SetCookie::fromString($cookie); if (!$sc->getDomain()) { $sc->setDomain($request->getHost()); } $this->setCookie($sc); } } } public function addCookieHeader(RequestInterface $request) { $values = []; $scheme = $request->getScheme(); $host = $request->getHost(); $path = $request->getPath(); foreach ($this->cookies as $cookie) { if ($cookie->matchesPath($path) && $cookie->matchesDomain($host) && !$cookie->isExpired() && (!$cookie->getSecure() || $scheme == 'https') ) { $values[] = $cookie->getName() . '=' . self::getCookieValue($cookie->getValue()); } } if ($values) { $request->setHeader('Cookie', implode('; ', $values)); } } /** * If a cookie already exists and the server asks to set it again with a * null value, the cookie must be deleted. * * @param SetCookie $cookie */ private function removeCookieIfEmpty(SetCookie $cookie) { $cookieValue = $cookie->getValue(); if ($cookieValue === null || $cookieValue === '') { $this->clear( $cookie->getDomain(), $cookie->getPath(), $cookie->getName() ); } } } src/Cookie/CookieJarInterface.php000064400000004774151742725260012773 0ustar00filename = $cookieFile; if (file_exists($cookieFile)) { $this->load($cookieFile); } } /** * Saves the file when shutting down */ public function __destruct() { $this->save($this->filename); } /** * Saves the cookies to a file. * * @param string $filename File to save * @throws \RuntimeException if the file cannot be found or created */ public function save($filename) { $json = []; foreach ($this as $cookie) { if ($cookie->getExpires() && !$cookie->getDiscard()) { $json[] = $cookie->toArray(); } } if (false === file_put_contents($filename, json_encode($json))) { // @codeCoverageIgnoreStart throw new \RuntimeException("Unable to save file {$filename}"); // @codeCoverageIgnoreEnd } } /** * Load cookies from a JSON formatted file. * * Old cookies are kept unless overwritten by newly loaded ones. * * @param string $filename Cookie file to load. * @throws \RuntimeException if the file cannot be loaded. */ public function load($filename) { $json = file_get_contents($filename); if (false === $json) { // @codeCoverageIgnoreStart throw new \RuntimeException("Unable to load file {$filename}"); // @codeCoverageIgnoreEnd } $data = Utils::jsonDecode($json, true); if (is_array($data)) { foreach (Utils::jsonDecode($json, true) as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (strlen($data)) { throw new \RuntimeException("Invalid cookie file: {$filename}"); } } } src/Cookie/SessionCookieJar.php000064400000002772151742725260012512 0ustar00sessionKey = $sessionKey; $this->load(); } /** * Saves cookies to session when shutting down */ public function __destruct() { $this->save(); } /** * Save cookies to the client session */ public function save() { $json = []; foreach ($this as $cookie) { if ($cookie->getExpires() && !$cookie->getDiscard()) { $json[] = $cookie->toArray(); } } $_SESSION[$this->sessionKey] = json_encode($json); } /** * Load the contents of the client session into the data array */ protected function load() { $cookieJar = isset($_SESSION[$this->sessionKey]) ? $_SESSION[$this->sessionKey] : null; $data = Utils::jsonDecode($cookieJar, true); if (is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (strlen($data)) { throw new \RuntimeException("Invalid cookie data"); } } } src/Cookie/SetCookie.php000064400000021737151742725260011167 0ustar00 null, 'Value' => null, 'Domain' => null, 'Path' => '/', 'Max-Age' => null, 'Expires' => null, 'Secure' => false, 'Discard' => false, 'HttpOnly' => false ]; /** @var array Cookie data */ private $data; /** * Create a new SetCookie object from a string * * @param string $cookie Set-Cookie header string * * @return self */ public static function fromString($cookie) { // Create the default return array $data = self::$defaults; // Explode the cookie string using a series of semicolons $pieces = array_filter(array_map('trim', explode(';', $cookie))); // The name of the cookie (first kvp) must include an equal sign. if (empty($pieces) || !strpos($pieces[0], '=')) { return new self($data); } // Add the cookie pieces into the parsed data array foreach ($pieces as $part) { $cookieParts = explode('=', $part, 2); $key = trim($cookieParts[0]); $value = isset($cookieParts[1]) ? trim($cookieParts[1], " \n\r\t\0\x0B\"") : true; // Only check for non-cookies when cookies have been found if (empty($data['Name'])) { $data['Name'] = $key; $data['Value'] = $value; } else { foreach (array_keys(self::$defaults) as $search) { if (!strcasecmp($search, $key)) { $data[$search] = $value; continue 2; } } $data[$key] = $value; } } return new self($data); } /** * @param array $data Array of cookie data provided by a Cookie parser */ public function __construct(array $data = []) { $this->data = array_replace(self::$defaults, $data); // Extract the Expires value and turn it into a UNIX timestamp if needed if (!$this->getExpires() && $this->getMaxAge()) { // Calculate the Expires date $this->setExpires(time() + $this->getMaxAge()); } elseif ($this->getExpires() && !is_numeric($this->getExpires())) { $this->setExpires($this->getExpires()); } } public function __toString() { $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; foreach ($this->data as $k => $v) { if ($k != 'Name' && $k != 'Value' && $v !== null && $v !== false) { if ($k == 'Expires') { $str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; } else { $str .= ($v === true ? $k : "{$k}={$v}") . '; '; } } } return rtrim($str, '; '); } public function toArray() { return $this->data; } /** * Get the cookie name * * @return string */ public function getName() { return $this->data['Name']; } /** * Set the cookie name * * @param string $name Cookie name */ public function setName($name) { $this->data['Name'] = $name; } /** * Get the cookie value * * @return string */ public function getValue() { return $this->data['Value']; } /** * Set the cookie value * * @param string $value Cookie value */ public function setValue($value) { $this->data['Value'] = $value; } /** * Get the domain * * @return string|null */ public function getDomain() { return $this->data['Domain']; } /** * Set the domain of the cookie * * @param string $domain */ public function setDomain($domain) { $this->data['Domain'] = $domain; } /** * Get the path * * @return string */ public function getPath() { return $this->data['Path']; } /** * Set the path of the cookie * * @param string $path Path of the cookie */ public function setPath($path) { $this->data['Path'] = $path; } /** * Maximum lifetime of the cookie in seconds * * @return int|null */ public function getMaxAge() { return $this->data['Max-Age']; } /** * Set the max-age of the cookie * * @param int $maxAge Max age of the cookie in seconds */ public function setMaxAge($maxAge) { $this->data['Max-Age'] = $maxAge; } /** * The UNIX timestamp when the cookie Expires * * @return mixed */ public function getExpires() { return $this->data['Expires']; } /** * Set the unix timestamp for which the cookie will expire * * @param int $timestamp Unix timestamp */ public function setExpires($timestamp) { $this->data['Expires'] = is_numeric($timestamp) ? (int) $timestamp : strtotime($timestamp); } /** * Get whether or not this is a secure cookie * * @return null|bool */ public function getSecure() { return $this->data['Secure']; } /** * Set whether or not the cookie is secure * * @param bool $secure Set to true or false if secure */ public function setSecure($secure) { $this->data['Secure'] = $secure; } /** * Get whether or not this is a session cookie * * @return null|bool */ public function getDiscard() { return $this->data['Discard']; } /** * Set whether or not this is a session cookie * * @param bool $discard Set to true or false if this is a session cookie */ public function setDiscard($discard) { $this->data['Discard'] = $discard; } /** * Get whether or not this is an HTTP only cookie * * @return bool */ public function getHttpOnly() { return $this->data['HttpOnly']; } /** * Set whether or not this is an HTTP only cookie * * @param bool $httpOnly Set to true or false if this is HTTP only */ public function setHttpOnly($httpOnly) { $this->data['HttpOnly'] = $httpOnly; } /** * Check if the cookie matches a path value * * @param string $path Path to check against * * @return bool */ public function matchesPath($path) { return !$this->getPath() || 0 === stripos($path, $this->getPath()); } /** * Check if the cookie matches a domain value * * @param string $domain Domain to check against * * @return bool */ public function matchesDomain($domain) { // Remove the leading '.' as per spec in RFC 6265. // http://tools.ietf.org/html/rfc6265#section-5.2.3 $cookieDomain = ltrim($this->getDomain(), '.'); // Domain not set or exact match. if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { return true; } // Matching the subdomain according to RFC 6265. // http://tools.ietf.org/html/rfc6265#section-5.1.3 if (filter_var($domain, FILTER_VALIDATE_IP)) { return false; } return (bool) preg_match('/\.' . preg_quote($cookieDomain) . '$/i', $domain); } /** * Check if the cookie is expired * * @return bool */ public function isExpired() { return $this->getExpires() !== null && time() > $this->getExpires(); } /** * Check if the cookie is valid according to RFC 6265 * * @return bool|string Returns true if valid or an error message if invalid */ public function validate() { // Names must not be empty, but can be 0 $name = $this->getName(); if (empty($name) && !is_numeric($name)) { return 'The cookie name must not be empty'; } // Check if any of the invalid characters are present in the cookie name if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { return "Cookie name must not cannot invalid characters: =,; \\t\\r\\n\\013\\014"; } // Value must not be empty, but can be 0 $value = $this->getValue(); if (empty($value) && !is_numeric($value)) { return 'The cookie value must not be empty'; } // Domains must not be empty, but can be 0 // A "0" is not a valid internet domain, but may be used as server name // in a private network. $domain = $this->getDomain(); if (empty($domain) && !is_numeric($domain)) { return 'The cookie domain must not be empty'; } return true; } } src/Event/AbstractEvent.php000064400000000571151742725260011710 0ustar00propagationStopped; } public function stopPropagation() { $this->propagationStopped = true; } } src/Event/AbstractRequestEvent.php000064400000002241151742725260013255 0ustar00transaction = $transaction; } /** * Get the HTTP client associated with the event. * * @return ClientInterface */ public function getClient() { return $this->transaction->client; } /** * Get the request object * * @return RequestInterface */ public function getRequest() { return $this->transaction->request; } /** * Get the number of transaction retries. * * @return int */ public function getRetryCount() { return $this->transaction->retries; } /** * @return Transaction */ public function getTransaction() { return $this->transaction; } } src/Event/AbstractRetryableEvent.php000064400000002701151742725260013557 0ustar00transaction->state = 'retry'; if ($afterDelay) { $this->transaction->request->getConfig()->set('delay', $afterDelay); } $this->stopPropagation(); } } src/Event/AbstractTransferEvent.php000064400000003154151742725260013415 0ustar00transaction->transferInfo; } return isset($this->transaction->transferInfo[$name]) ? $this->transaction->transferInfo[$name] : null; } /** * Returns true/false if a response is available. * * @return bool */ public function hasResponse() { return !($this->transaction->response instanceof FutureInterface); } /** * Get the response. * * @return ResponseInterface|null */ public function getResponse() { return $this->hasResponse() ? $this->transaction->response : null; } /** * Intercept the request and associate a response * * @param ResponseInterface $response Response to set */ public function intercept(ResponseInterface $response) { $this->transaction->response = $response; $this->transaction->exception = null; $this->stopPropagation(); } } src/Event/BeforeEvent.php000064400000001304151742725260011342 0ustar00transaction->response = $response; $this->transaction->exception = null; $this->stopPropagation(); } } src/Event/CompleteEvent.php000064400000000664151742725260011720 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher */ class Emitter implements EmitterInterface { /** @var array */ private $listeners = []; /** @var array */ private $sorted = []; public function on($eventName, callable $listener, $priority = 0) { if ($priority === 'first') { $priority = isset($this->listeners[$eventName]) ? max(array_keys($this->listeners[$eventName])) + 1 : 1; } elseif ($priority === 'last') { $priority = isset($this->listeners[$eventName]) ? min(array_keys($this->listeners[$eventName])) - 1 : -1; } $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName]); } public function once($eventName, callable $listener, $priority = 0) { $onceListener = function ( EventInterface $event ) use (&$onceListener, $eventName, $listener, $priority) { $this->removeListener($eventName, $onceListener); $listener($event, $eventName); }; $this->on($eventName, $onceListener, $priority); } public function removeListener($eventName, callable $listener) { if (empty($this->listeners[$eventName])) { return; } foreach ($this->listeners[$eventName] as $priority => $listeners) { if (false !== ($key = array_search($listener, $listeners, true))) { unset( $this->listeners[$eventName][$priority][$key], $this->sorted[$eventName] ); } } } public function listeners($eventName = null) { // Return all events in a sorted priority order if ($eventName === null) { foreach (array_keys($this->listeners) as $eventName) { if (empty($this->sorted[$eventName])) { $this->listeners($eventName); } } return $this->sorted; } // Return the listeners for a specific event, sorted in priority order if (empty($this->sorted[$eventName])) { $this->sorted[$eventName] = []; if (isset($this->listeners[$eventName])) { krsort($this->listeners[$eventName], SORT_NUMERIC); foreach ($this->listeners[$eventName] as $listeners) { foreach ($listeners as $listener) { $this->sorted[$eventName][] = $listener; } } } } return $this->sorted[$eventName]; } public function hasListeners($eventName) { return !empty($this->listeners[$eventName]); } public function emit($eventName, EventInterface $event) { if (isset($this->listeners[$eventName])) { foreach ($this->listeners($eventName) as $listener) { $listener($event, $eventName); if ($event->isPropagationStopped()) { break; } } } return $event; } public function attach(SubscriberInterface $subscriber) { foreach ($subscriber->getEvents() as $eventName => $listeners) { if (is_array($listeners[0])) { foreach ($listeners as $listener) { $this->on( $eventName, [$subscriber, $listener[0]], isset($listener[1]) ? $listener[1] : 0 ); } } else { $this->on( $eventName, [$subscriber, $listeners[0]], isset($listeners[1]) ? $listeners[1] : 0 ); } } } public function detach(SubscriberInterface $subscriber) { foreach ($subscriber->getEvents() as $eventName => $listener) { $this->removeListener($eventName, [$subscriber, $listener[0]]); } } } src/Event/EmitterInterface.php000064400000006726151742725260012405 0ustar00transaction->exception; } } src/Event/ErrorEvent.php000064400000001244151742725260011234 0ustar00transaction->exception; } } src/Event/EventInterface.php000064400000001041151742725260012036 0ustar00emitter) { $this->emitter = new Emitter(); } return $this->emitter; } } src/Event/ListenerAttacherTrait.php000064400000006424151742725260013413 0ustar00getEmitter(); foreach ($listeners as $el) { if ($el['once']) { $emitter->once($el['name'], $el['fn'], $el['priority']); } else { $emitter->on($el['name'], $el['fn'], $el['priority']); } } } /** * Extracts the allowed events from the provided array, and ignores anything * else in the array. The event listener must be specified as a callable or * as an array of event listener data ("name", "fn", "priority", "once"). * * @param array $source Array containing callables or hashes of data to be * prepared as event listeners. * @param array $events Names of events to look for in the provided $source * array. Other keys are ignored. * @return array */ private function prepareListeners(array $source, array $events) { $listeners = []; foreach ($events as $name) { if (isset($source[$name])) { $this->buildListener($name, $source[$name], $listeners); } } return $listeners; } /** * Creates a complete event listener definition from the provided array of * listener data. Also works recursively if more than one listeners are * contained in the provided array. * * @param string $name Name of the event the listener is for. * @param array|callable $data Event listener data to prepare. * @param array $listeners Array of listeners, passed by reference. * * @throws \InvalidArgumentException if the event data is malformed. */ private function buildListener($name, $data, &$listeners) { static $defaults = ['priority' => 0, 'once' => false]; // If a callable is provided, normalize it to the array format. if (is_callable($data)) { $data = ['fn' => $data]; } // Prepare the listener and add it to the array, recursively. if (isset($data['fn'])) { $data['name'] = $name; $listeners[] = $data + $defaults; } elseif (is_array($data)) { foreach ($data as $listenerData) { $this->buildListener($name, $listenerData, $listeners); } } else { throw new \InvalidArgumentException('Each event listener must be a ' . 'callable or an associative array containing a "fn" key.'); } } } src/Event/ProgressEvent.php000064400000003137151742725260011752 0ustar00downloadSize = $downloadSize; $this->downloaded = $downloaded; $this->uploadSize = $uploadSize; $this->uploaded = $uploaded; } } src/Event/RequestEvents.php000064400000003142151742725260011755 0ustar00 ['methodName']] * - ['eventName' => ['methodName', $priority]] * - ['eventName' => [['methodName'], ['otherMethod']] * - ['eventName' => [['methodName'], ['otherMethod', $priority]] * - ['eventName' => [['methodName', $priority], ['otherMethod', $priority]] * * @return array */ public function getEvents(); } src/Exception/BadResponseException.php000064400000000240151742725260014075 0ustar00response = $response; } /** * Get the associated response * * @return ResponseInterface|null */ public function getResponse() { return $this->response; } } src/Exception/RequestException.php000064400000006535151742725260013335 0ustar00getStatusCode() : 0; parent::__construct($message, $code, $previous); $this->request = $request; $this->response = $response; } /** * Wrap non-RequestExceptions with a RequestException * * @param RequestInterface $request * @param \Exception $e * * @return RequestException */ public static function wrapException(RequestInterface $request, \Exception $e) { if ($e instanceof RequestException) { return $e; } elseif ($e instanceof ConnectException) { return new HttpConnectException($e->getMessage(), $request, null, $e); } else { return new RequestException($e->getMessage(), $request, null, $e); } } /** * Factory method to create a new exception with a normalized error message * * @param RequestInterface $request Request * @param ResponseInterface $response Response received * @param \Exception $previous Previous exception * * @return self */ public static function create( RequestInterface $request, ResponseInterface $response = null, \Exception $previous = null ) { if (!$response) { return new self('Error completing request', $request, null, $previous); } $level = floor($response->getStatusCode() / 100); if ($level == '4') { $label = 'Client error response'; $className = __NAMESPACE__ . '\\ClientException'; } elseif ($level == '5') { $label = 'Server error response'; $className = __NAMESPACE__ . '\\ServerException'; } else { $label = 'Unsuccessful response'; $className = __CLASS__; } $message = $label . ' [url] ' . $request->getUrl() . ' [status code] ' . $response->getStatusCode() . ' [reason phrase] ' . $response->getReasonPhrase(); return new $className($message, $request, $response, $previous); } /** * Get the request that caused the exception * * @return RequestInterface */ public function getRequest() { return $this->request; } /** * Get the associated response * * @return ResponseInterface|null */ public function getResponse() { return $this->response; } /** * Check if a response was received * * @return bool */ public function hasResponse() { return $this->response !== null; } } src/Exception/ServerException.php000064400000000241151742725260013137 0ustar00error = $error; } /** * Get the associated error * * @return \LibXMLError|null */ public function getError() { return $this->error; } } src/HasDataTrait.php000064400000003164151742725260010374 0ustar00data); } public function offsetGet($offset) { return isset($this->data[$offset]) ? $this->data[$offset] : null; } public function offsetSet($offset, $value) { $this->data[$offset] = $value; } public function offsetExists($offset) { return isset($this->data[$offset]); } public function offsetUnset($offset) { unset($this->data[$offset]); } public function toArray() { return $this->data; } public function count() { return count($this->data); } /** * Get a value from the collection using a path syntax to retrieve nested * data. * * @param string $path Path to traverse and retrieve a value from * * @return mixed|null */ public function getPath($path) { return Utils::getPath($this->data, $path); } /** * Set a value into a nested array key. Keys will be created as needed to * set the value. * * @param string $path Path to set * @param mixed $value Value to set at the key * * @throws \RuntimeException when trying to setPath using a nested path * that travels through a scalar value */ public function setPath($path, $value) { Utils::setPath($this->data, $path, $value); } } src/Message/AbstractMessage.php000064400000016171151742725260012521 0ustar00getBody(); } public function getProtocolVersion() { return $this->protocolVersion; } public function getBody() { return $this->body; } public function setBody(StreamInterface $body = null) { if ($body === null) { // Setting a null body will remove the body of the request $this->removeHeader('Content-Length'); $this->removeHeader('Transfer-Encoding'); } $this->body = $body; } public function addHeader($header, $value) { if (is_array($value)) { $current = array_merge($this->getHeaderAsArray($header), $value); } else { $current = $this->getHeaderAsArray($header); $current[] = (string) $value; } $this->setHeader($header, $current); } public function addHeaders(array $headers) { foreach ($headers as $name => $header) { $this->addHeader($name, $header); } } public function getHeader($header) { $name = strtolower($header); return isset($this->headers[$name]) ? implode(', ', $this->headers[$name]) : ''; } public function getHeaderAsArray($header) { $name = strtolower($header); return isset($this->headers[$name]) ? $this->headers[$name] : []; } public function getHeaders() { $headers = []; foreach ($this->headers as $name => $values) { $headers[$this->headerNames[$name]] = $values; } return $headers; } public function setHeader($header, $value) { $header = trim($header); $name = strtolower($header); $this->headerNames[$name] = $header; if (is_array($value)) { foreach ($value as &$v) { $v = trim($v); } $this->headers[$name] = $value; } else { $this->headers[$name] = [trim($value)]; } } public function setHeaders(array $headers) { $this->headers = $this->headerNames = []; foreach ($headers as $key => $value) { $this->addHeader($key, $value); } } public function hasHeader($header) { return isset($this->headers[strtolower($header)]); } public function removeHeader($header) { $name = strtolower($header); unset($this->headers[$name], $this->headerNames[$name]); } /** * Parse an array of header values containing ";" separated data into an * array of associative arrays representing the header key value pair * data of the header. When a parameter does not contain a value, but just * contains a key, this function will inject a key with a '' string value. * * @param MessageInterface $message That contains the header * @param string $header Header to retrieve from the message * * @return array Returns the parsed header values. */ public static function parseHeader(MessageInterface $message, $header) { static $trimmed = "\"' \n\t\r"; $params = $matches = []; foreach (self::normalizeHeader($message, $header) as $val) { $part = []; foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { $m = $matches[0]; if (isset($m[1])) { $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); } else { $part[] = trim($m[0], $trimmed); } } } if ($part) { $params[] = $part; } } return $params; } /** * Converts an array of header values that may contain comma separated * headers into an array of headers with no comma separated values. * * @param MessageInterface $message That contains the header * @param string $header Header to retrieve from the message * * @return array Returns the normalized header field values. */ public static function normalizeHeader(MessageInterface $message, $header) { $h = $message->getHeaderAsArray($header); for ($i = 0, $total = count($h); $i < $total; $i++) { if (strpos($h[$i], ',') === false) { continue; } foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $h[$i]) as $v) { $h[] = trim($v); } unset($h[$i]); } return $h; } /** * Gets the start-line and headers of a message as a string * * @param MessageInterface $message * * @return string */ public static function getStartLineAndHeaders(MessageInterface $message) { return static::getStartLine($message) . self::getHeadersAsString($message); } /** * Gets the headers of a message as a string * * @param MessageInterface $message * * @return string */ public static function getHeadersAsString(MessageInterface $message) { $result = ''; foreach ($message->getHeaders() as $name => $values) { $result .= "\r\n{$name}: " . implode(', ', $values); } return $result; } /** * Gets the start line of a message * * @param MessageInterface $message * * @return string * @throws \InvalidArgumentException */ public static function getStartLine(MessageInterface $message) { if ($message instanceof RequestInterface) { return trim($message->getMethod() . ' ' . $message->getResource()) . ' HTTP/' . $message->getProtocolVersion(); } elseif ($message instanceof ResponseInterface) { return 'HTTP/' . $message->getProtocolVersion() . ' ' . $message->getStatusCode() . ' ' . $message->getReasonPhrase(); } else { throw new \InvalidArgumentException('Unknown message type'); } } /** * Accepts and modifies the options provided to the message in the * constructor. * * Can be overridden in subclasses as necessary. * * @param array $options Options array passed by reference. */ protected function handleOptions(array &$options) { if (isset($options['protocol_version'])) { $this->protocolVersion = $options['protocol_version']; } } } src/Message/AppliesHeadersInterface.php000064400000001414151742725260014155 0ustar00then($onFulfilled, $onRejected, $onProgress), [$future, 'wait'], [$future, 'cancel'] ); } public function getStatusCode() { return $this->_value->getStatusCode(); } public function setStatusCode($code) { $this->_value->setStatusCode($code); } public function getReasonPhrase() { return $this->_value->getReasonPhrase(); } public function setReasonPhrase($phrase) { $this->_value->setReasonPhrase($phrase); } public function getEffectiveUrl() { return $this->_value->getEffectiveUrl(); } public function setEffectiveUrl($url) { $this->_value->setEffectiveUrl($url); } public function json(array $config = []) { return $this->_value->json($config); } public function xml(array $config = []) { return $this->_value->xml($config); } public function __toString() { try { return $this->_value->__toString(); } catch (\Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); return ''; } } public function getProtocolVersion() { return $this->_value->getProtocolVersion(); } public function setBody(StreamInterface $body = null) { $this->_value->setBody($body); } public function getBody() { return $this->_value->getBody(); } public function getHeaders() { return $this->_value->getHeaders(); } public function getHeader($header) { return $this->_value->getHeader($header); } public function getHeaderAsArray($header) { return $this->_value->getHeaderAsArray($header); } public function hasHeader($header) { return $this->_value->hasHeader($header); } public function removeHeader($header) { $this->_value->removeHeader($header); } public function addHeader($header, $value) { $this->_value->addHeader($header, $value); } public function addHeaders(array $headers) { $this->_value->addHeaders($headers); } public function setHeader($header, $value) { $this->_value->setHeader($header, $value); } public function setHeaders(array $headers) { $this->_value->setHeaders($headers); } } src/Message/MessageFactory.php000064400000026406151742725260012367 0ustar00 1, 'timeout' => 1, 'verify' => 1, 'ssl_key' => 1, 'cert' => 1, 'proxy' => 1, 'debug' => 1, 'save_to' => 1, 'stream' => 1, 'expect' => 1, 'future' => 1 ]; /** @var array Default allow_redirects request option settings */ private static $defaultRedirect = [ 'max' => 5, 'strict' => false, 'referer' => false, 'protocols' => ['http', 'https'] ]; /** * @param array $customOptions Associative array of custom request option * names mapping to functions used to apply * the option. The function accepts the request * and the option value to apply. */ public function __construct(array $customOptions = []) { $this->errorPlugin = new HttpError(); $this->redirectPlugin = new Redirect(); $this->customOptions = $customOptions; } public function createResponse( $statusCode, array $headers = [], $body = null, array $options = [] ) { if (null !== $body) { $body = Stream::factory($body); } return new Response($statusCode, $headers, $body, $options); } public function createRequest($method, $url, array $options = []) { // Handle the request protocol version option that needs to be // specified in the request constructor. if (isset($options['version'])) { $options['config']['protocol_version'] = $options['version']; unset($options['version']); } $request = new Request($method, $url, [], null, isset($options['config']) ? $options['config'] : []); unset($options['config']); // Use a POST body by default if (strtoupper($method) == 'POST' && !isset($options['body']) && !isset($options['json']) ) { $options['body'] = []; } if ($options) { $this->applyOptions($request, $options); } return $request; } /** * Create a request or response object from an HTTP message string * * @param string $message Message to parse * * @return RequestInterface|ResponseInterface * @throws \InvalidArgumentException if unable to parse a message */ public function fromMessage($message) { static $parser; if (!$parser) { $parser = new MessageParser(); } // Parse a response if (strtoupper(substr($message, 0, 4)) == 'HTTP') { $data = $parser->parseResponse($message); return $this->createResponse( $data['code'], $data['headers'], $data['body'] === '' ? null : $data['body'], $data ); } // Parse a request if (!($data = ($parser->parseRequest($message)))) { throw new \InvalidArgumentException('Unable to parse request'); } return $this->createRequest( $data['method'], Url::buildUrl($data['request_url']), [ 'headers' => $data['headers'], 'body' => $data['body'] === '' ? null : $data['body'], 'config' => [ 'protocol_version' => $data['protocol_version'] ] ] ); } /** * Apply POST fields and files to a request to attempt to give an accurate * representation. * * @param RequestInterface $request Request to update * @param array $body Body to apply */ protected function addPostData(RequestInterface $request, array $body) { static $fields = ['string' => true, 'array' => true, 'NULL' => true, 'boolean' => true, 'double' => true, 'integer' => true]; $post = new PostBody(); foreach ($body as $key => $value) { if (isset($fields[gettype($value)])) { $post->setField($key, $value); } elseif ($value instanceof PostFileInterface) { $post->addFile($value); } else { $post->addFile(new PostFile($key, $value)); } } if ($request->getHeader('Content-Type') == 'multipart/form-data') { $post->forceMultipartUpload(true); } $request->setBody($post); } protected function applyOptions( RequestInterface $request, array $options = [] ) { $config = $request->getConfig(); $emitter = $request->getEmitter(); foreach ($options as $key => $value) { if (isset(self::$configMap[$key])) { $config[$key] = $value; continue; } switch ($key) { case 'allow_redirects': if ($value === false) { continue 2; } if ($value === true) { $value = self::$defaultRedirect; } elseif (!is_array($value)) { throw new Iae('allow_redirects must be true, false, or array'); } else { // Merge the default settings with the provided settings $value += self::$defaultRedirect; } $config['redirect'] = $value; $emitter->attach($this->redirectPlugin); break; case 'decode_content': if ($value === false) { continue 2; } $config['decode_content'] = true; if ($value !== true) { $request->setHeader('Accept-Encoding', $value); } break; case 'headers': if (!is_array($value)) { throw new Iae('header value must be an array'); } foreach ($value as $k => $v) { $request->setHeader($k, $v); } break; case 'exceptions': if ($value === true) { $emitter->attach($this->errorPlugin); } break; case 'body': if (is_array($value)) { $this->addPostData($request, $value); } elseif ($value !== null) { $request->setBody(Stream::factory($value)); } break; case 'auth': if (!$value) { continue 2; } if (is_array($value)) { $type = isset($value[2]) ? strtolower($value[2]) : 'basic'; } else { $type = strtolower($value); } $config['auth'] = $value; if ($type == 'basic') { $request->setHeader( 'Authorization', 'Basic ' . base64_encode("$value[0]:$value[1]") ); } elseif ($type == 'digest') { // @todo: Do not rely on curl $config->setPath('curl/' . CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); $config->setPath('curl/' . CURLOPT_USERPWD, "$value[0]:$value[1]"); } break; case 'query': if ($value instanceof Query) { $original = $request->getQuery(); // Do not overwrite existing query string variables by // overwriting the object with the query string data passed // in the URL $value->overwriteWith($original->toArray()); $request->setQuery($value); } elseif (is_array($value)) { // Do not overwrite existing query string variables $query = $request->getQuery(); foreach ($value as $k => $v) { if (!isset($query[$k])) { $query[$k] = $v; } } } else { throw new Iae('query must be an array or Query object'); } break; case 'cookies': if ($value === true) { static $cookie = null; if (!$cookie) { $cookie = new Cookie(); } $emitter->attach($cookie); } elseif (is_array($value)) { $emitter->attach( new Cookie(CookieJar::fromArray($value, $request->getHost())) ); } elseif ($value instanceof CookieJarInterface) { $emitter->attach(new Cookie($value)); } elseif ($value !== false) { throw new Iae('cookies must be an array, true, or CookieJarInterface'); } break; case 'events': if (!is_array($value)) { throw new Iae('events must be an array'); } $this->attachListeners($request, $this->prepareListeners( $value, ['before', 'complete', 'error', 'progress', 'end'] ) ); break; case 'subscribers': if (!is_array($value)) { throw new Iae('subscribers must be an array'); } foreach ($value as $subscribers) { $emitter->attach($subscribers); } break; case 'json': $request->setBody(Stream::factory(json_encode($value))); if (!$request->hasHeader('Content-Type')) { $request->setHeader('Content-Type', 'application/json'); } break; default: // Check for custom handler functions. if (isset($this->customOptions[$key])) { $fn = $this->customOptions[$key]; $fn($request, $value); continue 2; } throw new Iae("No method can handle the {$key} config key"); } } } } src/Message/MessageFactoryInterface.php000064400000006311151742725260014201 0ustar00getHeaders() as $name => $values) { * echo $name . ": " . implode(", ", $values); * } * * @return array Returns an associative array of the message's headers. */ public function getHeaders(); /** * Retrieve a header by the given case-insensitive name. * * @param string $header Case-insensitive header name. * * @return string */ public function getHeader($header); /** * Retrieves a header by the given case-insensitive name as an array of strings. * * @param string $header Case-insensitive header name. * * @return string[] */ public function getHeaderAsArray($header); /** * Checks if a header exists by the given case-insensitive name. * * @param string $header Case-insensitive header name. * * @return bool Returns true if any header names match the given header * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ public function hasHeader($header); /** * Remove a specific header by case-insensitive name. * * @param string $header Case-insensitive header name. */ public function removeHeader($header); /** * Appends a header value to any existing values associated with the * given header name. * * @param string $header Header name to add * @param string $value Value of the header */ public function addHeader($header, $value); /** * Merges in an associative array of headers. * * Each array key MUST be a string representing the case-insensitive name * of a header. Each value MUST be either a string or an array of strings. * For each value, the value is appended to any existing header of the same * name, or, if a header does not already exist by the given name, then the * header is added. * * @param array $headers Associative array of headers to add to the message */ public function addHeaders(array $headers); /** * Sets a header, replacing any existing values of any headers with the * same case-insensitive name. * * The header values MUST be a string or an array of strings. * * @param string $header Header name * @param string|array $value Header value(s) */ public function setHeader($header, $value); /** * Sets headers, replacing any headers that have already been set on the * message. * * The array keys MUST be a string. The array values must be either a * string or an array of strings. * * @param array $headers Headers to set. */ public function setHeaders(array $headers); } src/Message/MessageParser.php000064400000012211151742725260012201 0ustar00parseMessage($message))) { return false; } // Parse the protocol and protocol version if (isset($parts['start_line'][2])) { $startParts = explode('/', $parts['start_line'][2]); $protocol = strtoupper($startParts[0]); $version = isset($startParts[1]) ? $startParts[1] : '1.1'; } else { $protocol = 'HTTP'; $version = '1.1'; } $parsed = [ 'method' => strtoupper($parts['start_line'][0]), 'protocol' => $protocol, 'protocol_version' => $version, 'headers' => $parts['headers'], 'body' => $parts['body'] ]; $parsed['request_url'] = $this->getUrlPartsFromMessage( (isset($parts['start_line'][1]) ? $parts['start_line'][1] : ''), $parsed); return $parsed; } /** * Parse an HTTP response message into an associative array of parts. * * @param string $message HTTP response to parse * * @return array|bool Returns false if the message is invalid */ public function parseResponse($message) { if (!($parts = $this->parseMessage($message))) { return false; } list($protocol, $version) = explode('/', trim($parts['start_line'][0])); return [ 'protocol' => $protocol, 'protocol_version' => $version, 'code' => $parts['start_line'][1], 'reason_phrase' => isset($parts['start_line'][2]) ? $parts['start_line'][2] : '', 'headers' => $parts['headers'], 'body' => $parts['body'] ]; } /** * Parse a message into parts * * @param string $message Message to parse * * @return array|bool */ private function parseMessage($message) { if (!$message) { return false; } $startLine = null; $headers = []; $body = ''; // Iterate over each line in the message, accounting for line endings $lines = preg_split('/(\\r?\\n)/', $message, -1, PREG_SPLIT_DELIM_CAPTURE); for ($i = 0, $totalLines = count($lines); $i < $totalLines; $i += 2) { $line = $lines[$i]; // If two line breaks were encountered, then this is the end of body if (empty($line)) { if ($i < $totalLines - 1) { $body = implode('', array_slice($lines, $i + 2)); } break; } // Parse message headers if (!$startLine) { $startLine = explode(' ', $line, 3); } elseif (strpos($line, ':')) { $parts = explode(':', $line, 2); $key = trim($parts[0]); $value = isset($parts[1]) ? trim($parts[1]) : ''; if (!isset($headers[$key])) { $headers[$key] = $value; } elseif (!is_array($headers[$key])) { $headers[$key] = [$headers[$key], $value]; } else { $headers[$key][] = $value; } } } return [ 'start_line' => $startLine, 'headers' => $headers, 'body' => $body ]; } /** * Create URL parts from HTTP message parts * * @param string $requestUrl Associated URL * @param array $parts HTTP message parts * * @return array */ private function getUrlPartsFromMessage($requestUrl, array $parts) { // Parse the URL information from the message $urlParts = ['path' => $requestUrl, 'scheme' => 'http']; // Check for the Host header if (isset($parts['headers']['Host'])) { $urlParts['host'] = $parts['headers']['Host']; } elseif (isset($parts['headers']['host'])) { $urlParts['host'] = $parts['headers']['host']; } else { $urlParts['host'] = null; } if (false === strpos($urlParts['host'], ':')) { $urlParts['port'] = ''; } else { $hostParts = explode(':', $urlParts['host']); $urlParts['host'] = trim($hostParts[0]); $urlParts['port'] = (int) trim($hostParts[1]); if ($urlParts['port'] == 443) { $urlParts['scheme'] = 'https'; } } // Check if a query is present $path = $urlParts['path']; $qpos = strpos($path, '?'); if ($qpos) { $urlParts['query'] = substr($path, $qpos + 1); $urlParts['path'] = substr($path, 0, $qpos); } else { $urlParts['query'] = ''; } return $urlParts; } } src/Message/Request.php000064400000011070151742725260011072 0ustar00setUrl($url); $this->method = strtoupper($method); $this->handleOptions($options); $this->transferOptions = new Collection($options); $this->addPrepareEvent(); if ($body !== null) { $this->setBody($body); } if ($headers) { foreach ($headers as $key => $value) { $this->addHeader($key, $value); } } } public function __clone() { if ($this->emitter) { $this->emitter = clone $this->emitter; } $this->transferOptions = clone $this->transferOptions; $this->url = clone $this->url; } public function setUrl($url) { $this->url = $url instanceof Url ? $url : Url::fromString($url); $this->updateHostHeaderFromUrl(); } public function getUrl() { return (string) $this->url; } public function setQuery($query) { $this->url->setQuery($query); } public function getQuery() { return $this->url->getQuery(); } public function setMethod($method) { $this->method = strtoupper($method); } public function getMethod() { return $this->method; } public function getScheme() { return $this->url->getScheme(); } public function setScheme($scheme) { $this->url->setScheme($scheme); } public function getPort() { return $this->url->getPort(); } public function setPort($port) { $this->url->setPort($port); $this->updateHostHeaderFromUrl(); } public function getHost() { return $this->url->getHost(); } public function setHost($host) { $this->url->setHost($host); $this->updateHostHeaderFromUrl(); } public function getPath() { return '/' . ltrim($this->url->getPath(), '/'); } public function setPath($path) { $this->url->setPath($path); } public function getResource() { $resource = $this->getPath(); if ($query = (string) $this->url->getQuery()) { $resource .= '?' . $query; } return $resource; } public function getConfig() { return $this->transferOptions; } protected function handleOptions(array &$options) { parent::handleOptions($options); // Use a custom emitter if one is specified, and remove it from // options that are exposed through getConfig() if (isset($options['emitter'])) { $this->emitter = $options['emitter']; unset($options['emitter']); } } /** * Adds a subscriber that ensures a request's body is prepared before * sending. */ private function addPrepareEvent() { static $subscriber; if (!$subscriber) { $subscriber = new Prepare(); } $this->getEmitter()->attach($subscriber); } private function updateHostHeaderFromUrl() { $port = $this->url->getPort(); $scheme = $this->url->getScheme(); if ($host = $this->url->getHost()) { if (($port == 80 && $scheme == 'http') || ($port == 443 && $scheme == 'https') ) { $this->setHeader('Host', $host); } else { $this->setHeader('Host', "{$host}:{$port}"); } } } } src/Message/RequestInterface.php000064400000006055151742725260012722 0ustar00 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 208 => 'Already Reported', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Reserved for WebDAV advanced collections expired proposal', 426 => 'Upgrade required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates (Experimental)', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required', ]; /** @var string The reason phrase of the response (human readable code) */ private $reasonPhrase; /** @var string The status code of the response */ private $statusCode; /** @var string The effective URL that returned this response */ private $effectiveUrl; /** * @param int|string $statusCode The response status code (e.g. 200) * @param array $headers The response headers * @param StreamInterface $body The body of the response * @param array $options Response message options * - reason_phrase: Set a custom reason phrase * - protocol_version: Set a custom protocol version */ public function __construct( $statusCode, array $headers = [], StreamInterface $body = null, array $options = [] ) { $this->statusCode = (int) $statusCode; $this->handleOptions($options); // Assume a reason phrase if one was not applied as an option if (!$this->reasonPhrase && isset(self::$statusTexts[$this->statusCode]) ) { $this->reasonPhrase = self::$statusTexts[$this->statusCode]; } if ($headers) { $this->setHeaders($headers); } if ($body) { $this->setBody($body); } } public function getStatusCode() { return $this->statusCode; } public function setStatusCode($code) { return $this->statusCode = (int) $code; } public function getReasonPhrase() { return $this->reasonPhrase; } public function setReasonPhrase($phrase) { return $this->reasonPhrase = $phrase; } public function json(array $config = []) { try { return Utils::jsonDecode( (string) $this->getBody(), isset($config['object']) ? !$config['object'] : true, 512, isset($config['big_int_strings']) ? JSON_BIGINT_AS_STRING : 0 ); } catch (\InvalidArgumentException $e) { throw new ParseException( $e->getMessage(), $this ); } } public function xml(array $config = []) { $disableEntities = libxml_disable_entity_loader(true); $internalErrors = libxml_use_internal_errors(true); try { // Allow XML to be retrieved even if there is no response body $xml = new \SimpleXMLElement( (string) $this->getBody() ?: '', isset($config['libxml_options']) ? $config['libxml_options'] : LIBXML_NONET, false, isset($config['ns']) ? $config['ns'] : '', isset($config['ns_is_prefix']) ? $config['ns_is_prefix'] : false ); libxml_disable_entity_loader($disableEntities); libxml_use_internal_errors($internalErrors); } catch (\Exception $e) { libxml_disable_entity_loader($disableEntities); libxml_use_internal_errors($internalErrors); throw new XmlParseException( 'Unable to parse response body into XML: ' . $e->getMessage(), $this, $e, (libxml_get_last_error()) ?: null ); } return $xml; } public function getEffectiveUrl() { return $this->effectiveUrl; } public function setEffectiveUrl($url) { $this->effectiveUrl = $url; } /** * Accepts and modifies the options provided to the response in the * constructor. * * @param array $options Options array passed by reference. */ protected function handleOptions(array &$options = []) { parent::handleOptions($options); if (isset($options['reason_phrase'])) { $this->reasonPhrase = $options['reason_phrase']; } } } src/Message/ResponseInterface.php000064400000007646151742725260013077 0ustar00 'text/vnd.in3d.3dml', '3g2' => 'video/3gpp2', '3gp' => 'video/3gpp', '7z' => 'application/x-7z-compressed', 'aab' => 'application/x-authorware-bin', 'aac' => 'audio/x-aac', 'aam' => 'application/x-authorware-map', 'aas' => 'application/x-authorware-seg', 'abw' => 'application/x-abiword', 'ac' => 'application/pkix-attr-cert', 'acc' => 'application/vnd.americandynamics.acc', 'ace' => 'application/x-ace-compressed', 'acu' => 'application/vnd.acucobol', 'acutc' => 'application/vnd.acucorp', 'adp' => 'audio/adpcm', 'aep' => 'application/vnd.audiograph', 'afm' => 'application/x-font-type1', 'afp' => 'application/vnd.ibm.modcap', 'ahead' => 'application/vnd.ahead.space', 'ai' => 'application/postscript', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'air' => 'application/vnd.adobe.air-application-installer-package+zip', 'ait' => 'application/vnd.dvb.ait', 'ami' => 'application/vnd.amiga.ami', 'apk' => 'application/vnd.android.package-archive', 'application' => 'application/x-ms-application', 'apr' => 'application/vnd.lotus-approach', 'asa' => 'text/plain', 'asax' => 'application/octet-stream', 'asc' => 'application/pgp-signature', 'ascx' => 'text/plain', 'asf' => 'video/x-ms-asf', 'ashx' => 'text/plain', 'asm' => 'text/x-asm', 'asmx' => 'text/plain', 'aso' => 'application/vnd.accpac.simply.aso', 'asp' => 'text/plain', 'aspx' => 'text/plain', 'asx' => 'video/x-ms-asf', 'atc' => 'application/vnd.acucorp', 'atom' => 'application/atom+xml', 'atomcat' => 'application/atomcat+xml', 'atomsvc' => 'application/atomsvc+xml', 'atx' => 'application/vnd.antix.game-component', 'au' => 'audio/basic', 'avi' => 'video/x-msvideo', 'aw' => 'application/applixware', 'axd' => 'text/plain', 'azf' => 'application/vnd.airzip.filesecure.azf', 'azs' => 'application/vnd.airzip.filesecure.azs', 'azw' => 'application/vnd.amazon.ebook', 'bat' => 'application/x-msdownload', 'bcpio' => 'application/x-bcpio', 'bdf' => 'application/x-font-bdf', 'bdm' => 'application/vnd.syncml.dm+wbxml', 'bed' => 'application/vnd.realvnc.bed', 'bh2' => 'application/vnd.fujitsu.oasysprs', 'bin' => 'application/octet-stream', 'bmi' => 'application/vnd.bmi', 'bmp' => 'image/bmp', 'book' => 'application/vnd.framemaker', 'box' => 'application/vnd.previewsystems.box', 'boz' => 'application/x-bzip2', 'bpk' => 'application/octet-stream', 'btif' => 'image/prs.btif', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'c' => 'text/x-c', 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', 'c4d' => 'application/vnd.clonk.c4group', 'c4f' => 'application/vnd.clonk.c4group', 'c4g' => 'application/vnd.clonk.c4group', 'c4p' => 'application/vnd.clonk.c4group', 'c4u' => 'application/vnd.clonk.c4group', 'cab' => 'application/vnd.ms-cab-compressed', 'car' => 'application/vnd.curl.car', 'cat' => 'application/vnd.ms-pki.seccat', 'cc' => 'text/x-c', 'cct' => 'application/x-director', 'ccxml' => 'application/ccxml+xml', 'cdbcmsg' => 'application/vnd.contact.cmsg', 'cdf' => 'application/x-netcdf', 'cdkey' => 'application/vnd.mediastation.cdkey', 'cdmia' => 'application/cdmi-capability', 'cdmic' => 'application/cdmi-container', 'cdmid' => 'application/cdmi-domain', 'cdmio' => 'application/cdmi-object', 'cdmiq' => 'application/cdmi-queue', 'cdx' => 'chemical/x-cdx', 'cdxml' => 'application/vnd.chemdraw+xml', 'cdy' => 'application/vnd.cinderella', 'cer' => 'application/pkix-cert', 'cfc' => 'application/x-coldfusion', 'cfm' => 'application/x-coldfusion', 'cgm' => 'image/cgm', 'chat' => 'application/x-chat', 'chm' => 'application/vnd.ms-htmlhelp', 'chrt' => 'application/vnd.kde.kchart', 'cif' => 'chemical/x-cif', 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', 'cil' => 'application/vnd.ms-artgalry', 'cla' => 'application/vnd.claymore', 'class' => 'application/java-vm', 'clkk' => 'application/vnd.crick.clicker.keyboard', 'clkp' => 'application/vnd.crick.clicker.palette', 'clkt' => 'application/vnd.crick.clicker.template', 'clkw' => 'application/vnd.crick.clicker.wordbank', 'clkx' => 'application/vnd.crick.clicker', 'clp' => 'application/x-msclip', 'cmc' => 'application/vnd.cosmocaller', 'cmdf' => 'chemical/x-cmdf', 'cml' => 'chemical/x-cml', 'cmp' => 'application/vnd.yellowriver-custom-menu', 'cmx' => 'image/x-cmx', 'cod' => 'application/vnd.rim.cod', 'com' => 'application/x-msdownload', 'conf' => 'text/plain', 'cpio' => 'application/x-cpio', 'cpp' => 'text/x-c', 'cpt' => 'application/mac-compactpro', 'crd' => 'application/x-mscardfile', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'cryptonote' => 'application/vnd.rig.cryptonote', 'cs' => 'text/plain', 'csh' => 'application/x-csh', 'csml' => 'chemical/x-csml', 'csp' => 'application/vnd.commonspace', 'css' => 'text/css', 'cst' => 'application/x-director', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'curl' => 'text/vnd.curl', 'cww' => 'application/prs.cww', 'cxt' => 'application/x-director', 'cxx' => 'text/x-c', 'dae' => 'model/vnd.collada+xml', 'daf' => 'application/vnd.mobius.daf', 'dataless' => 'application/vnd.fdsn.seed', 'davmount' => 'application/davmount+xml', 'dcr' => 'application/x-director', 'dcurl' => 'text/vnd.curl.dcurl', 'dd2' => 'application/vnd.oma.dd2+xml', 'ddd' => 'application/vnd.fujixerox.ddd', 'deb' => 'application/x-debian-package', 'def' => 'text/plain', 'deploy' => 'application/octet-stream', 'der' => 'application/x-x509-ca-cert', 'dfac' => 'application/vnd.dreamfactory', 'dic' => 'text/x-c', 'dir' => 'application/x-director', 'dis' => 'application/vnd.mobius.dis', 'dist' => 'application/octet-stream', 'distz' => 'application/octet-stream', 'djv' => 'image/vnd.djvu', 'djvu' => 'image/vnd.djvu', 'dll' => 'application/x-msdownload', 'dmg' => 'application/octet-stream', 'dms' => 'application/octet-stream', 'dna' => 'application/vnd.dna', 'doc' => 'application/msword', 'docm' => 'application/vnd.ms-word.document.macroenabled.12', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dot' => 'application/msword', 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dp' => 'application/vnd.osgi.dp', 'dpg' => 'application/vnd.dpgraph', 'dra' => 'audio/vnd.dra', 'dsc' => 'text/prs.lines.tag', 'dssc' => 'application/dssc+der', 'dtb' => 'application/x-dtbook+xml', 'dtd' => 'application/xml-dtd', 'dts' => 'audio/vnd.dts', 'dtshd' => 'audio/vnd.dts.hd', 'dump' => 'application/octet-stream', 'dvi' => 'application/x-dvi', 'dwf' => 'model/vnd.dwf', 'dwg' => 'image/vnd.dwg', 'dxf' => 'image/vnd.dxf', 'dxp' => 'application/vnd.spotfire.dxp', 'dxr' => 'application/x-director', 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', 'ecma' => 'application/ecmascript', 'edm' => 'application/vnd.novadigm.edm', 'edx' => 'application/vnd.novadigm.edx', 'efif' => 'application/vnd.picsel', 'ei6' => 'application/vnd.pg.osasli', 'elc' => 'application/octet-stream', 'eml' => 'message/rfc822', 'emma' => 'application/emma+xml', 'eol' => 'audio/vnd.digital-winds', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'es3' => 'application/vnd.eszigno3+xml', 'esf' => 'application/vnd.epson.esf', 'et3' => 'application/vnd.eszigno3+xml', 'etx' => 'text/x-setext', 'exe' => 'application/x-msdownload', 'exi' => 'application/exi', 'ext' => 'application/vnd.novadigm.ext', 'ez' => 'application/andrew-inset', 'ez2' => 'application/vnd.ezpix-album', 'ez3' => 'application/vnd.ezpix-package', 'f' => 'text/x-fortran', 'f4v' => 'video/x-f4v', 'f77' => 'text/x-fortran', 'f90' => 'text/x-fortran', 'fbs' => 'image/vnd.fastbidsheet', 'fcs' => 'application/vnd.isac.fcs', 'fdf' => 'application/vnd.fdf', 'fe_launch' => 'application/vnd.denovo.fcselayout-link', 'fg5' => 'application/vnd.fujitsu.oasysgp', 'fgd' => 'application/x-director', 'fh' => 'image/x-freehand', 'fh4' => 'image/x-freehand', 'fh5' => 'image/x-freehand', 'fh7' => 'image/x-freehand', 'fhc' => 'image/x-freehand', 'fig' => 'application/x-xfig', 'fli' => 'video/x-fli', 'flo' => 'application/vnd.micrografx.flo', 'flv' => 'video/x-flv', 'flw' => 'application/vnd.kde.kivio', 'flx' => 'text/vnd.fmi.flexstor', 'fly' => 'text/vnd.fly', 'fm' => 'application/vnd.framemaker', 'fnc' => 'application/vnd.frogans.fnc', 'for' => 'text/x-fortran', 'fpx' => 'image/vnd.fpx', 'frame' => 'application/vnd.framemaker', 'fsc' => 'application/vnd.fsc.weblaunch', 'fst' => 'image/vnd.fst', 'ftc' => 'application/vnd.fluxtime.clip', 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', 'fvt' => 'video/vnd.fvt', 'fxp' => 'application/vnd.adobe.fxp', 'fxpl' => 'application/vnd.adobe.fxp', 'fzs' => 'application/vnd.fuzzysheet', 'g2w' => 'application/vnd.geoplan', 'g3' => 'image/g3fax', 'g3w' => 'application/vnd.geospace', 'gac' => 'application/vnd.groove-account', 'gdl' => 'model/vnd.gdl', 'geo' => 'application/vnd.dynageo', 'gex' => 'application/vnd.geometry-explorer', 'ggb' => 'application/vnd.geogebra.file', 'ggt' => 'application/vnd.geogebra.tool', 'ghf' => 'application/vnd.groove-help', 'gif' => 'image/gif', 'gim' => 'application/vnd.groove-identity-message', 'gmx' => 'application/vnd.gmx', 'gnumeric' => 'application/x-gnumeric', 'gph' => 'application/vnd.flographit', 'gqf' => 'application/vnd.grafeq', 'gqs' => 'application/vnd.grafeq', 'gram' => 'application/srgs', 'gre' => 'application/vnd.geometry-explorer', 'grv' => 'application/vnd.groove-injector', 'grxml' => 'application/srgs+xml', 'gsf' => 'application/x-font-ghostscript', 'gtar' => 'application/x-gtar', 'gtm' => 'application/vnd.groove-tool-message', 'gtw' => 'model/vnd.gtw', 'gv' => 'text/vnd.graphviz', 'gxt' => 'application/vnd.geonext', 'h' => 'text/x-c', 'h261' => 'video/h261', 'h263' => 'video/h263', 'h264' => 'video/h264', 'hal' => 'application/vnd.hal+xml', 'hbci' => 'application/vnd.hbci', 'hdf' => 'application/x-hdf', 'hh' => 'text/x-c', 'hlp' => 'application/winhlp', 'hpgl' => 'application/vnd.hp-hpgl', 'hpid' => 'application/vnd.hp-hpid', 'hps' => 'application/vnd.hp-hps', 'hqx' => 'application/mac-binhex40', 'hta' => 'application/octet-stream', 'htc' => 'text/html', 'htke' => 'application/vnd.kenameaapp', 'htm' => 'text/html', 'html' => 'text/html', 'hvd' => 'application/vnd.yamaha.hv-dic', 'hvp' => 'application/vnd.yamaha.hv-voice', 'hvs' => 'application/vnd.yamaha.hv-script', 'i2g' => 'application/vnd.intergeo', 'icc' => 'application/vnd.iccprofile', 'ice' => 'x-conference/x-cooltalk', 'icm' => 'application/vnd.iccprofile', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ief' => 'image/ief', 'ifb' => 'text/calendar', 'ifm' => 'application/vnd.shana.informed.formdata', 'iges' => 'model/iges', 'igl' => 'application/vnd.igloader', 'igm' => 'application/vnd.insors.igm', 'igs' => 'model/iges', 'igx' => 'application/vnd.micrografx.igx', 'iif' => 'application/vnd.shana.informed.interchange', 'imp' => 'application/vnd.accpac.simply.imp', 'ims' => 'application/vnd.ms-ims', 'in' => 'text/plain', 'ini' => 'text/plain', 'ipfix' => 'application/ipfix', 'ipk' => 'application/vnd.shana.informed.package', 'irm' => 'application/vnd.ibm.rights-management', 'irp' => 'application/vnd.irepository.package+xml', 'iso' => 'application/octet-stream', 'itp' => 'application/vnd.shana.informed.formtemplate', 'ivp' => 'application/vnd.immervision-ivp', 'ivu' => 'application/vnd.immervision-ivu', 'jad' => 'text/vnd.sun.j2me.app-descriptor', 'jam' => 'application/vnd.jam', 'jar' => 'application/java-archive', 'java' => 'text/x-java-source', 'jisp' => 'application/vnd.jisp', 'jlt' => 'application/vnd.hp-jlyt', 'jnlp' => 'application/x-java-jnlp-file', 'joda' => 'application/vnd.joost.joda-archive', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'jpgm' => 'video/jpm', 'jpgv' => 'video/jpeg', 'jpm' => 'video/jpm', 'js' => 'text/javascript', 'json' => 'application/json', 'kar' => 'audio/midi', 'karbon' => 'application/vnd.kde.karbon', 'kfo' => 'application/vnd.kde.kformula', 'kia' => 'application/vnd.kidspiration', 'kml' => 'application/vnd.google-earth.kml+xml', 'kmz' => 'application/vnd.google-earth.kmz', 'kne' => 'application/vnd.kinar', 'knp' => 'application/vnd.kinar', 'kon' => 'application/vnd.kde.kontour', 'kpr' => 'application/vnd.kde.kpresenter', 'kpt' => 'application/vnd.kde.kpresenter', 'ksp' => 'application/vnd.kde.kspread', 'ktr' => 'application/vnd.kahootz', 'ktx' => 'image/ktx', 'ktz' => 'application/vnd.kahootz', 'kwd' => 'application/vnd.kde.kword', 'kwt' => 'application/vnd.kde.kword', 'lasxml' => 'application/vnd.las.las+xml', 'latex' => 'application/x-latex', 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', 'les' => 'application/vnd.hhe.lesson-player', 'lha' => 'application/octet-stream', 'link66' => 'application/vnd.route66.link66+xml', 'list' => 'text/plain', 'list3820' => 'application/vnd.ibm.modcap', 'listafp' => 'application/vnd.ibm.modcap', 'log' => 'text/plain', 'lostxml' => 'application/lost+xml', 'lrf' => 'application/octet-stream', 'lrm' => 'application/vnd.ms-lrm', 'ltf' => 'application/vnd.frogans.ltf', 'lvp' => 'audio/vnd.lucent.voice', 'lwp' => 'application/vnd.lotus-wordpro', 'lzh' => 'application/octet-stream', 'm13' => 'application/x-msmediaview', 'm14' => 'application/x-msmediaview', 'm1v' => 'video/mpeg', 'm21' => 'application/mp21', 'm2a' => 'audio/mpeg', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'audio/x-mpegurl', 'm3u8' => 'application/vnd.apple.mpegurl', 'm4a' => 'audio/mp4', 'm4u' => 'video/vnd.mpegurl', 'm4v' => 'video/mp4', 'ma' => 'application/mathematica', 'mads' => 'application/mads+xml', 'mag' => 'application/vnd.ecowin.chart', 'maker' => 'application/vnd.framemaker', 'man' => 'text/troff', 'mathml' => 'application/mathml+xml', 'mb' => 'application/mathematica', 'mbk' => 'application/vnd.mobius.mbk', 'mbox' => 'application/mbox', 'mc1' => 'application/vnd.medcalcdata', 'mcd' => 'application/vnd.mcd', 'mcurl' => 'text/vnd.curl.mcurl', 'mdb' => 'application/x-msaccess', 'mdi' => 'image/vnd.ms-modi', 'me' => 'text/troff', 'mesh' => 'model/mesh', 'meta4' => 'application/metalink4+xml', 'mets' => 'application/mets+xml', 'mfm' => 'application/vnd.mfmp', 'mgp' => 'application/vnd.osgeo.mapguide.package', 'mgz' => 'application/vnd.proteus.magazine', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mif' => 'application/vnd.mif', 'mime' => 'message/rfc822', 'mj2' => 'video/mj2', 'mjp2' => 'video/mj2', 'mlp' => 'application/vnd.dolby.mlp', 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', 'mmf' => 'application/vnd.smaf', 'mmr' => 'image/vnd.fujixerox.edmics-mmr', 'mny' => 'application/x-msmoney', 'mobi' => 'application/x-mobipocket-ebook', 'mods' => 'application/mods+xml', 'mov' => 'video/quicktime', 'movie' => 'video/x-sgi-movie', 'mp2' => 'audio/mpeg', 'mp21' => 'application/mp21', 'mp2a' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4s' => 'application/mp4', 'mp4v' => 'video/mp4', 'mpc' => 'application/vnd.mophun.certificate', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'mpga' => 'audio/mpeg', 'mpkg' => 'application/vnd.apple.installer+xml', 'mpm' => 'application/vnd.blueice.multipass', 'mpn' => 'application/vnd.mophun.application', 'mpp' => 'application/vnd.ms-project', 'mpt' => 'application/vnd.ms-project', 'mpy' => 'application/vnd.ibm.minipay', 'mqy' => 'application/vnd.mobius.mqy', 'mrc' => 'application/marc', 'mrcx' => 'application/marcxml+xml', 'ms' => 'text/troff', 'mscml' => 'application/mediaservercontrol+xml', 'mseed' => 'application/vnd.fdsn.mseed', 'mseq' => 'application/vnd.mseq', 'msf' => 'application/vnd.epson.msf', 'msh' => 'model/mesh', 'msi' => 'application/x-msdownload', 'msl' => 'application/vnd.mobius.msl', 'msty' => 'application/vnd.muvee.style', 'mts' => 'model/vnd.mts', 'mus' => 'application/vnd.musician', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', 'mwf' => 'application/vnd.mfer', 'mxf' => 'application/mxf', 'mxl' => 'application/vnd.recordare.musicxml', 'mxml' => 'application/xv+xml', 'mxs' => 'application/vnd.triscape.mxs', 'mxu' => 'video/vnd.mpegurl', 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', 'n3' => 'text/n3', 'nb' => 'application/mathematica', 'nbp' => 'application/vnd.wolfram.player', 'nc' => 'application/x-netcdf', 'ncx' => 'application/x-dtbncx+xml', 'ngdat' => 'application/vnd.nokia.n-gage.data', 'nlu' => 'application/vnd.neurolanguage.nlu', 'nml' => 'application/vnd.enliven', 'nnd' => 'application/vnd.noblenet-directory', 'nns' => 'application/vnd.noblenet-sealer', 'nnw' => 'application/vnd.noblenet-web', 'npx' => 'image/vnd.net-fpx', 'nsf' => 'application/vnd.lotus-notes', 'oa2' => 'application/vnd.fujitsu.oasys2', 'oa3' => 'application/vnd.fujitsu.oasys3', 'oas' => 'application/vnd.fujitsu.oasys', 'obd' => 'application/x-msbinder', 'oda' => 'application/oda', 'odb' => 'application/vnd.oasis.opendocument.database', 'odc' => 'application/vnd.oasis.opendocument.chart', 'odf' => 'application/vnd.oasis.opendocument.formula', 'odft' => 'application/vnd.oasis.opendocument.formula-template', 'odg' => 'application/vnd.oasis.opendocument.graphics', 'odi' => 'application/vnd.oasis.opendocument.image', 'odm' => 'application/vnd.oasis.opendocument.text-master', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odt' => 'application/vnd.oasis.opendocument.text', 'oga' => 'audio/ogg', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'onepkg' => 'application/onenote', 'onetmp' => 'application/onenote', 'onetoc' => 'application/onenote', 'onetoc2' => 'application/onenote', 'opf' => 'application/oebps-package+xml', 'oprc' => 'application/vnd.palm', 'org' => 'application/vnd.lotus-organizer', 'osf' => 'application/vnd.yamaha.openscoreformat', 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 'otc' => 'application/vnd.oasis.opendocument.chart-template', 'otf' => 'application/x-font-otf', 'otg' => 'application/vnd.oasis.opendocument.graphics-template', 'oth' => 'application/vnd.oasis.opendocument.text-web', 'oti' => 'application/vnd.oasis.opendocument.image-template', 'otp' => 'application/vnd.oasis.opendocument.presentation-template', 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', 'ott' => 'application/vnd.oasis.opendocument.text-template', 'oxt' => 'application/vnd.openofficeorg.extension', 'p' => 'text/x-pascal', 'p10' => 'application/pkcs10', 'p12' => 'application/x-pkcs12', 'p7b' => 'application/x-pkcs7-certificates', 'p7c' => 'application/pkcs7-mime', 'p7m' => 'application/pkcs7-mime', 'p7r' => 'application/x-pkcs7-certreqresp', 'p7s' => 'application/pkcs7-signature', 'p8' => 'application/pkcs8', 'pas' => 'text/x-pascal', 'paw' => 'application/vnd.pawaafile', 'pbd' => 'application/vnd.powerbuilder6', 'pbm' => 'image/x-portable-bitmap', 'pcf' => 'application/x-font-pcf', 'pcl' => 'application/vnd.hp-pcl', 'pclxl' => 'application/vnd.hp-pclxl', 'pct' => 'image/x-pict', 'pcurl' => 'application/vnd.curl.pcurl', 'pcx' => 'image/x-pcx', 'pdb' => 'application/vnd.palm', 'pdf' => 'application/pdf', 'pfa' => 'application/x-font-type1', 'pfb' => 'application/x-font-type1', 'pfm' => 'application/x-font-type1', 'pfr' => 'application/font-tdpfr', 'pfx' => 'application/x-pkcs12', 'pgm' => 'image/x-portable-graymap', 'pgn' => 'application/x-chess-pgn', 'pgp' => 'application/pgp-encrypted', 'php' => 'text/x-php', 'phps' => 'application/x-httpd-phps', 'pic' => 'image/x-pict', 'pkg' => 'application/octet-stream', 'pki' => 'application/pkixcmp', 'pkipath' => 'application/pkix-pkipath', 'plb' => 'application/vnd.3gpp.pic-bw-large', 'plc' => 'application/vnd.mobius.plc', 'plf' => 'application/vnd.pocketlearn', 'pls' => 'application/pls+xml', 'pml' => 'application/vnd.ctc-posml', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'portpkg' => 'application/vnd.macports.portpkg', 'pot' => 'application/vnd.ms-powerpoint', 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', 'ppd' => 'application/vnd.cups-ppd', 'ppm' => 'image/x-portable-pixmap', 'pps' => 'application/vnd.ms-powerpoint', 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'ppt' => 'application/vnd.ms-powerpoint', 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pqa' => 'application/vnd.palm', 'prc' => 'application/x-mobipocket-ebook', 'pre' => 'application/vnd.lotus-freelance', 'prf' => 'application/pics-rules', 'ps' => 'application/postscript', 'psb' => 'application/vnd.3gpp.pic-bw-small', 'psd' => 'image/vnd.adobe.photoshop', 'psf' => 'application/x-font-linux-psf', 'pskcxml' => 'application/pskc+xml', 'ptid' => 'application/vnd.pvi.ptid1', 'pub' => 'application/x-mspublisher', 'pvb' => 'application/vnd.3gpp.pic-bw-var', 'pwn' => 'application/vnd.3m.post-it-notes', 'pya' => 'audio/vnd.ms-playready.media.pya', 'pyv' => 'video/vnd.ms-playready.media.pyv', 'qam' => 'application/vnd.epson.quickanime', 'qbo' => 'application/vnd.intu.qbo', 'qfx' => 'application/vnd.intu.qfx', 'qps' => 'application/vnd.publishare-delta-tree', 'qt' => 'video/quicktime', 'qwd' => 'application/vnd.quark.quarkxpress', 'qwt' => 'application/vnd.quark.quarkxpress', 'qxb' => 'application/vnd.quark.quarkxpress', 'qxd' => 'application/vnd.quark.quarkxpress', 'qxl' => 'application/vnd.quark.quarkxpress', 'qxt' => 'application/vnd.quark.quarkxpress', 'ra' => 'audio/x-pn-realaudio', 'ram' => 'audio/x-pn-realaudio', 'rar' => 'application/x-rar-compressed', 'ras' => 'image/x-cmu-raster', 'rb' => 'text/plain', 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', 'rdf' => 'application/rdf+xml', 'rdz' => 'application/vnd.data-vision.rdz', 'rep' => 'application/vnd.businessobjects', 'res' => 'application/x-dtbresource+xml', 'resx' => 'text/xml', 'rgb' => 'image/x-rgb', 'rif' => 'application/reginfo+xml', 'rip' => 'audio/vnd.rip', 'rl' => 'application/resource-lists+xml', 'rlc' => 'image/vnd.fujixerox.edmics-rlc', 'rld' => 'application/resource-lists-diff+xml', 'rm' => 'application/vnd.rn-realmedia', 'rmi' => 'audio/midi', 'rmp' => 'audio/x-pn-realaudio-plugin', 'rms' => 'application/vnd.jcp.javame.midlet-rms', 'rnc' => 'application/relax-ng-compact-syntax', 'roff' => 'text/troff', 'rp9' => 'application/vnd.cloanto.rp9', 'rpss' => 'application/vnd.nokia.radio-presets', 'rpst' => 'application/vnd.nokia.radio-preset', 'rq' => 'application/sparql-query', 'rs' => 'application/rls-services+xml', 'rsd' => 'application/rsd+xml', 'rss' => 'application/rss+xml', 'rtf' => 'application/rtf', 'rtx' => 'text/richtext', 's' => 'text/x-asm', 'saf' => 'application/vnd.yamaha.smaf-audio', 'sbml' => 'application/sbml+xml', 'sc' => 'application/vnd.ibm.secure-container', 'scd' => 'application/x-msschedule', 'scm' => 'application/vnd.lotus-screencam', 'scq' => 'application/scvp-cv-request', 'scs' => 'application/scvp-cv-response', 'scurl' => 'text/vnd.curl.scurl', 'sda' => 'application/vnd.stardivision.draw', 'sdc' => 'application/vnd.stardivision.calc', 'sdd' => 'application/vnd.stardivision.impress', 'sdkd' => 'application/vnd.solent.sdkm+xml', 'sdkm' => 'application/vnd.solent.sdkm+xml', 'sdp' => 'application/sdp', 'sdw' => 'application/vnd.stardivision.writer', 'see' => 'application/vnd.seemail', 'seed' => 'application/vnd.fdsn.seed', 'sema' => 'application/vnd.sema', 'semd' => 'application/vnd.semd', 'semf' => 'application/vnd.semf', 'ser' => 'application/java-serialized-object', 'setpay' => 'application/set-payment-initiation', 'setreg' => 'application/set-registration-initiation', 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', 'sfs' => 'application/vnd.spotfire.sfs', 'sgl' => 'application/vnd.stardivision.writer-global', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'sh' => 'application/x-sh', 'shar' => 'application/x-shar', 'shf' => 'application/shf+xml', 'sig' => 'application/pgp-signature', 'silo' => 'model/mesh', 'sis' => 'application/vnd.symbian.install', 'sisx' => 'application/vnd.symbian.install', 'sit' => 'application/x-stuffit', 'sitx' => 'application/x-stuffitx', 'skd' => 'application/vnd.koan', 'skm' => 'application/vnd.koan', 'skp' => 'application/vnd.koan', 'skt' => 'application/vnd.koan', 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', 'slt' => 'application/vnd.epson.salt', 'sm' => 'application/vnd.stepmania.stepchart', 'smf' => 'application/vnd.stardivision.math', 'smi' => 'application/smil+xml', 'smil' => 'application/smil+xml', 'snd' => 'audio/basic', 'snf' => 'application/x-font-snf', 'so' => 'application/octet-stream', 'spc' => 'application/x-pkcs7-certificates', 'spf' => 'application/vnd.yamaha.smaf-phrase', 'spl' => 'application/x-futuresplash', 'spot' => 'text/vnd.in3d.spot', 'spp' => 'application/scvp-vp-response', 'spq' => 'application/scvp-vp-request', 'spx' => 'audio/ogg', 'src' => 'application/x-wais-source', 'sru' => 'application/sru+xml', 'srx' => 'application/sparql-results+xml', 'sse' => 'application/vnd.kodak-descriptor', 'ssf' => 'application/vnd.epson.ssf', 'ssml' => 'application/ssml+xml', 'st' => 'application/vnd.sailingtracker.track', 'stc' => 'application/vnd.sun.xml.calc.template', 'std' => 'application/vnd.sun.xml.draw.template', 'stf' => 'application/vnd.wt.stf', 'sti' => 'application/vnd.sun.xml.impress.template', 'stk' => 'application/hyperstudio', 'stl' => 'application/vnd.ms-pki.stl', 'str' => 'application/vnd.pg.format', 'stw' => 'application/vnd.sun.xml.writer.template', 'sub' => 'image/vnd.dvb.subtitle', 'sus' => 'application/vnd.sus-calendar', 'susp' => 'application/vnd.sus-calendar', 'sv4cpio' => 'application/x-sv4cpio', 'sv4crc' => 'application/x-sv4crc', 'svc' => 'application/vnd.dvb.service', 'svd' => 'application/vnd.svd', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', 'swa' => 'application/x-director', 'swf' => 'application/x-shockwave-flash', 'swi' => 'application/vnd.aristanetworks.swi', 'sxc' => 'application/vnd.sun.xml.calc', 'sxd' => 'application/vnd.sun.xml.draw', 'sxg' => 'application/vnd.sun.xml.writer.global', 'sxi' => 'application/vnd.sun.xml.impress', 'sxm' => 'application/vnd.sun.xml.math', 'sxw' => 'application/vnd.sun.xml.writer', 't' => 'text/troff', 'tao' => 'application/vnd.tao.intent-module-archive', 'tar' => 'application/x-tar', 'tcap' => 'application/vnd.3gpp2.tcap', 'tcl' => 'application/x-tcl', 'teacher' => 'application/vnd.smart.teacher', 'tei' => 'application/tei+xml', 'teicorpus' => 'application/tei+xml', 'tex' => 'application/x-tex', 'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'text' => 'text/plain', 'tfi' => 'application/thraud+xml', 'tfm' => 'application/x-tex-tfm', 'thmx' => 'application/vnd.ms-officetheme', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'tmo' => 'application/vnd.tmobile-livetv', 'torrent' => 'application/x-bittorrent', 'tpl' => 'application/vnd.groove-tool-template', 'tpt' => 'application/vnd.trid.tpt', 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trm' => 'application/x-msterminal', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'application/x-font-ttf', 'ttf' => 'application/x-font-ttf', 'ttl' => 'text/turtle', 'twd' => 'application/vnd.simtech-mindmapper', 'twds' => 'application/vnd.simtech-mindmapper', 'txd' => 'application/vnd.genomatix.tuxedo', 'txf' => 'application/vnd.mobius.txf', 'txt' => 'text/plain', 'u32' => 'application/x-authorware-bin', 'udeb' => 'application/x-debian-package', 'ufd' => 'application/vnd.ufdl', 'ufdl' => 'application/vnd.ufdl', 'umj' => 'application/vnd.umajin', 'unityweb' => 'application/vnd.unity', 'uoml' => 'application/vnd.uoml+xml', 'uri' => 'text/uri-list', 'uris' => 'text/uri-list', 'urls' => 'text/uri-list', 'ustar' => 'application/x-ustar', 'utz' => 'application/vnd.uiq.theme', 'uu' => 'text/x-uuencode', 'uva' => 'audio/vnd.dece.audio', 'uvd' => 'application/vnd.dece.data', 'uvf' => 'application/vnd.dece.data', 'uvg' => 'image/vnd.dece.graphic', 'uvh' => 'video/vnd.dece.hd', 'uvi' => 'image/vnd.dece.graphic', 'uvm' => 'video/vnd.dece.mobile', 'uvp' => 'video/vnd.dece.pd', 'uvs' => 'video/vnd.dece.sd', 'uvt' => 'application/vnd.dece.ttml+xml', 'uvu' => 'video/vnd.uvvu.mp4', 'uvv' => 'video/vnd.dece.video', 'uvva' => 'audio/vnd.dece.audio', 'uvvd' => 'application/vnd.dece.data', 'uvvf' => 'application/vnd.dece.data', 'uvvg' => 'image/vnd.dece.graphic', 'uvvh' => 'video/vnd.dece.hd', 'uvvi' => 'image/vnd.dece.graphic', 'uvvm' => 'video/vnd.dece.mobile', 'uvvp' => 'video/vnd.dece.pd', 'uvvs' => 'video/vnd.dece.sd', 'uvvt' => 'application/vnd.dece.ttml+xml', 'uvvu' => 'video/vnd.uvvu.mp4', 'uvvv' => 'video/vnd.dece.video', 'uvvx' => 'application/vnd.dece.unspecified', 'uvx' => 'application/vnd.dece.unspecified', 'vcd' => 'application/x-cdlink', 'vcf' => 'text/x-vcard', 'vcg' => 'application/vnd.groove-vcard', 'vcs' => 'text/x-vcalendar', 'vcx' => 'application/vnd.vcx', 'vis' => 'application/vnd.visionary', 'viv' => 'video/vnd.vivo', 'vor' => 'application/vnd.stardivision.writer', 'vox' => 'application/x-authorware-bin', 'vrml' => 'model/vrml', 'vsd' => 'application/vnd.visio', 'vsf' => 'application/vnd.vsf', 'vss' => 'application/vnd.visio', 'vst' => 'application/vnd.visio', 'vsw' => 'application/vnd.visio', 'vtu' => 'model/vnd.vtu', 'vxml' => 'application/voicexml+xml', 'w3d' => 'application/x-director', 'wad' => 'application/x-doom', 'wav' => 'audio/x-wav', 'wax' => 'audio/x-ms-wax', 'wbmp' => 'image/vnd.wap.wbmp', 'wbs' => 'application/vnd.criticaltools.wbs+xml', 'wbxml' => 'application/vnd.wap.wbxml', 'wcm' => 'application/vnd.ms-works', 'wdb' => 'application/vnd.ms-works', 'weba' => 'audio/webm', 'webm' => 'video/webm', 'webp' => 'image/webp', 'wg' => 'application/vnd.pmi.widget', 'wgt' => 'application/widget', 'wks' => 'application/vnd.ms-works', 'wm' => 'video/x-ms-wm', 'wma' => 'audio/x-ms-wma', 'wmd' => 'application/x-ms-wmd', 'wmf' => 'application/x-msmetafile', 'wml' => 'text/vnd.wap.wml', 'wmlc' => 'application/vnd.wap.wmlc', 'wmls' => 'text/vnd.wap.wmlscript', 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'wmv' => 'video/x-ms-wmv', 'wmx' => 'video/x-ms-wmx', 'wmz' => 'application/x-ms-wmz', 'woff' => 'application/x-font-woff', 'wpd' => 'application/vnd.wordperfect', 'wpl' => 'application/vnd.ms-wpl', 'wps' => 'application/vnd.ms-works', 'wqd' => 'application/vnd.wqd', 'wri' => 'application/x-mswrite', 'wrl' => 'model/vrml', 'wsdl' => 'application/wsdl+xml', 'wspolicy' => 'application/wspolicy+xml', 'wtb' => 'application/vnd.webturbo', 'wvx' => 'video/x-ms-wvx', 'x32' => 'application/x-authorware-bin', 'x3d' => 'application/vnd.hzn-3d-crossword', 'xap' => 'application/x-silverlight-app', 'xar' => 'application/vnd.xara', 'xbap' => 'application/x-ms-xbap', 'xbd' => 'application/vnd.fujixerox.docuworks.binder', 'xbm' => 'image/x-xbitmap', 'xdf' => 'application/xcap-diff+xml', 'xdm' => 'application/vnd.syncml.dm+xml', 'xdp' => 'application/vnd.adobe.xdp+xml', 'xdssc' => 'application/dssc+xml', 'xdw' => 'application/vnd.fujixerox.docuworks', 'xenc' => 'application/xenc+xml', 'xer' => 'application/patch-ops-error+xml', 'xfdf' => 'application/vnd.adobe.xfdf', 'xfdl' => 'application/vnd.xfdl', 'xht' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml', 'xhvml' => 'application/xv+xml', 'xif' => 'image/vnd.xiff', 'xla' => 'application/vnd.ms-excel', 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', 'xlc' => 'application/vnd.ms-excel', 'xlm' => 'application/vnd.ms-excel', 'xls' => 'application/vnd.ms-excel', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlt' => 'application/vnd.ms-excel', 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xlw' => 'application/vnd.ms-excel', 'xml' => 'application/xml', 'xo' => 'application/vnd.olpc-sugar', 'xop' => 'application/xop+xml', 'xpi' => 'application/x-xpinstall', 'xpm' => 'image/x-xpixmap', 'xpr' => 'application/vnd.is-xpr', 'xps' => 'application/vnd.ms-xpsdocument', 'xpw' => 'application/vnd.intercon.formnet', 'xpx' => 'application/vnd.intercon.formnet', 'xsl' => 'application/xml', 'xslt' => 'application/xslt+xml', 'xsm' => 'application/vnd.syncml+xml', 'xspf' => 'application/xspf+xml', 'xul' => 'application/vnd.mozilla.xul+xml', 'xvm' => 'application/xv+xml', 'xvml' => 'application/xv+xml', 'xwd' => 'image/x-xwindowdump', 'xyz' => 'chemical/x-xyz', 'yaml' => 'text/yaml', 'yang' => 'application/yang', 'yin' => 'application/yin+xml', 'yml' => 'text/yaml', 'zaz' => 'application/vnd.zzazz.deck+xml', 'zip' => 'application/zip', 'zir' => 'application/vnd.zul', 'zirz' => 'application/vnd.zul', 'zmm' => 'application/vnd.handheld-entertainment+xml' ); /** * Get a singleton instance of the class * * @return self * @codeCoverageIgnore */ public static function getInstance() { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Get a mimetype value from a file extension * * @param string $extension File extension * * @return string|null * */ public function fromExtension($extension) { $extension = strtolower($extension); return isset($this->mimetypes[$extension]) ? $this->mimetypes[$extension] : null; } /** * Get a mimetype from a filename * * @param string $filename Filename to generate a mimetype from * * @return string|null */ public function fromFilename($filename) { return $this->fromExtension(pathinfo($filename, PATHINFO_EXTENSION)); } } src/Pool.php000064400000025574151742725260007005 0ustar00client = $client; $this->iter = $this->coerceIterable($requests); $this->deferred = new Deferred(); $this->promise = $this->deferred->promise(); $this->poolSize = isset($options['pool_size']) ? $options['pool_size'] : 25; $this->eventListeners = $this->prepareListeners( $options, ['before', 'complete', 'error', 'end'] ); } /** * Sends multiple requests in parallel and returns an array of responses * and exceptions that uses the same ordering as the provided requests. * * IMPORTANT: This method keeps every request and response in memory, and * as such, is NOT recommended when sending a large number or an * indeterminate number of requests concurrently. * * @param ClientInterface $client Client used to send the requests * @param array|\Iterator $requests Requests to send in parallel * @param array $options Passes through the options available in * {@see GuzzleHttp\Pool::__construct} * * @return BatchResults Returns a container for the results. * @throws \InvalidArgumentException if the event format is incorrect. */ public static function batch( ClientInterface $client, $requests, array $options = [] ) { $hash = new \SplObjectStorage(); foreach ($requests as $request) { $hash->attach($request); } // In addition to the normally run events when requests complete, add // and event to continuously track the results of transfers in the hash. (new self($client, $requests, RequestEvents::convertEventArray( $options, ['end'], [ 'priority' => RequestEvents::LATE, 'fn' => function (EndEvent $e) use ($hash) { $hash[$e->getRequest()] = $e->getException() ? $e->getException() : $e->getResponse(); } ] )))->wait(); return new BatchResults($hash); } /** * Creates a Pool and immediately sends the requests. * * @param ClientInterface $client Client used to send the requests * @param array|\Iterator $requests Requests to send in parallel * @param array $options Passes through the options available in * {@see GuzzleHttp\Pool::__construct} */ public static function send( ClientInterface $client, $requests, array $options = [] ) { $pool = new self($client, $requests, $options); $pool->wait(); } private function getPoolSize() { return is_callable($this->poolSize) ? call_user_func($this->poolSize, count($this->waitQueue)) : $this->poolSize; } /** * Add as many requests as possible up to the current pool limit. */ private function addNextRequests() { $limit = max($this->getPoolSize() - count($this->waitQueue), 0); while ($limit--) { if (!$this->addNextRequest()) { break; } } } public function wait() { if ($this->isRealized) { return false; } // Seed the pool with N number of requests. $this->addNextRequests(); // Stop if the pool was cancelled while transferring requests. if ($this->isRealized) { return false; } // Wait on any outstanding FutureResponse objects. while ($response = array_pop($this->waitQueue)) { try { $response->wait(); } catch (\Exception $e) { // Eat exceptions because they should be handled asynchronously } $this->addNextRequests(); } // Clean up no longer needed state. $this->isRealized = true; $this->waitQueue = $this->eventListeners = []; $this->client = $this->iter = null; $this->deferred->resolve(true); return true; } /** * {@inheritdoc} * * Attempt to cancel all outstanding requests (requests that are queued for * dereferencing). Returns true if all outstanding requests can be * cancelled. * * @return bool */ public function cancel() { if ($this->isRealized) { return false; } $success = $this->isRealized = true; foreach ($this->waitQueue as $response) { if (!$response->cancel()) { $success = false; } } return $success; } /** * Returns a promise that is invoked when the pool completed. There will be * no passed value. * * {@inheritdoc} */ public function then( callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null ) { return $this->promise->then($onFulfilled, $onRejected, $onProgress); } public function promise() { return $this->promise; } private function coerceIterable($requests) { if ($requests instanceof \Iterator) { return $requests; } elseif (is_array($requests)) { return new \ArrayIterator($requests); } throw new \InvalidArgumentException('Expected Iterator or array. ' . 'Found ' . Core::describeType($requests)); } /** * Adds the next request to pool and tracks what requests need to be * dereferenced when completing the pool. */ private function addNextRequest() { add_next: if ($this->isRealized || !$this->iter || !$this->iter->valid()) { return false; } $request = $this->iter->current(); $this->iter->next(); if (!($request instanceof RequestInterface)) { throw new \InvalidArgumentException(sprintf( 'All requests in the provided iterator must implement ' . 'RequestInterface. Found %s', Core::describeType($request) )); } // Be sure to use "lazy" futures, meaning they do not send right away. $request->getConfig()->set('future', 'lazy'); $hash = spl_object_hash($request); $this->attachListeners($request, $this->eventListeners); $request->getEmitter()->on('before', [$this, '_trackRetries'], RequestEvents::EARLY); $response = $this->client->send($request); $this->waitQueue[$hash] = $response; $promise = $response->promise(); // Don't recursively call itself for completed or rejected responses. if ($promise instanceof FulfilledPromise || $promise instanceof RejectedPromise ) { try { $this->finishResponse($request, $response->wait(), $hash); } catch (\Exception $e) { $this->finishResponse($request, $e, $hash); } goto add_next; } // Use this function for both resolution and rejection. $thenFn = function ($value) use ($request, $hash) { $this->finishResponse($request, $value, $hash); if (!$request->getConfig()->get('_pool_retries')) { $this->addNextRequests(); } }; $promise->then($thenFn, $thenFn); return true; } public function _trackRetries(BeforeEvent $e) { $e->getRequest()->getConfig()->set('_pool_retries', $e->getRetryCount()); } private function finishResponse($request, $value, $hash) { unset($this->waitQueue[$hash]); $result = $value instanceof ResponseInterface ? ['request' => $request, 'response' => $value, 'error' => null] : ['request' => $request, 'response' => null, 'error' => $value]; $this->deferred->notify($result); } } src/Post/MultipartBody.php000064400000005646151742725260011616 0ustar00boundary = $boundary ?: uniqid(); $this->stream = $this->createStream($fields, $files); } /** * Get the boundary * * @return string */ public function getBoundary() { return $this->boundary; } public function isWritable() { return false; } /** * Get the string needed to transfer a POST field */ private function getFieldString($name, $value) { return sprintf( "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", $this->boundary, $name, $value ); } /** * Get the headers needed before transferring the content of a POST file */ private function getFileHeaders(PostFileInterface $file) { $headers = ''; foreach ($file->getHeaders() as $key => $value) { $headers .= "{$key}: {$value}\r\n"; } return "--{$this->boundary}\r\n" . trim($headers) . "\r\n\r\n"; } /** * Create the aggregate stream that will be used to upload the POST data */ protected function createStream(array $fields, array $files) { $stream = new AppendStream(); foreach ($fields as $name => $fieldValues) { foreach ((array) $fieldValues as $value) { $stream->addStream( Stream::factory($this->getFieldString($name, $value)) ); } } foreach ($files as $file) { if (!$file instanceof PostFileInterface) { throw new \InvalidArgumentException('All POST fields must ' . 'implement PostFieldInterface'); } $stream->addStream( Stream::factory($this->getFileHeaders($file)) ); $stream->addStream($file->getContent()); $stream->addStream(Stream::factory("\r\n")); } // Add the trailing boundary with CRLF $stream->addStream(Stream::factory("--{$this->boundary}--\r\n")); return $stream; } } src/Post/PostBody.php000064400000014135151742725260010553 0ustar00files || $this->forceMultipart) { $request->setHeader( 'Content-Type', 'multipart/form-data; boundary=' . $this->getBody()->getBoundary() ); } elseif ($this->fields && !$request->hasHeader('Content-Type')) { $request->setHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); } if ($size = $this->getSize()) { $request->setHeader('Content-Length', $size); } } public function forceMultipartUpload($force) { $this->forceMultipart = $force; } public function setAggregator(callable $aggregator) { $this->aggregator = $aggregator; } public function setField($name, $value) { $this->fields[$name] = $value; $this->mutate(); } public function replaceFields(array $fields) { $this->fields = $fields; $this->mutate(); } public function getField($name) { return isset($this->fields[$name]) ? $this->fields[$name] : null; } public function removeField($name) { unset($this->fields[$name]); $this->mutate(); } public function getFields($asString = false) { if (!$asString) { return $this->fields; } $query = new Query($this->fields); $query->setEncodingType(Query::RFC1738); $query->setAggregator($this->getAggregator()); return (string) $query; } public function hasField($name) { return isset($this->fields[$name]); } public function getFile($name) { foreach ($this->files as $file) { if ($file->getName() == $name) { return $file; } } return null; } public function getFiles() { return $this->files; } public function addFile(PostFileInterface $file) { $this->files[] = $file; $this->mutate(); } public function clearFiles() { $this->files = []; $this->mutate(); } /** * Returns the numbers of fields + files * * @return int */ public function count() { return count($this->files) + count($this->fields); } public function __toString() { return (string) $this->getBody(); } public function getContents($maxLength = -1) { return $this->getBody()->getContents(); } public function close() { $this->detach(); } public function detach() { $this->detached = true; $this->fields = $this->files = []; if ($this->body) { $this->body->close(); $this->body = null; } } public function attach($stream) { throw new CannotAttachException(); } public function eof() { return $this->getBody()->eof(); } public function tell() { return $this->body ? $this->body->tell() : 0; } public function isSeekable() { return true; } public function isReadable() { return true; } public function isWritable() { return false; } public function getSize() { return $this->getBody()->getSize(); } public function seek($offset, $whence = SEEK_SET) { return $this->getBody()->seek($offset, $whence); } public function read($length) { return $this->getBody()->read($length); } public function write($string) { return false; } public function getMetadata($key = null) { return $key ? null : []; } /** * Return a stream object that is built from the POST fields and files. * * If one has already been created, the previously created stream will be * returned. */ private function getBody() { if ($this->body) { return $this->body; } elseif ($this->files || $this->forceMultipart) { return $this->body = $this->createMultipart(); } elseif ($this->fields) { return $this->body = $this->createUrlEncoded(); } else { return $this->body = Stream::factory(); } } /** * Get the aggregator used to join multi-valued field parameters * * @return callable */ final protected function getAggregator() { if (!$this->aggregator) { $this->aggregator = Query::phpAggregator(); } return $this->aggregator; } /** * Creates a multipart/form-data body stream * * @return MultipartBody */ private function createMultipart() { // Flatten the nested query string values using the correct aggregator return new MultipartBody( call_user_func($this->getAggregator(), $this->fields), $this->files ); } /** * Creates an application/x-www-form-urlencoded stream body * * @return StreamInterface */ private function createUrlEncoded() { return Stream::factory($this->getFields(true)); } /** * Get rid of any cached data */ private function mutate() { $this->body = null; } } src/Post/PostBodyInterface.php000064400000005336151742725260012377 0ustar00headers = $headers; $this->name = $name; $this->prepareContent($content); $this->prepareFilename($filename); $this->prepareDefaultHeaders(); } public function getName() { return $this->name; } public function getFilename() { return $this->filename; } public function getContent() { return $this->content; } public function getHeaders() { return $this->headers; } /** * Prepares the contents of a POST file. * * @param mixed $content Content of the POST file */ private function prepareContent($content) { $this->content = $content; if (!($this->content instanceof StreamInterface)) { $this->content = Stream::factory($this->content); } elseif ($this->content instanceof MultipartBody) { if (!$this->hasHeader('Content-Disposition')) { $disposition = 'form-data; name="' . $this->name .'"'; $this->headers['Content-Disposition'] = $disposition; } if (!$this->hasHeader('Content-Type')) { $this->headers['Content-Type'] = sprintf( "multipart/form-data; boundary=%s", $this->content->getBoundary() ); } } } /** * Applies a file name to the POST file based on various checks. * * @param string|null $filename Filename to apply (or null to guess) */ private function prepareFilename($filename) { $this->filename = $filename; if (!$this->filename) { $this->filename = $this->content->getMetadata('uri'); } if (!$this->filename || substr($this->filename, 0, 6) === 'php://') { $this->filename = $this->name; } } /** * Applies default Content-Disposition and Content-Type headers if needed. */ private function prepareDefaultHeaders() { // Set a default content-disposition header if one was no provided if (!$this->hasHeader('Content-Disposition')) { $this->headers['Content-Disposition'] = sprintf( 'form-data; name="%s"; filename="%s"', $this->name, basename($this->filename) ); } // Set a default Content-Type if one was not supplied if (!$this->hasHeader('Content-Type')) { $this->headers['Content-Type'] = Mimetypes::getInstance() ->fromFilename($this->filename) ?: 'text/plain'; } } /** * Check if a specific header exists on the POST file by name. * * @param string $name Case-insensitive header to check * * @return bool */ private function hasHeader($name) { return isset(array_change_key_case($this->headers)[strtolower($name)]); } } src/Post/PostFileInterface.php000064400000001404151742725260012351 0ustar00setEncodingType($urlEncoding); } $qp->parseInto($q, $query, $urlEncoding); return $q; } /** * Convert the query string parameters to a query string string * * @return string */ public function __toString() { if (!$this->data) { return ''; } // The default aggregator is statically cached static $defaultAggregator; if (!$this->aggregator) { if (!$defaultAggregator) { $defaultAggregator = self::phpAggregator(); } $this->aggregator = $defaultAggregator; } $result = ''; $aggregator = $this->aggregator; $encoder = $this->encoding; foreach ($aggregator($this->data) as $key => $values) { foreach ($values as $value) { if ($result) { $result .= '&'; } $result .= $encoder($key); if ($value !== null) { $result .= '=' . $encoder($value); } } } return $result; } /** * Controls how multi-valued query string parameters are aggregated into a * string. * * $query->setAggregator($query::duplicateAggregator()); * * @param callable $aggregator Callable used to convert a deeply nested * array of query string variables into a flattened array of key value * pairs. The callable accepts an array of query data and returns a * flattened array of key value pairs where each value is an array of * strings. */ public function setAggregator(callable $aggregator) { $this->aggregator = $aggregator; } /** * Specify how values are URL encoded * * @param string|bool $type One of 'RFC1738', 'RFC3986', or false to disable encoding * * @throws \InvalidArgumentException */ public function setEncodingType($type) { switch ($type) { case self::RFC3986: $this->encoding = 'rawurlencode'; break; case self::RFC1738: $this->encoding = 'urlencode'; break; case false: $this->encoding = function ($v) { return $v; }; break; default: throw new \InvalidArgumentException('Invalid URL encoding type'); } } /** * Query string aggregator that does not aggregate nested query string * values and allows duplicates in the resulting array. * * Example: http://test.com?q=1&q=2 * * @return callable */ public static function duplicateAggregator() { return function (array $data) { return self::walkQuery($data, '', function ($key, $prefix) { return is_int($key) ? $prefix : "{$prefix}[{$key}]"; }); }; } /** * Aggregates nested query string variables using the same technique as * ``http_build_query()``. * * @param bool $numericIndices Pass false to not include numeric indices * when multi-values query string parameters are present. * * @return callable */ public static function phpAggregator($numericIndices = true) { return function (array $data) use ($numericIndices) { return self::walkQuery( $data, '', function ($key, $prefix) use ($numericIndices) { return !$numericIndices && is_int($key) ? "{$prefix}[]" : "{$prefix}[{$key}]"; } ); }; } /** * Easily create query aggregation functions by providing a key prefix * function to this query string array walker. * * @param array $query Query string to walk * @param string $keyPrefix Key prefix (start with '') * @param callable $prefixer Function used to create a key prefix * * @return array */ public static function walkQuery(array $query, $keyPrefix, callable $prefixer) { $result = []; foreach ($query as $key => $value) { if ($keyPrefix) { $key = $prefixer($key, $keyPrefix); } if (is_array($value)) { $result += self::walkQuery($value, $key, $prefixer); } elseif (isset($result[$key])) { $result[$key][] = $value; } else { $result[$key] = array($value); } } return $result; } } src/QueryParser.php000064400000011406151742725260010343 0ustar00duplicates = false; $this->numericIndices = true; $decoder = self::getDecoder($urlEncoding); foreach (explode('&', $str) as $kvp) { $parts = explode('=', $kvp, 2); $key = $decoder($parts[0]); $value = isset($parts[1]) ? $decoder($parts[1]) : null; // Special handling needs to be taken for PHP nested array syntax if (strpos($key, '[') !== false) { $this->parsePhpValue($key, $value, $result); continue; } if (!isset($result[$key])) { $result[$key] = $value; } else { $this->duplicates = true; if (!is_array($result[$key])) { $result[$key] = [$result[$key]]; } $result[$key][] = $value; } } $query->replace($result); if (!$this->numericIndices) { $query->setAggregator(Query::phpAggregator(false)); } elseif ($this->duplicates) { $query->setAggregator(Query::duplicateAggregator()); } } /** * Returns a callable that is used to URL decode query keys and values. * * @param string|bool $type One of true, false, RFC3986, and RFC1738 * * @return callable|string */ private static function getDecoder($type) { if ($type === true) { return function ($value) { return rawurldecode(str_replace('+', ' ', $value)); }; } elseif ($type == Query::RFC3986) { return 'rawurldecode'; } elseif ($type == Query::RFC1738) { return 'urldecode'; } else { return function ($str) { return $str; }; } } /** * Parses a PHP style key value pair. * * @param string $key Key to parse (e.g., "foo[a][b]") * @param string|null $value Value to set * @param array $result Result to modify by reference */ private function parsePhpValue($key, $value, array &$result) { $node =& $result; $keyBuffer = ''; for ($i = 0, $t = strlen($key); $i < $t; $i++) { switch ($key[$i]) { case '[': if ($keyBuffer) { $this->prepareNode($node, $keyBuffer); $node =& $node[$keyBuffer]; $keyBuffer = ''; } break; case ']': $k = $this->cleanKey($node, $keyBuffer); $this->prepareNode($node, $k); $node =& $node[$k]; $keyBuffer = ''; break; default: $keyBuffer .= $key[$i]; break; } } if (isset($node)) { $this->duplicates = true; $node[] = $value; } else { $node = $value; } } /** * Prepares a value in the array at the given key. * * If the key already exists, the key value is converted into an array. * * @param array $node Result node to modify * @param string $key Key to add or modify in the node */ private function prepareNode(&$node, $key) { if (!isset($node[$key])) { $node[$key] = null; } elseif (!is_array($node[$key])) { $node[$key] = [$node[$key]]; } } /** * Returns the appropriate key based on the node and key. */ private function cleanKey($node, $key) { if ($key === '') { $key = $node ? (string) count($node) : 0; // Found a [] key, so track this to ensure that we disable numeric // indexing of keys in the resolved query aggregator. $this->numericIndices = false; } return $key; } } src/RequestFsm.php000064400000011723151742725260010161 0ustar00mf = $messageFactory; $this->maxTransitions = $maxTransitions; $this->handler = $handler; } /** * Runs the state machine until a terminal state is entered or the * optionally supplied $finalState is entered. * * @param Transaction $trans Transaction being transitioned. * * @throws \Exception if a terminal state throws an exception. */ public function __invoke(Transaction $trans) { $trans->_transitionCount = 0; if (!$trans->state) { $trans->state = 'before'; } transition: if (++$trans->_transitionCount > $this->maxTransitions) { throw new StateException("Too many state transitions were " . "encountered ({$trans->_transitionCount}). This likely " . "means that a combination of event listeners are in an " . "infinite loop."); } switch ($trans->state) { case 'before': goto before; case 'complete': goto complete; case 'error': goto error; case 'retry': goto retry; case 'send': goto send; case 'end': goto end; default: throw new StateException("Invalid state: {$trans->state}"); } before: { try { $trans->request->getEmitter()->emit('before', new BeforeEvent($trans)); $trans->state = 'send'; if ((bool) $trans->response) { $trans->state = 'complete'; } } catch (\Exception $e) { $trans->state = 'error'; $trans->exception = $e; } goto transition; } complete: { try { if ($trans->response instanceof FutureInterface) { // Futures will have their own end events emitted when // dereferenced. return; } $trans->state = 'end'; $trans->response->setEffectiveUrl($trans->request->getUrl()); $trans->request->getEmitter()->emit('complete', new CompleteEvent($trans)); } catch (\Exception $e) { $trans->state = 'error'; $trans->exception = $e; } goto transition; } error: { try { // Convert non-request exception to a wrapped exception $trans->exception = RequestException::wrapException( $trans->request, $trans->exception ); $trans->state = 'end'; $trans->request->getEmitter()->emit('error', new ErrorEvent($trans)); // An intercepted request (not retried) transitions to complete if (!$trans->exception && $trans->state !== 'retry') { $trans->state = 'complete'; } } catch (\Exception $e) { $trans->state = 'end'; $trans->exception = $e; } goto transition; } retry: { $trans->retries++; $trans->response = null; $trans->exception = null; $trans->state = 'before'; goto transition; } send: { $fn = $this->handler; $trans->response = FutureResponse::proxy( $fn(RingBridge::prepareRingRequest($trans)), function ($value) use ($trans) { RingBridge::completeRingResponse($trans, $value, $this->mf, $this); $this($trans); return $trans->response; } ); return; } end: { $trans->request->getEmitter()->emit('end', new EndEvent($trans)); // Throw exceptions in the terminal event if the exception // was not handled by an "end" event listener. if ($trans->exception) { if (!($trans->exception instanceof RequestException)) { $trans->exception = RequestException::wrapException( $trans->request, $trans->exception ); } throw $trans->exception; } } } } src/RingBridge.php000064400000013432151742725260010076 0ustar00getConfig()->toArray(); $url = $request->getUrl(); // No need to calculate the query string twice (in URL and query). $qs = ($pos = strpos($url, '?')) ? substr($url, $pos + 1) : null; return [ 'scheme' => $request->getScheme(), 'http_method' => $request->getMethod(), 'url' => $url, 'uri' => $request->getPath(), 'headers' => $request->getHeaders(), 'body' => $request->getBody(), 'version' => $request->getProtocolVersion(), 'client' => $options, 'query_string' => $qs, 'future' => isset($options['future']) ? $options['future'] : false ]; } /** * Creates a Ring request from a request object AND prepares the callbacks. * * @param Transaction $trans Transaction to update. * * @return array Converted Guzzle Ring request. */ public static function prepareRingRequest(Transaction $trans) { // Clear out the transaction state when initiating. $trans->exception = null; $request = self::createRingRequest($trans->request); // Emit progress events if any progress listeners are registered. if ($trans->request->getEmitter()->hasListeners('progress')) { $emitter = $trans->request->getEmitter(); $request['client']['progress'] = function ($a, $b, $c, $d) use ($trans, $emitter) { $emitter->emit('progress', new ProgressEvent($trans, $a, $b, $c, $d)); }; } return $request; } /** * Handles the process of processing a response received from a ring * handler. The created response is added to the transaction, and the * transaction stat is set appropriately. * * @param Transaction $trans Owns request and response. * @param array $response Ring response array * @param MessageFactoryInterface $messageFactory Creates response objects. */ public static function completeRingResponse( Transaction $trans, array $response, MessageFactoryInterface $messageFactory ) { $trans->state = 'complete'; $trans->transferInfo = isset($response['transfer_stats']) ? $response['transfer_stats'] : []; if (!empty($response['status'])) { $options = []; if (isset($response['version'])) { $options['protocol_version'] = $response['version']; } if (isset($response['reason'])) { $options['reason_phrase'] = $response['reason']; } $trans->response = $messageFactory->createResponse( $response['status'], isset($response['headers']) ? $response['headers'] : [], isset($response['body']) ? $response['body'] : null, $options ); if (isset($response['effective_url'])) { $trans->response->setEffectiveUrl($response['effective_url']); } } elseif (empty($response['error'])) { // When nothing was returned, then we need to add an error. $response['error'] = self::getNoRingResponseException($trans->request); } if (isset($response['error'])) { $trans->state = 'error'; $trans->exception = $response['error']; } } /** * Creates a Guzzle request object using a ring request array. * * @param array $request Ring request * * @return Request * @throws \InvalidArgumentException for incomplete requests. */ public static function fromRingRequest(array $request) { $options = []; if (isset($request['version'])) { $options['protocol_version'] = $request['version']; } if (!isset($request['http_method'])) { throw new \InvalidArgumentException('No http_method'); } return new Request( $request['http_method'], Core::url($request), isset($request['headers']) ? $request['headers'] : [], isset($request['body']) ? Stream::factory($request['body']) : null, $options ); } /** * Get an exception that can be used when a RingPHP handler does not * populate a response. * * @param RequestInterface $request * * @return RequestException */ public static function getNoRingResponseException(RequestInterface $request) { $message = <<cookieJar = $cookieJar ?: new CookieJar(); } public function getEvents() { // Fire the cookie plugin complete event before redirecting return [ 'before' => ['onBefore'], 'complete' => ['onComplete', RequestEvents::REDIRECT_RESPONSE + 10] ]; } /** * Get the cookie cookieJar * * @return CookieJarInterface */ public function getCookieJar() { return $this->cookieJar; } public function onBefore(BeforeEvent $event) { $this->cookieJar->addCookieHeader($event->getRequest()); } public function onComplete(CompleteEvent $event) { $this->cookieJar->extractCookies( $event->getRequest(), $event->getResponse() ); } } src/Subscriber/History.php000064400000012050151742725260011621 0ustar00limit = $limit; } public function getEvents() { return [ 'complete' => ['onComplete', RequestEvents::EARLY], 'error' => ['onError', RequestEvents::EARLY], ]; } /** * Convert to a string that contains all request and response headers * * @return string */ public function __toString() { $lines = array(); foreach ($this->transactions as $entry) { $response = isset($entry['response']) ? $entry['response'] : ''; $lines[] = '> ' . trim($entry['sent_request']) . "\n\n< " . trim($response) . "\n"; } return implode("\n", $lines); } public function onComplete(CompleteEvent $event) { $this->add($event->getRequest(), $event->getResponse()); } public function onError(ErrorEvent $event) { // Only track when no response is present, meaning this didn't ever // emit a complete event if (!$event->getResponse()) { $this->add($event->getRequest()); } } /** * Returns an Iterator that yields associative array values where each * associative array contains the following key value pairs: * * - request: Representing the actual request that was received. * - sent_request: A clone of the request that will not be mutated. * - response: The response that was received (if available). * * @return \Iterator */ public function getIterator() { return new \ArrayIterator($this->transactions); } /** * Get all of the requests sent through the plugin. * * Requests can be modified after they are logged by the history * subscriber. By default this method will return the actual request * instances that were received. Pass true to this method if you wish to * get copies of the requests that represent the request state when it was * initially logged by the history subscriber. * * @param bool $asSent Set to true to get clones of the requests that have * not been mutated since the request was received by * the history subscriber. * * @return RequestInterface[] */ public function getRequests($asSent = false) { return array_map(function ($t) use ($asSent) { return $asSent ? $t['sent_request'] : $t['request']; }, $this->transactions); } /** * Get the number of requests in the history * * @return int */ public function count() { return count($this->transactions); } /** * Get the last request sent. * * Requests can be modified after they are logged by the history * subscriber. By default this method will return the actual request * instance that was received. Pass true to this method if you wish to get * a copy of the request that represents the request state when it was * initially logged by the history subscriber. * * @param bool $asSent Set to true to get a clone of the last request that * has not been mutated since the request was received * by the history subscriber. * * @return RequestInterface */ public function getLastRequest($asSent = false) { return $asSent ? end($this->transactions)['sent_request'] : end($this->transactions)['request']; } /** * Get the last response in the history * * @return ResponseInterface|null */ public function getLastResponse() { return end($this->transactions)['response']; } /** * Clears the history */ public function clear() { $this->transactions = array(); } /** * Add a request to the history * * @param RequestInterface $request Request to add * @param ResponseInterface $response Response of the request */ private function add( RequestInterface $request, ResponseInterface $response = null ) { $this->transactions[] = [ 'request' => $request, 'sent_request' => clone $request, 'response' => $response ]; if (count($this->transactions) > $this->limit) { array_shift($this->transactions); } } } src/Subscriber/HttpError.php000064400000001706151742725260012117 0ustar00 ['onComplete', RequestEvents::VERIFY_RESPONSE]]; } /** * Throw a RequestException on an HTTP protocol error * * @param CompleteEvent $event Emitted event * @throws RequestException */ public function onComplete(CompleteEvent $event) { $code = (string) $event->getResponse()->getStatusCode(); // Throw an exception for an unsuccessful response if ($code[0] >= 4) { throw RequestException::create( $event->getRequest(), $event->getResponse() ); } } } src/Subscriber/Mock.php000064400000007757151742725260011073 0ustar00factory = new MessageFactory(); $this->readBodies = $readBodies; $this->addMultiple($items); } public function getEvents() { // Fire the event last, after signing return ['before' => ['onBefore', RequestEvents::SIGN_REQUEST - 10]]; } /** * @throws \OutOfBoundsException|\Exception */ public function onBefore(BeforeEvent $event) { if (!$item = array_shift($this->queue)) { throw new \OutOfBoundsException('Mock queue is empty'); } elseif ($item instanceof RequestException) { throw $item; } // Emulate reading a response body $request = $event->getRequest(); if ($this->readBodies && $request->getBody()) { while (!$request->getBody()->eof()) { $request->getBody()->read(8096); } } $saveTo = $event->getRequest()->getConfig()->get('save_to'); if (null !== $saveTo) { $body = $item->getBody(); if (is_resource($saveTo)) { fwrite($saveTo, $body); } elseif (is_string($saveTo)) { file_put_contents($saveTo, $body); } elseif ($saveTo instanceof StreamInterface) { $saveTo->write($body); } } $event->intercept($item); } public function count() { return count($this->queue); } /** * Add a response to the end of the queue * * @param string|ResponseInterface $response Response or path to response file * * @return self * @throws \InvalidArgumentException if a string or Response is not passed */ public function addResponse($response) { if (is_string($response)) { $response = file_exists($response) ? $this->factory->fromMessage(file_get_contents($response)) : $this->factory->fromMessage($response); } elseif (!($response instanceof ResponseInterface)) { throw new \InvalidArgumentException('Response must a message ' . 'string, response object, or path to a file'); } $this->queue[] = $response; return $this; } /** * Add an exception to the end of the queue * * @param RequestException $e Exception to throw when the request is executed * * @return self */ public function addException(RequestException $e) { $this->queue[] = $e; return $this; } /** * Add multiple items to the queue * * @param array $items Items to add */ public function addMultiple(array $items) { foreach ($items as $item) { if ($item instanceof RequestException) { $this->addException($item); } else { $this->addResponse($item); } } } /** * Clear the queue */ public function clearQueue() { $this->queue = []; } } src/Subscriber/Prepare.php000064400000010457151742725260011567 0ustar00 ['onBefore', RequestEvents::PREPARE_REQUEST]]; } public function onBefore(BeforeEvent $event) { $request = $event->getRequest(); // Set the appropriate Content-Type for a request if one is not set and // there are form fields if (!($body = $request->getBody())) { return; } $this->addContentLength($request, $body); if ($body instanceof AppliesHeadersInterface) { // Synchronize the body with the request headers $body->applyRequestHeaders($request); } elseif (!$request->hasHeader('Content-Type')) { $this->addContentType($request, $body); } $this->addExpectHeader($request, $body); } private function addContentType( RequestInterface $request, StreamInterface $body ) { if (!($uri = $body->getMetadata('uri'))) { return; } // Guess the content-type based on the stream's "uri" metadata value. // The file extension is used to determine the appropriate mime-type. if ($contentType = Mimetypes::getInstance()->fromFilename($uri)) { $request->setHeader('Content-Type', $contentType); } } private function addContentLength( RequestInterface $request, StreamInterface $body ) { // Set the Content-Length header if it can be determined, and never // send a Transfer-Encoding: chunked and Content-Length header in // the same request. if ($request->hasHeader('Content-Length')) { // Remove transfer-encoding if content-length is set. $request->removeHeader('Transfer-Encoding'); return; } if ($request->hasHeader('Transfer-Encoding')) { return; } if (null !== ($size = $body->getSize())) { $request->setHeader('Content-Length', $size); $request->removeHeader('Transfer-Encoding'); } elseif ('1.1' == $request->getProtocolVersion()) { // Use chunked Transfer-Encoding if there is no determinable // content-length header and we're using HTTP/1.1. $request->setHeader('Transfer-Encoding', 'chunked'); $request->removeHeader('Content-Length'); } } private function addExpectHeader( RequestInterface $request, StreamInterface $body ) { // Determine if the Expect header should be used if ($request->hasHeader('Expect')) { return; } $expect = $request->getConfig()['expect']; // Return if disabled or if you're not using HTTP/1.1 if ($expect === false || $request->getProtocolVersion() !== '1.1') { return; } // The expect header is unconditionally enabled if ($expect === true) { $request->setHeader('Expect', '100-Continue'); return; } // By default, send the expect header when the payload is > 1mb if ($expect === null) { $expect = 1048576; } // Always add if the body cannot be rewound, the size cannot be // determined, or the size is greater than the cutoff threshold $size = $body->getSize(); if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { $request->setHeader('Expect', '100-Continue'); } } } src/Subscriber/Redirect.php000064400000014077151742725260011734 0ustar00 ['onComplete', RequestEvents::REDIRECT_RESPONSE]]; } /** * Rewind the entity body of the request if needed * * @param RequestInterface $redirectRequest * @throws CouldNotRewindStreamException */ public static function rewindEntityBody(RequestInterface $redirectRequest) { // Rewind the entity body of the request if needed if ($body = $redirectRequest->getBody()) { // Only rewind the body if some of it has been read already, and // throw an exception if the rewind fails if ($body->tell() && !$body->seek(0)) { throw new CouldNotRewindStreamException( 'Unable to rewind the non-seekable request body after redirecting', $redirectRequest ); } } } /** * Called when a request receives a redirect response * * @param CompleteEvent $event Event emitted * @throws TooManyRedirectsException */ public function onComplete(CompleteEvent $event) { $response = $event->getResponse(); if (substr($response->getStatusCode(), 0, 1) != '3' || !$response->hasHeader('Location') ) { return; } $request = $event->getRequest(); $config = $request->getConfig(); // Increment the redirect and initialize the redirect state. if ($redirectCount = $config['redirect_count']) { $config['redirect_count'] = ++$redirectCount; } else { $config['redirect_scheme'] = $request->getScheme(); $config['redirect_count'] = $redirectCount = 1; } $max = $config->getPath('redirect/max') ?: 5; if ($redirectCount > $max) { throw new TooManyRedirectsException( "Will not follow more than {$redirectCount} redirects", $request ); } $this->modifyRedirectRequest($request, $response); $event->retry(); } private function modifyRedirectRequest( RequestInterface $request, ResponseInterface $response ) { $config = $request->getConfig(); $protocols = $config->getPath('redirect/protocols') ?: ['http', 'https']; // Use a GET request if this is an entity enclosing request and we are // not forcing RFC compliance, but rather emulating what all browsers // would do. $statusCode = $response->getStatusCode(); if ($statusCode == 303 || ($statusCode <= 302 && $request->getBody() && !$config->getPath('redirect/strict')) ) { $request->setMethod('GET'); $request->setBody(null); } $previousUrl = $request->getUrl(); $this->setRedirectUrl($request, $response, $protocols); $this->rewindEntityBody($request); // Add the Referer header if it is told to do so and only // add the header if we are not redirecting from https to http. if ($config->getPath('redirect/referer') && ($request->getScheme() == 'https' || $request->getScheme() == $config['redirect_scheme']) ) { $url = Url::fromString($previousUrl); $url->setUsername(null); $url->setPassword(null); $request->setHeader('Referer', (string) $url); } else { $request->removeHeader('Referer'); } } /** * Set the appropriate URL on the request based on the location header * * @param RequestInterface $request * @param ResponseInterface $response * @param array $protocols */ private function setRedirectUrl( RequestInterface $request, ResponseInterface $response, array $protocols ) { $location = $response->getHeader('Location'); $location = Url::fromString($location); // Combine location with the original URL if it is not absolute. if (!$location->isAbsolute()) { $originalUrl = Url::fromString($request->getUrl()); // Remove query string parameters and just take what is present on // the redirect Location header $originalUrl->getQuery()->clear(); $location = $originalUrl->combine($location); } // Ensure that the redirect URL is allowed based on the protocols. if (!in_array($location->getScheme(), $protocols)) { throw new BadResponseException( sprintf( 'Redirect URL, %s, does not use one of the allowed redirect protocols: %s', $location, implode(', ', $protocols) ), $request, $response ); } $request->setUrl($location); } } src/ToArrayInterface.php000064400000000361151742725260011261 0ustar00client = $client; $this->request = $request; $this->_future = $future; } } src/UriTemplate.php000064400000020001151742725260010303 0ustar00 array('prefix' => '', 'joiner' => ',', 'query' => false), '+' => array('prefix' => '', 'joiner' => ',', 'query' => false), '#' => array('prefix' => '#', 'joiner' => ',', 'query' => false), '.' => array('prefix' => '.', 'joiner' => '.', 'query' => false), '/' => array('prefix' => '/', 'joiner' => '/', 'query' => false), ';' => array('prefix' => ';', 'joiner' => ';', 'query' => true), '?' => array('prefix' => '?', 'joiner' => '&', 'query' => true), '&' => array('prefix' => '&', 'joiner' => '&', 'query' => true) ); /** @var array Delimiters */ private static $delims = array(':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='); /** @var array Percent encoded delimiters */ private static $delimsPct = array('%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%3B', '%3D'); public function expand($template, array $variables) { if (false === strpos($template, '{')) { return $template; } $this->template = $template; $this->variables = $variables; return preg_replace_callback( '/\{([^\}]+)\}/', [$this, 'expandMatch'], $this->template ); } /** * Parse an expression into parts * * @param string $expression Expression to parse * * @return array Returns an associative array of parts */ private function parseExpression($expression) { $result = array(); if (isset(self::$operatorHash[$expression[0]])) { $result['operator'] = $expression[0]; $expression = substr($expression, 1); } else { $result['operator'] = ''; } foreach (explode(',', $expression) as $value) { $value = trim($value); $varspec = array(); if ($colonPos = strpos($value, ':')) { $varspec['value'] = substr($value, 0, $colonPos); $varspec['modifier'] = ':'; $varspec['position'] = (int) substr($value, $colonPos + 1); } elseif (substr($value, -1) == '*') { $varspec['modifier'] = '*'; $varspec['value'] = substr($value, 0, -1); } else { $varspec['value'] = (string) $value; $varspec['modifier'] = ''; } $result['values'][] = $varspec; } return $result; } /** * Process an expansion * * @param array $matches Matches met in the preg_replace_callback * * @return string Returns the replacement string */ private function expandMatch(array $matches) { static $rfc1738to3986 = array('+' => '%20', '%7e' => '~'); $replacements = array(); $parsed = self::parseExpression($matches[1]); $prefix = self::$operatorHash[$parsed['operator']]['prefix']; $joiner = self::$operatorHash[$parsed['operator']]['joiner']; $useQuery = self::$operatorHash[$parsed['operator']]['query']; foreach ($parsed['values'] as $value) { if (!isset($this->variables[$value['value']])) { continue; } $variable = $this->variables[$value['value']]; $actuallyUseQuery = $useQuery; $expanded = ''; if (is_array($variable)) { $isAssoc = $this->isAssoc($variable); $kvp = array(); foreach ($variable as $key => $var) { if ($isAssoc) { $key = rawurlencode($key); $isNestedArray = is_array($var); } else { $isNestedArray = false; } if (!$isNestedArray) { $var = rawurlencode($var); if ($parsed['operator'] == '+' || $parsed['operator'] == '#' ) { $var = $this->decodeReserved($var); } } if ($value['modifier'] == '*') { if ($isAssoc) { if ($isNestedArray) { // Nested arrays must allow for deeply nested // structures. $var = strtr( http_build_query([$key => $var]), $rfc1738to3986 ); } else { $var = $key . '=' . $var; } } elseif ($key > 0 && $actuallyUseQuery) { $var = $value['value'] . '=' . $var; } } $kvp[$key] = $var; } if (empty($variable)) { $actuallyUseQuery = false; } elseif ($value['modifier'] == '*') { $expanded = implode($joiner, $kvp); if ($isAssoc) { // Don't prepend the value name when using the explode // modifier with an associative array. $actuallyUseQuery = false; } } else { if ($isAssoc) { // When an associative array is encountered and the // explode modifier is not set, then the result must be // a comma separated list of keys followed by their // respective values. foreach ($kvp as $k => &$v) { $v = $k . ',' . $v; } } $expanded = implode(',', $kvp); } } else { if ($value['modifier'] == ':') { $variable = substr($variable, 0, $value['position']); } $expanded = rawurlencode($variable); if ($parsed['operator'] == '+' || $parsed['operator'] == '#') { $expanded = $this->decodeReserved($expanded); } } if ($actuallyUseQuery) { if (!$expanded && $joiner != '&') { $expanded = $value['value']; } else { $expanded = $value['value'] . '=' . $expanded; } } $replacements[] = $expanded; } $ret = implode($joiner, $replacements); if ($ret && $prefix) { return $prefix . $ret; } return $ret; } /** * Determines if an array is associative. * * This makes the assumption that input arrays are sequences or hashes. * This assumption is a tradeoff for accuracy in favor of speed, but it * should work in almost every case where input is supplied for a URI * template. * * @param array $array Array to check * * @return bool */ private function isAssoc(array $array) { return $array && array_keys($array)[0] !== 0; } /** * Removes percent encoding on reserved characters (used with + and # * modifiers). * * @param string $string String to fix * * @return string */ private function decodeReserved($string) { return str_replace(self::$delimsPct, self::$delims, $string); } } src/Url.php000064400000037012151742725260006624 0ustar00 80, 'https' => 443, 'ftp' => 21]; private static $pathPattern = '/[^a-zA-Z0-9\-\._~!\$&\'\(\)\*\+,;=%:@\/]+|%(?![A-Fa-f0-9]{2})/'; private static $queryPattern = '/[^a-zA-Z0-9\-\._~!\$\'\(\)\*\+,;%:@\/\?=&]+|%(?![A-Fa-f0-9]{2})/'; /** @var Query|string Query part of the URL */ private $query; /** * Factory method to create a new URL from a URL string * * @param string $url Full URL used to create a Url object * * @return Url * @throws \InvalidArgumentException */ public static function fromString($url) { static $defaults = ['scheme' => null, 'host' => null, 'path' => null, 'port' => null, 'query' => null, 'user' => null, 'pass' => null, 'fragment' => null]; if (false === ($parts = parse_url($url))) { throw new \InvalidArgumentException('Unable to parse malformed ' . 'url: ' . $url); } $parts += $defaults; // Convert the query string into a Query object if ($parts['query'] || 0 !== strlen($parts['query'])) { $parts['query'] = Query::fromString($parts['query']); } return new static($parts['scheme'], $parts['host'], $parts['user'], $parts['pass'], $parts['port'], $parts['path'], $parts['query'], $parts['fragment']); } /** * Build a URL from parse_url parts. The generated URL will be a relative * URL if a scheme or host are not provided. * * @param array $parts Array of parse_url parts * * @return string */ public static function buildUrl(array $parts) { $url = $scheme = ''; if (!empty($parts['scheme'])) { $scheme = $parts['scheme']; $url .= $scheme . ':'; } if (!empty($parts['host'])) { $url .= '//'; if (isset($parts['user'])) { $url .= $parts['user']; if (isset($parts['pass'])) { $url .= ':' . $parts['pass']; } $url .= '@'; } $url .= $parts['host']; // Only include the port if it is not the default port of the scheme if (isset($parts['port']) && (!isset(self::$defaultPorts[$scheme]) || $parts['port'] != self::$defaultPorts[$scheme]) ) { $url .= ':' . $parts['port']; } } // Add the path component if present if (isset($parts['path']) && strlen($parts['path'])) { // Always ensure that the path begins with '/' if set and something // is before the path if (!empty($parts['host']) && $parts['path'][0] != '/') { $url .= '/'; } $url .= $parts['path']; } // Add the query string if present if (isset($parts['query'])) { $queryStr = (string) $parts['query']; if ($queryStr || $queryStr === '0') { $url .= '?' . $queryStr; } } // Ensure that # is only added to the url if fragment contains anything. if (isset($parts['fragment'])) { $url .= '#' . $parts['fragment']; } return $url; } /** * Create a new URL from URL parts * * @param string $scheme Scheme of the URL * @param string $host Host of the URL * @param string $username Username of the URL * @param string $password Password of the URL * @param int $port Port of the URL * @param string $path Path of the URL * @param Query|array|string $query Query string of the URL * @param string $fragment Fragment of the URL */ public function __construct( $scheme, $host, $username = null, $password = null, $port = null, $path = null, $query = null, $fragment = null ) { $this->scheme = strtolower($scheme); $this->host = $host; $this->port = $port; $this->username = $username; $this->password = $password; $this->fragment = $fragment; if ($query) { $this->setQuery($query); } $this->setPath($path); } /** * Clone the URL */ public function __clone() { if ($this->query instanceof Query) { $this->query = clone $this->query; } } /** * Returns the URL as a URL string * * @return string */ public function __toString() { return static::buildUrl($this->getParts()); } /** * Get the parts of the URL as an array * * @return array */ public function getParts() { return array( 'scheme' => $this->scheme, 'user' => $this->username, 'pass' => $this->password, 'host' => $this->host, 'port' => $this->port, 'path' => $this->path, 'query' => $this->query, 'fragment' => $this->fragment, ); } /** * Set the host of the request. * * @param string $host Host to set (e.g. www.yahoo.com, yahoo.com) * * @return Url */ public function setHost($host) { if (strpos($host, ':') === false) { $this->host = $host; } else { list($host, $port) = explode(':', $host); $this->host = $host; $this->setPort($port); } } /** * Get the host part of the URL * * @return string */ public function getHost() { return $this->host; } /** * Set the scheme part of the URL (http, https, ftp, etc.) * * @param string $scheme Scheme to set */ public function setScheme($scheme) { // Remove the default port if one is specified if ($this->port && isset(self::$defaultPorts[$this->scheme]) && self::$defaultPorts[$this->scheme] == $this->port ) { $this->port = null; } $this->scheme = strtolower($scheme); } /** * Get the scheme part of the URL * * @return string */ public function getScheme() { return $this->scheme; } /** * Set the port part of the URL * * @param int $port Port to set */ public function setPort($port) { $this->port = $port; } /** * Get the port part of the URl. * * If no port was set, this method will return the default port for the * scheme of the URI. * * @return int|null */ public function getPort() { if ($this->port) { return $this->port; } elseif (isset(self::$defaultPorts[$this->scheme])) { return self::$defaultPorts[$this->scheme]; } return null; } /** * Set the path part of the URL. * * The provided URL is URL encoded as necessary. * * @param string $path Path string to set */ public function setPath($path) { $this->path = self::encodePath($path); } /** * Removes dot segments from a URL * @link http://tools.ietf.org/html/rfc3986#section-5.2.4 */ public function removeDotSegments() { static $noopPaths = ['' => true, '/' => true, '*' => true]; static $ignoreSegments = ['.' => true, '..' => true]; if (isset($noopPaths[$this->path])) { return; } $results = []; $segments = $this->getPathSegments(); foreach ($segments as $segment) { if ($segment == '..') { array_pop($results); } elseif (!isset($ignoreSegments[$segment])) { $results[] = $segment; } } $newPath = implode('/', $results); // Add the leading slash if necessary if (substr($this->path, 0, 1) === '/' && substr($newPath, 0, 1) !== '/' ) { $newPath = '/' . $newPath; } // Add the trailing slash if necessary if ($newPath != '/' && isset($ignoreSegments[end($segments)])) { $newPath .= '/'; } $this->path = $newPath; } /** * Add a relative path to the currently set path. * * @param string $relativePath Relative path to add */ public function addPath($relativePath) { if ($relativePath != '/' && is_string($relativePath) && strlen($relativePath) > 0 ) { // Add a leading slash if needed if ($relativePath[0] !== '/' && substr($this->path, -1, 1) !== '/' ) { $relativePath = '/' . $relativePath; } $this->setPath($this->path . $relativePath); } } /** * Get the path part of the URL * * @return string */ public function getPath() { return $this->path; } /** * Get the path segments of the URL as an array * * @return array */ public function getPathSegments() { return explode('/', $this->path); } /** * Set the password part of the URL * * @param string $password Password to set */ public function setPassword($password) { $this->password = $password; } /** * Get the password part of the URL * * @return null|string */ public function getPassword() { return $this->password; } /** * Set the username part of the URL * * @param string $username Username to set */ public function setUsername($username) { $this->username = $username; } /** * Get the username part of the URl * * @return null|string */ public function getUsername() { return $this->username; } /** * Get the query part of the URL as a Query object * * @return Query */ public function getQuery() { // Convert the query string to a query object if not already done. if (!$this->query instanceof Query) { $this->query = $this->query === null ? new Query() : Query::fromString($this->query); } return $this->query; } /** * Set the query part of the URL. * * You may provide a query string as a string and pass $rawString as true * to provide a query string that is not parsed until a call to getQuery() * is made. Setting a raw query string will still encode invalid characters * in a query string. * * @param Query|string|array $query Query string value to set. Can * be a string that will be parsed into a Query object, an array * of key value pairs, or a Query object. * @param bool $rawString Set to true when providing a raw query string. * * @throws \InvalidArgumentException */ public function setQuery($query, $rawString = false) { if ($query instanceof Query) { $this->query = $query; } elseif (is_string($query)) { if (!$rawString) { $this->query = Query::fromString($query); } else { // Ensure the query does not have illegal characters. $this->query = preg_replace_callback( self::$queryPattern, [__CLASS__, 'encodeMatch'], $query ); } } elseif (is_array($query)) { $this->query = new Query($query); } else { throw new \InvalidArgumentException('Query must be a Query, ' . 'array, or string. Got ' . Core::describeType($query)); } } /** * Get the fragment part of the URL * * @return null|string */ public function getFragment() { return $this->fragment; } /** * Set the fragment part of the URL * * @param string $fragment Fragment to set */ public function setFragment($fragment) { $this->fragment = $fragment; } /** * Check if this is an absolute URL * * @return bool */ public function isAbsolute() { return $this->scheme && $this->host; } /** * Combine the URL with another URL and return a new URL instance. * * Follows the rules specific in RFC 3986 section 5.4. * * @param string $url Relative URL to combine with * * @return Url * @throws \InvalidArgumentException * @link http://tools.ietf.org/html/rfc3986#section-5.4 */ public function combine($url) { $url = static::fromString($url); // Use the more absolute URL as the base URL if (!$this->isAbsolute() && $url->isAbsolute()) { $url = $url->combine($this); } $parts = $url->getParts(); // Passing a URL with a scheme overrides everything if ($parts['scheme']) { return clone $url; } // Setting a host overrides the entire rest of the URL if ($parts['host']) { return new static( $this->scheme, $parts['host'], $parts['user'], $parts['pass'], $parts['port'], $parts['path'], $parts['query'] instanceof Query ? clone $parts['query'] : $parts['query'], $parts['fragment'] ); } if (!$parts['path'] && $parts['path'] !== '0') { // The relative URL has no path, so check if it is just a query $path = $this->path ?: ''; $query = $parts['query'] ?: $this->query; } else { $query = $parts['query']; if ($parts['path'][0] == '/' || !$this->path) { // Overwrite the existing path if the rel path starts with "/" $path = $parts['path']; } else { // If the relative URL does not have a path or the base URL // path does not end in a "/" then overwrite the existing path // up to the last "/" $path = substr($this->path, 0, strrpos($this->path, '/') + 1) . $parts['path']; } } $result = new self( $this->scheme, $this->host, $this->username, $this->password, $this->port, $path, $query instanceof Query ? clone $query : $query, $parts['fragment'] ); if ($path) { $result->removeDotSegments(); } return $result; } /** * Encodes the path part of a URL without double-encoding percent-encoded * key value pairs. * * @param string $path Path to encode * * @return string */ public static function encodePath($path) { static $cb = [__CLASS__, 'encodeMatch']; return preg_replace_callback(self::$pathPattern, $cb, $path); } private static function encodeMatch(array $match) { return rawurlencode($match[0]); } } src/Utils.php000064400000016046151742725260007166 0ustar00expand($template, $variables); } /** * Wrapper for JSON decode that implements error detection with helpful * error messages. * * @param string $json JSON data to parse * @param bool $assoc When true, returned objects will be converted * into associative arrays. * @param int $depth User specified recursion depth. * @param int $options Bitmask of JSON decode options. * * @return mixed * @throws \InvalidArgumentException if the JSON cannot be parsed. * @link http://www.php.net/manual/en/function.json-decode.php */ public static function jsonDecode($json, $assoc = false, $depth = 512, $options = 0) { if ($json === '' || $json === null) { return null; } static $jsonErrors = [ JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH - Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH - Underflow or the modes mismatch', JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR - Unexpected control character found', JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX - Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8 - Malformed UTF-8 characters, possibly incorrectly encoded' ]; $data = \json_decode($json, $assoc, $depth, $options); if (JSON_ERROR_NONE !== json_last_error()) { $last = json_last_error(); throw new \InvalidArgumentException( 'Unable to parse JSON data: ' . (isset($jsonErrors[$last]) ? $jsonErrors[$last] : 'Unknown error') ); } return $data; } /** * Get the default User-Agent string to use with Guzzle * * @return string */ public static function getDefaultUserAgent() { static $defaultAgent = ''; if (!$defaultAgent) { $defaultAgent = 'Guzzle/' . ClientInterface::VERSION; if (extension_loaded('curl')) { $defaultAgent .= ' curl/' . curl_version()['version']; } $defaultAgent .= ' PHP/' . PHP_VERSION; } return $defaultAgent; } /** * Create a default handler to use based on the environment * * @throws \RuntimeException if no viable Handler is available. */ public static function getDefaultHandler() { $default = $future = null; if (extension_loaded('curl')) { $config = [ 'select_timeout' => getenv('GUZZLE_CURL_SELECT_TIMEOUT') ?: 1 ]; if ($maxHandles = getenv('GUZZLE_CURL_MAX_HANDLES')) { $config['max_handles'] = $maxHandles; } if (function_exists('curl_reset')) { $default = new CurlHandler(); $future = new CurlMultiHandler($config); } else { $default = new CurlMultiHandler($config); } } if (ini_get('allow_url_fopen')) { $default = !$default ? new StreamHandler() : Middleware::wrapStreaming($default, new StreamHandler()); } elseif (!$default) { throw new \RuntimeException('Guzzle requires cURL, the ' . 'allow_url_fopen ini setting, or a custom HTTP handler.'); } return $future ? Middleware::wrapFuture($default, $future) : $default; } } build000077700000000000151742725260005603 5ustar00docs000077700000000000151742725260005434 5ustar00tests000077700000000000151742725260005646 5ustar00