pax_global_header00006660000000000000000000000064125270005450014512gustar00rootroot0000000000000052 comment=dbbb91d7f6c191e5e405e900e3102ac7f261bc0b RingPHP-1.1.0/000077500000000000000000000000001252700054500127605ustar00rootroot00000000000000RingPHP-1.1.0/.gitignore000066400000000000000000000000631252700054500147470ustar00rootroot00000000000000vendor build/artifacts/ composer.lock docs/_build/ RingPHP-1.1.0/.travis.yml000066400000000000000000000004701252700054500150720ustar00rootroot00000000000000language: php php: - 5.4 - 5.5 - 5.6 - 7.0 - hhvm before_script: - composer self-update - composer install --no-interaction --prefer-source --dev - ~/.nvm/nvm.sh install v0.6.14 - ~/.nvm/nvm.sh run v0.6.14 script: - make test matrix: allow_failures: - php: hhvm fast_finish: true RingPHP-1.1.0/CHANGELOG.md000066400000000000000000000032741252700054500145770ustar00rootroot00000000000000# CHANGELOG ## 1.1.0 - 2015-05-19 * Added `CURL_HTTP_VERSION_2_0` * The PHP stream wrapper handler now sets `allow_self_signed` to `false` to match the cURL handler when `verify` is set to `true` or a certificate file. * Ensuring that a directory exists before using the `save_to` option. * Response protocol version is now correctly extracted from a response. * Fixed a bug in which the result of `CurlFactory::retryFailedRewind` did not return an array. ## 1.0.7 - 2015-03-29 * PHP 7 fixes. ## 1.0.6 - 2015-02-26 * Bug fix: futures now extend from React's PromiseInterface to ensure that they are properly forwarded down the promise chain. * The multi handle of the CurlMultiHandler is now created lazily. ## 1.0.5 - 2014-12-10 * Adding more error information to PHP stream wrapper exceptions. * Added digest auth integration test support to test server. ## 1.0.4 - 2014-12-01 * Added support for older versions of cURL that do not have CURLOPT_TIMEOUT_MS. * Setting debug to `false` does not enable debug output. * Added a fix to the StreamHandler to return a `FutureArrayInterface` when an error occurs. ## 1.0.3 - 2014-11-03 * Setting the `header` stream option as a string to be compatible with GAE. * Header parsing now ensures that header order is maintained in the parsed message. ## 1.0.2 - 2014-10-28 * Now correctly honoring a `version` option is supplied in a request. See https://github.com/guzzle/RingPHP/pull/8 ## 1.0.1 - 2014-10-26 * Fixed a header parsing issue with the `CurlHandler` and `CurlMultiHandler` that caused cURL requests with multiple responses to merge repsonses together (e.g., requests with digest authentication). ## 1.0.0 - 2014-10-12 * Initial release. RingPHP-1.1.0/LICENSE000066400000000000000000000021271252700054500137670ustar00rootroot00000000000000Copyright (c) 2014 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. RingPHP-1.1.0/Makefile000066400000000000000000000016031252700054500144200ustar00rootroot00000000000000all: clean coverage docs docs: cd docs && make html view-docs: open docs/_build/html/index.html start-server: stop-server node tests/Client/server.js &> /dev/null & stop-server: @PID=$(shell ps axo pid,command \ | grep 'tests/Client/server.js' \ | grep -v grep \ | cut -f 1 -d " "\ ) && [ -n "$$PID" ] && kill $$PID || true test: start-server vendor/bin/phpunit $(TEST) $(MAKE) stop-server coverage: start-server vendor/bin/phpunit --coverage-html=build/artifacts/coverage $(TEST) $(MAKE) stop-server view-coverage: open build/artifacts/coverage/index.html clean: rm -rf build/artifacts/* cd docs && make clean tag: $(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1")) @echo Tagging $(TAG) chag update -m '$(TAG) ()' git add -A git commit -m '$(TAG) release' chag tag perf: start-server php tests/perf.php $(MAKE) stop-server .PHONY: docs RingPHP-1.1.0/README.rst000066400000000000000000000026411252700054500144520ustar00rootroot00000000000000======= RingPHP ======= Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function. RingPHP be used to power HTTP clients and servers through a PHP function that accepts a request hash and returns a response hash that is fulfilled using a `promise `_, allowing RingPHP to support both synchronous and asynchronous workflows. By abstracting the implementation details of different HTTP clients and servers, RingPHP allows you to utilize pluggable HTTP clients and servers without tying your application to a specific implementation. .. code-block:: php 'GET', 'uri' => '/', 'headers' => [ 'host' => ['www.google.com'], 'x-foo' => ['baz'] ] ]); $response->then(function (array $response) { echo $response['status']; }); $response->wait(); RingPHP is inspired by Clojure's `Ring `_, which, in turn, was inspired by Python's WSGI and Ruby's Rack. RingPHP is utilized as the handler layer in `Guzzle `_ 5.0+ to send HTTP requests. Documentation ------------- See http://ringphp.readthedocs.org/ for the full online documentation. RingPHP-1.1.0/composer.json000066400000000000000000000017161252700054500155070ustar00rootroot00000000000000{ "name": "guzzlehttp/ringphp", "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", "license": "MIT", "authors": [ { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" } ], "require": { "php": ">=5.4.0", "guzzlehttp/streams": "~3.0", "react/promise": "~2.0" }, "require-dev": { "ext-curl": "*", "phpunit/phpunit": "~4.0" }, "suggest": { "ext-curl": "Guzzle will use specific adapters if cURL is present" }, "autoload": { "psr-4": { "GuzzleHttp\\Ring\\": "src/" } }, "autoload-dev": { "psr-4": { "GuzzleHttp\\Tests\\Ring\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "1.1-dev" } } } RingPHP-1.1.0/docs/000077500000000000000000000000001252700054500137105ustar00rootroot00000000000000RingPHP-1.1.0/docs/Makefile000066400000000000000000000127141252700054500153550ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GuzzleRing.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GuzzleRing.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/GuzzleRing" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GuzzleRing" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." RingPHP-1.1.0/docs/client_handlers.rst000066400000000000000000000125711252700054500176060ustar00rootroot00000000000000=============== Client Handlers =============== Client handlers accept a request array and return a future response array that can be used synchronously as an array or asynchronously using a promise. Built-In Handlers ----------------- RingPHP comes with three built-in client handlers. Stream Handler ~~~~~~~~~~~~~~ The ``GuzzleHttp\Ring\Client\StreamHandler`` uses PHP's `http stream wrapper `_ to send requests. .. note:: This handler cannot send requests concurrently. You can provide an associative array of custom stream context options to the StreamHandler using the ``stream_context`` key of the ``client`` request option. .. code-block:: php use GuzzleHttp\Ring\Client\StreamHandler; $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => ['httpbin.org']], 'client' => [ 'stream_context' => [ 'http' => [ 'request_fulluri' => true, 'method' => 'HEAD' ], 'socket' => [ 'bindto' => '127.0.0.1:0' ], 'ssl' => [ 'verify_peer' => false ] ] ] ]); // Even though it's already completed, you can still use a promise $response->then(function ($response) { echo $response['status']; // 200 }); // Or access the response using the future interface echo $response['status']; // 200 cURL Handler ~~~~~~~~~~~~ The ``GuzzleHttp\Ring\Client\CurlHandler`` can be used with PHP 5.5+ to send requests using cURL easy handles. This handler is great for sending requests one at a time because the execute and select loop is implemented in C code which executes faster and consumes less memory than using PHP's ``curl_multi_*`` interface. .. note:: This handler cannot send requests concurrently. When using the CurlHandler, custom curl options can be specified as an associative array of `cURL option constants `_ mapping to values in the ``client`` option of a requst using the **curl** key. .. code-block:: php use GuzzleHttp\Ring\Client\CurlHandler; $handler = new CurlHandler(); $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['curl' => [CURLOPT_LOW_SPEED_LIMIT => 10]] ]; $response = $handler($request); // The response can be used directly as an array. echo $response['status']; // 200 // Or, it can be used as a promise (that has already fulfilled). $response->then(function ($response) { echo $response['status']; // 200 }); cURL Multi Handler ~~~~~~~~~~~~~~~~~~ The ``GuzzleHttp\Ring\Client\CurlMultiHandler`` transfers requests using cURL's `multi API `_. The ``CurlMultiHandler`` is great for sending requests concurrently. .. code-block:: php use GuzzleHttp\Ring\Client\CurlMultiHandler; $handler = new CurlMultiHandler(); $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]] ]; // this call returns a future array immediately. $response = $handler($request); // Ideally, you should use the promise API to not block. $response ->then(function ($response) { // Got the response at some point in the future echo $response['status']; // 200 // Don't break the chain return $response; })->then(function ($response) { // ... }); // If you really need to block, then you can use the response as an // associative array. This will block until it has completed. echo $response['status']; // 200 Just like the ``CurlHandler``, the ``CurlMultiHandler`` accepts custom curl option in the ``curl`` key of the ``client`` request option. Mock Handler ~~~~~~~~~~~~ The ``GuzzleHttp\Ring\Client\MockHandler`` is used to return mock responses. When constructed, the handler can be configured to return the same response array over and over, a future response, or a the evaluation of a callback function. .. code-block:: php use GuzzleHttp\Ring\Client\MockHandler; // Return a canned response. $mock = new MockHandler(['status' => 200]); $response = $mock([]); assert(200 == $response['status']); assert([] == $response['headers']); Implementing Handlers --------------------- Client handlers are just PHP callables (functions or classes that have the ``__invoke`` magic method). The callable accepts a request array and MUST return an instance of ``GuzzleHttp\Ring\Future\FutureArrayInterface`` so that the response can be used by both blocking and non-blocking consumers. Handlers need to follow a few simple rules: 1. Do not throw exceptions. If an error is encountered, return an array that contains the ``error`` key that maps to an ``\Exception`` value. 2. If the request has a ``delay`` client option, then the handler should only send the request after the specified delay time in seconds. Blocking handlers may find it convenient to just let the ``GuzzleHttp\Ring\Core::doSleep($request)`` function handle this for them. 3. Always return an instance of ``GuzzleHttp\Ring\Future\FutureArrayInterface``. 4. Complete any outstanding requests when the handler is destructed. RingPHP-1.1.0/docs/client_middleware.rst000066400000000000000000000125011252700054500201140ustar00rootroot00000000000000================= Client Middleware ================= Middleware intercepts requests before they are sent over the wire and can be used to add functionality to handlers. Modifying Requests ------------------ Let's say you wanted to modify requests before they are sent over the wire so that they always add specific headers. This can be accomplished by creating a function that accepts a handler and returns a new function that adds the composed behavior. .. code-block:: php use GuzzleHttp\Ring\Client\CurlHandler; $handler = new CurlHandler(); $addHeaderHandler = function (callable $handler, array $headers = []) { return function (array $request) use ($handler, $headers) { // Add our custom headers foreach ($headers as $key => $value) { $request['headers'][$key] = $value; } // Send the request using the handler and return the response. return $handler($request); } }; // Create a new handler that adds headers to each request. $handler = $addHeaderHandler($handler, [ 'X-AddMe' => 'hello', 'Authorization' => 'Basic xyz' ]); $response = $handler([ 'http_method' => 'GET', 'headers' => ['Host' => ['httpbin.org']] ]); Modifying Responses ------------------- You can change a response as it's returned from a middleware. Remember that responses returned from an handler (including middleware) must implement ``GuzzleHttp\Ring\Future\FutureArrayInterface``. In order to be a good citizen, you should not expect that the responses returned through your middleware will be completed synchronously. Instead, you should use the ``GuzzleHttp\Ring\Core::proxy()`` function to modify the response when the underlying promise is resolved. This function is a helper function that makes it easy to create a new instance of ``FutureArrayInterface`` that wraps an existing ``FutureArrayInterface`` object. Let's say you wanted to add headers to a response as they are returned from your middleware, but you want to make sure you aren't causing future responses to be dereferenced right away. You can achieve this by modifying the incoming request and using the ``Core::proxy`` function. .. code-block:: php use GuzzleHttp\Ring\Core; use GuzzleHttp\Ring\Client\CurlHandler; $handler = new CurlHandler(); $responseHeaderHandler = function (callable $handler, array $headers) { return function (array $request) use ($handler, $headers) { // Send the request using the wrapped handler. return Core::proxy($handler($request), function ($response) use ($headers) { // Add the headers to the response when it is available. foreach ($headers as $key => $value) { $response['headers'][$key] = (array) $value; } // Note that you can return a regular response array when using // the proxy method. return $response; }); } }; // Create a new handler that adds headers to each response. $handler = $responseHeaderHandler($handler, ['X-Header' => 'hello!']); $response = $handler([ 'http_method' => 'GET', 'headers' => ['Host' => ['httpbin.org']] ]); assert($response['headers']['X-Header'] == 'hello!'); Built-In Middleware ------------------- RingPHP comes with a few basic client middlewares that modify requests and responses. Streaming Middleware ~~~~~~~~~~~~~~~~~~~~ If you want to send all requests with the ``streaming`` option to a specific handler but other requests to a different handler, then use the streaming middleware. .. code-block:: php use GuzzleHttp\Ring\Client\CurlHandler; use GuzzleHttp\Ring\Client\StreamHandler; use GuzzleHttp\Ring\Client\Middleware; $defaultHandler = new CurlHandler(); $streamingHandler = new StreamHandler(); $streamingHandler = Middleware::wrapStreaming( $defaultHandler, $streamingHandler ); // Send the request using the streaming handler. $response = $streamingHandler([ 'http_method' => 'GET', 'headers' => ['Host' => ['www.google.com']], 'stream' => true ]); // Send the request using the default handler. $response = $streamingHandler([ 'http_method' => 'GET', 'headers' => ['Host' => ['www.google.com']] ]); Future Middleware ~~~~~~~~~~~~~~~~~ If you want to send all requests with the ``future`` option to a specific handler but other requests to a different handler, then use the future middleware. .. code-block:: php use GuzzleHttp\Ring\Client\CurlHandler; use GuzzleHttp\Ring\Client\CurlMultiHandler; use GuzzleHttp\Ring\Client\Middleware; $defaultHandler = new CurlHandler(); $futureHandler = new CurlMultiHandler(); $futureHandler = Middleware::wrapFuture( $defaultHandler, $futureHandler ); // Send the request using the blocking CurlHandler. $response = $futureHandler([ 'http_method' => 'GET', 'headers' => ['Host' => ['www.google.com']] ]); // Send the request using the non-blocking CurlMultiHandler. $response = $futureHandler([ 'http_method' => 'GET', 'headers' => ['Host' => ['www.google.com']], 'future' => true ]); RingPHP-1.1.0/docs/conf.py000066400000000000000000000011451252700054500152100ustar00rootroot00000000000000import sys, os import sphinx_rtd_theme from sphinx.highlighting import lexers from pygments.lexers.web import PhpLexer lexers['php'] = PhpLexer(startinline=True, linenos=1) lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1) primary_domain = 'php' extensions = [] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'index' project = u'RingPHP' copyright = u'2014, Michael Dowling' version = '1.0.0-alpha' exclude_patterns = ['_build'] html_title = "RingPHP" html_short_title = "RingPHP" html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] RingPHP-1.1.0/docs/futures.rst000066400000000000000000000130371252700054500161430ustar00rootroot00000000000000======= Futures ======= Futures represent a computation that may have not yet completed. RingPHP uses hybrid of futures and promises to provide a consistent API that can be used for both blocking and non-blocking consumers. Promises -------- You can get the result of a future when it is ready using the promise interface of a future. Futures expose a promise API via a ``then()`` method that utilizes `React's promise library `_. You should use this API when you do not wish to block. .. code-block:: php use GuzzleHttp\Ring\Client\CurlMultiHandler; $request = [ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => ['httpbin.org']] ]; $response = $handler($request); // Use the then() method to use the promise API of the future. $response->then(function ($response) { echo $response['status']; }); You can get the promise used by a future, an instance of ``React\Promise\PromiseInterface``, by calling the ``promise()`` method. .. code-block:: php $response = $handler($request); $promise = $response->promise(); $promise->then(function ($response) { echo $response['status']; }); This promise value can be used with React's `aggregate promise functions `_. Waiting ------- You can wait on a future to complete and retrieve the value, or *dereference* the future, using the ``wait()`` method. Calling the ``wait()`` method of a future will block until the result is available. The result is then returned or an exception is thrown if and exception was encountered while waiting on the the result. Subsequent calls to dereference a future will return the previously completed result or throw the previously encountered exception. Futures can be cancelled, which stops the computation if possible. .. code-block:: php use GuzzleHttp\Ring\Client\CurlMultiHandler; $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => ['httpbin.org']] ]); // You can explicitly call block to wait on a result. $realizedResponse = $response->wait(); // Future responses can be used like a regular PHP array. echo $response['status']; In addition to explicitly calling the ``wait()`` function, using a future like a normal value will implicitly trigger the ``wait()`` function. Future Responses ---------------- RingPHP uses futures to return asynchronous responses immediately. Client handlers always return future responses that implement ``GuzzleHttp\Ring\Future\ArrayFutureInterface``. These future responses act just like normal PHP associative arrays for blocking access and provide a promise interface for non-blocking access. .. code-block:: php use GuzzleHttp\Ring\Client\CurlMultiHandler; $handler = new CurlMultiHandler(); $request = [ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['Host' => ['www.google.com']] ]; $response = $handler($request); // Use the promise API for non-blocking access to the response. The actual // response value will be delivered to the promise. $response->then(function ($response) { echo $response['status']; }); // You can wait (block) until the future is completed. $response->wait(); // This will implicitly call wait(), and will block too! $response['status']; .. important:: Futures that are not completed by the time the underlying handler is destructed will be completed when the handler is shutting down. Cancelling ---------- Futures can be cancelled if they have not already been dereferenced. RingPHP futures are typically implemented with the ``GuzzleHttp\Ring\Future\BaseFutureTrait``. This trait provides the cancellation functionality that should be common to most implementations. Cancelling a future response will try to prevent the request from sending over the wire. When a future is cancelled, the cancellation function is invoked and performs the actual work needed to cancel the request from sending if possible (e.g., telling an event loop to stop sending a request or to close a socket). If no cancellation function is provided, then a request cannot be cancelled. If a cancel function is provided, then it should accept the future as an argument and return true if the future was successfully cancelled or false if it could not be cancelled. Wrapping an existing Promise ---------------------------- You can easily create a future from any existing promise using the ``GuzzleHttp\Ring\Future\FutureValue`` class. This class's constructor accepts a promise as the first argument, a wait function as the second argument, and a cancellation function as the third argument. The dereference function is used to force the promise to resolve (for example, manually ticking an event loop). The cancel function is optional and is used to tell the thing that created the promise that it can stop computing the result (for example, telling an event loop to stop transferring a request). .. code-block:: php use GuzzleHttp\Ring\Future\FutureValue; use React\Promise\Deferred; $deferred = new Deferred(); $promise = $deferred->promise(); $f = new FutureValue( $promise, function () use ($deferred) { // This function is responsible for blocking and resolving the // promise. Here we pass in a reference to the deferred so that // it can be resolved or rejected. $deferred->resolve('foo'); } ); RingPHP-1.1.0/docs/index.rst000066400000000000000000000026411252700054500155540ustar00rootroot00000000000000======= RingPHP ======= Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function. RingPHP be used to power HTTP clients and servers through a PHP function that accepts a request hash and returns a response hash that is fulfilled using a `promise `_, allowing RingPHP to support both synchronous and asynchronous workflows. By abstracting the implementation details of different HTTP clients and servers, RingPHP allows you to utilize pluggable HTTP clients and servers without tying your application to a specific implementation. .. toctree:: :maxdepth: 2 spec futures client_middleware client_handlers testing .. code-block:: php 'GET', 'uri' => '/', 'headers' => [ 'host' => ['www.google.com'], 'x-foo' => ['baz'] ] ]); $response->then(function (array $response) { echo $response['status']; }); $response->wait(); RingPHP is inspired by Clojure's `Ring `_, which, in turn, was inspired by Python's WSGI and Ruby's Rack. RingPHP is utilized as the handler layer in `Guzzle `_ 5.0+ to send HTTP requests. RingPHP-1.1.0/docs/requirements.txt000066400000000000000000000000211252700054500171650ustar00rootroot00000000000000sphinx_rtd_theme RingPHP-1.1.0/docs/spec.rst000066400000000000000000000274121252700054500154020ustar00rootroot00000000000000============= Specification ============= RingPHP applications consist of handlers, requests, responses, and middleware. Handlers -------- Handlers are implemented as a PHP ``callable`` that accept a request array and return a response array (``GuzzleHttp\Ring\Future\FutureArrayInterface``). For example: .. code-block:: php use GuzzleHttp\Ring\Future\CompletedFutureArray; $mockHandler = function (array $request) { return new CompletedFutureArray([ 'status' => 200, 'headers' => ['X-Foo' => ['Bar']], 'body' => 'Hello!' ]); }; This handler returns the same response each time it is invoked. All RingPHP handlers must return a ``GuzzleHttp\Ring\Future\FutureArrayInterface``. Use ``GuzzleHttp\Ring\Future\CompletedFutureArray`` when returning a response that has already completed. Requests -------- A request array is a PHP associative array that contains the configuration settings need to send a request. .. code-block:: php $request = [ 'http_method' => 'GET', 'scheme' => 'http', 'uri' => '/', 'body' => 'hello!', 'client' => ['timeout' => 1.0], 'headers' => [ 'host' => ['httpbin.org'], 'X-Foo' => ['baz', 'bar'] ] ]; The request array contains the following key value pairs: request_method (string, required) The HTTP request method, must be all caps corresponding to a HTTP request method, such as ``GET`` or ``POST``. scheme (string) The transport protocol, must be one of ``http`` or ``https``. Defaults to ``http``. uri (string, required) The request URI excluding the query string. Must start with "/". query_string (string) The query string, if present (e.g., ``foo=bar``). version (string) HTTP protocol version. Defaults to ``1.1``. headers (required, array) Associative array of headers. Each key represents the header name. Each value contains an array of strings where each entry of the array SHOULD be sent over the wire on a separate header line. body (string, fopen resource, ``Iterator``, ``GuzzleHttp\Stream\StreamInterface``) The body of the request, if present. Can be a string, resource returned from fopen, an ``Iterator`` that yields chunks of data, an object that implemented ``__toString``, or a ``GuzzleHttp\Stream\StreamInterface``. future (bool, string) Controls the asynchronous behavior of a response. Set to ``true`` or omit the ``future`` option to *request* that a request will be completed asynchronously. Keep in mind that your request might not necessarily be completed asynchronously based on the handler you are using. Set the ``future`` option to ``false`` to request that a synchronous response be provided. You can provide a string value to specify fine-tuned future behaviors that may be specific to the underlying handlers you are using. There are, however, some common future options that handlers should implement if possible. lazy Requests that the handler does not open and send the request immediately, but rather only opens and sends the request once the future is dereferenced. This option is often useful for sending a large number of requests concurrently to allow handlers to take better advantage of non-blocking transfers by first building up a pool of requests. If an handler does not implement or understand a provided string value, then the request MUST be treated as if the user provided ``true`` rather than the string value. Future responses created by asynchronous handlers MUST attempt to complete any outstanding future responses when they are destructed. Asynchronous handlers MAY choose to automatically complete responses when the number of outstanding requests reaches an handler-specific threshold. Client Specific Options ~~~~~~~~~~~~~~~~~~~~~~~ The following options are only used in ring client handlers. .. _client-options: client (array) Associative array of client specific transfer options. The ``client`` request key value pair can contain the following keys: cert (string, array) Set to a string to specify the path to a file containing a PEM formatted SSL client side certificate. If a password is required, then set ``cert`` to an array containing the path to the PEM file in the first array element followed by the certificate password in the second array element. connect_timeout (float) Float describing the number of seconds to wait while trying to connect to a server. Use ``0`` to wait indefinitely (the default behavior). debug (bool, fopen() resource) Set to true or set to a PHP stream returned by fopen() to enable debug output with the handler used to send a request. If set to ``true``, the output is written to PHP's STDOUT. If a PHP ``fopen`` resource handle is provided, the output is written to the stream. "Debug output" is handler specific: different handlers will yield different output and various various level of detail. For example, when using cURL to transfer requests, cURL's `CURLOPT_VERBOSE `_ will be used. When using the PHP stream wrapper, `stream notifications `_ will be emitted. decode_content (bool) Specify whether or not ``Content-Encoding`` responses (gzip, deflate, etc.) are automatically decoded. Set to ``true`` to automatically decode encoded responses. Set to ``false`` to not decode responses. By default, content is *not* decoded automatically. delay (int) The number of milliseconds to delay before sending the request. This is often used for delaying before retrying a request. Handlers SHOULD implement this if possible, but it is not a strict requirement. progress (function) Defines a function to invoke when transfer progress is made. The function accepts the following arguments: 1. The total number of bytes expected to be downloaded 2. The number of bytes downloaded so far 3. The number of bytes expected to be uploaded 4. The number of bytes uploaded so far proxy (string, array) Pass a string to specify an HTTP proxy, or an associative array to specify different proxies for different protocols where the scheme is the key and the value is the proxy address. .. code-block:: php $request = [ 'http_method' => 'GET', 'headers' => ['host' => ['httpbin.org']], 'client' => [ // Use different proxies for different URI schemes. 'proxy' => [ 'http' => 'http://proxy.example.com:5100', 'https' => 'https://proxy.example.com:6100' ] ] ]; ssl_key (string, array) Specify the path to a file containing a private SSL key in PEM format. If a password is required, then set to an array containing the path to the SSL key in the first array element followed by the password required for the certificate in the second element. save_to (string, fopen resource, ``GuzzleHttp\Stream\StreamInterface``) Specifies where the body of the response is downloaded. Pass a string to open a local file on disk and save the output to the file. Pass an fopen resource to save the output to a PHP stream resource. Pass a ``GuzzleHttp\Stream\StreamInterface`` to save the output to a Guzzle StreamInterface. Omitting this option will typically save the body of a response to a PHP temp stream. stream (bool) Set to true to stream a response rather than download it all up-front. This option will only be utilized when the corresponding handler supports it. timeout (float) Float describing the timeout of the request in seconds. Use 0 to wait indefinitely (the default behavior). verify (bool, string) Describes the SSL certificate verification behavior of a request. Set to true to enable SSL certificate verification using the system CA bundle when available (the default). Set to false to disable certificate verification (this is insecure!). Set to a string to provide the path to a CA bundle on disk to enable verification using a custom certificate. version (string) HTTP protocol version to use with the request. Server Specific Options ~~~~~~~~~~~~~~~~~~~~~~~ The following options are only used in ring server handlers. server_port (integer) The port on which the request is being handled. This is only used with ring servers, and is required. server_name (string) The resolved server name, or the server IP address. Required when using a Ring server. remote_addr (string) The IP address of the client or the last proxy that sent the request. Required when using a Ring server. Responses --------- A response is an array-like object that implements ``GuzzleHttp\Ring\Future\FutureArrayInterface``. Responses contain the following key value pairs: body (string, fopen resource, ``Iterator``, ``GuzzleHttp\Stream\StreamInterface``) The body of the response, if present. Can be a string, resource returned from fopen, an ``Iterator`` that yields chunks of data, an object that implemented ``__toString``, or a ``GuzzleHttp\Stream\StreamInterface``. effective_url (string) The URL that returned the resulting response. error (``\Exception``) Contains an exception describing any errors that were encountered during the transfer. headers (Required, array) Associative array of headers. Each key represents the header name. Each value contains an array of strings where each entry of the array is a header line. The headers array MAY be an empty array in the event an error occurred before a response was received. reason (string) Optional reason phrase. This option should be provided when the reason phrase does not match the typical reason phrase associated with the ``status`` code. See `RFC 7231 `_ for a list of HTTP reason phrases mapped to status codes. status (Required, integer) The HTTP status code. The status code MAY be set to ``null`` in the event an error occurred before a response was received (e.g., a networking error). transfer_stats (array) Provides an associative array of arbitrary transfer statistics if provided by the underlying handler. version (string) HTTP protocol version. Defaults to ``1.1``. Middleware ---------- Ring middleware augments the functionality of handlers by invoking them in the process of generating responses. Middleware is typically implemented as a higher-order function that takes one or more handlers as arguments followed by an optional associative array of options as the last argument, returning a new handler with the desired compound behavior. Here's an example of a middleware that adds a Content-Type header to each request. .. code-block:: php use GuzzleHttp\Ring\Client\CurlHandler; use GuzzleHttp\Ring\Core; $contentTypeHandler = function(callable $handler, $contentType) { return function (array $request) use ($handler, $contentType) { return $handler(Core::setHeader('Content-Type', $contentType)); }; }; $baseHandler = new CurlHandler(); $wrappedHandler = $contentTypeHandler($baseHandler, 'text/html'); $response = $wrappedHandler([/** request hash **/]); RingPHP-1.1.0/docs/testing.rst000066400000000000000000000040571252700054500161250ustar00rootroot00000000000000======= Testing ======= RingPHP tests client handlers using `PHPUnit `_ and a built-in node.js web server. Running Tests ------------- First, install the dependencies using `Composer `_. composer.phar install Next, run the unit tests using ``Make``. make test The tests are also run on Travis-CI on each commit: https://travis-ci.org/guzzle/guzzle-ring Test Server ----------- Testing client handlers usually involves actually sending HTTP requests. RingPHP provides a node.js web server that returns canned responses and keep a list of the requests that have been received. The server can then be queried to get a list of the requests that were sent by the client so that you can ensure that the client serialized and transferred requests as intended. The server keeps a list of queued responses and returns responses that are popped off of the queue as HTTP requests are received. When there are not more responses to serve, the server returns a 500 error response. The test server uses the ``GuzzleHttp\Tests\Ring\Client\Server`` class to control the server. .. code-block:: php use GuzzleHttp\Ring\Client\StreamHandler; use GuzzleHttp\Tests\Ring\Client\Server; // First return a 200 followed by a 404 response. Server::enqueue([ ['status' => 200], ['status' => 404] ]); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'uri' => '/' ]); assert(200 == $response['status']); $response = $handler([ 'http_method' => 'HEAD', 'headers' => ['host' => [Server::$host]], 'uri' => '/' ]); assert(404 == $response['status']); After requests have been sent, you can get a list of the requests as they were sent over the wire to ensure they were sent correctly. .. code-block:: php $received = Server::received(); assert('GET' == $received[0]['http_method']); assert('HEAD' == $received[1]['http_method']); RingPHP-1.1.0/phpunit.xml.dist000066400000000000000000000005431252700054500161350ustar00rootroot00000000000000 tests src RingPHP-1.1.0/src/000077500000000000000000000000001252700054500135475ustar00rootroot00000000000000RingPHP-1.1.0/src/Client/000077500000000000000000000000001252700054500147655ustar00rootroot00000000000000RingPHP-1.1.0/src/Client/ClientUtils.php000066400000000000000000000053231252700054500177400ustar00rootroot00000000000000getDefaultOptions($request, $headers); $this->applyMethod($request, $options); if (isset($request['client'])) { $this->applyHandlerOptions($request, $options); } $this->applyHeaders($request, $options); unset($options['_headers']); // Add handler options from the request's configuration options if (isset($request['client']['curl'])) { $options = $this->applyCustomCurlOptions( $request['client']['curl'], $options ); } if (!$handle) { $handle = curl_init(); } $body = $this->getOutputBody($request, $options); curl_setopt_array($handle, $options); return [$handle, &$headers, $body]; } /** * Creates a response hash from a cURL result. * * @param callable $handler Handler that was used. * @param array $request Request that sent. * @param array $response Response hash to update. * @param array $headers Headers received during transfer. * @param resource $body Body fopen response. * * @return array */ public static function createResponse( callable $handler, array $request, array $response, array $headers, $body ) { if (isset($response['transfer_stats']['url'])) { $response['effective_url'] = $response['transfer_stats']['url']; } if (!empty($headers)) { $startLine = explode(' ', array_shift($headers), 3); $headerList = Core::headersFromLines($headers); $response['headers'] = $headerList; $response['version'] = isset($startLine[0]) ? substr($startLine[0], 5) : null; $response['status'] = isset($startLine[1]) ? (int) $startLine[1] : null; $response['reason'] = isset($startLine[2]) ? $startLine[2] : null; $response['body'] = $body; Core::rewindBody($response); } return !empty($response['curl']['errno']) || !isset($response['status']) ? self::createErrorResponse($handler, $request, $response) : $response; } private static function createErrorResponse( callable $handler, array $request, array $response ) { static $connectionErrors = [ CURLE_OPERATION_TIMEOUTED => true, CURLE_COULDNT_RESOLVE_HOST => true, CURLE_COULDNT_CONNECT => true, CURLE_SSL_CONNECT_ERROR => true, CURLE_GOT_NOTHING => true, ]; // Retry when nothing is present or when curl failed to rewind. if (!isset($response['err_message']) && (empty($response['curl']['errno']) || $response['curl']['errno'] == 65) ) { return self::retryFailedRewind($handler, $request, $response); } $message = isset($response['err_message']) ? $response['err_message'] : sprintf('cURL error %s: %s', $response['curl']['errno'], isset($response['curl']['error']) ? $response['curl']['error'] : 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html'); $error = isset($response['curl']['errno']) && isset($connectionErrors[$response['curl']['errno']]) ? new ConnectException($message) : new RingException($message); return $response + [ 'status' => null, 'reason' => null, 'body' => null, 'headers' => [], 'error' => $error, ]; } private function getOutputBody(array $request, array &$options) { // Determine where the body of the response (if any) will be streamed. if (isset($options[CURLOPT_WRITEFUNCTION])) { return $request['client']['save_to']; } if (isset($options[CURLOPT_FILE])) { return $options[CURLOPT_FILE]; } if ($request['http_method'] != 'HEAD') { // Create a default body if one was not provided return $options[CURLOPT_FILE] = fopen('php://temp', 'w+'); } return null; } private function getDefaultOptions(array $request, array &$headers) { $url = Core::url($request); $startingResponse = false; $options = [ '_headers' => $request['headers'], CURLOPT_CUSTOMREQUEST => $request['http_method'], CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, CURLOPT_CONNECTTIMEOUT => 150, CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$headers, &$startingResponse) { $value = trim($h); if ($value === '') { $startingResponse = true; } elseif ($startingResponse) { $startingResponse = false; $headers = [$value]; } else { $headers[] = $value; } return strlen($h); }, ]; if (isset($request['version'])) { if ($request['version'] == 2.0) { $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; } else if ($request['version'] == 1.1) { $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } else { $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; } } if (defined('CURLOPT_PROTOCOLS')) { $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } return $options; } private function applyMethod(array $request, array &$options) { if (isset($request['body'])) { $this->applyBody($request, $options); return; } switch ($request['http_method']) { case 'PUT': case 'POST': // See http://tools.ietf.org/html/rfc7230#section-3.3.2 if (!Core::hasHeader($request, 'Content-Length')) { $options[CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; } break; case 'HEAD': $options[CURLOPT_NOBODY] = true; unset( $options[CURLOPT_WRITEFUNCTION], $options[CURLOPT_READFUNCTION], $options[CURLOPT_FILE], $options[CURLOPT_INFILE] ); } } private function applyBody(array $request, array &$options) { $contentLength = Core::firstHeader($request, 'Content-Length'); $size = $contentLength !== null ? (int) $contentLength : null; // Send the body as a string if the size is less than 1MB OR if the // [client][curl][body_as_string] request value is set. if (($size !== null && $size < 1000000) || isset($request['client']['curl']['body_as_string']) || is_string($request['body']) ) { $options[CURLOPT_POSTFIELDS] = Core::body($request); // Don't duplicate the Content-Length header $this->removeHeader('Content-Length', $options); $this->removeHeader('Transfer-Encoding', $options); } else { $options[CURLOPT_UPLOAD] = true; if ($size !== null) { // Let cURL handle setting the Content-Length header $options[CURLOPT_INFILESIZE] = $size; $this->removeHeader('Content-Length', $options); } $this->addStreamingBody($request, $options); } // If the Expect header is not present, prevent curl from adding it if (!Core::hasHeader($request, 'Expect')) { $options[CURLOPT_HTTPHEADER][] = 'Expect:'; } // cURL sometimes adds a content-type by default. Prevent this. if (!Core::hasHeader($request, 'Content-Type')) { $options[CURLOPT_HTTPHEADER][] = 'Content-Type:'; } } private function addStreamingBody(array $request, array &$options) { $body = $request['body']; if ($body instanceof StreamInterface) { $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { return (string) $body->read($length); }; if (!isset($options[CURLOPT_INFILESIZE])) { if ($size = $body->getSize()) { $options[CURLOPT_INFILESIZE] = $size; } } } elseif (is_resource($body)) { $options[CURLOPT_INFILE] = $body; } elseif ($body instanceof \Iterator) { $buf = ''; $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, &$buf) { if ($body->valid()) { $buf .= $body->current(); $body->next(); } $result = (string) substr($buf, 0, $length); $buf = substr($buf, $length); return $result; }; } else { throw new \InvalidArgumentException('Invalid request body provided'); } } private function applyHeaders(array $request, array &$options) { foreach ($options['_headers'] as $name => $values) { foreach ($values as $value) { $options[CURLOPT_HTTPHEADER][] = "$name: $value"; } } // Remove the Accept header if one was not set if (!Core::hasHeader($request, 'Accept')) { $options[CURLOPT_HTTPHEADER][] = 'Accept:'; } } /** * Takes an array of curl options specified in the 'curl' option of a * request's configuration array and maps them to CURLOPT_* options. * * This method is only called when a request has a 'curl' config setting. * * @param array $config Configuration array of custom curl option * @param array $options Array of existing curl options * * @return array Returns a new array of curl options */ private function applyCustomCurlOptions(array $config, array $options) { $curlOptions = []; foreach ($config as $key => $value) { if (is_int($key)) { $curlOptions[$key] = $value; } } return $curlOptions + $options; } /** * Remove a header from the options array. * * @param string $name Case-insensitive header to remove * @param array $options Array of options to modify */ private function removeHeader($name, array &$options) { foreach (array_keys($options['_headers']) as $key) { if (!strcasecmp($key, $name)) { unset($options['_headers'][$key]); return; } } } /** * Applies an array of request client options to a the options array. * * This method uses a large switch rather than double-dispatch to save on * high overhead of calling functions in PHP. */ private function applyHandlerOptions(array $request, array &$options) { foreach ($request['client'] as $key => $value) { switch ($key) { // Violating PSR-4 to provide more room. case 'verify': if ($value === false) { unset($options[CURLOPT_CAINFO]); $options[CURLOPT_SSL_VERIFYHOST] = 0; $options[CURLOPT_SSL_VERIFYPEER] = false; continue; } $options[CURLOPT_SSL_VERIFYHOST] = 2; $options[CURLOPT_SSL_VERIFYPEER] = true; if (is_string($value)) { $options[CURLOPT_CAINFO] = $value; if (!file_exists($value)) { throw new \InvalidArgumentException( "SSL CA bundle not found: $value" ); } } break; case 'decode_content': if ($value === false) { continue; } $accept = Core::firstHeader($request, 'Accept-Encoding'); if ($accept) { $options[CURLOPT_ENCODING] = $accept; } else { $options[CURLOPT_ENCODING] = ''; // Don't let curl send the header over the wire $options[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; } break; case 'save_to': if (is_string($value)) { if (!is_dir(dirname($value))) { throw new \RuntimeException(sprintf( 'Directory %s does not exist for save_to value of %s', dirname($value), $value )); } $value = new LazyOpenStream($value, 'w+'); } if ($value instanceof StreamInterface) { $options[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($value) { return $value->write($write); }; } elseif (is_resource($value)) { $options[CURLOPT_FILE] = $value; } else { throw new \InvalidArgumentException('save_to must be a ' . 'GuzzleHttp\Stream\StreamInterface or resource'); } break; case 'timeout': if (defined('CURLOPT_TIMEOUT_MS')) { $options[CURLOPT_TIMEOUT_MS] = $value * 1000; } else { $options[CURLOPT_TIMEOUT] = $value; } break; case 'connect_timeout': if (defined('CURLOPT_CONNECTTIMEOUT_MS')) { $options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000; } else { $options[CURLOPT_CONNECTTIMEOUT] = $value; } break; case 'proxy': if (!is_array($value)) { $options[CURLOPT_PROXY] = $value; } elseif (isset($request['scheme'])) { $scheme = $request['scheme']; if (isset($value[$scheme])) { $options[CURLOPT_PROXY] = $value[$scheme]; } } break; case 'cert': if (is_array($value)) { $options[CURLOPT_SSLCERTPASSWD] = $value[1]; $value = $value[0]; } if (!file_exists($value)) { throw new \InvalidArgumentException( "SSL certificate not found: {$value}" ); } $options[CURLOPT_SSLCERT] = $value; break; case 'ssl_key': if (is_array($value)) { $options[CURLOPT_SSLKEYPASSWD] = $value[1]; $value = $value[0]; } if (!file_exists($value)) { throw new \InvalidArgumentException( "SSL private key not found: {$value}" ); } $options[CURLOPT_SSLKEY] = $value; break; case 'progress': if (!is_callable($value)) { throw new \InvalidArgumentException( 'progress client option must be callable' ); } $options[CURLOPT_NOPROGRESS] = false; $options[CURLOPT_PROGRESSFUNCTION] = function () use ($value) { $args = func_get_args(); // PHP 5.5 pushed the handle onto the start of the args if (is_resource($args[0])) { array_shift($args); } call_user_func_array($value, $args); }; break; case 'debug': if ($value) { $options[CURLOPT_STDERR] = Core::getDebugResource($value); $options[CURLOPT_VERBOSE] = true; } break; } } } /** * This function ensures that a response was set on a transaction. If one * was not set, then the request is retried if possible. This error * typically means you are sending a payload, curl encountered a * "Connection died, retrying a fresh connect" error, tried to rewind the * stream, and then encountered a "necessary data rewind wasn't possible" * error, causing the request to be sent through curl_multi_info_read() * without an error status. */ private static function retryFailedRewind( callable $handler, array $request, array $response ) { // If there is no body, then there is some other kind of issue. This // is weird and should probably never happen. if (!isset($request['body'])) { $response['err_message'] = 'No response was received for a request ' . 'with no body. This could mean that you are saturating your ' . 'network.'; return self::createErrorResponse($handler, $request, $response); } if (!Core::rewindBody($request)) { $response['err_message'] = 'The connection unexpectedly failed ' . 'without providing an error. The request would have been ' . 'retried, but attempting to rewind the request body failed.'; return self::createErrorResponse($handler, $request, $response); } // Retry no more than 3 times before giving up. if (!isset($request['curl']['retries'])) { $request['curl']['retries'] = 1; } elseif ($request['curl']['retries'] == 2) { $response['err_message'] = 'The cURL request was retried 3 times ' . 'and did no succeed. cURL was unable to rewind the body of ' . 'the request and subsequent retries resulted in the same ' . 'error. Turn on the debug option to see what went wrong. ' . 'See https://bugs.php.net/bug.php?id=47204 for more information.'; return self::createErrorResponse($handler, $request, $response); } else { $request['curl']['retries']++; } return $handler($request); } } RingPHP-1.1.0/src/Client/CurlHandler.php000066400000000000000000000077431252700054500177140ustar00rootroot00000000000000handles = $this->ownedHandles = []; $this->factory = isset($options['handle_factory']) ? $options['handle_factory'] : new CurlFactory(); $this->maxHandles = isset($options['max_handles']) ? $options['max_handles'] : 5; } public function __destruct() { foreach ($this->handles as $handle) { if (is_resource($handle)) { curl_close($handle); } } } /** * @param array $request * * @return CompletedFutureArray */ public function __invoke(array $request) { return new CompletedFutureArray( $this->_invokeAsArray($request) ); } /** * @internal * * @param array $request * * @return array */ public function _invokeAsArray(array $request) { $factory = $this->factory; // Ensure headers are by reference. They're updated elsewhere. $result = $factory($request, $this->checkoutEasyHandle()); $h = $result[0]; $hd =& $result[1]; $bd = $result[2]; Core::doSleep($request); curl_exec($h); $response = ['transfer_stats' => curl_getinfo($h)]; $response['curl']['error'] = curl_error($h); $response['curl']['errno'] = curl_errno($h); $response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']); $this->releaseEasyHandle($h); return CurlFactory::createResponse([$this, '_invokeAsArray'], $request, $response, $hd, $bd); } private function checkoutEasyHandle() { // Find an unused handle in the cache if (false !== ($key = array_search(false, $this->ownedHandles, true))) { $this->ownedHandles[$key] = true; return $this->handles[$key]; } // Add a new handle $handle = curl_init(); $id = (int) $handle; $this->handles[$id] = $handle; $this->ownedHandles[$id] = true; return $handle; } private function releaseEasyHandle($handle) { $id = (int) $handle; if (count($this->ownedHandles) > $this->maxHandles) { curl_close($this->handles[$id]); unset($this->handles[$id], $this->ownedHandles[$id]); } else { // curl_reset doesn't clear these out for some reason static $unsetValues = [ CURLOPT_HEADERFUNCTION => null, CURLOPT_WRITEFUNCTION => null, CURLOPT_READFUNCTION => null, CURLOPT_PROGRESSFUNCTION => null, ]; curl_setopt_array($handle, $unsetValues); curl_reset($handle); $this->ownedHandles[$id] = false; } } } RingPHP-1.1.0/src/Client/CurlMultiHandler.php000066400000000000000000000167261252700054500207300ustar00rootroot00000000000000_mh = $options['mh']; } $this->factory = isset($options['handle_factory']) ? $options['handle_factory'] : new CurlFactory(); $this->selectTimeout = isset($options['select_timeout']) ? $options['select_timeout'] : 1; $this->maxHandles = isset($options['max_handles']) ? $options['max_handles'] : 100; } public function __get($name) { if ($name === '_mh') { return $this->_mh = curl_multi_init(); } throw new \BadMethodCallException(); } public function __destruct() { // Finish any open connections before terminating the script. if ($this->handles) { $this->execute(); } if (isset($this->_mh)) { curl_multi_close($this->_mh); unset($this->_mh); } } public function __invoke(array $request) { $factory = $this->factory; $result = $factory($request); $entry = [ 'request' => $request, 'response' => [], 'handle' => $result[0], 'headers' => &$result[1], 'body' => $result[2], 'deferred' => new Deferred(), ]; $id = (int) $result[0]; $future = new FutureArray( $entry['deferred']->promise(), [$this, 'execute'], function () use ($id) { return $this->cancel($id); } ); $this->addRequest($entry); // Transfer outstanding requests if there are too many open handles. if (count($this->handles) >= $this->maxHandles) { $this->execute(); } return $future; } /** * Runs until all outstanding connections have completed. */ public function execute() { do { if ($this->active && curl_multi_select($this->_mh, $this->selectTimeout) === -1 ) { // Perform a usleep if a select returns -1. // See: https://bugs.php.net/bug.php?id=61141 usleep(250); } // Add any delayed futures if needed. if ($this->delays) { $this->addDelays(); } do { $mrc = curl_multi_exec($this->_mh, $this->active); } while ($mrc === CURLM_CALL_MULTI_PERFORM); $this->processMessages(); // If there are delays but no transfers, then sleep for a bit. if (!$this->active && $this->delays) { usleep(500); } } while ($this->active || $this->handles); } private function addRequest(array &$entry) { $id = (int) $entry['handle']; $this->handles[$id] = $entry; // If the request is a delay, then add the reques to the curl multi // pool only after the specified delay. if (isset($entry['request']['client']['delay'])) { $this->delays[$id] = microtime(true) + ($entry['request']['client']['delay'] / 1000); } elseif (empty($entry['request']['future'])) { curl_multi_add_handle($this->_mh, $entry['handle']); } else { curl_multi_add_handle($this->_mh, $entry['handle']); // "lazy" futures are only sent once the pool has many requests. if ($entry['request']['future'] !== 'lazy') { do { $mrc = curl_multi_exec($this->_mh, $this->active); } while ($mrc === CURLM_CALL_MULTI_PERFORM); $this->processMessages(); } } } private function removeProcessed($id) { if (isset($this->handles[$id])) { curl_multi_remove_handle( $this->_mh, $this->handles[$id]['handle'] ); curl_close($this->handles[$id]['handle']); unset($this->handles[$id], $this->delays[$id]); } } /** * Cancels a handle from sending and removes references to it. * * @param int $id Handle ID to cancel and remove. * * @return bool True on success, false on failure. */ private function cancel($id) { // Cannot cancel if it has been processed. if (!isset($this->handles[$id])) { return false; } $handle = $this->handles[$id]['handle']; unset($this->delays[$id], $this->handles[$id]); curl_multi_remove_handle($this->_mh, $handle); curl_close($handle); return true; } private function addDelays() { $currentTime = microtime(true); foreach ($this->delays as $id => $delay) { if ($currentTime >= $delay) { unset($this->delays[$id]); curl_multi_add_handle( $this->_mh, $this->handles[$id]['handle'] ); } } } private function processMessages() { while ($done = curl_multi_info_read($this->_mh)) { $id = (int) $done['handle']; if (!isset($this->handles[$id])) { // Probably was cancelled. continue; } $entry = $this->handles[$id]; $entry['response']['transfer_stats'] = curl_getinfo($done['handle']); if ($done['result'] !== CURLM_OK) { $entry['response']['curl']['errno'] = $done['result']; if (function_exists('curl_strerror')) { $entry['response']['curl']['error'] = curl_strerror($done['result']); } } $result = CurlFactory::createResponse( $this, $entry['request'], $entry['response'], $entry['headers'], $entry['body'] ); $this->removeProcessed($id); $entry['deferred']->resolve($result); } } } RingPHP-1.1.0/src/Client/Middleware.php000066400000000000000000000036131252700054500175560ustar00rootroot00000000000000result = $result; } public function __invoke(array $request) { Core::doSleep($request); $response = is_callable($this->result) ? call_user_func($this->result, $request) : $this->result; if (is_array($response)) { $response = new CompletedFutureArray($response + [ 'status' => null, 'body' => null, 'headers' => [], 'reason' => null, 'effective_url' => null, ]); } elseif (!$response instanceof FutureArrayInterface) { throw new \InvalidArgumentException( 'Response must be an array or FutureArrayInterface. Found ' . Core::describeType($request) ); } return $response; } } RingPHP-1.1.0/src/Client/StreamHandler.php000066400000000000000000000314701252700054500202340ustar00rootroot00000000000000options = $options; } public function __invoke(array $request) { $url = Core::url($request); Core::doSleep($request); try { // Does not support the expect header. $request = Core::removeHeader($request, 'Expect'); $stream = $this->createStream($url, $request); return $this->createResponse($request, $url, $stream); } catch (RingException $e) { return $this->createErrorResponse($url, $e); } } private function createResponse(array $request, $url, $stream) { $hdrs = $this->lastHeaders; $this->lastHeaders = null; $parts = explode(' ', array_shift($hdrs), 3); $response = [ 'version' => substr($parts[0], 5), 'status' => $parts[1], 'reason' => isset($parts[2]) ? $parts[2] : null, 'headers' => Core::headersFromLines($hdrs), 'effective_url' => $url, ]; $stream = $this->checkDecode($request, $response, $stream); // If not streaming, then drain the response into a stream. if (empty($request['client']['stream'])) { $dest = isset($request['client']['save_to']) ? $request['client']['save_to'] : fopen('php://temp', 'r+'); $stream = $this->drain($stream, $dest); } $response['body'] = $stream; return new CompletedFutureArray($response); } private function checkDecode(array $request, array $response, $stream) { // Automatically decode responses when instructed. if (!empty($request['client']['decode_content'])) { switch (Core::firstHeader($response, 'Content-Encoding', true)) { case 'gzip': case 'deflate': $stream = new InflateStream(Stream::factory($stream)); break; } } return $stream; } /** * Drains the stream into the "save_to" client option. * * @param resource $stream * @param string|resource|StreamInterface $dest * * @return Stream * @throws \RuntimeException when the save_to option is invalid. */ private function drain($stream, $dest) { if (is_resource($stream)) { if (!is_resource($dest)) { $stream = Stream::factory($stream); } else { stream_copy_to_stream($stream, $dest); fclose($stream); rewind($dest); return $dest; } } // Stream the response into the destination stream $dest = is_string($dest) ? new Stream(Utils::open($dest, 'r+')) : Stream::factory($dest); Utils::copyToStream($stream, $dest); $dest->seek(0); $stream->close(); return $dest; } /** * Creates an error response for the given stream. * * @param string $url * @param RingException $e * * @return array */ private function createErrorResponse($url, RingException $e) { // Determine if the error was a networking error. $message = $e->getMessage(); // This list can probably get more comprehensive. if (strpos($message, 'getaddrinfo') // DNS lookup failed || strpos($message, 'Connection refused') ) { $e = new ConnectException($e->getMessage(), 0, $e); } return new CompletedFutureArray([ 'status' => null, 'body' => null, 'headers' => [], 'effective_url' => $url, 'error' => $e ]); } /** * Create a resource and check to ensure it was created successfully * * @param callable $callback Callable that returns stream resource * * @return resource * @throws \RuntimeException on error */ private function createResource(callable $callback) { $errors = null; set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { $errors[] = [ 'message' => $msg, 'file' => $file, 'line' => $line ]; return true; }); $resource = $callback(); restore_error_handler(); if (!$resource) { $message = 'Error creating resource: '; foreach ($errors as $err) { foreach ($err as $key => $value) { $message .= "[$key] $value" . PHP_EOL; } } throw new RingException(trim($message)); } return $resource; } private function createStream($url, array $request) { static $methods; if (!$methods) { $methods = array_flip(get_class_methods(__CLASS__)); } // HTTP/1.1 streams using the PHP stream wrapper require a // Connection: close header if ((!isset($request['version']) || $request['version'] == '1.1') && !Core::hasHeader($request, 'Connection') ) { $request['headers']['Connection'] = ['close']; } // Ensure SSL is verified by default if (!isset($request['client']['verify'])) { $request['client']['verify'] = true; } $params = []; $options = $this->getDefaultOptions($request); if (isset($request['client'])) { foreach ($request['client'] as $key => $value) { $method = "add_{$key}"; if (isset($methods[$method])) { $this->{$method}($request, $options, $value, $params); } } } return $this->createStreamResource( $url, $request, $options, $this->createContext($request, $options, $params) ); } private function getDefaultOptions(array $request) { $headers = ""; foreach ($request['headers'] as $name => $value) { foreach ((array) $value as $val) { $headers .= "$name: $val\r\n"; } } $context = [ 'http' => [ 'method' => $request['http_method'], 'header' => $headers, 'protocol_version' => isset($request['version']) ? $request['version'] : 1.1, 'ignore_errors' => true, 'follow_location' => 0, ], ]; $body = Core::body($request); if (isset($body)) { $context['http']['content'] = $body; // Prevent the HTTP handler from adding a Content-Type header. if (!Core::hasHeader($request, 'Content-Type')) { $context['http']['header'] .= "Content-Type:\r\n"; } } $context['http']['header'] = rtrim($context['http']['header']); return $context; } private function add_proxy(array $request, &$options, $value, &$params) { if (!is_array($value)) { $options['http']['proxy'] = $value; } else { $scheme = isset($request['scheme']) ? $request['scheme'] : 'http'; if (isset($value[$scheme])) { $options['http']['proxy'] = $value[$scheme]; } } } private function add_timeout(array $request, &$options, $value, &$params) { $options['http']['timeout'] = $value; } private function add_verify(array $request, &$options, $value, &$params) { if ($value === true) { // PHP 5.6 or greater will find the system cert by default. When // < 5.6, use the Guzzle bundled cacert. if (PHP_VERSION_ID < 50600) { $options['ssl']['cafile'] = ClientUtils::getDefaultCaBundle(); } } elseif (is_string($value)) { $options['ssl']['cafile'] = $value; if (!file_exists($value)) { throw new RingException("SSL CA bundle not found: $value"); } } elseif ($value === false) { $options['ssl']['verify_peer'] = false; $options['ssl']['allow_self_signed'] = true; return; } else { throw new RingException('Invalid verify request option'); } $options['ssl']['verify_peer'] = true; $options['ssl']['allow_self_signed'] = false; } private function add_cert(array $request, &$options, $value, &$params) { if (is_array($value)) { $options['ssl']['passphrase'] = $value[1]; $value = $value[0]; } if (!file_exists($value)) { throw new RingException("SSL certificate not found: {$value}"); } $options['ssl']['local_cert'] = $value; } private function add_progress(array $request, &$options, $value, &$params) { $fn = function ($code, $_1, $_2, $_3, $transferred, $total) use ($value) { if ($code == STREAM_NOTIFY_PROGRESS) { $value($total, $transferred, null, null); } }; // Wrap the existing function if needed. $params['notification'] = isset($params['notification']) ? Core::callArray([$params['notification'], $fn]) : $fn; } private function add_debug(array $request, &$options, $value, &$params) { if ($value === false) { return; } static $map = [ STREAM_NOTIFY_CONNECT => 'CONNECT', STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', STREAM_NOTIFY_PROGRESS => 'PROGRESS', STREAM_NOTIFY_FAILURE => 'FAILURE', STREAM_NOTIFY_COMPLETED => 'COMPLETED', STREAM_NOTIFY_RESOLVE => 'RESOLVE', ]; static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; $value = Core::getDebugResource($value); $ident = $request['http_method'] . ' ' . Core::url($request); $fn = function () use ($ident, $value, $map, $args) { $passed = func_get_args(); $code = array_shift($passed); fprintf($value, '<%s> [%s] ', $ident, $map[$code]); foreach (array_filter($passed) as $i => $v) { fwrite($value, $args[$i] . ': "' . $v . '" '); } fwrite($value, "\n"); }; // Wrap the existing function if needed. $params['notification'] = isset($params['notification']) ? Core::callArray([$params['notification'], $fn]) : $fn; } private function applyCustomOptions(array $request, array &$options) { if (!isset($request['client']['stream_context'])) { return; } if (!is_array($request['client']['stream_context'])) { throw new RingException('stream_context must be an array'); } $options = array_replace_recursive( $options, $request['client']['stream_context'] ); } private function createContext(array $request, array $options, array $params) { $this->applyCustomOptions($request, $options); return $this->createResource( function () use ($request, $options, $params) { return stream_context_create($options, $params); }, $request, $options ); } private function createStreamResource( $url, array $request, array $options, $context ) { return $this->createResource( function () use ($url, $context) { if (false === strpos($url, 'http')) { trigger_error("URL is invalid: {$url}", E_USER_WARNING); return null; } $resource = fopen($url, 'r', null, $context); $this->lastHeaders = $http_response_header; return $resource; }, $request, $options ); } } RingPHP-1.1.0/src/Core.php000066400000000000000000000254071252700054500151600ustar00rootroot00000000000000 $value) { if (!strcasecmp($name, $header)) { $result = array_merge($result, $value); } } } return $result; } /** * Gets a header value from a message as a string or null * * This method searches through the "headers" key of a message for a header * using a case-insensitive search. The lines of the header are imploded * using commas into a single string return value. * * @param array $message Request or response hash. * @param string $header Header to retrieve * * @return string|null Returns the header string if found, or null if not. */ public static function header($message, $header) { $match = self::headerLines($message, $header); return $match ? implode(', ', $match) : null; } /** * Returns the first header value from a message as a string or null. If * a header line contains multiple values separated by a comma, then this * function will return the first value in the list. * * @param array $message Request or response hash. * @param string $header Header to retrieve * * @return string|null Returns the value as a string if found. */ public static function firstHeader($message, $header) { if (!empty($message['headers'])) { foreach ($message['headers'] as $name => $value) { if (!strcasecmp($name, $header)) { // Return the match itself if it is a single value. $pos = strpos($value[0], ','); return $pos ? substr($value[0], 0, $pos) : $value[0]; } } } return null; } /** * Returns true if a message has the provided case-insensitive header. * * @param array $message Request or response hash. * @param string $header Header to check * * @return bool */ public static function hasHeader($message, $header) { if (!empty($message['headers'])) { foreach ($message['headers'] as $name => $value) { if (!strcasecmp($name, $header)) { return true; } } } return false; } /** * Parses an array of header lines into an associative array of headers. * * @param array $lines Header lines array of strings in the following * format: "Name: Value" * @return array */ public static function headersFromLines($lines) { $headers = []; foreach ($lines as $line) { $parts = explode(':', $line, 2); $headers[trim($parts[0])][] = isset($parts[1]) ? trim($parts[1]) : null; } return $headers; } /** * Removes a header from a message using a case-insensitive comparison. * * @param array $message Message that contains 'headers' * @param string $header Header to remove * * @return array */ public static function removeHeader(array $message, $header) { if (isset($message['headers'])) { foreach (array_keys($message['headers']) as $key) { if (!strcasecmp($header, $key)) { unset($message['headers'][$key]); } } } return $message; } /** * Replaces any existing case insensitive headers with the given value. * * @param array $message Message that contains 'headers' * @param string $header Header to set. * @param array $value Value to set. * * @return array */ public static function setHeader(array $message, $header, array $value) { $message = self::removeHeader($message, $header); $message['headers'][$header] = $value; return $message; } /** * Creates a URL string from a request. * * If the "url" key is present on the request, it is returned, otherwise * the url is built up based on the scheme, host, uri, and query_string * request values. * * @param array $request Request to get the URL from * * @return string Returns the request URL as a string. * @throws \InvalidArgumentException if no Host header is present. */ public static function url(array $request) { if (isset($request['url'])) { return $request['url']; } $uri = (isset($request['scheme']) ? $request['scheme'] : 'http') . '://'; if ($host = self::header($request, 'host')) { $uri .= $host; } else { throw new \InvalidArgumentException('No Host header was provided'); } if (isset($request['uri'])) { $uri .= $request['uri']; } if (isset($request['query_string'])) { $uri .= '?' . $request['query_string']; } return $uri; } /** * Reads the body of a message into a string. * * @param array|FutureArrayInterface $message Array containing a "body" key * * @return null|string Returns the body as a string or null if not set. * @throws \InvalidArgumentException if a request body is invalid. */ public static function body($message) { if (!isset($message['body'])) { return null; } if ($message['body'] instanceof StreamInterface) { return (string) $message['body']; } switch (gettype($message['body'])) { case 'string': return $message['body']; case 'resource': return stream_get_contents($message['body']); case 'object': if ($message['body'] instanceof \Iterator) { return implode('', iterator_to_array($message['body'])); } elseif (method_exists($message['body'], '__toString')) { return (string) $message['body']; } default: throw new \InvalidArgumentException('Invalid request body: ' . self::describeType($message['body'])); } } /** * Rewind the body of the provided message if possible. * * @param array $message Message that contains a 'body' field. * * @return bool Returns true on success, false on failure */ public static function rewindBody($message) { if ($message['body'] instanceof StreamInterface) { return $message['body']->seek(0); } if ($message['body'] instanceof \Generator) { return false; } if ($message['body'] instanceof \Iterator) { $message['body']->rewind(); return true; } if (is_resource($message['body'])) { return rewind($message['body']); } return is_string($message['body']) || (is_object($message['body']) && method_exists($message['body'], '__toString')); } /** * Debug function used to describe the provided value type and class. * * @param mixed $input * * @return string Returns a string containing the type of the variable and * if a class is provided, the class name. */ public static function describeType($input) { switch (gettype($input)) { case 'object': return 'object(' . get_class($input) . ')'; case 'array': return 'array(' . count($input) . ')'; default: ob_start(); var_dump($input); // normalize float vs double return str_replace('double(', 'float(', rtrim(ob_get_clean())); } } /** * Sleep for the specified amount of time specified in the request's * ['client']['delay'] option if present. * * This function should only be used when a non-blocking sleep is not * possible. * * @param array $request Request to sleep */ public static function doSleep(array $request) { if (isset($request['client']['delay'])) { usleep($request['client']['delay'] * 1000); } } /** * Returns a proxied future that modifies the dereferenced value of another * future using a promise. * * @param FutureArrayInterface $future Future to wrap with a new future * @param callable $onFulfilled Invoked when the future fulfilled * @param callable $onRejected Invoked when the future rejected * @param callable $onProgress Invoked when the future progresses * * @return FutureArray */ public static function proxy( FutureArrayInterface $future, callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null ) { return new FutureArray( $future->then($onFulfilled, $onRejected, $onProgress), [$future, 'wait'], [$future, 'cancel'] ); } /** * Returns a debug stream based on the provided variable. * * @param mixed $value Optional value * * @return resource */ public static function getDebugResource($value = null) { if (is_resource($value)) { return $value; } elseif (defined('STDOUT')) { return STDOUT; } else { return fopen('php://output', 'w'); } } } RingPHP-1.1.0/src/Exception/000077500000000000000000000000001252700054500155055ustar00rootroot00000000000000RingPHP-1.1.0/src/Exception/CancelledException.php000066400000000000000000000002021252700054500217410ustar00rootroot00000000000000wrappedPromise = $promise; $this->waitfn = $wait; $this->cancelfn = $cancel; } public function wait() { if (!$this->isRealized) { $this->addShadow(); if (!$this->isRealized && $this->waitfn) { $this->invokeWait(); } if (!$this->isRealized) { $this->error = new RingException('Waiting did not resolve future'); } } if ($this->error) { throw $this->error; } return $this->result; } public function promise() { return $this->wrappedPromise; } public function then( callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null ) { return $this->wrappedPromise->then($onFulfilled, $onRejected, $onProgress); } public function cancel() { if (!$this->isRealized) { $cancelfn = $this->cancelfn; $this->waitfn = $this->cancelfn = null; $this->isRealized = true; $this->error = new CancelledFutureAccessException(); if ($cancelfn) { $cancelfn($this); } } } private function addShadow() { // Get the result and error when the promise is resolved. Note that // calling this function might trigger the resolution immediately. $this->wrappedPromise->then( function ($value) { $this->isRealized = true; $this->result = $value; $this->waitfn = $this->cancelfn = null; }, function ($error) { $this->isRealized = true; $this->error = $error; $this->waitfn = $this->cancelfn = null; } ); } private function invokeWait() { try { $wait = $this->waitfn; $this->waitfn = null; $wait(); } catch (\Exception $e) { // Defer can throw to reject. $this->error = $e; $this->isRealized = true; } } } RingPHP-1.1.0/src/Future/CompletedFutureArray.php000066400000000000000000000015421252700054500216420ustar00rootroot00000000000000result[$offset]); } public function offsetGet($offset) { return $this->result[$offset]; } public function offsetSet($offset, $value) { $this->result[$offset] = $value; } public function offsetUnset($offset) { unset($this->result[$offset]); } public function count() { return count($this->result); } public function getIterator() { return new \ArrayIterator($this->result); } } RingPHP-1.1.0/src/Future/CompletedFutureValue.php000066400000000000000000000025411252700054500216400ustar00rootroot00000000000000result = $result; $this->error = $e; } public function wait() { if ($this->error) { throw $this->error; } return $this->result; } public function cancel() {} public function promise() { if (!$this->cachedPromise) { $this->cachedPromise = $this->error ? new RejectedPromise($this->error) : new FulfilledPromise($this->result); } return $this->cachedPromise; } public function then( callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null ) { return $this->promise()->then($onFulfilled, $onRejected, $onProgress); } } RingPHP-1.1.0/src/Future/FutureArray.php000066400000000000000000000013761252700054500200120ustar00rootroot00000000000000_value[$offset]); } public function offsetGet($offset) { return $this->_value[$offset]; } public function offsetSet($offset, $value) { $this->_value[$offset] = $value; } public function offsetUnset($offset) { unset($this->_value[$offset]); } public function count() { return count($this->_value); } public function getIterator() { return new \ArrayIterator($this->_value); } } RingPHP-1.1.0/src/Future/FutureArrayInterface.php000066400000000000000000000003251252700054500216240ustar00rootroot00000000000000_value = $this->wait(); } } RingPHP-1.1.0/tests/000077500000000000000000000000001252700054500141225ustar00rootroot00000000000000RingPHP-1.1.0/tests/Client/000077500000000000000000000000001252700054500153405ustar00rootroot00000000000000RingPHP-1.1.0/tests/Client/CurlFactoryTest.php000066400000000000000000000635531252700054500211620ustar00rootroot00000000000000 200, 'headers' => [ 'Foo' => ['Bar'], 'Baz' => ['bam'], 'Content-Length' => [2], ], 'body' => 'hi', ]]); $stream = Stream::factory(); $request = [ 'http_method' => 'PUT', 'headers' => [ 'host' => [Server::$url], 'Hi' => [' 123'], ], 'body' => 'testing', 'client' => ['save_to' => $stream], ]; $f = new CurlFactory(); $result = $f($request); $this->assertInternalType('array', $result); $this->assertCount(3, $result); $this->assertInternalType('resource', $result[0]); $this->assertInternalType('array', $result[1]); $this->assertSame($stream, $result[2]); curl_close($result[0]); $this->assertEquals('PUT', $_SERVER['_curl'][CURLOPT_CUSTOMREQUEST]); $this->assertEquals( 'http://http://127.0.0.1:8125/', $_SERVER['_curl'][CURLOPT_URL] ); // Sends via post fields when the request is small enough $this->assertEquals('testing', $_SERVER['_curl'][CURLOPT_POSTFIELDS]); $this->assertEquals(0, $_SERVER['_curl'][CURLOPT_RETURNTRANSFER]); $this->assertEquals(0, $_SERVER['_curl'][CURLOPT_HEADER]); $this->assertEquals(150, $_SERVER['_curl'][CURLOPT_CONNECTTIMEOUT]); $this->assertInstanceOf('Closure', $_SERVER['_curl'][CURLOPT_HEADERFUNCTION]); if (defined('CURLOPT_PROTOCOLS')) { $this->assertEquals( CURLPROTO_HTTP | CURLPROTO_HTTPS, $_SERVER['_curl'][CURLOPT_PROTOCOLS] ); } $this->assertContains('Expect:', $_SERVER['_curl'][CURLOPT_HTTPHEADER]); $this->assertContains('Accept:', $_SERVER['_curl'][CURLOPT_HTTPHEADER]); $this->assertContains('Content-Type:', $_SERVER['_curl'][CURLOPT_HTTPHEADER]); $this->assertContains('Hi: 123', $_SERVER['_curl'][CURLOPT_HTTPHEADER]); $this->assertContains('host: http://127.0.0.1:8125/', $_SERVER['_curl'][CURLOPT_HTTPHEADER]); } public function testSendsHeadRequests() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'HEAD', 'headers' => ['host' => [Server::$host]], ]); $response->wait(); $this->assertEquals(true, $_SERVER['_curl'][CURLOPT_NOBODY]); $checks = [CURLOPT_WRITEFUNCTION, CURLOPT_READFUNCTION, CURLOPT_FILE, CURLOPT_INFILE]; foreach ($checks as $check) { $this->assertArrayNotHasKey($check, $_SERVER['_curl']); } $this->assertEquals('HEAD', Server::received()[0]['http_method']); } public function testCanAddCustomCurlOptions() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['curl' => [CURLOPT_LOW_SPEED_LIMIT => 10]], ]); $this->assertEquals(10, $_SERVER['_curl'][CURLOPT_LOW_SPEED_LIMIT]); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage SSL CA bundle not found: /does/not/exist */ public function testValidatesVerify() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['verify' => '/does/not/exist'], ]); } public function testCanSetVerifyToFile() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['verify' => __FILE__], ]); $this->assertEquals(__FILE__, $_SERVER['_curl'][CURLOPT_CAINFO]); $this->assertEquals(2, $_SERVER['_curl'][CURLOPT_SSL_VERIFYHOST]); $this->assertEquals(true, $_SERVER['_curl'][CURLOPT_SSL_VERIFYPEER]); } public function testAddsVerifyAsTrue() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['verify' => true], ]); $this->assertEquals(2, $_SERVER['_curl'][CURLOPT_SSL_VERIFYHOST]); $this->assertEquals(true, $_SERVER['_curl'][CURLOPT_SSL_VERIFYPEER]); $this->assertArrayNotHasKey(CURLOPT_CAINFO, $_SERVER['_curl']); } public function testCanDisableVerify() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['verify' => false], ]); $this->assertEquals(0, $_SERVER['_curl'][CURLOPT_SSL_VERIFYHOST]); $this->assertEquals(false, $_SERVER['_curl'][CURLOPT_SSL_VERIFYPEER]); } public function testAddsProxy() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['proxy' => 'http://bar.com'], ]); $this->assertEquals('http://bar.com', $_SERVER['_curl'][CURLOPT_PROXY]); } public function testAddsViaScheme() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'scheme' => 'http', 'headers' => ['host' => ['foo.com']], 'client' => [ 'proxy' => ['http' => 'http://bar.com', 'https' => 'https://t'], ], ]); $this->assertEquals('http://bar.com', $_SERVER['_curl'][CURLOPT_PROXY]); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage SSL private key not found: /does/not/exist */ public function testValidatesSslKey() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['ssl_key' => '/does/not/exist'], ]); } public function testAddsSslKey() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['ssl_key' => __FILE__], ]); $this->assertEquals(__FILE__, $_SERVER['_curl'][CURLOPT_SSLKEY]); } public function testAddsSslKeyWithPassword() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['ssl_key' => [__FILE__, 'test']], ]); $this->assertEquals(__FILE__, $_SERVER['_curl'][CURLOPT_SSLKEY]); $this->assertEquals('test', $_SERVER['_curl'][CURLOPT_SSLKEYPASSWD]); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage SSL certificate not found: /does/not/exist */ public function testValidatesCert() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['cert' => '/does/not/exist'], ]); } public function testAddsCert() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['cert' => __FILE__], ]); $this->assertEquals(__FILE__, $_SERVER['_curl'][CURLOPT_SSLCERT]); } public function testAddsCertWithPassword() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['cert' => [__FILE__, 'test']], ]); $this->assertEquals(__FILE__, $_SERVER['_curl'][CURLOPT_SSLCERT]); $this->assertEquals('test', $_SERVER['_curl'][CURLOPT_SSLCERTPASSWD]); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage progress client option must be callable */ public function testValidatesProgress() { $f = new CurlFactory(); $f([ 'http_method' => 'GET', 'headers' => ['host' => ['foo.com']], 'client' => ['progress' => 'foo'], ]); } public function testEmitsDebugInfoToStream() { $res = fopen('php://memory', 'r+'); Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'HEAD', 'headers' => ['host' => [Server::$host]], 'client' => ['debug' => $res], ]); $response->wait(); rewind($res); $output = str_replace("\r", '', stream_get_contents($res)); $this->assertContains( "> HEAD / HTTP/1.1\nhost: 127.0.0.1:8125\n\n", $output ); $this->assertContains("< HTTP/1.1 200", $output); fclose($res); } public function testEmitsProgressToFunction() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $called = []; $response = $a([ 'http_method' => 'HEAD', 'headers' => ['host' => [Server::$host]], 'client' => [ 'progress' => function () use (&$called) { $called[] = func_get_args(); }, ], ]); $response->wait(); $this->assertNotEmpty($called); foreach ($called as $call) { $this->assertCount(4, $call); } } private function addDecodeResponse($withEncoding = true) { $content = gzencode('test'); $response = [ 'status' => 200, 'reason' => 'OK', 'headers' => ['Content-Length' => [strlen($content)]], 'body' => $content, ]; if ($withEncoding) { $response['headers']['Content-Encoding'] = ['gzip']; } Server::flush(); Server::enqueue([$response]); return $content; } public function testDecodesGzippedResponses() { $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['decode_content' => true], ]); $response->wait(); $this->assertEquals('test', Core::body($response)); $this->assertEquals('', $_SERVER['_curl'][CURLOPT_ENCODING]); $sent = Server::received()[0]; $this->assertNull(Core::header($sent, 'Accept-Encoding')); } public function testDecodesGzippedResponsesWithHeader() { $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => [ 'host' => [Server::$host], 'Accept-Encoding' => ['gzip'], ], 'client' => ['decode_content' => true], ]); $response->wait(); $this->assertEquals('gzip', $_SERVER['_curl'][CURLOPT_ENCODING]); $sent = Server::received()[0]; $this->assertEquals('gzip', Core::header($sent, 'Accept-Encoding')); $this->assertEquals('test', Core::body($response)); } public function testDoesNotForceDecode() { $content = $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['decode_content' => false], ]); $response->wait(); $sent = Server::received()[0]; $this->assertNull(Core::header($sent, 'Accept-Encoding')); $this->assertEquals($content, Core::body($response)); } public function testProtocolVersion() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'version' => 1.0, ]); $this->assertEquals(CURL_HTTP_VERSION_1_0, $_SERVER['_curl'][CURLOPT_HTTP_VERSION]); } /** * @expectedException \InvalidArgumentException */ public function testValidatesSaveTo() { $handler = new CurlMultiHandler(); $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['save_to' => true], ]); } public function testSavesToStream() { $stream = fopen('php://memory', 'r+'); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => [ 'decode_content' => true, 'save_to' => $stream, ], ]); $response->wait(); rewind($stream); $this->assertEquals('test', stream_get_contents($stream)); } public function testSavesToGuzzleStream() { $stream = Stream::factory(); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => [ 'decode_content' => true, 'save_to' => $stream, ], ]); $response->wait(); $this->assertEquals('test', (string) $stream); } public function testSavesToFileOnDisk() { $tmpfile = tempnam(sys_get_temp_dir(), 'testfile'); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => [ 'decode_content' => true, 'save_to' => $tmpfile, ], ]); $response->wait(); $this->assertEquals('test', file_get_contents($tmpfile)); unlink($tmpfile); } /** * @expectedException \InvalidArgumentException */ public function testValidatesBody() { $handler = new CurlMultiHandler(); $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'body' => false, ]); } public function testAddsLargePayloadFromStreamWithNoSizeUsingChunked() { $stream = Stream::factory('foo'); $stream = FnStream::decorate($stream, [ 'getSize' => function () { return null; } ]); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'body' => $stream, ]); $response->wait(); $sent = Server::received()[0]; $this->assertEquals('chunked', Core::header($sent, 'Transfer-Encoding')); $this->assertNull(Core::header($sent, 'Content-Length')); $this->assertEquals('foo', $sent['body']); } public function testAddsPayloadFromIterator() { $iter = new \ArrayIterator(['f', 'o', 'o']); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'body' => $iter, ]); $response->wait(); $sent = Server::received()[0]; $this->assertEquals('chunked', Core::header($sent, 'Transfer-Encoding')); $this->assertNull(Core::header($sent, 'Content-Length')); $this->assertEquals('foo', $sent['body']); } public function testAddsPayloadFromResource() { $res = fopen('php://memory', 'r+'); $data = str_repeat('.', 1000000); fwrite($res, $data); rewind($res); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => [ 'host' => [Server::$host], 'content-length' => [1000000], ], 'body' => $res, ]); $response->wait(); $sent = Server::received()[0]; $this->assertNull(Core::header($sent, 'Transfer-Encoding')); $this->assertEquals(1000000, Core::header($sent, 'Content-Length')); $this->assertEquals($data, $sent['body']); } public function testAddsContentLengthFromStream() { $stream = Stream::factory('foo'); $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'body' => $stream, ]); $response->wait(); $sent = Server::received()[0]; $this->assertEquals(3, Core::header($sent, 'Content-Length')); $this->assertNull(Core::header($sent, 'Transfer-Encoding')); $this->assertEquals('foo', $sent['body']); } public function testDoesNotAddMultipleContentLengthHeaders() { $this->addDecodeResponse(); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => [ 'host' => [Server::$host], 'content-length' => [3], ], 'body' => 'foo', ]); $response->wait(); $sent = Server::received()[0]; $this->assertEquals(3, Core::header($sent, 'Content-Length')); $this->assertNull(Core::header($sent, 'Transfer-Encoding')); $this->assertEquals('foo', $sent['body']); } public function testSendsPostWithNoBodyOrDefaultContentType() { Server::flush(); Server::enqueue([['status' => 200]]); $handler = new CurlMultiHandler(); $response = $handler([ 'http_method' => 'POST', 'uri' => '/', 'headers' => ['host' => [Server::$host]], ]); $response->wait(); $received = Server::received()[0]; $this->assertEquals('POST', $received['http_method']); $this->assertNull(Core::header($received, 'content-type')); $this->assertSame('0', Core::firstHeader($received, 'content-length')); } public function testParseProtocolVersion() { $res = CurlFactory::createResponse( function () {}, [], ['curl' => ['errno' => null]], ['HTTP/1.1 200 Ok'], null ); $this->assertSame('1.1', $res['version']); } public function testFailsWhenNoResponseAndNoBody() { $res = CurlFactory::createResponse(function () {}, [], [], [], null); $this->assertInstanceOf('GuzzleHttp\Ring\Exception\RingException', $res['error']); $this->assertContains( 'No response was received for a request with no body', $res['error']->getMessage() ); } public function testFailsWhenCannotRewindRetry() { $res = CurlFactory::createResponse(function () {}, [ 'body' => new NoSeekStream(Stream::factory('foo')) ], [], [], null); $this->assertInstanceOf('GuzzleHttp\Ring\Exception\RingException', $res['error']); $this->assertContains( 'rewind the request body failed', $res['error']->getMessage() ); } public function testRetriesWhenBodyCanBeRewound() { $callHandler = $called = false; $res = CurlFactory::createResponse(function () use (&$callHandler) { $callHandler = true; return ['status' => 200]; }, [ 'body' => FnStream::decorate(Stream::factory('test'), [ 'seek' => function () use (&$called) { $called = true; return true; } ]) ], [], [], null); $this->assertTrue($callHandler); $this->assertTrue($called); $this->assertEquals('200', $res['status']); } public function testFailsWhenRetryMoreThanThreeTimes() { $call = 0; $mock = new MockHandler(function (array $request) use (&$mock, &$call) { $call++; return CurlFactory::createResponse($mock, $request, [], [], null); }); $response = $mock([ 'http_method' => 'GET', 'body' => 'test', ]); $this->assertEquals(3, $call); $this->assertArrayHasKey('error', $response); $this->assertContains( 'The cURL request was retried 3 times', $response['error']->getMessage() ); } public function testHandles100Continue() { Server::flush(); Server::enqueue([ [ 'status' => '200', 'reason' => 'OK', 'headers' => [ 'Test' => ['Hello'], 'Content-Length' => ['4'], ], 'body' => 'test', ], ]); $request = [ 'http_method' => 'PUT', 'headers' => [ 'Host' => [Server::$host], 'Expect' => ['100-Continue'], ], 'body' => 'test', ]; $handler = new CurlMultiHandler(); $response = $handler($request)->wait(); $this->assertEquals(200, $response['status']); $this->assertEquals('OK', $response['reason']); $this->assertEquals(['Hello'], $response['headers']['Test']); $this->assertEquals(['4'], $response['headers']['Content-Length']); $this->assertEquals('test', Core::body($response)); } public function testCreatesConnectException() { $m = new \ReflectionMethod('GuzzleHttp\Ring\Client\CurlFactory', 'createErrorResponse'); $m->setAccessible(true); $response = $m->invoke( null, function () {}, [], [ 'err_message' => 'foo', 'curl' => [ 'errno' => CURLE_COULDNT_CONNECT, ] ] ); $this->assertInstanceOf('GuzzleHttp\Ring\Exception\ConnectException', $response['error']); } public function testParsesLastResponseOnly() { $response1 = [ 'status' => 301, 'headers' => [ 'Content-Length' => ['0'], 'Location' => ['/foo'] ] ]; $response2 = [ 'status' => 200, 'headers' => [ 'Content-Length' => ['0'], 'Foo' => ['bar'] ] ]; Server::flush(); Server::enqueue([$response1, $response2]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['Host' => [Server::$host]], 'client' => [ 'curl' => [ CURLOPT_FOLLOWLOCATION => true ] ] ])->wait(); $this->assertEquals(1, $response['transfer_stats']['redirect_count']); $this->assertEquals('http://127.0.0.1:8125/foo', $response['effective_url']); $this->assertEquals(['bar'], $response['headers']['Foo']); $this->assertEquals(200, $response['status']); $this->assertFalse(Core::hasHeader($response, 'Location')); } public function testMaintainsMultiHeaderOrder() { Server::flush(); Server::enqueue([ [ 'status' => 200, 'headers' => [ 'Content-Length' => ['0'], 'Foo' => ['a', 'b'], 'foo' => ['c', 'd'], ] ] ]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['Host' => [Server::$host]] ])->wait(); $this->assertEquals( ['a', 'b', 'c', 'd'], Core::headerLines($response, 'Foo') ); } /** * @expectedException \RuntimeException * @expectedExceptionMessage Directory /path/to/does/not does not exist for save_to value of /path/to/does/not/exist.txt */ public function testThrowsWhenDirNotFound() { $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$url]], 'client' => ['save_to' => '/path/to/does/not/exist.txt'], ]; $f = new CurlFactory(); $f($request); } } } RingPHP-1.1.0/tests/Client/CurlHandlerTest.php000066400000000000000000000052431252700054500211200ustar00rootroot00000000000000markTestSkipped('curl_reset() is not available'); } } protected function getHandler($factory = null, $options = []) { return new CurlHandler($options); } public function testCanSetMaxHandles() { $a = new CurlHandler(['max_handles' => 10]); $this->assertEquals(10, $this->readAttribute($a, 'maxHandles')); } public function testCreatesCurlErrors() { $handler = new CurlHandler(); $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => ['localhost:123']], 'client' => ['timeout' => 0.001, 'connect_timeout' => 0.001], ]); $this->assertNull($response['status']); $this->assertNull($response['reason']); $this->assertEquals([], $response['headers']); $this->assertInstanceOf( 'GuzzleHttp\Ring\Exception\RingException', $response['error'] ); $this->assertEquals( 1, preg_match('/^cURL error \d+: .*$/', $response['error']->getMessage()) ); } public function testReleasesAdditionalEasyHandles() { Server::flush(); $response = [ 'status' => 200, 'headers' => ['Content-Length' => [4]], 'body' => 'test', ]; Server::enqueue([$response, $response, $response, $response]); $a = new CurlHandler(['max_handles' => 2]); $fn = function () use (&$calls, $a, &$fn) { if (++$calls < 4) { $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['progress' => $fn], ]); } }; $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => [ 'progress' => $fn, ], ]; $a($request); $this->assertCount(2, $this->readAttribute($a, 'handles')); } public function testReusesHandles() { Server::flush(); $response = ['status' => 200]; Server::enqueue([$response, $response]); $a = new CurlHandler(); $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], ]; $a($request); $a($request); } } RingPHP-1.1.0/tests/Client/CurlMultiHandlerTest.php000066400000000000000000000126121252700054500221310ustar00rootroot00000000000000 200]]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], ]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $this->assertEquals(200, $response['status']); $this->assertArrayHasKey('transfer_stats', $response); $realUrl = trim($response['transfer_stats']['url'], '/'); $this->assertEquals(trim(Server::$url, '/'), $realUrl); $this->assertArrayHasKey('effective_url', $response); $this->assertEquals( trim(Server::$url, '/'), trim($response['effective_url'], '/') ); } public function testCreatesErrorResponses() { $url = 'http://localhost:123/'; $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => ['localhost:123']], ]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $this->assertNull($response['status']); $this->assertNull($response['reason']); $this->assertEquals([], $response['headers']); $this->assertArrayHasKey('error', $response); $this->assertContains('cURL error ', $response['error']->getMessage()); $this->assertArrayHasKey('transfer_stats', $response); $this->assertEquals( trim($url, '/'), trim($response['transfer_stats']['url'], '/') ); $this->assertArrayHasKey('effective_url', $response); $this->assertEquals( trim($url, '/'), trim($response['effective_url'], '/') ); } public function testSendsFuturesWhenDestructed() { Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], ]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $a->__destruct(); $this->assertEquals(200, $response['status']); } public function testCanSetMaxHandles() { $a = new CurlMultiHandler(['max_handles' => 2]); $this->assertEquals(2, $this->readAttribute($a, 'maxHandles')); } public function testCanSetSelectTimeout() { $a = new CurlMultiHandler(['select_timeout' => 2]); $this->assertEquals(2, $this->readAttribute($a, 'selectTimeout')); } public function testSendsFuturesWhenMaxHandlesIsReached() { $request = [ 'http_method' => 'PUT', 'headers' => ['host' => [Server::$host]], 'future' => 'lazy', // passing this to control the test ]; $response = ['status' => 200]; Server::flush(); Server::enqueue([$response, $response, $response]); $a = new CurlMultiHandler(['max_handles' => 3]); for ($i = 0; $i < 5; $i++) { $responses[] = $a($request); } $this->assertCount(3, Server::received()); $responses[3]->cancel(); $responses[4]->cancel(); } public function testCanCancel() { Server::flush(); $response = ['status' => 200]; Server::enqueue(array_fill_keys(range(0, 10), $response)); $a = new CurlMultiHandler(); $responses = []; for ($i = 0; $i < 10; $i++) { $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'future' => 'lazy', ]); $response->cancel(); $responses[] = $response; } $this->assertCount(0, Server::received()); foreach ($responses as $response) { $this->assertTrue($this->readAttribute($response, 'isRealized')); } } public function testCannotCancelFinished() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], ]); $response->wait(); $response->cancel(); } public function testDelaysInParallel() { Server::flush(); Server::enqueue([['status' => 200]]); $a = new CurlMultiHandler(); $expected = microtime(true) + (100 / 1000); $response = $a([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'client' => ['delay' => 100], ]); $response->wait(); $this->assertGreaterThanOrEqual($expected, microtime(true)); } public function testSendsNonLazyFutures() { $request = [ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'future' => true, ]; Server::flush(); Server::enqueue([['status' => 202]]); $a = new CurlMultiHandler(); $response = $a($request); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $this->assertEquals(202, $response['status']); } } RingPHP-1.1.0/tests/Client/MiddlewareTest.php000066400000000000000000000041631252700054500207720ustar00rootroot00000000000000 200]); $calledA = false; $a = function (array $req) use (&$calledA, $future) { $calledA = true; return $future; }; $calledB = false; $b = function (array $req) use (&$calledB) { $calledB = true; }; $s = Middleware::wrapFuture($a, $b); $s([]); $this->assertTrue($calledA); $this->assertFalse($calledB); } public function testFutureCallsStreamingHandler() { $future = new CompletedFutureArray(['status' => 200]); $calledA = false; $a = function (array $req) use (&$calledA) { $calledA = true; }; $calledB = false; $b = function (array $req) use (&$calledB, $future) { $calledB = true; return $future; }; $s = Middleware::wrapFuture($a, $b); $result = $s(['client' => ['future' => true]]); $this->assertFalse($calledA); $this->assertTrue($calledB); $this->assertSame($future, $result); } public function testStreamingCallsDefaultHandler() { $calledA = false; $a = function (array $req) use (&$calledA) { $calledA = true; }; $calledB = false; $b = function (array $req) use (&$calledB) { $calledB = true; }; $s = Middleware::wrapStreaming($a, $b); $s([]); $this->assertTrue($calledA); $this->assertFalse($calledB); } public function testStreamingCallsStreamingHandler() { $calledA = false; $a = function (array $req) use (&$calledA) { $calledA = true; }; $calledB = false; $b = function (array $req) use (&$calledB) { $calledB = true; }; $s = Middleware::wrapStreaming($a, $b); $s(['client' => ['stream' => true]]); $this->assertFalse($calledA); $this->assertTrue($calledB); } } RingPHP-1.1.0/tests/Client/MockHandlerTest.php000066400000000000000000000052661252700054500211110ustar00rootroot00000000000000 200]); $response = $mock([]); $this->assertEquals(200, $response['status']); $this->assertEquals([], $response['headers']); $this->assertNull($response['body']); $this->assertNull($response['reason']); $this->assertNull($response['effective_url']); } public function testReturnsFutures() { $deferred = new Deferred(); $future = new FutureArray( $deferred->promise(), function () use ($deferred) { $deferred->resolve(['status' => 200]); } ); $mock = new MockHandler($future); $response = $mock([]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $this->assertEquals(200, $response['status']); } public function testReturnsFuturesWithThenCall() { $deferred = new Deferred(); $future = new FutureArray( $deferred->promise(), function () use ($deferred) { $deferred->resolve(['status' => 200]); } ); $mock = new MockHandler($future); $response = $mock([]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $this->assertEquals(200, $response['status']); $req = null; $promise = $response->then(function ($value) use (&$req) { $req = $value; $this->assertEquals(200, $req['status']); }); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $this->assertEquals(200, $req['status']); } public function testReturnsFuturesAndProxiesCancel() { $c = null; $deferred = new Deferred(); $future = new FutureArray( $deferred->promise(), function () {}, function () use (&$c) { $c = true; return true; } ); $mock = new MockHandler($future); $response = $mock([]); $this->assertInstanceOf('GuzzleHttp\Ring\Future\FutureArray', $response); $response->cancel(); $this->assertTrue($c); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Response must be an array or FutureArrayInterface. Found */ public function testEnsuresMockIsValid() { $mock = new MockHandler('foo'); $mock([]); } } RingPHP-1.1.0/tests/Client/Server.php000066400000000000000000000117751252700054500173320ustar00rootroot00000000000000 [], 'reason' => '', 'body' => '']; $data[] = $response; } self::send('PUT', '/guzzle-server/responses', json_encode($data)); } /** * Get all of the received requests as a RingPHP request structure. * * @return array * @throws \RuntimeException */ public static function received() { if (!self::$started) { return []; } $response = self::send('GET', '/guzzle-server/requests'); $body = Core::body($response); $result = json_decode($body, true); if ($result === false) { throw new \RuntimeException('Error decoding response: ' . json_last_error()); } foreach ($result as &$res) { if (isset($res['uri'])) { $res['resource'] = $res['uri']; } if (isset($res['query_string'])) { $res['resource'] .= '?' . $res['query_string']; } if (!isset($res['resource'])) { $res['resource'] = ''; } // Ensure that headers are all arrays if (isset($res['headers'])) { foreach ($res['headers'] as &$h) { $h = (array) $h; } unset($h); } } unset($res); return $result; } /** * Stop running the node.js server */ public static function stop() { if (self::$started) { self::send('DELETE', '/guzzle-server'); } self::$started = false; } public static function wait($maxTries = 20) { $tries = 0; while (!self::isListening() && ++$tries < $maxTries) { usleep(100000); } if (!self::isListening()) { throw new \RuntimeException('Unable to contact node.js server'); } } public static function start() { if (self::$started) { return; } try { self::wait(); } catch (\Exception $e) { exec('node ' . __DIR__ . \DIRECTORY_SEPARATOR . 'server.js ' . self::$port . ' >> /tmp/server.log 2>&1 &'); self::wait(); } self::$started = true; } private static function isListening() { $response = self::send('GET', '/guzzle-server/perf', null, [ 'connect_timeout' => 1, 'timeout' => 1 ]); return !isset($response['error']); } private static function send( $method, $path, $body = null, array $client = [] ) { $handler = new StreamHandler(); $request = [ 'http_method' => $method, 'uri' => $path, 'request_port' => 8125, 'headers' => ['host' => ['127.0.0.1:8125']], 'body' => $body, 'client' => $client, ]; if ($body) { $request['headers']['content-length'] = [strlen($body)]; } return $handler($request); } } RingPHP-1.1.0/tests/Client/StreamHandlerTest.php000066400000000000000000000373761252700054500214620ustar00rootroot00000000000000queueRes(); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => [ 'host' => [Server::$host], 'Foo' => ['Bar'], ], ]); $this->assertEquals('1.1', $response['version']); $this->assertEquals(200, $response['status']); $this->assertEquals('OK', $response['reason']); $this->assertEquals(['Bar'], $response['headers']['Foo']); $this->assertEquals(['8'], $response['headers']['Content-Length']); $this->assertEquals('hi there', Core::body($response)); $sent = Server::received()[0]; $this->assertEquals('GET', $sent['http_method']); $this->assertEquals('/', $sent['resource']); $this->assertEquals(['127.0.0.1:8125'], $sent['headers']['host']); $this->assertEquals('Bar', Core::header($sent, 'foo')); } public function testAddsErrorToResponse() { $handler = new StreamHandler(); $result = $handler([ 'http_method' => 'GET', 'headers' => ['host' => ['localhost:123']], 'client' => ['timeout' => 0.01], ]); $this->assertInstanceOf( 'GuzzleHttp\Ring\Future\CompletedFutureArray', $result ); $this->assertNull($result['status']); $this->assertNull($result['body']); $this->assertEquals([], $result['headers']); $this->assertInstanceOf( 'GuzzleHttp\Ring\Exception\RingException', $result['error'] ); } public function testEnsuresTheHttpProtocol() { $handler = new StreamHandler(); $result = $handler([ 'http_method' => 'GET', 'url' => 'ftp://localhost:123', ]); $this->assertArrayHasKey('error', $result); $this->assertContains( 'URL is invalid: ftp://localhost:123', $result['error']->getMessage() ); } public function testStreamAttributeKeepsStreamOpen() { $this->queueRes(); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'PUT', 'uri' => '/foo', 'query_string' => 'baz=bar', 'headers' => [ 'host' => [Server::$host], 'Foo' => ['Bar'], ], 'body' => 'test', 'client' => ['stream' => true], ]); $this->assertEquals(200, $response['status']); $this->assertEquals('OK', $response['reason']); $this->assertEquals('8', Core::header($response, 'Content-Length')); $body = $response['body']; $this->assertTrue(is_resource($body)); $this->assertEquals('http', stream_get_meta_data($body)['wrapper_type']); $this->assertEquals('hi there', stream_get_contents($body)); fclose($body); $sent = Server::received()[0]; $this->assertEquals('PUT', $sent['http_method']); $this->assertEquals('/foo', $sent['uri']); $this->assertEquals('baz=bar', $sent['query_string']); $this->assertEquals('/foo?baz=bar', $sent['resource']); $this->assertEquals('127.0.0.1:8125', Core::header($sent, 'host')); $this->assertEquals('Bar', Core::header($sent, 'foo')); } public function testDrainsResponseIntoTempStream() { $this->queueRes(); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => [Server::$host]], ]); $body = $response['body']; $this->assertEquals('php://temp', stream_get_meta_data($body)['uri']); $this->assertEquals('hi', fread($body, 2)); fclose($body); } public function testDrainsResponseIntoSaveToBody() { $r = fopen('php://temp', 'r+'); $this->queueRes(); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => [Server::$host]], 'client' => ['save_to' => $r], ]); $body = $response['body']; $this->assertEquals('php://temp', stream_get_meta_data($body)['uri']); $this->assertEquals('hi', fread($body, 2)); $this->assertEquals(' there', stream_get_contents($r)); fclose($r); } public function testDrainsResponseIntoSaveToBodyAtPath() { $tmpfname = tempnam('/tmp', 'save_to_path'); $this->queueRes(); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => [Server::$host]], 'client' => ['save_to' => $tmpfname], ]); $body = $response['body']; $this->assertInstanceOf('GuzzleHttp\Stream\StreamInterface', $body); $this->assertEquals($tmpfname, $body->getMetadata('uri')); $this->assertEquals('hi', $body->read(2)); $body->close(); unlink($tmpfname); } public function testAutomaticallyDecompressGzip() { Server::flush(); $content = gzencode('test'); Server::enqueue([ [ 'status' => 200, 'reason' => 'OK', 'headers' => [ 'Content-Encoding' => ['gzip'], 'Content-Length' => [strlen($content)], ], 'body' => $content, ], ]); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'uri' => '/', 'client' => ['decode_content' => true], ]); $this->assertEquals('test', Core::body($response)); } public function testDoesNotForceGzipDecode() { Server::flush(); $content = gzencode('test'); Server::enqueue([ [ 'status' => 200, 'reason' => 'OK', 'headers' => [ 'Content-Encoding' => ['gzip'], 'Content-Length' => [strlen($content)], ], 'body' => $content, ], ]); $handler = new StreamHandler(); $response = $handler([ 'http_method' => 'GET', 'headers' => ['host' => [Server::$host]], 'uri' => '/', 'client' => ['stream' => true, 'decode_content' => false], ]); $this->assertSame($content, Core::body($response)); } public function testProtocolVersion() { $this->queueRes(); $handler = new StreamHandler(); $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => [Server::$host]], 'version' => 1.0, ]); $this->assertEquals(1.0, Server::received()[0]['version']); } protected function getSendResult(array $opts) { $this->queueRes(); $handler = new StreamHandler(); $opts['stream'] = true; return $handler([ 'http_method' => 'GET', 'uri' => '/', 'headers' => ['host' => [Server::$host]], 'client' => $opts, ]); } public function testAddsProxy() { $res = $this->getSendResult(['stream' => true, 'proxy' => '127.0.0.1:8125']); $opts = stream_context_get_options($res['body']); $this->assertEquals('127.0.0.1:8125', $opts['http']['proxy']); } public function testAddsTimeout() { $res = $this->getSendResult(['stream' => true, 'timeout' => 200]); $opts = stream_context_get_options($res['body']); $this->assertEquals(200, $opts['http']['timeout']); } public function testVerifiesVerifyIsValidIfPath() { $res = $this->getSendResult(['verify' => '/does/not/exist']); $this->assertContains( 'SSL CA bundle not found: /does/not/exist', (string) $res['error'] ); } public function testVerifyCanBeDisabled() { $res = $this->getSendResult(['verify' => false]); $this->assertArrayNotHasKey('error', $res); } public function testVerifiesCertIfValidPath() { $res = $this->getSendResult(['cert' => '/does/not/exist']); $this->assertContains( 'SSL certificate not found: /does/not/exist', (string) $res['error'] ); } public function testVerifyCanBeSetToPath() { $path = $path = ClientUtils::getDefaultCaBundle(); $res = $this->getSendResult(['verify' => $path]); $this->assertArrayNotHasKey('error', $res); $opts = stream_context_get_options($res['body']); $this->assertEquals(true, $opts['ssl']['verify_peer']); $this->assertEquals($path, $opts['ssl']['cafile']); $this->assertTrue(file_exists($opts['ssl']['cafile'])); } public function testUsesSystemDefaultBundle() { $path = $path = ClientUtils::getDefaultCaBundle(); $res = $this->getSendResult(['verify' => true]); $this->assertArrayNotHasKey('error', $res); $opts = stream_context_get_options($res['body']); if (PHP_VERSION_ID < 50600) { $this->assertEquals($path, $opts['ssl']['cafile']); } } public function testEnsuresVerifyOptionIsValid() { $res = $this->getSendResult(['verify' => 10]); $this->assertContains( 'Invalid verify request option', (string) $res['error'] ); } public function testCanSetPasswordWhenSettingCert() { $path = __FILE__; $res = $this->getSendResult(['cert' => [$path, 'foo']]); $opts = stream_context_get_options($res['body']); $this->assertEquals($path, $opts['ssl']['local_cert']); $this->assertEquals('foo', $opts['ssl']['passphrase']); } public function testDebugAttributeWritesToStream() { $this->queueRes(); $f = fopen('php://temp', 'w+'); $this->getSendResult(['debug' => $f]); fseek($f, 0); $contents = stream_get_contents($f); $this->assertContains(' [CONNECT]', $contents); $this->assertContains(' [FILE_SIZE_IS]', $contents); $this->assertContains(' [PROGRESS]', $contents); } public function testDebugAttributeWritesStreamInfoToBuffer() { $called = false; $this->queueRes(); $buffer = fopen('php://temp', 'r+'); $this->getSendResult([ 'progress' => function () use (&$called) { $called = true; }, 'debug' => $buffer, ]); fseek($buffer, 0); $contents = stream_get_contents($buffer); $this->assertContains(' [CONNECT]', $contents); $this->assertContains(' [FILE_SIZE_IS] message: "Content-Length: 8"', $contents); $this->assertContains(' [PROGRESS] bytes_max: "8"', $contents); $this->assertTrue($called); } public function testEmitsProgressInformation() { $called = []; $this->queueRes(); $this->getSendResult([ 'progress' => function () use (&$called) { $called[] = func_get_args(); }, ]); $this->assertNotEmpty($called); $this->assertEquals(8, $called[0][0]); $this->assertEquals(0, $called[0][1]); } public function testEmitsProgressInformationAndDebugInformation() { $called = []; $this->queueRes(); $buffer = fopen('php://memory', 'w+'); $this->getSendResult([ 'debug' => $buffer, 'progress' => function () use (&$called) { $called[] = func_get_args(); }, ]); $this->assertNotEmpty($called); $this->assertEquals(8, $called[0][0]); $this->assertEquals(0, $called[0][1]); rewind($buffer); $this->assertNotEmpty(stream_get_contents($buffer)); fclose($buffer); } public function testAddsProxyByProtocol() { $url = str_replace('http', 'tcp', Server::$url); $res = $this->getSendResult(['proxy' => ['http' => $url]]); $opts = stream_context_get_options($res['body']); $this->assertEquals($url, $opts['http']['proxy']); } public function testPerformsShallowMergeOfCustomContextOptions() { $res = $this->getSendResult([ 'stream_context' => [ 'http' => [ 'request_fulluri' => true, 'method' => 'HEAD', ], 'socket' => [ 'bindto' => '127.0.0.1:0', ], 'ssl' => [ 'verify_peer' => false, ], ], ]); $opts = stream_context_get_options($res['body']); $this->assertEquals('HEAD', $opts['http']['method']); $this->assertTrue($opts['http']['request_fulluri']); $this->assertFalse($opts['ssl']['verify_peer']); $this->assertEquals('127.0.0.1:0', $opts['socket']['bindto']); } public function testEnsuresThatStreamContextIsAnArray() { $res = $this->getSendResult(['stream_context' => 'foo']); $this->assertContains( 'stream_context must be an array', (string) $res['error'] ); } public function testDoesNotAddContentTypeByDefault() { $this->queueRes(); $handler = new StreamHandler(); $handler([ 'http_method' => 'PUT', 'uri' => '/', 'headers' => ['host' => [Server::$host], 'content-length' => [3]], 'body' => 'foo', ]); $req = Server::received()[0]; $this->assertEquals('', Core::header($req, 'Content-Type')); $this->assertEquals(3, Core::header($req, 'Content-Length')); } private function queueRes() { Server::flush(); Server::enqueue([ [ 'status' => 200, 'reason' => 'OK', 'headers' => [ 'Foo' => ['Bar'], 'Content-Length' => [8], ], 'body' => 'hi there', ], ]); } public function testSupports100Continue() { Server::flush(); Server::enqueue([ [ 'status' => '200', 'reason' => 'OK', 'headers' => [ 'Test' => ['Hello'], 'Content-Length' => ['4'], ], 'body' => 'test', ], ]); $request = [ 'http_method' => 'PUT', 'headers' => [ 'Host' => [Server::$host], 'Expect' => ['100-Continue'], ], 'body' => 'test', ]; $handler = new StreamHandler(); $response = $handler($request); $this->assertEquals(200, $response['status']); $this->assertEquals('OK', $response['reason']); $this->assertEquals(['Hello'], $response['headers']['Test']); $this->assertEquals(['4'], $response['headers']['Content-Length']); $this->assertEquals('test', Core::body($response)); } } RingPHP-1.1.0/tests/Client/server.js000066400000000000000000000202471252700054500172110ustar00rootroot00000000000000/** * Guzzle node.js test server to return queued responses to HTTP requests and * expose a RESTful API for enqueueing responses and retrieving the requests * that have been received. * * - Delete all requests that have been received: * > DELETE /guzzle-server/requests * > Host: 127.0.0.1:8125 * * - Enqueue responses * > PUT /guzzle-server/responses * > Host: 127.0.0.1:8125 * > * > [{'status': 200, 'reason': 'OK', 'headers': {}, 'body': '' }] * * - Get the received requests * > GET /guzzle-server/requests * > Host: 127.0.0.1:8125 * * < HTTP/1.1 200 OK * < * < [{'http_method': 'GET', 'uri': '/', 'headers': {}, 'body': 'string'}] * * - Attempt access to the secure area * > GET /secure/by-digest/qop-auth/guzzle-server/requests * > Host: 127.0.0.1:8125 * * < HTTP/1.1 401 Unauthorized * < WWW-Authenticate: Digest realm="Digest Test", qop="auth", nonce="0796e98e1aeef43141fab2a66bf4521a", algorithm="MD5", stale="false" * < * < 401 Unauthorized * * - Shutdown the server * > DELETE /guzzle-server * > Host: 127.0.0.1:8125 * * @package Guzzle PHP * @license See the LICENSE file that was distributed with this source code. */ var http = require('http'); var url = require('url'); /** * Guzzle node.js server * @class */ var GuzzleServer = function(port, log) { this.port = port; this.log = log; this.responses = []; this.requests = []; var that = this; var md5 = function(input) { var crypto = require('crypto'); var hasher = crypto.createHash('md5'); hasher.update(input); return hasher.digest('hex'); } /** * Node.js HTTP server authentication module. * * It is only initialized on demand (by loadAuthentifier). This avoids * requiring the dependency to http-auth on standard operations, and the * performance hit at startup. */ var auth; /** * Provides authentication handlers (Basic, Digest). */ var loadAuthentifier = function(type, options) { var typeId = type; if (type == 'digest') { typeId += '.'+(options && options.qop ? options.qop : 'none'); } if (!loadAuthentifier[typeId]) { if (!auth) { try { auth = require('http-auth'); } catch (e) { if (e.code == 'MODULE_NOT_FOUND') { return; } } } switch (type) { case 'digest': var digestParams = { realm: 'Digest Test', login: 'me', password: 'test' }; if (options && options.qop) { digestParams.qop = options.qop; } loadAuthentifier[typeId] = auth.digest(digestParams, function(username, callback) { callback(md5(digestParams.login + ':' + digestParams.realm + ':' + digestParams.password)); }); break } } return loadAuthentifier[typeId]; }; var firewallRequest = function(request, req, res, requestHandlerCallback) { var securedAreaUriParts = request.uri.match(/^\/secure\/by-(digest)(\/qop-([^\/]*))?(\/.*)$/); if (securedAreaUriParts) { var authentifier = loadAuthentifier(securedAreaUriParts[1], { qop: securedAreaUriParts[2] }); if (!authentifier) { res.writeHead(501, 'HTTP authentication not implemented', { 'Content-Length': 0 }); res.end(); return; } authentifier.check(req, res, function(req, res) { req.url = securedAreaUriParts[4]; requestHandlerCallback(request, req, res); }); } else { requestHandlerCallback(request, req, res); } }; var controlRequest = function(request, req, res) { if (req.url == '/guzzle-server/perf') { res.writeHead(200, 'OK', {'Content-Length': 16}); res.end('Body of response'); } else if (req.method == 'DELETE') { if (req.url == '/guzzle-server/requests') { // Clear the received requests that.requests = []; res.writeHead(200, 'OK', { 'Content-Length': 0 }); res.end(); if (that.log) { console.log('Flushing requests'); } } else if (req.url == '/guzzle-server') { // Shutdown the server res.writeHead(200, 'OK', { 'Content-Length': 0, 'Connection': 'close' }); res.end(); if (that.log) { console.log('Shutting down'); } that.server.close(); } } else if (req.method == 'GET') { if (req.url === '/guzzle-server/requests') { if (that.log) { console.log('Sending received requests'); } // Get received requests var body = JSON.stringify(that.requests); res.writeHead(200, 'OK', { 'Content-Length': body.length }); res.end(body); } } else if (req.method == 'PUT' && req.url == '/guzzle-server/responses') { if (that.log) { console.log('Adding responses...'); } if (!request.body) { if (that.log) { console.log('No response data was provided'); } res.writeHead(400, 'NO RESPONSES IN REQUEST', { 'Content-Length': 0 }); } else { that.responses = eval('(' + request.body + ')'); for (var i = 0; i < that.responses.length; i++) { if (that.responses[i].body) { that.responses[i].body = new Buffer(that.responses[i].body, 'base64'); } } if (that.log) { console.log(that.responses); } res.writeHead(200, 'OK', { 'Content-Length': 0 }); } res.end(); } }; var receivedRequest = function(request, req, res) { if (req.url.indexOf('/guzzle-server') === 0) { controlRequest(request, req, res); } else if (req.url.indexOf('/guzzle-server') == -1 && !that.responses.length) { res.writeHead(500); res.end('No responses in queue'); } else { if (that.log) { console.log('Returning response from queue and adding request'); } that.requests.push(request); var response = that.responses.shift(); res.writeHead(response.status, response.reason, response.headers); res.end(response.body); } }; this.start = function() { that.server = http.createServer(function(req, res) { var parts = url.parse(req.url, false); var request = { http_method: req.method, scheme: parts.scheme, uri: parts.pathname, query_string: parts.query, headers: req.headers, version: req.httpVersion, body: '' }; // Receive each chunk of the request body req.addListener('data', function(chunk) { request.body += chunk; }); // Called when the request completes req.addListener('end', function() { firewallRequest(request, req, res, receivedRequest); }); }); that.server.listen(this.port, '127.0.0.1'); if (this.log) { console.log('Server running at http://127.0.0.1:8125/'); } }; }; // Get the port from the arguments port = process.argv.length >= 3 ? process.argv[2] : 8125; log = process.argv.length >= 4 ? process.argv[3] : false; // Start the server server = new GuzzleServer(port, log); server.start(); RingPHP-1.1.0/tests/CoreTest.php000066400000000000000000000215121252700054500163640ustar00rootroot00000000000000assertNull(Core::header([], 'Foo')); $this->assertNull(Core::firstHeader([], 'Foo')); } public function testChecksIfHasHeader() { $message = [ 'headers' => [ 'Foo' => ['Bar', 'Baz'], 'foo' => ['hello'], 'bar' => ['1'] ] ]; $this->assertTrue(Core::hasHeader($message, 'Foo')); $this->assertTrue(Core::hasHeader($message, 'foo')); $this->assertTrue(Core::hasHeader($message, 'FoO')); $this->assertTrue(Core::hasHeader($message, 'bar')); $this->assertFalse(Core::hasHeader($message, 'barr')); } public function testReturnsFirstHeaderWhenSimple() { $this->assertEquals('Bar', Core::firstHeader([ 'headers' => ['Foo' => ['Bar', 'Baz']], ], 'Foo')); } public function testReturnsFirstHeaderWhenMultiplePerLine() { $this->assertEquals('Bar', Core::firstHeader([ 'headers' => ['Foo' => ['Bar, Baz']], ], 'Foo')); } public function testExtractsCaseInsensitiveHeader() { $this->assertEquals( 'hello', Core::header(['headers' => ['foo' => ['hello']]], 'FoO') ); } public function testExtractsCaseInsensitiveHeaderLines() { $this->assertEquals( ['a', 'b', 'c', 'd'], Core::headerLines([ 'headers' => [ 'foo' => ['a', 'b'], 'Foo' => ['c', 'd'] ] ], 'foo') ); } public function testExtractsHeaderLines() { $this->assertEquals( ['bar', 'baz'], Core::headerLines([ 'headers' => [ 'Foo' => ['bar', 'baz'], ], ], 'Foo') ); } public function testExtractsHeaderAsString() { $this->assertEquals( 'bar, baz', Core::header([ 'headers' => [ 'Foo' => ['bar', 'baz'], ], ], 'Foo', true) ); } public function testReturnsNullWhenHeaderNotFound() { $this->assertNull(Core::header(['headers' => []], 'Foo')); } public function testRemovesHeaders() { $message = [ 'headers' => [ 'foo' => ['bar'], 'Foo' => ['bam'], 'baz' => ['123'], ], ]; $this->assertSame($message, Core::removeHeader($message, 'bam')); $this->assertEquals([ 'headers' => ['baz' => ['123']], ], Core::removeHeader($message, 'foo')); } public function testCreatesUrl() { $req = [ 'scheme' => 'http', 'headers' => ['host' => ['foo.com']], 'uri' => '/', ]; $this->assertEquals('http://foo.com/', Core::url($req)); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage No Host header was provided */ public function testEnsuresHostIsAvailableWhenCreatingUrls() { Core::url([]); } public function testCreatesUrlWithQueryString() { $req = [ 'scheme' => 'http', 'headers' => ['host' => ['foo.com']], 'uri' => '/', 'query_string' => 'foo=baz', ]; $this->assertEquals('http://foo.com/?foo=baz', Core::url($req)); } public function testUsesUrlIfSet() { $req = ['url' => 'http://foo.com']; $this->assertEquals('http://foo.com', Core::url($req)); } public function testReturnsNullWhenNoBody() { $this->assertNull(Core::body([])); } public function testReturnsStreamAsString() { $this->assertEquals( 'foo', Core::body(['body' => Stream::factory('foo')]) ); } public function testReturnsString() { $this->assertEquals('foo', Core::body(['body' => 'foo'])); } public function testReturnsResourceContent() { $r = fopen('php://memory', 'w+'); fwrite($r, 'foo'); rewind($r); $this->assertEquals('foo', Core::body(['body' => $r])); fclose($r); } public function testReturnsIteratorContent() { $a = new \ArrayIterator(['a', 'b', 'cd', '']); $this->assertEquals('abcd', Core::body(['body' => $a])); } public function testReturnsObjectToString() { $this->assertEquals('foo', Core::body(['body' => new StrClass])); } /** * @expectedException \InvalidArgumentException */ public function testEnsuresBodyIsValid() { Core::body(['body' => false]); } public function testParsesHeadersFromLines() { $lines = ['Foo: bar', 'Foo: baz', 'Abc: 123', 'Def: a, b']; $this->assertEquals([ 'Foo' => ['bar', 'baz'], 'Abc' => ['123'], 'Def' => ['a, b'], ], Core::headersFromLines($lines)); } public function testParsesHeadersFromLinesWithMultipleLines() { $lines = ['Foo: bar', 'Foo: baz', 'Foo: 123']; $this->assertEquals([ 'Foo' => ['bar', 'baz', '123'], ], Core::headersFromLines($lines)); } public function testCreatesArrayCallFunctions() { $called = []; $a = function ($a, $b) use (&$called) { $called['a'] = func_get_args(); }; $b = function ($a, $b) use (&$called) { $called['b'] = func_get_args(); }; $c = Core::callArray([$a, $b]); $c(1, 2); $this->assertEquals([1, 2], $called['a']); $this->assertEquals([1, 2], $called['b']); } public function testRewindsGuzzleStreams() { $str = Stream::factory('foo'); $this->assertTrue(Core::rewindBody(['body' => $str])); } public function testRewindsStreams() { $str = Stream::factory('foo')->detach(); $this->assertTrue(Core::rewindBody(['body' => $str])); } public function testRewindsIterators() { $iter = new \ArrayIterator(['foo']); $this->assertTrue(Core::rewindBody(['body' => $iter])); } public function testRewindsStrings() { $this->assertTrue(Core::rewindBody(['body' => 'hi'])); } public function testRewindsToStrings() { $this->assertTrue(Core::rewindBody(['body' => new StrClass()])); } public function typeProvider() { return [ ['foo', 'string(3) "foo"'], [true, 'bool(true)'], [false, 'bool(false)'], [10, 'int(10)'], [1.0, 'float(1)'], [new StrClass(), 'object(GuzzleHttp\Tests\Ring\StrClass)'], [['foo'], 'array(1)'] ]; } /** * @dataProvider typeProvider */ public function testDescribesType($input, $output) { $this->assertEquals($output, Core::describeType($input)); } public function testDoesSleep() { $t = microtime(true); $expected = $t + (100 / 1000); Core::doSleep(['client' => ['delay' => 100]]); $this->assertGreaterThanOrEqual($expected, microtime(true)); } public function testProxiesFuture() { $f = new CompletedFutureArray(['status' => 200]); $res = null; $proxied = Core::proxy($f, function ($value) use (&$res) { $value['foo'] = 'bar'; $res = $value; return $value; }); $this->assertNotSame($f, $proxied); $this->assertEquals(200, $f->wait()['status']); $this->assertArrayNotHasKey('foo', $f->wait()); $this->assertEquals('bar', $proxied->wait()['foo']); $this->assertEquals(200, $proxied->wait()['status']); } public function testProxiesDeferredFuture() { $d = new Deferred(); $f = new FutureArray($d->promise()); $f2 = Core::proxy($f); $d->resolve(['foo' => 'bar']); $this->assertEquals('bar', $f['foo']); $this->assertEquals('bar', $f2['foo']); } public function testProxiesDeferredFutureFailure() { $d = new Deferred(); $f = new FutureArray($d->promise()); $f2 = Core::proxy($f); $d->reject(new \Exception('foo')); try { $f2['hello?']; $this->fail('did not throw'); } catch (\Exception $e) { $this->assertEquals('foo', $e->getMessage()); } } } final class StrClass { public function __toString() { return 'foo'; } } RingPHP-1.1.0/tests/Future/000077500000000000000000000000001252700054500153745ustar00rootroot00000000000000RingPHP-1.1.0/tests/Future/CompletedFutureArrayTest.php000066400000000000000000000013071252700054500230540ustar00rootroot00000000000000 'bar']); $this->assertEquals('bar', $f['foo']); $this->assertFalse(isset($f['baz'])); $f['abc'] = '123'; $this->assertTrue(isset($f['abc'])); $this->assertEquals(['foo' => 'bar', 'abc' => '123'], iterator_to_array($f)); $this->assertEquals(2, count($f)); unset($f['abc']); $this->assertEquals(1, count($f)); $this->assertEquals(['foo' => 'bar'], iterator_to_array($f)); } } RingPHP-1.1.0/tests/Future/CompletedFutureValueTest.php000066400000000000000000000022211252700054500230460ustar00rootroot00000000000000assertEquals('hi', $f->wait()); $f->cancel(); $a = null; $f->then(function ($v) use (&$a) { $a = $v; }); $this->assertSame('hi', $a); } public function testThrows() { $ex = new \Exception('foo'); $f = new CompletedFutureValue(null, $ex); $f->cancel(); try { $f->wait(); $this->fail('did not throw'); } catch (\Exception $e) { $this->assertSame($e, $ex); } } public function testMarksAsCancelled() { $ex = new CancelledFutureAccessException(); $f = new CompletedFutureValue(null, $ex); try { $f->wait(); $this->fail('did not throw'); } catch (\Exception $e) { $this->assertSame($e, $ex); } } } RingPHP-1.1.0/tests/Future/FutureArrayTest.php000066400000000000000000000031401252700054500212140ustar00rootroot00000000000000promise(), function () use (&$c, $deferred) { $c = true; $deferred->resolve(['status' => 200]); } ); $this->assertFalse($c); $this->assertFalse($this->readAttribute($f, 'isRealized')); $this->assertEquals(200, $f['status']); $this->assertTrue($c); } public function testActsLikeArray() { $deferred = new Deferred(); $f = new FutureArray( $deferred->promise(), function () use (&$c, $deferred) { $deferred->resolve(['status' => 200]); } ); $this->assertTrue(isset($f['status'])); $this->assertEquals(200, $f['status']); $this->assertEquals(['status' => 200], $f->wait()); $this->assertEquals(1, count($f)); $f['baz'] = 10; $this->assertEquals(10, $f['baz']); unset($f['baz']); $this->assertFalse(isset($f['baz'])); $this->assertEquals(['status' => 200], iterator_to_array($f)); } /** * @expectedException \RuntimeException */ public function testThrowsWhenAccessingInvalidProperty() { $deferred = new Deferred(); $f = new FutureArray($deferred->promise(), function () {}); $f->foo; } } RingPHP-1.1.0/tests/Future/FutureValueTest.php000066400000000000000000000055461252700054500212260ustar00rootroot00000000000000promise(), function () use ($deferred, &$called) { $called++; $deferred->resolve('foo'); } ); $this->assertEquals('foo', $f->wait()); $this->assertEquals(1, $called); $this->assertEquals('foo', $f->wait()); $this->assertEquals(1, $called); $f->cancel(); $this->assertTrue($this->readAttribute($f, 'isRealized')); } /** * @expectedException \GuzzleHttp\Ring\Exception\CancelledFutureAccessException */ public function testThrowsWhenAccessingCancelled() { $f = new FutureValue( (new Deferred())->promise(), function () {}, function () { return true; } ); $f->cancel(); $f->wait(); } /** * @expectedException \OutOfBoundsException */ public function testThrowsWhenDerefFailure() { $called = false; $deferred = new Deferred(); $f = new FutureValue( $deferred->promise(), function () use(&$called) { $called = true; } ); $deferred->reject(new \OutOfBoundsException()); $f->wait(); $this->assertFalse($called); } /** * @expectedException \GuzzleHttp\Ring\Exception\RingException * @expectedExceptionMessage Waiting did not resolve future */ public function testThrowsWhenDerefDoesNotResolve() { $deferred = new Deferred(); $f = new FutureValue( $deferred->promise(), function () use(&$called) { $called = true; } ); $f->wait(); } public function testThrowingCancelledFutureAccessExceptionCancels() { $deferred = new Deferred(); $f = new FutureValue( $deferred->promise(), function () use ($deferred) { throw new CancelledFutureAccessException(); } ); try { $f->wait(); $this->fail('did not throw'); } catch (CancelledFutureAccessException $e) {} } /** * @expectedException \Exception * @expectedExceptionMessage foo */ public function testThrowingExceptionInDerefMarksAsFailed() { $deferred = new Deferred(); $f = new FutureValue( $deferred->promise(), function () { throw new \Exception('foo'); } ); $f->wait(); } } RingPHP-1.1.0/tests/bootstrap.php000066400000000000000000000003311252700054500166450ustar00rootroot00000000000000