generated from Nodarx/template
Initial service DAV module
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
name: continuous-integration
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release/*
|
||||
pull_request:
|
||||
jobs:
|
||||
unit-testing:
|
||||
name: PHPUnit (PHP ${{ matrix.php-versions }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
coverage: ['xdebug']
|
||||
code-style: ['yes']
|
||||
code-analysis: ['no']
|
||||
include:
|
||||
- php-versions: '7.1'
|
||||
code-style: 'yes'
|
||||
code-analysis: 'yes'
|
||||
- php-versions: '8.4'
|
||||
code-style: 'yes'
|
||||
code-analysis: 'yes'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup PHP, with composer and extensions
|
||||
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ctype, curl, mbstring, xdebug
|
||||
coverage: ${{ matrix.coverage }}
|
||||
tools: composer
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --no-progress --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Code Analysis (PHP CS-Fixer)
|
||||
if: matrix.code-style == 'yes'
|
||||
run: PHP_CS_FIXER_IGNORE_ENV=true php vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
|
||||
- name: Code Analysis (PHPStan)
|
||||
if: matrix.code-analysis == 'yes'
|
||||
run: composer phpstan
|
||||
|
||||
- name: Run application server
|
||||
run: php -S localhost:8000 -t tests/www 2>/dev/null &
|
||||
|
||||
- name: Test with phpunit
|
||||
run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
if: matrix.coverage != 'none'
|
||||
@@ -0,0 +1,9 @@
|
||||
# Composer
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# Tests
|
||||
tests/cov/
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->exclude('vendor')
|
||||
->in(__DIR__);
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
$config->setRules([
|
||||
'@PSR1' => true,
|
||||
'@Symfony' => true,
|
||||
'nullable_type_declaration' => [
|
||||
'syntax' => 'question_mark',
|
||||
],
|
||||
'nullable_type_declaration_for_default_null_value' => true,
|
||||
]);
|
||||
$config->setFinder($finder);
|
||||
return $config;
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
$config = PhpCsFixer\Config::create();
|
||||
$config->getFinder()
|
||||
->exclude('vendor')
|
||||
->in(__DIR__);
|
||||
$config->setRules([
|
||||
'@PSR1' => true,
|
||||
'@Symfony' => true
|
||||
]);
|
||||
|
||||
return $config;
|
||||
Vendored
+403
@@ -0,0 +1,403 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
5.1.13 (2025-09-09)
|
||||
------------------
|
||||
|
||||
* #262 properly compute absolute URL (@skjnldsv)
|
||||
|
||||
5.1.12 (2024-08-27)
|
||||
------------------
|
||||
|
||||
* #243 add cs-fixer v3 (@phil-davis)
|
||||
|
||||
5.1.11 (2024-07-26)
|
||||
------------------
|
||||
|
||||
* #241 PHP 8.4 compliance (@phil-davis)
|
||||
|
||||
5.1.10 (2023-08-18)
|
||||
------------------
|
||||
|
||||
* #225 Enhance tests/bootstrap.php to find autoloader in more environments (@phil-davis)
|
||||
|
||||
5.1.9 (2023-08-17)
|
||||
------------------
|
||||
|
||||
* #223 skip testParseMimeTypeOnInvalidMimeType (@phil-davis)
|
||||
|
||||
5.1.8 (2023-08-17)
|
||||
------------------
|
||||
|
||||
* #215 Improve CURLOPT_HTTPHEADER Setting Assignment (@amrita-shrestha)
|
||||
|
||||
5.1.7 (2023-06-26)
|
||||
------------------
|
||||
|
||||
* #98 and #176 Add more tests (@peter279k)
|
||||
* #207 fix: handle client disconnect properly with ignore_user_abort true (@kesselb)
|
||||
|
||||
5.1.6 (2022-07-15)
|
||||
------------------
|
||||
|
||||
* #187 Allow testSendToGetLargeContent peak memory usage to be specified externally (@phil-davis)
|
||||
* #188 Fix various small typos and grammar (@phil-davis)
|
||||
* #189 Fix typo in text of status code 203 'Non-Authoritative Information' (@phil-davis)
|
||||
|
||||
5.1.5 (2022-07-09)
|
||||
------------------
|
||||
|
||||
* #184 Remove 4GB file size workaround for 32bit OS / Stream Videos on IOS (@schoetju)
|
||||
|
||||
5.1.4 (2022-06-24)
|
||||
------------------
|
||||
|
||||
* #182 Fix encoding detection on PHP 8.1 (@come-nc)
|
||||
|
||||
5.1.3 (2021-11-04)
|
||||
------------------
|
||||
|
||||
* #179 version bump that was missed in 5.1.2 (@phil-davis)
|
||||
|
||||
5.1.2 (2021-11-04)
|
||||
-------------------------
|
||||
|
||||
* #169 Ensure $_SERVER keys are read as strings (@fredrik-eriksson)
|
||||
* #170 Fix deprecated usages on PHP 8.1 (@cedric-anne)
|
||||
* #175 Add resource size to CURL options in client (from #172 ) (@Dartui)
|
||||
|
||||
5.1.1 (2020-10-03)
|
||||
-------------------------
|
||||
|
||||
* #160: Added support for PHP 8.0 (@phil-davis)
|
||||
|
||||
5.1.0 (2020-01-31)
|
||||
-------------------------
|
||||
|
||||
* Added support for PHP 7.4, dropped support for PHP 7.0 (@phil-davis)
|
||||
* Updated testsuite for phpunit8, added phpstan coverage (@phil-davis)
|
||||
* Added autoload-dev for test classes (@C0pyR1ght)
|
||||
|
||||
5.0.5 (2019-11-28)
|
||||
-------------------------
|
||||
|
||||
* #138: Fixed possible infinite loop (@dpakach, @vfreex, @phil-davis)
|
||||
* #136: Improvement regex content-range (@ho4ho)
|
||||
|
||||
5.0.4 (2019-10-08)
|
||||
-------------------------
|
||||
|
||||
* #133: Fix short Content-Range download - Regression from 5.0.3 (@phil-davis)
|
||||
|
||||
5.0.3 (2019-10-08)
|
||||
-------------------------
|
||||
|
||||
* #119: Significantly improve file download speed by enabling mmap based stream_copy_to_stream (@vfreex)
|
||||
|
||||
5.0.2 (2019-09-12)
|
||||
-------------------------
|
||||
|
||||
* #125: Fix Strict Error if Response Body Empty (@WorksDev, @phil-davis)
|
||||
|
||||
5.0.1 (2019-09-11)
|
||||
-------------------------
|
||||
|
||||
* #121: fix "Trying to access array offset on value of type bool" in 7.4 (@remicollet)
|
||||
* #115: Reduce memory footprint when parsing HTTP result (@Gasol)
|
||||
* #114: Misc code improvements (@mrcnpdlk)
|
||||
* #111, #118: Added phpstan analysis (@DeepDiver1975, @staabm)
|
||||
* #107: Tested with php 7.3 (@DeepDiver1975)
|
||||
|
||||
|
||||
5.0.0 (2018-06-04)
|
||||
-------------------------
|
||||
|
||||
* #99: Previous CURL opts are not persisted anymore (@christiaan)
|
||||
* Final release
|
||||
|
||||
5.0.0-alpha1 (2018-02-16)
|
||||
-------------------------
|
||||
|
||||
* Now requires PHP 7.0+.
|
||||
* Supports sabre/event 4.x and 5.x
|
||||
* Depends on sabre/uri 2.
|
||||
* hhvm is no longer supported starting this release.
|
||||
* #65: It's now possible to supply request/response bodies using a callback
|
||||
functions. This allows very high-speed/low-memory responses to be created.
|
||||
(@petrkotek).
|
||||
* Strict typing is used everywhere this is applicable.
|
||||
* Removed `URLUtil` class. It was deprecated a long time ago, and most of
|
||||
its functions moved to the `sabre/uri` package.
|
||||
* Removed `Util` class. Most of its functions moved to the `functions.php`
|
||||
file.
|
||||
* #68: The `$method` and `$uri` arguments when constructing a Request object
|
||||
are now required.
|
||||
* When `Sapi::getRequest()` is called, we default to setting the HTTP Method
|
||||
to `CLI`.
|
||||
* The HTTP response is now initialized with HTTP code `500` instead of `null`,
|
||||
so if it's not changed, it will be emitted as 500.
|
||||
* #69: Sending `charset="UTF-8"` on Basic authentication challenges per
|
||||
[rfc7617][rfc7617].
|
||||
* #84: Added support for `SERVER_PROTOCOL HTTP/2.0` (@jens1o)
|
||||
|
||||
|
||||
4.2.3 (2017-06-12)
|
||||
------------------
|
||||
|
||||
* #74, #77: Work around 4GB file size limit at 32-Bit systems
|
||||
|
||||
|
||||
4.2.2 (2017-01-02)
|
||||
------------------
|
||||
|
||||
* #72: Handling clients that send invalid `Content-Length` headers.
|
||||
|
||||
|
||||
4.2.1 (2016-01-06)
|
||||
------------------
|
||||
|
||||
* #56: `getBodyAsString` now returns at most as many bytes as the contents of
|
||||
the `Content-Length` header. This allows users to pass much larger strings
|
||||
without having to copy and truncate them.
|
||||
* The client now sets a default `User-Agent` header identifying this library.
|
||||
|
||||
|
||||
4.2.0 (2016-01-04)
|
||||
------------------
|
||||
|
||||
* This package now supports sabre/event 3.0.
|
||||
|
||||
|
||||
4.1.0 (2015-09-04)
|
||||
------------------
|
||||
|
||||
* The async client wouldn't `wait()` for new http requests being started
|
||||
after the (previous) last request in the queue was resolved.
|
||||
* Added `Sabre\HTTP\Auth\Bearer`, to easily extract a OAuth2 bearer token.
|
||||
|
||||
|
||||
4.0.0 (2015-05-20)
|
||||
------------------
|
||||
|
||||
* Deprecated: All static functions from `Sabre\HTTP\URLUtil` and
|
||||
`Sabre\HTTP\Util` moved to a separate `functions.php`, which is also
|
||||
autoloaded. The old functions are still there, but will be removed in a
|
||||
future version. (#49)
|
||||
|
||||
|
||||
4.0.0-alpha3 (2015-05-19)
|
||||
-------------------------
|
||||
|
||||
* Added a parser for the HTTP `Prefer` header, as defined in [RFC7240][rfc7240].
|
||||
* Deprecated `Sabre\HTTP\Util::parseHTTPDate`, use `Sabre\HTTP\parseDate()`.
|
||||
* Deprecated `Sabre\HTTP\Util::toHTTPDate` use `Sabre\HTTP\toDate()`.
|
||||
|
||||
|
||||
4.0.0-alpha2 (2015-05-18)
|
||||
-------------------------
|
||||
|
||||
* #45: Don't send more data than what is promised in the HTTP content-length.
|
||||
(@dratini0).
|
||||
* #43: `getCredentials` returns null if incomplete. (@Hywan)
|
||||
* #48: Now using php-cs-fixer to make our CS consistent (yay!)
|
||||
* This includes fixes released in version 3.0.5.
|
||||
|
||||
|
||||
4.0.0-alpha1 (2015-02-25)
|
||||
-------------------------
|
||||
|
||||
* #41: Fixing bugs related to comparing URLs in `Request::getPath()`.
|
||||
* #41: This library now uses the `sabre/uri` package for uri handling.
|
||||
* Added `421 Misdirected Request` from the HTTP/2.0 spec.
|
||||
|
||||
|
||||
3.0.5 (2015-05-11)
|
||||
------------------
|
||||
|
||||
* #47 #35: When re-using the client and doing any request after a `HEAD`
|
||||
request, the client discards the body.
|
||||
|
||||
|
||||
3.0.4 (2014-12-10)
|
||||
------------------
|
||||
|
||||
* #38: The Authentication helpers no longer overwrite any existing
|
||||
`WWW-Authenticate` headers, but instead append new headers. This ensures
|
||||
that multiple authentication systems can exist in the same environment.
|
||||
|
||||
|
||||
3.0.3 (2014-12-03)
|
||||
------------------
|
||||
|
||||
* Hiding `Authorization` header value from `Request::__toString`.
|
||||
|
||||
|
||||
3.0.2 (2014-10-09)
|
||||
------------------
|
||||
|
||||
* When parsing `Accept:` headers, we're ignoring invalid parts. Before we
|
||||
would throw a PHP E_NOTICE.
|
||||
|
||||
|
||||
3.0.1 (2014-09-29)
|
||||
------------------
|
||||
|
||||
* Minor change in unittests.
|
||||
|
||||
|
||||
3.0.0 (2014-09-23)
|
||||
------------------
|
||||
|
||||
* `getHeaders()` now returns header values as an array, just like psr/http.
|
||||
* Added `hasHeader()`.
|
||||
|
||||
|
||||
2.1.0-alpha1 (2014-09-15)
|
||||
-------------------------
|
||||
|
||||
* Changed: Copied most of the header-semantics for the PSR draft for
|
||||
representing HTTP messages. [Reference here][psr-http].
|
||||
* This means that `setHeaders()` does not wipe out every existing header
|
||||
anymore.
|
||||
* We also support multiple headers with the same name.
|
||||
* Use `Request::getHeaderAsArray()` and `Response::getHeaderAsArray()` to
|
||||
get a hold off multiple headers with the same name.
|
||||
* If you use `getHeader()`, and there's more than 1 header with that name, we
|
||||
concatenate all these with a comma.
|
||||
* `addHeader()` will now preserve an existing header with that name, and add a
|
||||
second header with the same name.
|
||||
* The message class should be a lot faster now for looking up headers. No more
|
||||
array traversal, because we maintain a tiny index.
|
||||
* Added: `URLUtil::resolve()` to make resolving relative urls super easy.
|
||||
* Switched to PSR-4.
|
||||
* #12: Circumventing CURL's FOLLOW_LOCATION and doing it in PHP instead. This
|
||||
fixes compatibility issues with people that have open_basedir turned on.
|
||||
* Added: Content negotiation now correctly support mime-type parameters such as
|
||||
charset.
|
||||
* Changed: `Util::negotiate()` is now deprecated. Use
|
||||
`Util::negotiateContentType()` instead.
|
||||
* #14: The client now only follows http and https urls.
|
||||
|
||||
|
||||
2.0.4 (2014-07-14)
|
||||
------------------
|
||||
|
||||
* Changed: No longer escaping @ in urls when it's not needed.
|
||||
* Fixed: #7: Client now correctly deals with responses without a body.
|
||||
|
||||
|
||||
2.0.3 (2014-04-17)
|
||||
------------------
|
||||
|
||||
* Now works on hhvm!
|
||||
* Fixed: Now throwing an error when a Request object is being created with
|
||||
arguments that were valid for sabre/http 1.0. Hopefully this will aid with
|
||||
debugging for upgraders.
|
||||
|
||||
|
||||
2.0.2 (2014-02-09)
|
||||
------------------
|
||||
|
||||
* Fixed: Potential security problem in the client.
|
||||
|
||||
|
||||
2.0.1 (2014-01-09)
|
||||
------------------
|
||||
|
||||
* Fixed: getBodyAsString on an empty body now works.
|
||||
* Fixed: Version string
|
||||
|
||||
|
||||
2.0.0 (2014-01-08)
|
||||
------------------
|
||||
|
||||
* Removed: Request::createFromPHPRequest. This is now handled by
|
||||
Sapi::getRequest.
|
||||
|
||||
|
||||
2.0.0alpha6 (2014-01-03)
|
||||
------------------------
|
||||
|
||||
* Added: Asynchronous HTTP client. See examples/asyncclient.php.
|
||||
* Fixed: Issue #4: Don't escape colon (:) when it's not needed.
|
||||
* Fixed: Fixed a bug in the content negotation script.
|
||||
* Fixed: Fallback for when CURLOPT_POSTREDIR is not defined (mainly for hhvm).
|
||||
* Added: The Request and Response object now have a `__toString()` method that
|
||||
serializes the objects into a standard HTTP message. This is mainly for
|
||||
debugging purposes.
|
||||
* Changed: Added Response::getStatusText(). This method returns the
|
||||
human-readable HTTP status message. This part has been removed from
|
||||
Response::getStatus(), which now always returns just the status code as an
|
||||
int.
|
||||
* Changed: Response::send() is now Sapi::sendResponse($response).
|
||||
* Changed: Request::createFromPHPRequest is now Sapi::getRequest().
|
||||
* Changed: Message::getBodyAsStream and Message::getBodyAsString were added. The
|
||||
existing Message::getBody changed its behavior, so be careful.
|
||||
|
||||
|
||||
2.0.0alpha5 (2013-11-07)
|
||||
------------------------
|
||||
|
||||
* Added: HTTP Status 451 Unavailable For Legal Reasons. Fight government
|
||||
censorship!
|
||||
* Added: Ability to catch and retry http requests in the client when a curl
|
||||
error occurs.
|
||||
* Changed: Request::getPath does not return the query part of the url, so
|
||||
everything after the ? is stripped.
|
||||
* Added: a reverse proxy example.
|
||||
|
||||
|
||||
2.0.0alpha4 (2013-08-07)
|
||||
------------------------
|
||||
|
||||
* Fixed: Doing a GET request with the client uses the last used HTTP method
|
||||
instead.
|
||||
* Added: HttpException
|
||||
* Added: The Client class can now automatically emit exceptions when HTTP errors
|
||||
occurred.
|
||||
|
||||
|
||||
2.0.0alpha3 (2013-07-24)
|
||||
------------------------
|
||||
|
||||
* Changed: Now depends on sabre/event package.
|
||||
* Changed: setHeaders() now overwrites any existing http headers.
|
||||
* Added: getQueryParameters to RequestInterface.
|
||||
* Added: Util::negotiate.
|
||||
* Added: RequestDecorator, ResponseDecorator.
|
||||
* Added: A very simple HTTP client.
|
||||
* Added: addHeaders() to append a list of new headers.
|
||||
* Fixed: Not erroring on unknown HTTP status codes.
|
||||
* Fixed: Throwing exceptions on invalid HTTP status codes (not 3 digits).
|
||||
* Fixed: Much better README.md
|
||||
* Changed: getBody() now uses a bitfield to specify what type to return.
|
||||
|
||||
|
||||
2.0.0alpha2 (2013-07-02)
|
||||
------------------------
|
||||
|
||||
* Added: Digest & AWS Authentication.
|
||||
* Added: Message::getHttpVersion and Message::setHttpVersion.
|
||||
* Added: Request::setRawServerArray, getRawServerValue.
|
||||
* Added: Request::createFromPHPRequest
|
||||
* Added: Response::send
|
||||
* Added: Request::getQueryParameters
|
||||
* Added: Utility for dealing with HTTP dates.
|
||||
* Added: Request::setPostData and Request::getPostData.
|
||||
* Added: Request::setAbsoluteUrl and Request::getAbsoluteUrl.
|
||||
* Added: URLUtil, methods for calculation relative and base urls.
|
||||
* Removed: Response::sendBody
|
||||
|
||||
|
||||
2.0.0alpha1 (2012-10-07)
|
||||
------------------------
|
||||
|
||||
* Fixed: Lots of small naming improvements
|
||||
* Added: Introduction of Message, MessageInterface, Response, ResponseInterface.
|
||||
|
||||
Before 2.0.0, this package was built-into SabreDAV, where it first appeared in
|
||||
January 2009.
|
||||
|
||||
[psr-http]: https://github.com/php-fig/fig-standards/blob/master/proposed/http-message.md
|
||||
[rfc7240]: http://tools.ietf.org/html/rfc7240
|
||||
[rfc7617]: https://tools.ietf.org/html/rfc7617
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (C) 2009-2017 fruux GmbH (https://fruux.com/)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name Sabre nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
Vendored
+747
@@ -0,0 +1,747 @@
|
||||
sabre/http
|
||||
==========
|
||||
|
||||
This library provides a toolkit to make working with the [HTTP protocol](https://tools.ietf.org/html/rfc2616) easier.
|
||||
|
||||
Most PHP scripts run within a HTTP request but accessing information about the
|
||||
HTTP request is cumbersome at least.
|
||||
|
||||
There's bad practices, inconsistencies and confusion. This library is
|
||||
effectively a wrapper around the following PHP constructs:
|
||||
|
||||
For Input:
|
||||
|
||||
* `$_GET`,
|
||||
* `$_POST`,
|
||||
* `$_SERVER`,
|
||||
* `php://input` or `$HTTP_RAW_POST_DATA`.
|
||||
|
||||
For output:
|
||||
|
||||
* `php://output` or `echo`,
|
||||
* `header()`.
|
||||
|
||||
What this library provides, is a `Request` object, and a `Response` object.
|
||||
|
||||
The objects are extendable and easily mockable.
|
||||
|
||||
Build status
|
||||
------------
|
||||
|
||||
| branch | status |
|
||||
|--------|---------------------------------------------------------------------------------------------------------------|
|
||||
| master | [](https://travis-ci.org/sabre-io/http) |
|
||||
| 4.2 | [](https://travis-ci.org/sabre-io/http) |
|
||||
| 3.0 | [](https://travis-ci.org/sabre-io/http) |
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Make sure you have [composer][1] installed. In your project directory, create,
|
||||
or edit a `composer.json` file, and make sure it contains something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"require" : {
|
||||
"sabre/http" : "~5.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After that, just hit `composer install` and you should be rolling.
|
||||
|
||||
Quick history
|
||||
-------------
|
||||
|
||||
This library came to existence in 2009, as a part of the [`sabre/dav`][2]
|
||||
project, which uses it heavily.
|
||||
|
||||
It got split off into a separate library to make it easier to manage
|
||||
releases and hopefully giving it use outside of the scope of just `sabre/dav`.
|
||||
|
||||
Although completely independently developed, this library has a LOT of
|
||||
overlap with [Symfony's `HttpFoundation`][3].
|
||||
|
||||
Said library does a lot more stuff and is significantly more popular,
|
||||
so if you are looking for something to fulfill this particular requirement,
|
||||
I'd recommend also considering [`HttpFoundation`][3].
|
||||
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
First and foremost, this library wraps the superglobals. The easiest way to
|
||||
instantiate a request object is as follows:
|
||||
|
||||
```php
|
||||
use Sabre\HTTP;
|
||||
|
||||
include 'vendor/autoload.php';
|
||||
|
||||
$request = HTTP\Sapi::getRequest();
|
||||
```
|
||||
|
||||
This line should only happen once in your entire application. Everywhere else
|
||||
you should pass this request object around using dependency injection.
|
||||
|
||||
You should always typehint on its interface:
|
||||
|
||||
```php
|
||||
function handleRequest(HTTP\RequestInterface $request) {
|
||||
|
||||
// Do something with this request :)
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
A response object you can just create as such:
|
||||
|
||||
```php
|
||||
use Sabre\HTTP;
|
||||
|
||||
include 'vendor/autoload.php';
|
||||
|
||||
$response = new HTTP\Response();
|
||||
$response->setStatus(201); // created !
|
||||
$response->setHeader('X-Foo', 'bar');
|
||||
$response->setBody(
|
||||
'success!'
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
After you fully constructed your response, you must call:
|
||||
|
||||
```php
|
||||
HTTP\Sapi::sendResponse($response);
|
||||
```
|
||||
|
||||
This line should generally also appear once in your application (at the very
|
||||
end).
|
||||
|
||||
Decorators
|
||||
----------
|
||||
|
||||
It may be useful to extend the `Request` and `Response` objects in your
|
||||
application, if you for example would like them to carry a bit more
|
||||
information about the current request.
|
||||
|
||||
For instance, you may want to add an `isLoggedIn` method to the Request
|
||||
object.
|
||||
|
||||
Simply extending Request and Response may pose some problems:
|
||||
|
||||
1. You may want to extend the objects with new behaviors differently, in
|
||||
different subsystems of your application,
|
||||
2. The `Sapi::getRequest` factory always returns an instance of
|
||||
`Request` so you would have to override the factory method as well,
|
||||
3. By controlling the instantiation and depend on specific `Request` and
|
||||
`Response` instances in your library or application, you make it harder to
|
||||
work with other applications which also use `sabre/http`.
|
||||
|
||||
In short: it would be bad design. Instead, it's recommended to use the
|
||||
[decorator pattern][6] to add new behavior where you need it. `sabre/http`
|
||||
provides helper classes to quickly do this.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
use Sabre\HTTP;
|
||||
|
||||
class MyRequest extends HTTP\RequestDecorator {
|
||||
|
||||
function isLoggedIn() {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Our application assumes that the true `Request` object was instantiated
|
||||
somewhere else, by some other subsystem. This could simply be a call like
|
||||
`$request = Sapi::getRequest()` at the top of your application,
|
||||
but could also be somewhere in a unit test.
|
||||
|
||||
All we know in the current subsystem, is that we received a `$request` and
|
||||
that it implements `Sabre\HTTP\RequestInterface`. To decorate this object,
|
||||
all we need to do is:
|
||||
|
||||
```php
|
||||
$request = new MyRequest($request);
|
||||
```
|
||||
|
||||
And that's it, we now have an `isLoggedIn` method, without having to mess
|
||||
with the core instances.
|
||||
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
This package also contains a simple wrapper around [cURL][4], which will allow
|
||||
you to write simple clients, using the `Request` and `Response` objects you're
|
||||
already familiar with.
|
||||
|
||||
It's by no means a replacement for something like [Guzzle][7], but it provides
|
||||
a simple and lightweight API for making the occasional API call.
|
||||
|
||||
### Usage
|
||||
|
||||
```php
|
||||
use Sabre\HTTP;
|
||||
|
||||
$request = new HTTP\Request('GET', 'http://example.org/');
|
||||
$request->setHeader('X-Foo', 'Bar');
|
||||
|
||||
$client = new HTTP\Client();
|
||||
$response = $client->send($request);
|
||||
|
||||
echo $response->getBodyAsString();
|
||||
```
|
||||
|
||||
The client emits 3 event using [`sabre/event`][5]. `beforeRequest`,
|
||||
`afterRequest` and `error`.
|
||||
|
||||
```php
|
||||
$client = new HTTP\Client();
|
||||
$client->on('beforeRequest', function($request) {
|
||||
|
||||
// You could use beforeRequest to for example inject a few extra headers.
|
||||
// into the Request object.
|
||||
|
||||
});
|
||||
|
||||
$client->on('afterRequest', function($request, $response) {
|
||||
|
||||
// The afterRequest event could be a good time to do some logging, or
|
||||
// do some rewriting in the response.
|
||||
|
||||
});
|
||||
|
||||
$client->on('error', function($request, $response, &$retry, $retryCount) {
|
||||
|
||||
// The error event is triggered for every response with a HTTP code higher
|
||||
// than 399.
|
||||
|
||||
});
|
||||
|
||||
$client->on('error:401', function($request, $response, &$retry, $retryCount) {
|
||||
|
||||
// You can also listen for specific error codes. This example shows how
|
||||
// to inject HTTP authentication headers if a 401 was returned.
|
||||
|
||||
if ($retryCount > 1) {
|
||||
// We're only going to retry exactly once.
|
||||
}
|
||||
|
||||
$request->setHeader('Authorization', 'Basic xxxxxxxxxx');
|
||||
$retry = true;
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
### Asynchronous requests
|
||||
|
||||
The `Client` also supports doing asynchronous requests. This is especially handy
|
||||
if you need to perform a number of requests, that are allowed to be executed
|
||||
in parallel.
|
||||
|
||||
The underlying system for this is simply [cURL's multi request handler][8],
|
||||
but this provides a much nicer API to handle this.
|
||||
|
||||
Sample usage:
|
||||
|
||||
```php
|
||||
|
||||
use Sabre\HTTP;
|
||||
|
||||
$request = new Request('GET', 'http://localhost/');
|
||||
$client = new Client();
|
||||
|
||||
// Executing 1000 requests
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$client->sendAsync(
|
||||
$request,
|
||||
function(ResponseInterface $response) {
|
||||
// Success handler
|
||||
},
|
||||
function($error) {
|
||||
// Error handler
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for all requests to get a result.
|
||||
$client->wait();
|
||||
|
||||
```
|
||||
|
||||
Check out `examples/asyncclient.php` for more information.
|
||||
|
||||
Writing a reverse proxy
|
||||
-----------------------
|
||||
|
||||
With all these tools combined, it becomes very easy to write a simple reverse
|
||||
http proxy.
|
||||
|
||||
```php
|
||||
use
|
||||
Sabre\HTTP\Sapi,
|
||||
Sabre\HTTP\Client;
|
||||
|
||||
// The url we're proxying to.
|
||||
$remoteUrl = 'http://example.org/';
|
||||
|
||||
// The url we're proxying from. Please note that this must be a relative url,
|
||||
// and basically acts as the base url.
|
||||
//
|
||||
// If your $remoteUrl doesn't end with a slash, this one probably shouldn't
|
||||
// either.
|
||||
$myBaseUrl = '/reverseproxy.php';
|
||||
// $myBaseUrl = '/~evert/sabre/http/examples/reverseproxy.php/';
|
||||
|
||||
$request = Sapi::getRequest();
|
||||
$request->setBaseUrl($myBaseUrl);
|
||||
|
||||
$subRequest = clone $request;
|
||||
|
||||
// Removing the Host header.
|
||||
$subRequest->removeHeader('Host');
|
||||
|
||||
// Rewriting the url.
|
||||
$subRequest->setUrl($remoteUrl . $request->getPath());
|
||||
|
||||
$client = new Client();
|
||||
|
||||
// Sends the HTTP request to the server
|
||||
$response = $client->send($subRequest);
|
||||
|
||||
// Sends the response back to the client that connected to the proxy.
|
||||
Sapi::sendResponse($response);
|
||||
```
|
||||
|
||||
The Request and Response API's
|
||||
------------------------------
|
||||
|
||||
### Request
|
||||
|
||||
```php
|
||||
|
||||
/**
|
||||
* Creates the request object
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $headers
|
||||
* @param resource $body
|
||||
*/
|
||||
public function __construct($method = null, $url = null, array $headers = null, $body = null);
|
||||
|
||||
/**
|
||||
* Returns the current HTTP method
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getMethod();
|
||||
|
||||
/**
|
||||
* Sets the HTTP method
|
||||
*
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
function setMethod($method);
|
||||
|
||||
/**
|
||||
* Returns the request url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getUrl();
|
||||
|
||||
/**
|
||||
* Sets the request url.
|
||||
*
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
function setUrl($url);
|
||||
|
||||
/**
|
||||
* Returns the absolute url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getAbsoluteUrl();
|
||||
|
||||
/**
|
||||
* Sets the absolute url.
|
||||
*
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
function setAbsoluteUrl($url);
|
||||
|
||||
/**
|
||||
* Returns the current base url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getBaseUrl();
|
||||
|
||||
/**
|
||||
* Sets a base url.
|
||||
*
|
||||
* This url is used for relative path calculations.
|
||||
*
|
||||
* The base url should default to /
|
||||
*
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
function setBaseUrl($url);
|
||||
|
||||
/**
|
||||
* Returns the relative path.
|
||||
*
|
||||
* This is being calculated using the base url. This path will not start
|
||||
* with a slash, so it will always return something like
|
||||
* 'example/path.html'.
|
||||
*
|
||||
* If the full path is equal to the base url, this method will return an
|
||||
* empty string.
|
||||
*
|
||||
* This method will also urldecode the path, and if the url was encoded as
|
||||
* ISO-8859-1, it will convert it to UTF-8.
|
||||
*
|
||||
* If the path is outside the base url, a LogicException will be thrown.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getPath();
|
||||
|
||||
/**
|
||||
* Returns the list of query parameters.
|
||||
*
|
||||
* This is equivalent to PHP's $_GET superglobal.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getQueryParameters();
|
||||
|
||||
/**
|
||||
* Returns the POST data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getPostData();
|
||||
|
||||
/**
|
||||
* Sets the post data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*
|
||||
* This would not have been needed, if POST data was accessible as
|
||||
* php://input, but unfortunately we need to special case it.
|
||||
*
|
||||
* @param array $postData
|
||||
* @return void
|
||||
*/
|
||||
function setPostData(array $postData);
|
||||
|
||||
/**
|
||||
* Returns an item from the _SERVER array.
|
||||
*
|
||||
* If the value does not exist in the array, null is returned.
|
||||
*
|
||||
* @param string $valueName
|
||||
* @return string|null
|
||||
*/
|
||||
function getRawServerValue($valueName);
|
||||
|
||||
/**
|
||||
* Sets the _SERVER array.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
function setRawServerData(array $data);
|
||||
|
||||
/**
|
||||
* Returns the body as a readable stream resource.
|
||||
*
|
||||
* Note that the stream may not be rewindable, and therefore may only be
|
||||
* read once.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
function getBodyAsStream();
|
||||
|
||||
/**
|
||||
* Returns the body as a string.
|
||||
*
|
||||
* Note that because the underlying data may be based on a stream, this
|
||||
* method could only work correctly the first time.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getBodyAsString();
|
||||
|
||||
/**
|
||||
* Returns the message body, as its internal representation.
|
||||
*
|
||||
* This could be either a string or a stream.
|
||||
*
|
||||
* @return resource|string
|
||||
*/
|
||||
function getBody();
|
||||
|
||||
/**
|
||||
* Updates the body resource with a new stream.
|
||||
*
|
||||
* @param resource $body
|
||||
* @return void
|
||||
*/
|
||||
function setBody($body);
|
||||
|
||||
/**
|
||||
* Returns all the HTTP headers as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getHeaders();
|
||||
|
||||
/**
|
||||
* Returns a specific HTTP header, based on its name.
|
||||
*
|
||||
* The name must be treated as case-insensitive.
|
||||
*
|
||||
* If the header does not exist, this method must return null.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string|null
|
||||
*/
|
||||
function getHeader($name);
|
||||
|
||||
/**
|
||||
* Updates a HTTP header.
|
||||
*
|
||||
* The case-sensitivity of the name value must be retained as-is.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
function setHeader($name, $value);
|
||||
|
||||
/**
|
||||
* Resets HTTP headers
|
||||
*
|
||||
* This method overwrites all existing HTTP headers
|
||||
*
|
||||
* @param array $headers
|
||||
* @return void
|
||||
*/
|
||||
function setHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Adds a new set of HTTP headers.
|
||||
*
|
||||
* Any header specified in the array that already exists will be
|
||||
* overwritten, but any other existing headers will be retained.
|
||||
*
|
||||
* @param array $headers
|
||||
* @return void
|
||||
*/
|
||||
function addHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Removes a HTTP header.
|
||||
*
|
||||
* The specified header name must be treated as case-insensitive.
|
||||
* This method should return true if the header was successfully deleted,
|
||||
* and false if the header did not exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function removeHeader($name);
|
||||
|
||||
/**
|
||||
* Sets the HTTP version.
|
||||
*
|
||||
* Should be 1.0, 1.1 or 2.0.
|
||||
*
|
||||
* @param string $version
|
||||
* @return void
|
||||
*/
|
||||
function setHttpVersion($version);
|
||||
|
||||
/**
|
||||
* Returns the HTTP version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getHttpVersion();
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```php
|
||||
/**
|
||||
* Returns the current HTTP status.
|
||||
*
|
||||
* This is the status-code as well as the human-readable string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getStatus();
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code.
|
||||
*
|
||||
* This can be either the full HTTP status code with human-readable string,
|
||||
* for example: "403 I can't let you do that, Dave".
|
||||
*
|
||||
* Or just the code, in which case the appropriate default message will be
|
||||
* added.
|
||||
*
|
||||
* @param string|int $status
|
||||
* @throws \InvalidArgumentExeption
|
||||
* @return void
|
||||
*/
|
||||
function setStatus($status);
|
||||
|
||||
/**
|
||||
* Returns the body as a readable stream resource.
|
||||
*
|
||||
* Note that the stream may not be rewindable, and therefore may only be
|
||||
* read once.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
function getBodyAsStream();
|
||||
|
||||
/**
|
||||
* Returns the body as a string.
|
||||
*
|
||||
* Note that because the underlying data may be based on a stream, this
|
||||
* method could only work correctly the first time.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getBodyAsString();
|
||||
|
||||
/**
|
||||
* Returns the message body, as its internal representation.
|
||||
*
|
||||
* This could be either a string or a stream.
|
||||
*
|
||||
* @return resource|string
|
||||
*/
|
||||
function getBody();
|
||||
|
||||
|
||||
/**
|
||||
* Updates the body resource with a new stream.
|
||||
*
|
||||
* @param resource $body
|
||||
* @return void
|
||||
*/
|
||||
function setBody($body);
|
||||
|
||||
/**
|
||||
* Returns all the HTTP headers as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function getHeaders();
|
||||
|
||||
/**
|
||||
* Returns a specific HTTP header, based on its name.
|
||||
*
|
||||
* The name must be treated as case-insensitive.
|
||||
*
|
||||
* If the header does not exist, this method must return null.
|
||||
*
|
||||
* @param string $name
|
||||
* @return string|null
|
||||
*/
|
||||
function getHeader($name);
|
||||
|
||||
/**
|
||||
* Updates a HTTP header.
|
||||
*
|
||||
* The case-sensitivity of the name value must be retained as-is.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
function setHeader($name, $value);
|
||||
|
||||
/**
|
||||
* Resets HTTP headers
|
||||
*
|
||||
* This method overwrites all existing HTTP headers
|
||||
*
|
||||
* @param array $headers
|
||||
* @return void
|
||||
*/
|
||||
function setHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Adds a new set of HTTP headers.
|
||||
*
|
||||
* Any header specified in the array that already exists will be
|
||||
* overwritten, but any other existing headers will be retained.
|
||||
*
|
||||
* @param array $headers
|
||||
* @return void
|
||||
*/
|
||||
function addHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Removes a HTTP header.
|
||||
*
|
||||
* The specified header name must be treated as case-insensitive.
|
||||
* This method should return true if the header was successfully deleted,
|
||||
* and false if the header did not exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function removeHeader($name);
|
||||
|
||||
/**
|
||||
* Sets the HTTP version.
|
||||
*
|
||||
* Should be 1.0, 1.1 or 2.0.
|
||||
*
|
||||
* @param string $version
|
||||
* @return void
|
||||
*/
|
||||
function setHttpVersion($version);
|
||||
|
||||
/**
|
||||
* Returns the HTTP version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getHttpVersion();
|
||||
```
|
||||
|
||||
Made at fruux
|
||||
-------------
|
||||
|
||||
This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
|
||||
|
||||
[1]: http://getcomposer.org/
|
||||
[2]: http://sabre.io/
|
||||
[3]: https://github.com/symfony/HttpFoundation
|
||||
[4]: http://php.net/curl
|
||||
[5]: https://github.com/fruux/sabre-event
|
||||
[6]: http://en.wikipedia.org/wiki/Decorator_pattern
|
||||
[7]: http://guzzlephp.org/
|
||||
[8]: http://php.net/curl_multi_init
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "sabre/http",
|
||||
"description" : "The sabre/http library provides utilities for dealing with http requests and responses. ",
|
||||
"keywords" : [ "HTTP" ],
|
||||
"homepage" : "https://github.com/fruux/sabre-http",
|
||||
"license" : "BSD-3-Clause",
|
||||
"require" : {
|
||||
"php" : "^7.1 || ^8.0",
|
||||
"ext-mbstring" : "*",
|
||||
"ext-ctype" : "*",
|
||||
"ext-curl" : "*",
|
||||
"sabre/event" : ">=4.0 <6.0",
|
||||
"sabre/uri" : "^2.0"
|
||||
},
|
||||
"require-dev" : {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1||3.63.2",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest" : {
|
||||
"ext-curl" : " to make http requests with the Client class"
|
||||
},
|
||||
"authors" : [
|
||||
{
|
||||
"name" : "Evert Pot",
|
||||
"email" : "me@evertpot.com",
|
||||
"homepage" : "http://evertpot.com/",
|
||||
"role" : "Developer"
|
||||
}
|
||||
],
|
||||
"support" : {
|
||||
"forum" : "https://groups.google.com/group/sabredav-discuss",
|
||||
"source" : "https://github.com/fruux/sabre-http"
|
||||
},
|
||||
"autoload" : {
|
||||
"files" : [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4" : {
|
||||
"Sabre\\HTTP\\" : "lib/"
|
||||
}
|
||||
},
|
||||
"autoload-dev" : {
|
||||
"psr-4" : {
|
||||
"Sabre\\HTTP\\" : "tests/HTTP"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpstan": [
|
||||
"phpstan analyse lib tests"
|
||||
],
|
||||
"cs-fixer": [
|
||||
"PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix"
|
||||
],
|
||||
"phpunit": [
|
||||
"phpunit --configuration tests/phpunit.xml"
|
||||
],
|
||||
"test": [
|
||||
"composer phpstan",
|
||||
"composer cs-fixer",
|
||||
"composer phpunit"
|
||||
]
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This example demonstrates the ability for clients to work asynchronously.
|
||||
*
|
||||
* By default up to 10 requests will be executed in parallel. HTTP connections
|
||||
* are re-used and DNS is cached, all thanks to the power of curl.
|
||||
*
|
||||
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
use Sabre\HTTP\Client;
|
||||
use Sabre\HTTP\Request;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the request we're repeating 1000 times.
|
||||
$request = new Request('GET', 'http://localhost/');
|
||||
$client = new Client();
|
||||
|
||||
for ($i = 0; $i < 1000; ++$i) {
|
||||
echo "$i sending\n";
|
||||
$client->sendAsync(
|
||||
$request,
|
||||
|
||||
// This is the 'success' callback
|
||||
function ($response) use ($i) {
|
||||
echo "$i -> ".$response->getStatus()."\n";
|
||||
},
|
||||
|
||||
// This is the 'error' callback. It is called for general connection
|
||||
// problems (such as not being able to connect to a host, dns errors,
|
||||
// etc.) and also cases where a response was returned, but it had a
|
||||
// status code of 400 or higher.
|
||||
function ($error) use ($i) {
|
||||
if (Client::STATUS_CURLERROR === $error['status']) {
|
||||
// Curl errors
|
||||
echo "$i -> curl error: ".$error['curl_errmsg']."\n";
|
||||
} else {
|
||||
// HTTP errors
|
||||
echo "$i -> ".$error['response']->getStatus()."\n";
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// After everything is done, we call 'wait'. This causes the client to wait for
|
||||
// all outstanding http requests to complete.
|
||||
$client->wait();
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This example shows how to do Basic authentication.
|
||||
* *.
|
||||
*
|
||||
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
$userList = [
|
||||
'user1' => 'password',
|
||||
'user2' => 'password',
|
||||
];
|
||||
|
||||
use Sabre\HTTP\Auth;
|
||||
use Sabre\HTTP\Response;
|
||||
use Sabre\HTTP\Sapi;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$request = Sapi::getRequest();
|
||||
$response = new Response();
|
||||
|
||||
$basicAuth = new Auth\Basic('Locked down area', $request, $response);
|
||||
if (!$userPass = $basicAuth->getCredentials()) {
|
||||
// No username or password given
|
||||
$basicAuth->requireLogin();
|
||||
} elseif (!isset($userList[$userPass[0]]) || $userList[$userPass[0]] !== $userPass[1]) {
|
||||
// Username or password are incorrect
|
||||
$basicAuth->requireLogin();
|
||||
} else {
|
||||
// Success !
|
||||
$response->setBody('You are logged in!');
|
||||
}
|
||||
|
||||
// Sending the response
|
||||
Sapi::sendResponse($response);
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This example shows how to make a HTTP request with the Request and Response
|
||||
* objects.
|
||||
*
|
||||
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
use Sabre\HTTP\Client;
|
||||
use Sabre\HTTP\Request;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructing the request.
|
||||
$request = new Request('GET', 'http://localhost/');
|
||||
|
||||
$client = new Client();
|
||||
// $client->addCurlSetting(CURLOPT_PROXY,'localhost:8888');
|
||||
$response = $client->send($request);
|
||||
|
||||
echo "Response:\n";
|
||||
|
||||
echo (string) $response;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This example shows how to do Digest authentication.
|
||||
* *.
|
||||
*
|
||||
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Markus Staab
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
$userList = [
|
||||
'user1' => 'password',
|
||||
'user2' => 'password',
|
||||
];
|
||||
|
||||
use Sabre\HTTP\Auth;
|
||||
use Sabre\HTTP\Response;
|
||||
use Sabre\HTTP\Sapi;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$request = Sapi::getRequest();
|
||||
$response = new Response();
|
||||
|
||||
$digestAuth = new Auth\Digest('Locked down area', $request, $response);
|
||||
$digestAuth->init();
|
||||
if (!$userName = $digestAuth->getUsername()) {
|
||||
// No username given
|
||||
$digestAuth->requireLogin();
|
||||
} elseif (!isset($userList[$userName]) || !$digestAuth->validatePassword($userList[$userName])) {
|
||||
// Username or password are incorrect
|
||||
$digestAuth->requireLogin();
|
||||
} else {
|
||||
// Success !
|
||||
$response->setBody('You are logged in!');
|
||||
}
|
||||
|
||||
// Sending the response
|
||||
Sapi::sendResponse($response);
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
// The url we're proxying to.
|
||||
$remoteUrl = 'http://example.org/';
|
||||
|
||||
// The url we're proxying from. Please note that this must be a relative url,
|
||||
// and basically acts as the base url.
|
||||
//
|
||||
// If your $remoteUrl doesn't end with a slash, this one probably shouldn't
|
||||
// either.
|
||||
$myBaseUrl = '/reverseproxy.php';
|
||||
// $myBaseUrl = '/~evert/sabre/http/examples/reverseproxy.php/';
|
||||
|
||||
use Sabre\HTTP\Client;
|
||||
use Sabre\HTTP\Sapi;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$request = Sapi::getRequest();
|
||||
$request->setBaseUrl($myBaseUrl);
|
||||
|
||||
$subRequest = clone $request;
|
||||
|
||||
// Removing the Host header.
|
||||
$subRequest->removeHeader('Host');
|
||||
|
||||
// Rewriting the url.
|
||||
$subRequest->setUrl($remoteUrl.$request->getPath());
|
||||
|
||||
$client = new Client();
|
||||
|
||||
// Sends the HTTP request to the server
|
||||
$response = $client->send($subRequest);
|
||||
|
||||
// Sends the response back to the client that connected to the proxy.
|
||||
Sapi::sendResponse($response);
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This simple example shows the capability of Request and Response objects to
|
||||
* serialize themselves as strings.
|
||||
*
|
||||
* This is mainly useful for debugging purposes.
|
||||
*
|
||||
* @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
use Sabre\HTTP\Request;
|
||||
use Sabre\HTTP\Response;
|
||||
|
||||
// Find the autoloader
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php',
|
||||
__DIR__.'/../../../autoload.php',
|
||||
__DIR__.'/vendor/autoload.php',
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$request = new Request('POST', '/foo');
|
||||
$request->setHeaders([
|
||||
'Host' => 'example.org',
|
||||
'Content-Type' => 'application/json',
|
||||
]);
|
||||
|
||||
$request->setBody(json_encode(['foo' => 'bar']));
|
||||
|
||||
echo $request;
|
||||
echo "\r\n\r\n";
|
||||
|
||||
$response = new Response(424);
|
||||
$response->setHeaders([
|
||||
'Content-Type' => 'text/plain',
|
||||
'Connection' => 'close',
|
||||
]);
|
||||
|
||||
$response->setBody('ABORT! ABORT!');
|
||||
|
||||
echo $response;
|
||||
|
||||
echo "\r\n";
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP AWS Authentication handler.
|
||||
*
|
||||
* Use this class to leverage amazon's AWS authentication header
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class AWS extends AbstractAuth
|
||||
{
|
||||
/**
|
||||
* The signature supplied by the HTTP client.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $signature;
|
||||
|
||||
/**
|
||||
* The accesskey supplied by the HTTP client.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $accessKey;
|
||||
|
||||
/**
|
||||
* An error code, if any.
|
||||
*
|
||||
* This value will be filled with one of the ERR_* constants
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $errorCode = 0;
|
||||
|
||||
public const ERR_NOAWSHEADER = 1;
|
||||
public const ERR_MD5CHECKSUMWRONG = 2;
|
||||
public const ERR_INVALIDDATEFORMAT = 3;
|
||||
public const ERR_REQUESTTIMESKEWED = 4;
|
||||
public const ERR_INVALIDSIGNATURE = 5;
|
||||
|
||||
/**
|
||||
* Gathers all information from the headers.
|
||||
*
|
||||
* This method needs to be called prior to anything else.
|
||||
*/
|
||||
public function init(): bool
|
||||
{
|
||||
$authHeader = $this->request->getHeader('Authorization');
|
||||
|
||||
if (null === $authHeader) {
|
||||
$this->errorCode = self::ERR_NOAWSHEADER;
|
||||
|
||||
return false;
|
||||
}
|
||||
$authHeader = explode(' ', $authHeader);
|
||||
|
||||
if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) {
|
||||
$this->errorCode = self::ERR_NOAWSHEADER;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
list($this->accessKey, $this->signature) = explode(':', $authHeader[1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username for the request.
|
||||
*/
|
||||
public function getAccessKey(): string
|
||||
{
|
||||
return $this->accessKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the signature based on the secretKey.
|
||||
*/
|
||||
public function validate(string $secretKey): bool
|
||||
{
|
||||
$contentMD5 = $this->request->getHeader('Content-MD5');
|
||||
|
||||
if ($contentMD5) {
|
||||
// We need to validate the integrity of the request
|
||||
$body = $this->request->getBody();
|
||||
$this->request->setBody($body);
|
||||
|
||||
if ($contentMD5 !== base64_encode(md5((string) $body, true))) {
|
||||
// content-md5 header did not match md5 signature of body
|
||||
$this->errorCode = self::ERR_MD5CHECKSUMWRONG;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$requestDate = $this->request->getHeader('x-amz-date')) {
|
||||
$requestDate = $this->request->getHeader('Date');
|
||||
}
|
||||
|
||||
if (!$this->validateRFC2616Date((string) $requestDate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$amzHeaders = $this->getAmzHeaders();
|
||||
|
||||
$signature = base64_encode(
|
||||
$this->hmacsha1($secretKey,
|
||||
$this->request->getMethod()."\n".
|
||||
$contentMD5."\n".
|
||||
$this->request->getHeader('Content-type')."\n".
|
||||
$requestDate."\n".
|
||||
$amzHeaders.
|
||||
$this->request->getUrl()
|
||||
)
|
||||
);
|
||||
|
||||
if ($this->signature !== $signature) {
|
||||
$this->errorCode = self::ERR_INVALIDSIGNATURE;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTTP 401 header, forcing login.
|
||||
*
|
||||
* This should be called when username and password are incorrect, or not supplied at all
|
||||
*/
|
||||
public function requireLogin()
|
||||
{
|
||||
$this->response->addHeader('WWW-Authenticate', 'AWS');
|
||||
$this->response->setStatus(401);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure the supplied value is a valid RFC2616 date.
|
||||
*
|
||||
* If we would just use strtotime to get a valid timestamp, we have no way of checking if a
|
||||
* user just supplied the word 'now' for the date header.
|
||||
*
|
||||
* This function also makes sure the Date header is within 15 minutes of the operating
|
||||
* system date, to prevent replay attacks.
|
||||
*/
|
||||
protected function validateRFC2616Date(string $dateHeader): bool
|
||||
{
|
||||
$date = HTTP\parseDate($dateHeader);
|
||||
|
||||
// Unknown format
|
||||
if (!$date) {
|
||||
$this->errorCode = self::ERR_INVALIDDATEFORMAT;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$min = new \DateTime('-15 minutes');
|
||||
$max = new \DateTime('+15 minutes');
|
||||
|
||||
// We allow 15 minutes around the current date/time
|
||||
if ($date > $max || $date < $min) {
|
||||
$this->errorCode = self::ERR_REQUESTTIMESKEWED;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of AMZ headers.
|
||||
*/
|
||||
protected function getAmzHeaders(): string
|
||||
{
|
||||
$amzHeaders = [];
|
||||
$headers = $this->request->getHeaders();
|
||||
foreach ($headers as $headerName => $headerValue) {
|
||||
if (0 === strpos(strtolower($headerName), 'x-amz-')) {
|
||||
$amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n";
|
||||
}
|
||||
}
|
||||
ksort($amzHeaders);
|
||||
|
||||
$headerStr = '';
|
||||
foreach ($amzHeaders as $h => $v) {
|
||||
$headerStr .= $h.':'.$v;
|
||||
}
|
||||
|
||||
return $headerStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an HMAC-SHA1 signature.
|
||||
*/
|
||||
private function hmacsha1(string $key, string $message): string
|
||||
{
|
||||
if (function_exists('hash_hmac')) {
|
||||
return hash_hmac('sha1', $message, $key, true);
|
||||
}
|
||||
|
||||
$blocksize = 64;
|
||||
if (strlen($key) > $blocksize) {
|
||||
$key = pack('H*', sha1($key));
|
||||
}
|
||||
$key = str_pad($key, $blocksize, chr(0x00));
|
||||
$ipad = str_repeat(chr(0x36), $blocksize);
|
||||
$opad = str_repeat(chr(0x5C), $blocksize);
|
||||
$hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message))));
|
||||
|
||||
return $hmac;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* HTTP Authentication base class.
|
||||
*
|
||||
* This class provides some common functionality for the various base classes.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class AbstractAuth
|
||||
{
|
||||
/**
|
||||
* Authentication realm.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $realm;
|
||||
|
||||
/**
|
||||
* Request object.
|
||||
*
|
||||
* @var RequestInterface
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Response object.
|
||||
*
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Creates the object.
|
||||
*/
|
||||
public function __construct(string $realm, RequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
$this->realm = $realm;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sends the needed HTTP header and status code (401) to force
|
||||
* the user to login.
|
||||
*/
|
||||
abstract public function requireLogin();
|
||||
|
||||
/**
|
||||
* Returns the HTTP realm.
|
||||
*/
|
||||
public function getRealm(): string
|
||||
{
|
||||
return $this->realm;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
/**
|
||||
* HTTP Basic authentication utility.
|
||||
*
|
||||
* This class helps you setup basic auth. The process is fairly simple:
|
||||
*
|
||||
* 1. Instantiate the class.
|
||||
* 2. Call getCredentials (this will return null or a user/pass pair)
|
||||
* 3. If you didn't get valid credentials, call 'requireLogin'
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Basic extends AbstractAuth
|
||||
{
|
||||
/**
|
||||
* This method returns a numeric array with a username and password as the
|
||||
* only elements.
|
||||
*
|
||||
* If no credentials were found, this method returns null.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCredentials()
|
||||
{
|
||||
$auth = $this->request->getHeader('Authorization');
|
||||
|
||||
if (!$auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('basic ' !== strtolower(substr($auth, 0, 6))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$credentials = explode(':', base64_decode(substr($auth, 6)), 2);
|
||||
|
||||
if (2 !== count($credentials)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sends the needed HTTP header and status code (401) to force
|
||||
* the user to login.
|
||||
*/
|
||||
public function requireLogin()
|
||||
{
|
||||
$this->response->addHeader('WWW-Authenticate', 'Basic realm="'.$this->realm.'", charset="UTF-8"');
|
||||
$this->response->setStatus(401);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
/**
|
||||
* HTTP Bearer authentication utility.
|
||||
*
|
||||
* This class helps you setup bearer auth. The process is fairly simple:
|
||||
*
|
||||
* 1. Instantiate the class.
|
||||
* 2. Call getToken (this will return null or a token as string)
|
||||
* 3. If you didn't get a valid token, call 'requireLogin'
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author François Kooman (fkooman@tuxed.net)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Bearer extends AbstractAuth
|
||||
{
|
||||
/**
|
||||
* This method returns a string with an access token.
|
||||
*
|
||||
* If no token was found, this method returns null.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
$auth = $this->request->getHeader('Authorization');
|
||||
|
||||
if (!$auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('bearer ' !== strtolower(substr($auth, 0, 7))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($auth, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sends the needed HTTP header and status code (401) to force
|
||||
* authentication.
|
||||
*/
|
||||
public function requireLogin()
|
||||
{
|
||||
$this->response->addHeader('WWW-Authenticate', 'Bearer realm="'.$this->realm.'"');
|
||||
$this->response->setStatus(401);
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* HTTP Digest Authentication handler.
|
||||
*
|
||||
* Use this class for easy http digest authentication.
|
||||
* Instructions:
|
||||
*
|
||||
* 1. Create the object
|
||||
* 2. Call the setRealm() method with the realm you plan to use
|
||||
* 3. Call the init method function.
|
||||
* 4. Call the getUserName() function. This function may return null if no
|
||||
* authentication information was supplied. Based on the username you
|
||||
* should check your internal database for either the associated password,
|
||||
* or the so-called A1 hash of the digest.
|
||||
* 5. Call either validatePassword() or validateA1(). This will return true
|
||||
* or false.
|
||||
* 6. To make sure an authentication prompt is displayed, call the
|
||||
* requireLogin() method.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Digest extends AbstractAuth
|
||||
{
|
||||
/**
|
||||
* These constants are used in setQOP();.
|
||||
*/
|
||||
public const QOP_AUTH = 1;
|
||||
public const QOP_AUTHINT = 2;
|
||||
|
||||
protected $nonce;
|
||||
protected $opaque;
|
||||
protected $digestParts;
|
||||
protected $A1;
|
||||
protected $qop = self::QOP_AUTH;
|
||||
|
||||
/**
|
||||
* Initializes the object.
|
||||
*/
|
||||
public function __construct(string $realm, RequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
$this->nonce = uniqid();
|
||||
$this->opaque = md5($realm);
|
||||
parent::__construct($realm, $request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers all information from the headers.
|
||||
*
|
||||
* This method needs to be called prior to anything else.
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$digest = $this->getDigest();
|
||||
$this->digestParts = $this->parseDigest((string) $digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the quality of protection value.
|
||||
*
|
||||
* Possible values are:
|
||||
* Sabre\HTTP\DigestAuth::QOP_AUTH
|
||||
* Sabre\HTTP\DigestAuth::QOP_AUTHINT
|
||||
*
|
||||
* Multiple values can be specified using logical OR.
|
||||
*
|
||||
* QOP_AUTHINT ensures integrity of the request body, but this is not
|
||||
* supported by most HTTP clients. QOP_AUTHINT also requires the entire
|
||||
* request body to be md5'ed, which can put strains on CPU and memory.
|
||||
*/
|
||||
public function setQOP(int $qop)
|
||||
{
|
||||
$this->qop = $qop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user.
|
||||
*
|
||||
* The A1 parameter should be md5($username . ':' . $realm . ':' . $password);
|
||||
*/
|
||||
public function validateA1(string $A1): bool
|
||||
{
|
||||
$this->A1 = $A1;
|
||||
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates authentication through a password. The actual password must be provided here.
|
||||
* It is strongly recommended not store the password in plain-text and use validateA1 instead.
|
||||
*/
|
||||
public function validatePassword(string $password): bool
|
||||
{
|
||||
$this->A1 = md5($this->digestParts['username'].':'.$this->realm.':'.$password);
|
||||
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username for the request.
|
||||
* Returns null if there were none.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->digestParts['username'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the digest challenge.
|
||||
*/
|
||||
protected function validate(): bool
|
||||
{
|
||||
if (!is_array($this->digestParts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$A2 = $this->request->getMethod().':'.$this->digestParts['uri'];
|
||||
|
||||
if ('auth-int' === $this->digestParts['qop']) {
|
||||
// Making sure we support this qop value
|
||||
if (!($this->qop & self::QOP_AUTHINT)) {
|
||||
return false;
|
||||
}
|
||||
// We need to add an md5 of the entire request body to the A2 part of the hash
|
||||
$body = $this->request->getBody();
|
||||
$this->request->setBody($body);
|
||||
$A2 .= ':'.md5($body);
|
||||
} elseif (!($this->qop & self::QOP_AUTH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$A2 = md5($A2);
|
||||
|
||||
$validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}");
|
||||
|
||||
return $this->digestParts['response'] === $validResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTTP 401 header, forcing login.
|
||||
*
|
||||
* This should be called when username and password are incorrect, or not supplied at all
|
||||
*/
|
||||
public function requireLogin()
|
||||
{
|
||||
$qop = '';
|
||||
switch ($this->qop) {
|
||||
case self::QOP_AUTH:
|
||||
$qop = 'auth';
|
||||
break;
|
||||
case self::QOP_AUTHINT:
|
||||
$qop = 'auth-int';
|
||||
break;
|
||||
case self::QOP_AUTH | self::QOP_AUTHINT:
|
||||
$qop = 'auth,auth-int';
|
||||
break;
|
||||
}
|
||||
|
||||
$this->response->addHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="'.$qop.'",nonce="'.$this->nonce.'",opaque="'.$this->opaque.'"');
|
||||
$this->response->setStatus(401);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the full digest string.
|
||||
*
|
||||
* It should be compatible with mod_php format and other webservers.
|
||||
*
|
||||
* If the header could not be found, null will be returned
|
||||
*/
|
||||
public function getDigest()
|
||||
{
|
||||
return $this->request->getHeader('Authorization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the different pieces of the digest string into an array.
|
||||
*
|
||||
* This method returns false if an incomplete digest was supplied
|
||||
*
|
||||
* @return bool|array
|
||||
*/
|
||||
protected function parseDigest(string $digest)
|
||||
{
|
||||
// protect against missing data
|
||||
$needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
|
||||
$data = [];
|
||||
|
||||
preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$data[$m[1]] = $m[2] ?: $m[3];
|
||||
unset($needed_parts[$m[1]]);
|
||||
}
|
||||
|
||||
return $needed_parts ? false : $data;
|
||||
}
|
||||
}
|
||||
Vendored
+620
@@ -0,0 +1,620 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
use Sabre\Event\EventEmitter;
|
||||
use Sabre\Uri;
|
||||
|
||||
/**
|
||||
* A rudimentary HTTP client.
|
||||
*
|
||||
* This object wraps PHP's curl extension and provides an easy way to send it a
|
||||
* Request object, and return a Response object.
|
||||
*
|
||||
* This is by no means intended as the next best HTTP client, but it does the
|
||||
* job and provides a simple integration with the rest of sabre/http.
|
||||
*
|
||||
* This client emits the following events:
|
||||
* beforeRequest(RequestInterface $request)
|
||||
* afterRequest(RequestInterface $request, ResponseInterface $response)
|
||||
* error(RequestInterface $request, ResponseInterface $response, bool &$retry, int $retryCount)
|
||||
* exception(RequestInterface $request, ClientException $e, bool &$retry, int $retryCount)
|
||||
*
|
||||
* The beforeRequest event allows you to do some last minute changes to the
|
||||
* request before it's done, such as adding authentication headers.
|
||||
*
|
||||
* The afterRequest event will be emitted after the request is completed
|
||||
* successfully.
|
||||
*
|
||||
* If a HTTP error is returned (status code higher than 399) the error event is
|
||||
* triggered. It's possible using this event to retry the request, by setting
|
||||
* retry to true.
|
||||
*
|
||||
* The amount of times a request has retried is passed as $retryCount, which
|
||||
* can be used to avoid retrying indefinitely. The first time the event is
|
||||
* called, this will be 0.
|
||||
*
|
||||
* It's also possible to intercept specific http errors, by subscribing to for
|
||||
* example 'error:401'.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Client extends EventEmitter
|
||||
{
|
||||
/**
|
||||
* List of curl settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $curlSettings = [];
|
||||
|
||||
/**
|
||||
* Whether exceptions should be thrown when a HTTP error is returned.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $throwExceptions = false;
|
||||
|
||||
/**
|
||||
* The maximum number of times we'll follow a redirect.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxRedirects = 5;
|
||||
|
||||
protected $headerLinesMap = [];
|
||||
|
||||
/**
|
||||
* Initializes the client.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// See https://github.com/sabre-io/http/pull/115#discussion_r241292068
|
||||
// Preserve compatibility for sub-classes that implements their own method `parseCurlResult`
|
||||
$separatedHeaders = __CLASS__ === get_class($this);
|
||||
|
||||
$this->curlSettings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
if ($separatedHeaders) {
|
||||
$this->curlSettings[CURLOPT_HEADERFUNCTION] = [$this, 'receiveCurlHeader'];
|
||||
} else {
|
||||
$this->curlSettings[CURLOPT_HEADER] = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function receiveCurlHeader($curlHandle, $headerLine)
|
||||
{
|
||||
$this->headerLinesMap[(int) $curlHandle][] = $headerLine;
|
||||
|
||||
return strlen($headerLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a HTTP server, and returns a response.
|
||||
*/
|
||||
public function send(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->emit('beforeRequest', [$request]);
|
||||
|
||||
$retryCount = 0;
|
||||
$redirects = 0;
|
||||
|
||||
do {
|
||||
$doRedirect = false;
|
||||
$retry = false;
|
||||
|
||||
try {
|
||||
$response = $this->doRequest($request);
|
||||
|
||||
$code = $response->getStatus();
|
||||
|
||||
// We are doing in-PHP redirects, because curl's
|
||||
// FOLLOW_LOCATION throws errors when PHP is configured with
|
||||
// open_basedir.
|
||||
//
|
||||
// https://github.com/fruux/sabre-http/issues/12
|
||||
if ($redirects < $this->maxRedirects && in_array($code, [301, 302, 307, 308])) {
|
||||
$oldLocation = $request->getUrl();
|
||||
|
||||
// Creating a new instance of the request object.
|
||||
$request = clone $request;
|
||||
|
||||
// Setting the new location
|
||||
$request->setUrl(Uri\resolve(
|
||||
$oldLocation,
|
||||
$response->getHeader('Location')
|
||||
));
|
||||
|
||||
$doRedirect = true;
|
||||
++$redirects;
|
||||
}
|
||||
|
||||
// This was a HTTP error
|
||||
if ($code >= 400) {
|
||||
$this->emit('error', [$request, $response, &$retry, $retryCount]);
|
||||
$this->emit('error:'.$code, [$request, $response, &$retry, $retryCount]);
|
||||
}
|
||||
} catch (ClientException $e) {
|
||||
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
||||
|
||||
// If retry was still set to false, it means no event handler
|
||||
// dealt with the problem. In this case we just re-throw the
|
||||
// exception.
|
||||
if (!$retry) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($retry) {
|
||||
++$retryCount;
|
||||
}
|
||||
} while ($retry || $doRedirect);
|
||||
|
||||
$this->emit('afterRequest', [$request, $response]);
|
||||
|
||||
if ($this->throwExceptions && $code >= 400) {
|
||||
throw new ClientHttpException($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a HTTP request asynchronously.
|
||||
*
|
||||
* Due to the nature of PHP, you must from time to time poll to see if any
|
||||
* new responses came in.
|
||||
*
|
||||
* After calling sendAsync, you must therefore occasionally call the poll()
|
||||
* method, or wait().
|
||||
*/
|
||||
public function sendAsync(RequestInterface $request, ?callable $success = null, ?callable $error = null)
|
||||
{
|
||||
$this->emit('beforeRequest', [$request]);
|
||||
$this->sendAsyncInternal($request, $success, $error);
|
||||
$this->poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if any http requests have gotten results, and if so,
|
||||
* call the appropriate success or error handlers.
|
||||
*
|
||||
* This method will return true if there are still requests waiting to
|
||||
* return, and false if all the work is done.
|
||||
*/
|
||||
public function poll(): bool
|
||||
{
|
||||
// nothing to do?
|
||||
if (!$this->curlMultiMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
do {
|
||||
$r = curl_multi_exec(
|
||||
$this->curlMultiHandle,
|
||||
$stillRunning
|
||||
);
|
||||
} while (CURLM_CALL_MULTI_PERFORM === $r);
|
||||
|
||||
$messagesInQueue = 0;
|
||||
do {
|
||||
messageQueue:
|
||||
|
||||
$status = curl_multi_info_read(
|
||||
$this->curlMultiHandle,
|
||||
$messagesInQueue
|
||||
);
|
||||
|
||||
if ($status && CURLMSG_DONE === $status['msg']) {
|
||||
$resourceId = (int) $status['handle'];
|
||||
list(
|
||||
$request,
|
||||
$successCallback,
|
||||
$errorCallback,
|
||||
$retryCount) = $this->curlMultiMap[$resourceId];
|
||||
unset($this->curlMultiMap[$resourceId]);
|
||||
|
||||
$curlHandle = $status['handle'];
|
||||
$curlResult = $this->parseResponse(curl_multi_getcontent($curlHandle), $curlHandle);
|
||||
$retry = false;
|
||||
|
||||
if (self::STATUS_CURLERROR === $curlResult['status']) {
|
||||
$e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']);
|
||||
$this->emit('exception', [$request, $e, &$retry, $retryCount]);
|
||||
|
||||
if ($retry) {
|
||||
++$retryCount;
|
||||
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
||||
goto messageQueue;
|
||||
}
|
||||
|
||||
$curlResult['request'] = $request;
|
||||
|
||||
if ($errorCallback) {
|
||||
$errorCallback($curlResult);
|
||||
}
|
||||
} elseif (self::STATUS_HTTPERROR === $curlResult['status']) {
|
||||
$this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]);
|
||||
$this->emit('error:'.$curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]);
|
||||
|
||||
if ($retry) {
|
||||
++$retryCount;
|
||||
$this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount);
|
||||
goto messageQueue;
|
||||
}
|
||||
|
||||
$curlResult['request'] = $request;
|
||||
|
||||
if ($errorCallback) {
|
||||
$errorCallback($curlResult);
|
||||
}
|
||||
} else {
|
||||
$this->emit('afterRequest', [$request, $curlResult['response']]);
|
||||
|
||||
if ($successCallback) {
|
||||
$successCallback($curlResult['response']);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ($messagesInQueue > 0);
|
||||
|
||||
return count($this->curlMultiMap) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes every HTTP request in the queue, and waits till they are all
|
||||
* completed.
|
||||
*/
|
||||
public function wait()
|
||||
{
|
||||
do {
|
||||
curl_multi_select($this->curlMultiHandle);
|
||||
$stillRunning = $this->poll();
|
||||
} while ($stillRunning);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is set to true, the Client will automatically throw exceptions
|
||||
* upon HTTP errors.
|
||||
*
|
||||
* This means that if a response came back with a status code greater than
|
||||
* or equal to 400, we will throw a ClientHttpException.
|
||||
*
|
||||
* This only works for the send() method. Throwing exceptions for
|
||||
* sendAsync() is not supported.
|
||||
*/
|
||||
public function setThrowExceptions(bool $throwExceptions)
|
||||
{
|
||||
$this->throwExceptions = $throwExceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a CURL setting.
|
||||
*
|
||||
* These settings will be included in every HTTP request.
|
||||
*/
|
||||
public function addCurlSetting(int $name, $value)
|
||||
{
|
||||
$this->curlSettings[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for performing a single request.
|
||||
*/
|
||||
protected function doRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$settings = $this->createCurlSettingsArray($request);
|
||||
|
||||
if (!$this->curlHandle) {
|
||||
$this->curlHandle = curl_init();
|
||||
} else {
|
||||
curl_reset($this->curlHandle);
|
||||
}
|
||||
|
||||
curl_setopt_array($this->curlHandle, $settings);
|
||||
$response = $this->curlExec($this->curlHandle);
|
||||
$response = $this->parseResponse($response, $this->curlHandle);
|
||||
if (self::STATUS_CURLERROR === $response['status']) {
|
||||
throw new ClientException($response['curl_errmsg'], $response['curl_errno']);
|
||||
}
|
||||
|
||||
return $response['response'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached curl handle.
|
||||
*
|
||||
* By keeping this resource around for the lifetime of this object, things
|
||||
* like persistent connections are possible.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $curlHandle;
|
||||
|
||||
/**
|
||||
* Handler for curl_multi requests.
|
||||
*
|
||||
* The first time sendAsync is used, this will be created.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
private $curlMultiHandle;
|
||||
|
||||
/**
|
||||
* Has a list of curl handles, as well as their associated success and
|
||||
* error callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $curlMultiMap = [];
|
||||
|
||||
/**
|
||||
* Turns a RequestInterface object into an array with settings that can be
|
||||
* fed to curl_setopt.
|
||||
*/
|
||||
protected function createCurlSettingsArray(RequestInterface $request): array
|
||||
{
|
||||
$settings = $this->curlSettings;
|
||||
|
||||
switch ($request->getMethod()) {
|
||||
case 'HEAD':
|
||||
$settings[CURLOPT_NOBODY] = true;
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
|
||||
break;
|
||||
case 'GET':
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = 'GET';
|
||||
break;
|
||||
default:
|
||||
$body = $request->getBody();
|
||||
if (is_resource($body)) {
|
||||
$bodyStat = fstat($body);
|
||||
|
||||
// This needs to be set to PUT, regardless of the actual
|
||||
// method used. Without it, INFILE will be ignored for some
|
||||
// reason.
|
||||
$settings[CURLOPT_PUT] = true;
|
||||
$settings[CURLOPT_INFILE] = $body;
|
||||
if (false !== $bodyStat && array_key_exists('size', $bodyStat)) {
|
||||
$settings[CURLOPT_INFILESIZE] = $bodyStat['size'];
|
||||
}
|
||||
} else {
|
||||
// For security we cast this to a string. If somehow an array could
|
||||
// be passed here, it would be possible for an attacker to use @ to
|
||||
// post local files.
|
||||
$settings[CURLOPT_POSTFIELDS] = (string) $body;
|
||||
}
|
||||
$settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
|
||||
break;
|
||||
}
|
||||
|
||||
$nHeaders = [];
|
||||
foreach ($request->getHeaders() as $key => $values) {
|
||||
foreach ($values as $value) {
|
||||
$nHeaders[] = $key.': '.$value;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] !== $nHeaders) {
|
||||
$settings[CURLOPT_HTTPHEADER] = $nHeaders;
|
||||
}
|
||||
$settings[CURLOPT_URL] = $request->getUrl();
|
||||
// FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM
|
||||
if (defined('CURLOPT_PROTOCOLS')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
// FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM
|
||||
if (defined('CURLOPT_REDIR_PROTOCOLS')) {
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public const STATUS_SUCCESS = 0;
|
||||
public const STATUS_CURLERROR = 1;
|
||||
public const STATUS_HTTPERROR = 2;
|
||||
|
||||
private function parseResponse(string $response, $curlHandle): array
|
||||
{
|
||||
$settings = $this->curlSettings;
|
||||
$separatedHeaders = isset($settings[CURLOPT_HEADERFUNCTION]) && (bool) $settings[CURLOPT_HEADERFUNCTION];
|
||||
|
||||
if ($separatedHeaders) {
|
||||
$resourceId = (int) $curlHandle;
|
||||
if (isset($this->headerLinesMap[$resourceId])) {
|
||||
$headers = $this->headerLinesMap[$resourceId];
|
||||
} else {
|
||||
$headers = [];
|
||||
}
|
||||
$response = $this->parseCurlResponse($headers, $response, $curlHandle);
|
||||
} else {
|
||||
$response = $this->parseCurlResult($response, $curlHandle);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the result of a curl call in a format that's a bit more
|
||||
* convenient to work with.
|
||||
*
|
||||
* The method returns an array with the following elements:
|
||||
* * status - one of the 3 STATUS constants.
|
||||
* * curl_errno - A curl error number. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * curl_errmsg - A current error message. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * response - Response object. Only set if status is STATUS_SUCCESS, or
|
||||
* STATUS_HTTPERROR.
|
||||
* * http_code - HTTP status code, as an int. Only set if Only set if
|
||||
* status is STATUS_SUCCESS, or STATUS_HTTPERROR
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function parseCurlResponse(array $headerLines, string $body, $curlHandle): array
|
||||
{
|
||||
list(
|
||||
$curlInfo,
|
||||
$curlErrNo,
|
||||
$curlErrMsg
|
||||
) = $this->curlStuff($curlHandle);
|
||||
|
||||
if ($curlErrNo) {
|
||||
return [
|
||||
'status' => self::STATUS_CURLERROR,
|
||||
'curl_errno' => $curlErrNo,
|
||||
'curl_errmsg' => $curlErrMsg,
|
||||
];
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatus($curlInfo['http_code']);
|
||||
$response->setBody($body);
|
||||
|
||||
foreach ($headerLines as $header) {
|
||||
$parts = explode(':', $header, 2);
|
||||
if (2 === count($parts)) {
|
||||
$response->addHeader(trim($parts[0]), trim($parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
$httpCode = $response->getStatus();
|
||||
|
||||
return [
|
||||
'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS,
|
||||
'response' => $response,
|
||||
'http_code' => $httpCode,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the result of a curl call in a format that's a bit more
|
||||
* convenient to work with.
|
||||
*
|
||||
* The method returns an array with the following elements:
|
||||
* * status - one of the 3 STATUS constants.
|
||||
* * curl_errno - A curl error number. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * curl_errmsg - A current error message. Only set if status is
|
||||
* STATUS_CURLERROR.
|
||||
* * response - Response object. Only set if status is STATUS_SUCCESS, or
|
||||
* STATUS_HTTPERROR.
|
||||
* * http_code - HTTP status code, as an int. Only set if Only set if
|
||||
* status is STATUS_SUCCESS, or STATUS_HTTPERROR
|
||||
*
|
||||
* @deprecated Use parseCurlResponse instead
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function parseCurlResult(string $response, $curlHandle): array
|
||||
{
|
||||
list(
|
||||
$curlInfo,
|
||||
$curlErrNo,
|
||||
$curlErrMsg
|
||||
) = $this->curlStuff($curlHandle);
|
||||
|
||||
if ($curlErrNo) {
|
||||
return [
|
||||
'status' => self::STATUS_CURLERROR,
|
||||
'curl_errno' => $curlErrNo,
|
||||
'curl_errmsg' => $curlErrMsg,
|
||||
];
|
||||
}
|
||||
|
||||
$headerBlob = substr($response, 0, $curlInfo['header_size']);
|
||||
// In the case of 204 No Content, strlen($response) == $curlInfo['header_size].
|
||||
// This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL
|
||||
// An exception will be thrown when calling getBodyAsString then
|
||||
$responseBody = substr($response, $curlInfo['header_size']) ?: '';
|
||||
|
||||
unset($response);
|
||||
|
||||
// In the case of 100 Continue, or redirects we'll have multiple lists
|
||||
// of headers for each separate HTTP response. We can easily split this
|
||||
// because they are separated by \r\n\r\n
|
||||
$headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
|
||||
|
||||
// We only care about the last set of headers
|
||||
$headerBlob = $headerBlob[count($headerBlob) - 1];
|
||||
|
||||
// Splitting headers
|
||||
$headerBlob = explode("\r\n", $headerBlob);
|
||||
|
||||
return $this->parseCurlResponse($headerBlob, $responseBody, $curlHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an asynchronous HTTP request.
|
||||
*
|
||||
* We keep this in a separate method, so we can call it without triggering
|
||||
* the beforeRequest event and don't do the poll().
|
||||
*/
|
||||
protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, int $retryCount = 0)
|
||||
{
|
||||
if (!$this->curlMultiHandle) {
|
||||
$this->curlMultiHandle = curl_multi_init();
|
||||
}
|
||||
$curl = curl_init();
|
||||
curl_setopt_array(
|
||||
$curl,
|
||||
$this->createCurlSettingsArray($request)
|
||||
);
|
||||
curl_multi_add_handle($this->curlMultiHandle, $curl);
|
||||
|
||||
$resourceId = (int) $curl;
|
||||
$this->headerLinesMap[$resourceId] = [];
|
||||
$this->curlMultiMap[$resourceId] = [
|
||||
$request,
|
||||
$success,
|
||||
$error,
|
||||
$retryCount,
|
||||
];
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
|
||||
/**
|
||||
* Calls curl_exec.
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function curlExec($curlHandle): string
|
||||
{
|
||||
$this->headerLinesMap[(int) $curlHandle] = [];
|
||||
|
||||
$result = curl_exec($curlHandle);
|
||||
if (false === $result) {
|
||||
$result = '';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bunch of information about a curl request.
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function curlStuff($curlHandle): array
|
||||
{
|
||||
return [
|
||||
curl_getinfo($curlHandle),
|
||||
curl_errno($curlHandle),
|
||||
curl_error($curlHandle),
|
||||
];
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This exception may be emitted by the HTTP\Client class, in case there was a
|
||||
* problem emitting the request.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ClientException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This exception represents a HTTP error coming from the Client.
|
||||
*
|
||||
* By default the Client will not emit these, this has to be explicitly enabled
|
||||
* with the setThrowExceptions method.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ClientHttpException extends \Exception implements HttpException
|
||||
{
|
||||
/**
|
||||
* Response object.
|
||||
*
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(ResponseInterface $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
parent::__construct($response->getStatusText(), $response->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* The http status code for the error.
|
||||
*/
|
||||
public function getHttpStatus(): int
|
||||
{
|
||||
return $this->response->getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full response object.
|
||||
*/
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* An exception representing a HTTP error.
|
||||
*
|
||||
* This can be used as a generic exception in your application, if you'd like
|
||||
* to map HTTP errors to exceptions.
|
||||
*
|
||||
* If you'd like to use this, create a new exception class, extending Exception
|
||||
* and implementing this interface.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface HttpException
|
||||
{
|
||||
/**
|
||||
* The http status code for the error.
|
||||
*
|
||||
* This may either be just the number, or a number and a human-readable
|
||||
* message, separated by a space.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHttpStatus();
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This is the abstract base class for both the Request and Response objects.
|
||||
*
|
||||
* This object contains a few simple methods that are shared by both.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class Message implements MessageInterface
|
||||
{
|
||||
/**
|
||||
* Request body.
|
||||
*
|
||||
* This should be a stream resource, string or a callback writing the body to php://output
|
||||
*
|
||||
* @var resource|string|callable
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
/**
|
||||
* Contains the list of HTTP headers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* HTTP message version (1.0, 1.1 or 2.0).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $httpVersion = '1.1';
|
||||
|
||||
/**
|
||||
* Returns the body as a readable stream resource.
|
||||
*
|
||||
* Note that the stream may not be rewindable, and therefore may only be
|
||||
* read once.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public function getBodyAsStream()
|
||||
{
|
||||
$body = $this->getBody();
|
||||
if (is_callable($this->body)) {
|
||||
$body = $this->getBodyAsString();
|
||||
}
|
||||
if (is_string($body) || null === $body) {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, (string) $body);
|
||||
rewind($stream);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body as a string.
|
||||
*
|
||||
* Note that because the underlying data may be based on a stream, this
|
||||
* method could only work correctly the first time.
|
||||
*/
|
||||
public function getBodyAsString(): string
|
||||
{
|
||||
$body = $this->getBody();
|
||||
if (is_string($body)) {
|
||||
return $body;
|
||||
}
|
||||
if (null === $body) {
|
||||
return '';
|
||||
}
|
||||
if (is_callable($body)) {
|
||||
ob_start();
|
||||
$body();
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
/**
|
||||
* @var string|int|null
|
||||
*/
|
||||
$contentLength = $this->getHeader('Content-Length');
|
||||
if (null !== $contentLength && (is_int($contentLength) || ctype_digit($contentLength))) {
|
||||
return stream_get_contents($body, (int) $contentLength);
|
||||
}
|
||||
|
||||
return stream_get_contents($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message body, as its internal representation.
|
||||
*
|
||||
* This could be either a string, a stream or a callback writing the body to php://output.
|
||||
*
|
||||
* @return resource|string|callable
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the body resource with a new stream, string or a callback writing the body to php://output.
|
||||
*
|
||||
* @param resource|string|callable $body
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the HTTP headers as an array.
|
||||
*
|
||||
* Every header is returned as an array, with one or more values.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->headers as $headerInfo) {
|
||||
$result[$headerInfo[0]] = $headerInfo[1];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return true or false, depending on if a HTTP header exists.
|
||||
*/
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return isset($this->headers[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific HTTP header, based on its name.
|
||||
*
|
||||
* The name must be treated as case-insensitive.
|
||||
* If the header does not exist, this method must return null.
|
||||
*
|
||||
* If a header appeared more than once in a HTTP request, this method will
|
||||
* concatenate all the values with a comma.
|
||||
*
|
||||
* Note that this not make sense for all headers. Some, such as
|
||||
* `Set-Cookie` cannot be logically combined with a comma. In those cases
|
||||
* you *should* use getHeaderAsArray().
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHeader(string $name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
if (isset($this->headers[$name])) {
|
||||
return implode(',', $this->headers[$name][1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a HTTP header as an array.
|
||||
*
|
||||
* For every time the HTTP header appeared in the request or response, an
|
||||
* item will appear in the array.
|
||||
*
|
||||
* If the header did not exist, this method will return an empty array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
if (isset($this->headers[$name])) {
|
||||
return $this->headers[$name][1];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a HTTP header.
|
||||
*
|
||||
* The case-sensitivity of the name value must be retained as-is.
|
||||
*
|
||||
* If the header already existed, it will be overwritten.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function setHeader(string $name, $value)
|
||||
{
|
||||
$this->headers[strtolower($name)] = [$name, (array) $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new set of HTTP headers.
|
||||
*
|
||||
* The headers array should contain headernames for keys, and their value
|
||||
* should be specified as either a string or an array.
|
||||
*
|
||||
* Any header that already existed will be overwritten.
|
||||
*/
|
||||
public function setHeaders(array $headers)
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
$this->setHeader($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a HTTP header.
|
||||
*
|
||||
* This method will not overwrite any existing HTTP header, but instead add
|
||||
* another value. Individual values can be retrieved with
|
||||
* getHeadersAsArray.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function addHeader(string $name, $value)
|
||||
{
|
||||
$lName = strtolower($name);
|
||||
if (isset($this->headers[$lName])) {
|
||||
$this->headers[$lName][1] = array_merge(
|
||||
$this->headers[$lName][1],
|
||||
(array) $value
|
||||
);
|
||||
} else {
|
||||
$this->headers[$lName] = [
|
||||
$name,
|
||||
(array) $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new set of HTTP headers.
|
||||
*
|
||||
* Any existing headers will not be overwritten.
|
||||
*/
|
||||
public function addHeaders(array $headers)
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
$this->addHeader($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a HTTP header.
|
||||
*
|
||||
* The specified header name must be treated as case-insensitive.
|
||||
* This method should return true if the header was successfully deleted,
|
||||
* and false if the header did not exist.
|
||||
*/
|
||||
public function removeHeader(string $name): bool
|
||||
{
|
||||
$name = strtolower($name);
|
||||
if (!isset($this->headers[$name])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->headers[$name]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP version.
|
||||
*
|
||||
* Should be 1.0, 1.1 or 2.0.
|
||||
*/
|
||||
public function setHttpVersion(string $version)
|
||||
{
|
||||
$this->httpVersion = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP version.
|
||||
*/
|
||||
public function getHttpVersion(): string
|
||||
{
|
||||
return $this->httpVersion;
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This trait contains a bunch of methods, shared by both the RequestDecorator
|
||||
* and the ResponseDecorator.
|
||||
*
|
||||
* Didn't seem needed to create a full class for this, so we're just
|
||||
* implementing it as a trait.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
trait MessageDecoratorTrait
|
||||
{
|
||||
/**
|
||||
* The inner request object.
|
||||
*
|
||||
* All method calls will be forwarded here.
|
||||
*
|
||||
* @var MessageInterface
|
||||
*/
|
||||
protected $inner;
|
||||
|
||||
/**
|
||||
* Returns the body as a readable stream resource.
|
||||
*
|
||||
* Note that the stream may not be rewindable, and therefore may only be
|
||||
* read once.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public function getBodyAsStream()
|
||||
{
|
||||
return $this->inner->getBodyAsStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body as a string.
|
||||
*
|
||||
* Note that because the underlying data may be based on a stream, this
|
||||
* method could only work correctly the first time.
|
||||
*/
|
||||
public function getBodyAsString(): string
|
||||
{
|
||||
return $this->inner->getBodyAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message body, as its internal representation.
|
||||
*
|
||||
* This could be either a string or a stream.
|
||||
*
|
||||
* @return resource|string
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->inner->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the body resource with a new stream.
|
||||
*
|
||||
* @param resource|string|callable $body
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->inner->setBody($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the HTTP headers as an array.
|
||||
*
|
||||
* Every header is returned as an array, with one or more values.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->inner->getHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return true or false, depending on if a HTTP header exists.
|
||||
*/
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return $this->inner->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific HTTP header, based on its name.
|
||||
*
|
||||
* The name must be treated as case-insensitive.
|
||||
* If the header does not exist, this method must return null.
|
||||
*
|
||||
* If a header appeared more than once in a HTTP request, this method will
|
||||
* concatenate all the values with a comma.
|
||||
*
|
||||
* Note that this not make sense for all headers. Some, such as
|
||||
* `Set-Cookie` cannot be logically combined with a comma. In those cases
|
||||
* you *should* use getHeaderAsArray().
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHeader(string $name)
|
||||
{
|
||||
return $this->inner->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a HTTP header as an array.
|
||||
*
|
||||
* For every time the HTTP header appeared in the request or response, an
|
||||
* item will appear in the array.
|
||||
*
|
||||
* If the header did not exist, this method will return an empty array.
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array
|
||||
{
|
||||
return $this->inner->getHeaderAsArray($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a HTTP header.
|
||||
*
|
||||
* The case-sensitivity of the name value must be retained as-is.
|
||||
*
|
||||
* If the header already existed, it will be overwritten.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function setHeader(string $name, $value)
|
||||
{
|
||||
$this->inner->setHeader($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new set of HTTP headers.
|
||||
*
|
||||
* The headers array should contain headernames for keys, and their value
|
||||
* should be specified as either a string or an array.
|
||||
*
|
||||
* Any header that already existed will be overwritten.
|
||||
*/
|
||||
public function setHeaders(array $headers)
|
||||
{
|
||||
$this->inner->setHeaders($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a HTTP header.
|
||||
*
|
||||
* This method will not overwrite any existing HTTP header, but instead add
|
||||
* another value. Individual values can be retrieved with
|
||||
* getHeadersAsArray.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function addHeader(string $name, $value)
|
||||
{
|
||||
$this->inner->addHeader($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new set of HTTP headers.
|
||||
*
|
||||
* Any existing headers will not be overwritten.
|
||||
*/
|
||||
public function addHeaders(array $headers)
|
||||
{
|
||||
$this->inner->addHeaders($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a HTTP header.
|
||||
*
|
||||
* The specified header name must be treated as case-insensitive.
|
||||
* This method should return true if the header was successfully deleted,
|
||||
* and false if the header did not exist.
|
||||
*/
|
||||
public function removeHeader(string $name): bool
|
||||
{
|
||||
return $this->inner->removeHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP version.
|
||||
*
|
||||
* Should be 1.0, 1.1 or 2.0.
|
||||
*/
|
||||
public function setHttpVersion(string $version)
|
||||
{
|
||||
$this->inner->setHttpVersion($version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP version.
|
||||
*/
|
||||
public function getHttpVersion(): string
|
||||
{
|
||||
return $this->inner->getHttpVersion();
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* The MessageInterface is the base interface that's used by both
|
||||
* the RequestInterface and ResponseInterface.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface MessageInterface
|
||||
{
|
||||
/**
|
||||
* Returns the body as a readable stream resource.
|
||||
*
|
||||
* Note that the stream may not be rewindable, and therefore may only be
|
||||
* read once.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public function getBodyAsStream();
|
||||
|
||||
/**
|
||||
* Returns the body as a string.
|
||||
*
|
||||
* Note that because the underlying data may be based on a stream, this
|
||||
* method could only work correctly the first time.
|
||||
*/
|
||||
public function getBodyAsString(): string;
|
||||
|
||||
/**
|
||||
* Returns the message body, as its internal representation.
|
||||
*
|
||||
* This could be either a string, a stream or a callback writing the body to php://output
|
||||
*
|
||||
* @return resource|string|callable
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Updates the body resource with a new stream.
|
||||
*
|
||||
* @param resource|string|callable $body
|
||||
*/
|
||||
public function setBody($body);
|
||||
|
||||
/**
|
||||
* Returns all the HTTP headers as an array.
|
||||
*
|
||||
* Every header is returned as an array, with one or more values.
|
||||
*/
|
||||
public function getHeaders(): array;
|
||||
|
||||
/**
|
||||
* Will return true or false, depending on if a HTTP header exists.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Returns a specific HTTP header, based on its name.
|
||||
*
|
||||
* The name must be treated as case-insensitive.
|
||||
* If the header does not exist, this method must return null.
|
||||
*
|
||||
* If a header appeared more than once in a HTTP request, this method will
|
||||
* concatenate all the values with a comma.
|
||||
*
|
||||
* Note that this not make sense for all headers. Some, such as
|
||||
* `Set-Cookie` cannot be logically combined with a comma. In those cases
|
||||
* you *should* use getHeaderAsArray().
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHeader(string $name);
|
||||
|
||||
/**
|
||||
* Returns a HTTP header as an array.
|
||||
*
|
||||
* For every time the HTTP header appeared in the request or response, an
|
||||
* item will appear in the array.
|
||||
*
|
||||
* If the header did not exist, this method will return an empty array.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaderAsArray(string $name): array;
|
||||
|
||||
/**
|
||||
* Updates a HTTP header.
|
||||
*
|
||||
* The case-sensitivity of the name value must be retained as-is.
|
||||
*
|
||||
* If the header already existed, it will be overwritten.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function setHeader(string $name, $value);
|
||||
|
||||
/**
|
||||
* Sets a new set of HTTP headers.
|
||||
*
|
||||
* The headers array should contain headernames for keys, and their value
|
||||
* should be specified as either a string or an array.
|
||||
*
|
||||
* Any header that already existed will be overwritten.
|
||||
*/
|
||||
public function setHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Adds a HTTP header.
|
||||
*
|
||||
* This method will not overwrite any existing HTTP header, but instead add
|
||||
* another value. Individual values can be retrieved with
|
||||
* getHeadersAsArray.
|
||||
*
|
||||
* @param string|string[] $value
|
||||
*/
|
||||
public function addHeader(string $name, $value);
|
||||
|
||||
/**
|
||||
* Adds a new set of HTTP headers.
|
||||
*
|
||||
* Any existing headers will not be overwritten.
|
||||
*/
|
||||
public function addHeaders(array $headers);
|
||||
|
||||
/**
|
||||
* Removes a HTTP header.
|
||||
*
|
||||
* The specified header name must be treated as case-insensitive.
|
||||
* This method should return true if the header was successfully deleted,
|
||||
* and false if the header did not exist.
|
||||
*/
|
||||
public function removeHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Sets the HTTP version.
|
||||
*
|
||||
* Should be 1.0, 1.1 or 2.0.
|
||||
*/
|
||||
public function setHttpVersion(string $version);
|
||||
|
||||
/**
|
||||
* Returns the HTTP version.
|
||||
*/
|
||||
public function getHttpVersion(): string;
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
use Sabre\Uri;
|
||||
|
||||
/**
|
||||
* The Request class represents a single HTTP request.
|
||||
*
|
||||
* You can either simply construct the object from scratch, or if you need
|
||||
* access to the current HTTP request, use Sapi::getRequest.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Request extends Message implements RequestInterface
|
||||
{
|
||||
/**
|
||||
* HTTP Method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $method;
|
||||
|
||||
/**
|
||||
* Request Url.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Creates the request object.
|
||||
*
|
||||
* @param resource|callable|string $body
|
||||
*/
|
||||
public function __construct(string $method, string $url, array $headers = [], $body = null)
|
||||
{
|
||||
$this->setMethod($method);
|
||||
$this->setUrl($url);
|
||||
$this->setHeaders($headers);
|
||||
$this->setBody($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current HTTP method.
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP method.
|
||||
*/
|
||||
public function setMethod(string $method)
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request url.
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request url.
|
||||
*/
|
||||
public function setUrl(string $url)
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of query parameters.
|
||||
*
|
||||
* This is equivalent to PHP's $_GET superglobal.
|
||||
*/
|
||||
public function getQueryParameters(): array
|
||||
{
|
||||
$url = $this->getUrl();
|
||||
if (false === ($index = strpos($url, '?'))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
parse_str(substr($url, $index + 1), $queryParams);
|
||||
|
||||
return $queryParams;
|
||||
}
|
||||
|
||||
protected $absoluteUrl;
|
||||
|
||||
/**
|
||||
* Sets the absolute url.
|
||||
*/
|
||||
public function setAbsoluteUrl(string $url)
|
||||
{
|
||||
$this->absoluteUrl = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url.
|
||||
*/
|
||||
public function getAbsoluteUrl(): string
|
||||
{
|
||||
if (!$this->absoluteUrl) {
|
||||
$url = $this->getUrl();
|
||||
if (parse_url($url, PHP_URL_SCHEME)) {
|
||||
// It's already an absolute URL
|
||||
$this->absoluteUrl = $url;
|
||||
} else {
|
||||
$host = $this->getHeader('Host')
|
||||
?? parse_url($url, PHP_URL_HOST)
|
||||
?? 'localhost';
|
||||
// Guessing we're a http endpoint.
|
||||
$this->absoluteUrl = "http://$host$url";
|
||||
}
|
||||
}
|
||||
|
||||
return $this->absoluteUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base url.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $baseUrl = '/';
|
||||
|
||||
/**
|
||||
* Sets a base url.
|
||||
*
|
||||
* This url is used for relative path calculations.
|
||||
*/
|
||||
public function setBaseUrl(string $url)
|
||||
{
|
||||
$this->baseUrl = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current base url.
|
||||
*/
|
||||
public function getBaseUrl(): string
|
||||
{
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path.
|
||||
*
|
||||
* This is being calculated using the base url. This path will not start
|
||||
* with a slash, so it will always return something like
|
||||
* 'example/path.html'.
|
||||
*
|
||||
* If the full path is equal to the base url, this method will return an
|
||||
* empty string.
|
||||
*
|
||||
* This method will also urldecode the path, and if the url was encoded as
|
||||
* ISO-8859-1, it will convert it to UTF-8.
|
||||
*
|
||||
* If the path is outside of the base url, a LogicException will be thrown.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
// Removing duplicated slashes.
|
||||
$uri = str_replace('//', '/', $this->getUrl());
|
||||
|
||||
$uri = Uri\normalize($uri);
|
||||
$baseUri = Uri\normalize($this->getBaseUrl());
|
||||
|
||||
if (0 === strpos($uri, $baseUri)) {
|
||||
// We're not interested in the query part (everything after the ?).
|
||||
list($uri) = explode('?', $uri);
|
||||
|
||||
return trim(decodePath(substr($uri, strlen($baseUri))), '/');
|
||||
}
|
||||
|
||||
if ($uri.'/' === $baseUri) {
|
||||
return '';
|
||||
}
|
||||
// A special case, if the baseUri was accessed without a trailing
|
||||
// slash, we'll accept it as well.
|
||||
|
||||
throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of PHP's $_POST.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $postData = [];
|
||||
|
||||
/**
|
||||
* Sets the post data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*
|
||||
* This would not have been needed, if POST data was accessible as
|
||||
* php://input, but unfortunately we need to special case it.
|
||||
*/
|
||||
public function setPostData(array $postData)
|
||||
{
|
||||
$this->postData = $postData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the POST data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*/
|
||||
public function getPostData(): array
|
||||
{
|
||||
return $this->postData;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array containing the raw _SERVER array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rawServerData;
|
||||
|
||||
/**
|
||||
* Returns an item from the _SERVER array.
|
||||
*
|
||||
* If the value does not exist in the array, null is returned.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRawServerValue(string $valueName)
|
||||
{
|
||||
return $this->rawServerData[$valueName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the _SERVER array.
|
||||
*/
|
||||
public function setRawServerData(array $data)
|
||||
{
|
||||
$this->rawServerData = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the request object as a string.
|
||||
*
|
||||
* This is useful for debugging purposes.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n";
|
||||
|
||||
foreach ($this->getHeaders() as $key => $value) {
|
||||
foreach ($value as $v) {
|
||||
if ('Authorization' === $key) {
|
||||
list($v) = explode(' ', $v, 2);
|
||||
$v .= ' REDACTED';
|
||||
}
|
||||
$out .= $key.': '.$v."\r\n";
|
||||
}
|
||||
}
|
||||
$out .= "\r\n";
|
||||
$out .= $this->getBodyAsString();
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* Request Decorator.
|
||||
*
|
||||
* This helper class allows you to easily create decorators for the Request
|
||||
* object.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class RequestDecorator implements RequestInterface
|
||||
{
|
||||
use MessageDecoratorTrait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(RequestInterface $inner)
|
||||
{
|
||||
$this->inner = $inner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current HTTP method.
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->inner->getMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP method.
|
||||
*/
|
||||
public function setMethod(string $method)
|
||||
{
|
||||
$this->inner->setMethod($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request url.
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->inner->getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request url.
|
||||
*/
|
||||
public function setUrl(string $url)
|
||||
{
|
||||
$this->inner->setUrl($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url.
|
||||
*/
|
||||
public function getAbsoluteUrl(): string
|
||||
{
|
||||
return $this->inner->getAbsoluteUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the absolute url.
|
||||
*/
|
||||
public function setAbsoluteUrl(string $url)
|
||||
{
|
||||
$this->inner->setAbsoluteUrl($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current base url.
|
||||
*/
|
||||
public function getBaseUrl(): string
|
||||
{
|
||||
return $this->inner->getBaseUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a base url.
|
||||
*
|
||||
* This url is used for relative path calculations.
|
||||
*
|
||||
* The base url should default to /
|
||||
*/
|
||||
public function setBaseUrl(string $url)
|
||||
{
|
||||
$this->inner->setBaseUrl($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path.
|
||||
*
|
||||
* This is being calculated using the base url. This path will not start
|
||||
* with a slash, so it will always return something like
|
||||
* 'example/path.html'.
|
||||
*
|
||||
* If the full path is equal to the base url, this method will return an
|
||||
* empty string.
|
||||
*
|
||||
* This method will also urldecode the path, and if the url was encoded as
|
||||
* ISO-8859-1, it will convert it to UTF-8.
|
||||
*
|
||||
* If the path is outside of the base url, a LogicException will be thrown.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->inner->getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of query parameters.
|
||||
*
|
||||
* This is equivalent to PHP's $_GET superglobal.
|
||||
*/
|
||||
public function getQueryParameters(): array
|
||||
{
|
||||
return $this->inner->getQueryParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the POST data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*/
|
||||
public function getPostData(): array
|
||||
{
|
||||
return $this->inner->getPostData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the post data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*
|
||||
* This would not have been needed, if POST data was accessible as
|
||||
* php://input, but unfortunately we need to special case it.
|
||||
*/
|
||||
public function setPostData(array $postData)
|
||||
{
|
||||
$this->inner->setPostData($postData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an item from the _SERVER array.
|
||||
*
|
||||
* If the value does not exist in the array, null is returned.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRawServerValue(string $valueName)
|
||||
{
|
||||
return $this->inner->getRawServerValue($valueName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the _SERVER array.
|
||||
*/
|
||||
public function setRawServerData(array $data)
|
||||
{
|
||||
$this->inner->setRawServerData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the request object as a string.
|
||||
*
|
||||
* This is useful for debugging purposes.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->inner->__toString();
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* The RequestInterface represents a HTTP request.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface RequestInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Returns the current HTTP method.
|
||||
*/
|
||||
public function getMethod(): string;
|
||||
|
||||
/**
|
||||
* Sets the HTTP method.
|
||||
*/
|
||||
public function setMethod(string $method);
|
||||
|
||||
/**
|
||||
* Returns the request url.
|
||||
*/
|
||||
public function getUrl(): string;
|
||||
|
||||
/**
|
||||
* Sets the request url.
|
||||
*/
|
||||
public function setUrl(string $url);
|
||||
|
||||
/**
|
||||
* Returns the absolute url.
|
||||
*/
|
||||
public function getAbsoluteUrl(): string;
|
||||
|
||||
/**
|
||||
* Sets the absolute url.
|
||||
*/
|
||||
public function setAbsoluteUrl(string $url);
|
||||
|
||||
/**
|
||||
* Returns the current base url.
|
||||
*/
|
||||
public function getBaseUrl(): string;
|
||||
|
||||
/**
|
||||
* Sets a base url.
|
||||
*
|
||||
* This url is used for relative path calculations.
|
||||
*
|
||||
* The base url should default to /
|
||||
*/
|
||||
public function setBaseUrl(string $url);
|
||||
|
||||
/**
|
||||
* Returns the relative path.
|
||||
*
|
||||
* This is being calculated using the base url. This path will not start
|
||||
* with a slash, so it will always return something like
|
||||
* 'example/path.html'.
|
||||
*
|
||||
* If the full path is equal to the base url, this method will return an
|
||||
* empty string.
|
||||
*
|
||||
* This method will also urldecode the path, and if the url was encoded as
|
||||
* ISO-8859-1, it will convert it to UTF-8.
|
||||
*
|
||||
* If the path is outside of the base url, a LogicException will be thrown.
|
||||
*/
|
||||
public function getPath(): string;
|
||||
|
||||
/**
|
||||
* Returns the list of query parameters.
|
||||
*
|
||||
* This is equivalent to PHP's $_GET superglobal.
|
||||
*/
|
||||
public function getQueryParameters(): array;
|
||||
|
||||
/**
|
||||
* Returns the POST data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*/
|
||||
public function getPostData(): array;
|
||||
|
||||
/**
|
||||
* Sets the post data.
|
||||
*
|
||||
* This is equivalent to PHP's $_POST superglobal.
|
||||
*
|
||||
* This would not have been needed, if POST data was accessible as
|
||||
* php://input, but unfortunately we need to special case it.
|
||||
*/
|
||||
public function setPostData(array $postData);
|
||||
|
||||
/**
|
||||
* Returns an item from the _SERVER array.
|
||||
*
|
||||
* If the value does not exist in the array, null is returned.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRawServerValue(string $valueName);
|
||||
|
||||
/**
|
||||
* Sets the _SERVER array.
|
||||
*/
|
||||
public function setRawServerData(array $data);
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This class represents a single HTTP response.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Response extends Message implements ResponseInterface
|
||||
{
|
||||
/**
|
||||
* This is the list of currently registered HTTP status codes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $statusCodes = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status', // RFC 4918
|
||||
208 => 'Already Reported', // RFC 5842
|
||||
226 => 'IM Used', // RFC 3229
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
308 => 'Permanent Redirect',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Requested Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
418 => 'I\'m a teapot', // RFC 2324
|
||||
421 => 'Misdirected Request', // RFC7540 (HTTP/2)
|
||||
422 => 'Unprocessable Entity', // RFC 4918
|
||||
423 => 'Locked', // RFC 4918
|
||||
424 => 'Failed Dependency', // RFC 4918
|
||||
426 => 'Upgrade Required',
|
||||
428 => 'Precondition Required', // RFC 6585
|
||||
429 => 'Too Many Requests', // RFC 6585
|
||||
431 => 'Request Header Fields Too Large', // RFC 6585
|
||||
451 => 'Unavailable For Legal Reasons', // draft-tbray-http-legally-restricted-status
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version not supported',
|
||||
506 => 'Variant Also Negotiates',
|
||||
507 => 'Insufficient Storage', // RFC 4918
|
||||
508 => 'Loop Detected', // RFC 5842
|
||||
509 => 'Bandwidth Limit Exceeded', // non-standard
|
||||
510 => 'Not extended',
|
||||
511 => 'Network Authentication Required', // RFC 6585
|
||||
];
|
||||
|
||||
/**
|
||||
* HTTP status code.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* HTTP status text.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $statusText;
|
||||
|
||||
/**
|
||||
* Creates the response object.
|
||||
*
|
||||
* @param string|int $status
|
||||
* @param resource $body
|
||||
*/
|
||||
public function __construct($status = 500, ?array $headers = null, $body = null)
|
||||
{
|
||||
if (null !== $status) {
|
||||
$this->setStatus($status);
|
||||
}
|
||||
if (null !== $headers) {
|
||||
$this->setHeaders($headers);
|
||||
}
|
||||
if (null !== $body) {
|
||||
$this->setBody($body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current HTTP status code.
|
||||
*/
|
||||
public function getStatus(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable status string.
|
||||
*
|
||||
* In the case of a 200, this may for example be 'OK'.
|
||||
*/
|
||||
public function getStatusText(): string
|
||||
{
|
||||
return $this->statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code.
|
||||
*
|
||||
* This can be either the full HTTP status code with human-readable string,
|
||||
* for example: "403 I can't let you do that, Dave".
|
||||
*
|
||||
* Or just the code, in which case the appropriate default message will be
|
||||
* added.
|
||||
*
|
||||
* @param string|int $status
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
if (is_int($status) || ctype_digit($status)) {
|
||||
$statusCode = $status;
|
||||
$statusText = self::$statusCodes[$status] ?? 'Unknown';
|
||||
} else {
|
||||
list(
|
||||
$statusCode,
|
||||
$statusText
|
||||
) = explode(' ', $status, 2);
|
||||
$statusCode = (int) $statusCode;
|
||||
}
|
||||
if ($statusCode < 100 || $statusCode > 999) {
|
||||
throw new \InvalidArgumentException('The HTTP status code must be exactly 3 digits');
|
||||
}
|
||||
|
||||
$this->status = $statusCode;
|
||||
$this->statusText = $statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the response object as a string.
|
||||
*
|
||||
* This is useful for debugging purposes.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$str = 'HTTP/'.$this->httpVersion.' '.$this->getStatus().' '.$this->getStatusText()."\r\n";
|
||||
foreach ($this->getHeaders() as $key => $value) {
|
||||
foreach ($value as $v) {
|
||||
$str .= $key.': '.$v."\r\n";
|
||||
}
|
||||
}
|
||||
$str .= "\r\n";
|
||||
$str .= $this->getBodyAsString();
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* Response Decorator.
|
||||
*
|
||||
* This helper class allows you to easily create decorators for the Response
|
||||
* object.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ResponseDecorator implements ResponseInterface
|
||||
{
|
||||
use MessageDecoratorTrait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(ResponseInterface $inner)
|
||||
{
|
||||
$this->inner = $inner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current HTTP status code.
|
||||
*/
|
||||
public function getStatus(): int
|
||||
{
|
||||
return $this->inner->getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable status string.
|
||||
*
|
||||
* In the case of a 200, this may for example be 'OK'.
|
||||
*/
|
||||
public function getStatusText(): string
|
||||
{
|
||||
return $this->inner->getStatusText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code.
|
||||
*
|
||||
* This can be either the full HTTP status code with human-readable string,
|
||||
* for example: "403 I can't let you do that, Dave".
|
||||
*
|
||||
* Or just the code, in which case the appropriate default message will be
|
||||
* added.
|
||||
*
|
||||
* @param string|int $status
|
||||
*/
|
||||
public function setStatus($status)
|
||||
{
|
||||
$this->inner->setStatus($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the request object as a string.
|
||||
*
|
||||
* This is useful for debugging purposes.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->inner->__toString();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This interface represents a HTTP response.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
interface ResponseInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Returns the current HTTP status code.
|
||||
*/
|
||||
public function getStatus(): int;
|
||||
|
||||
/**
|
||||
* Returns the human-readable status string.
|
||||
*
|
||||
* In the case of a 200, this may for example be 'OK'.
|
||||
*/
|
||||
public function getStatusText(): string;
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code.
|
||||
*
|
||||
* This can be either the full HTTP status code with human-readable string,
|
||||
* for example: "403 I can't let you do that, Dave".
|
||||
*
|
||||
* Or just the code, in which case the appropriate default message will be
|
||||
* added.
|
||||
*
|
||||
* @param string|int $status
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setStatus($status);
|
||||
}
|
||||
Vendored
+240
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* PHP SAPI.
|
||||
*
|
||||
* This object is responsible for:
|
||||
* 1. Constructing a Request object based on the current HTTP request sent to
|
||||
* the PHP process.
|
||||
* 2. Sending the Response object back to the client.
|
||||
*
|
||||
* It could be said that this class provides a mapping between the Request and
|
||||
* Response objects, and php's:
|
||||
*
|
||||
* * $_SERVER
|
||||
* * $_POST
|
||||
* * $_FILES
|
||||
* * php://input
|
||||
* * echo()
|
||||
* * header()
|
||||
* * php://output
|
||||
*
|
||||
* You can choose to either call all these methods statically, but you can also
|
||||
* instantiate this as an object to allow for polymorphism.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Sapi
|
||||
{
|
||||
/**
|
||||
* This static method will create a new Request object, based on the
|
||||
* current PHP request.
|
||||
*/
|
||||
public static function getRequest(): Request
|
||||
{
|
||||
$serverArr = $_SERVER;
|
||||
|
||||
if ('cli' === PHP_SAPI) {
|
||||
// If we're running off the CLI, we're going to set some default
|
||||
// settings.
|
||||
$serverArr['REQUEST_URI'] = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$serverArr['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
|
||||
}
|
||||
|
||||
$r = self::createFromServerArray($serverArr);
|
||||
$r->setBody(fopen('php://input', 'r'));
|
||||
$r->setPostData($_POST);
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the HTTP response back to a HTTP client.
|
||||
*
|
||||
* This calls php's header() function and streams the body to php://output.
|
||||
*/
|
||||
public static function sendResponse(ResponseInterface $response)
|
||||
{
|
||||
header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText());
|
||||
foreach ($response->getHeaders() as $key => $value) {
|
||||
foreach ($value as $k => $v) {
|
||||
if (0 === $k) {
|
||||
header($key.': '.$v);
|
||||
} else {
|
||||
header($key.': '.$v, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = $response->getBody();
|
||||
if (null === $body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_callable($body)) {
|
||||
$body();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$contentLength = $response->getHeader('Content-Length');
|
||||
if (null !== $contentLength) {
|
||||
$output = fopen('php://output', 'wb');
|
||||
if (is_resource($body) && 'stream' == get_resource_type($body)) {
|
||||
// a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119
|
||||
$left = (int) $contentLength;
|
||||
// copy with 4MiB chunks
|
||||
$chunk_size = 4 * 1024 * 1024;
|
||||
stream_set_chunk_size($output, $chunk_size);
|
||||
// If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size.
|
||||
$contentRange = $response->getHeader('Content-Range');
|
||||
// Matching "Content-Range: bytes 1234-5678/7890"
|
||||
if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) {
|
||||
// 4kB should be the default page size on most architectures
|
||||
$pageSize = 4096;
|
||||
$offset = (int) $matches[1];
|
||||
$delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0;
|
||||
if ($delta > 0) {
|
||||
$left -= stream_copy_to_stream($body, $output, min($delta, $left));
|
||||
}
|
||||
}
|
||||
while ($left > 0) {
|
||||
$copied = stream_copy_to_stream($body, $output, min($left, $chunk_size));
|
||||
// stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure
|
||||
// But when the $maxLength is greater than the total number of bytes remaining in the stream,
|
||||
// It returns the negative number of bytes copied
|
||||
// So break the loop in such cases.
|
||||
if ($copied <= 0) {
|
||||
break;
|
||||
}
|
||||
// Abort on client disconnect.
|
||||
// With ignore_user_abort(true), the script is not aborted on client disconnect.
|
||||
// To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there.
|
||||
if (1 === ignore_user_abort() && 1 === connection_aborted()) {
|
||||
break;
|
||||
}
|
||||
$left -= $copied;
|
||||
}
|
||||
} else {
|
||||
fwrite($output, $body, (int) $contentLength);
|
||||
}
|
||||
} else {
|
||||
file_put_contents('php://output', $body);
|
||||
}
|
||||
|
||||
if (is_resource($body)) {
|
||||
fclose($body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This static method will create a new Request object, based on a PHP
|
||||
* $_SERVER array.
|
||||
*
|
||||
* REQUEST_URI and REQUEST_METHOD are required.
|
||||
*/
|
||||
public static function createFromServerArray(array $serverArray): Request
|
||||
{
|
||||
$headers = [];
|
||||
$method = null;
|
||||
$url = null;
|
||||
$httpVersion = '1.1';
|
||||
|
||||
$protocol = 'http';
|
||||
$hostName = 'localhost';
|
||||
|
||||
foreach ($serverArray as $key => $value) {
|
||||
$key = (string) $key;
|
||||
switch ($key) {
|
||||
case 'SERVER_PROTOCOL':
|
||||
if ('HTTP/1.0' === $value) {
|
||||
$httpVersion = '1.0';
|
||||
} elseif ('HTTP/2.0' === $value) {
|
||||
$httpVersion = '2.0';
|
||||
}
|
||||
break;
|
||||
case 'REQUEST_METHOD':
|
||||
$method = $value;
|
||||
break;
|
||||
case 'REQUEST_URI':
|
||||
$url = $value;
|
||||
break;
|
||||
|
||||
// These sometimes show up without a HTTP_ prefix
|
||||
case 'CONTENT_TYPE':
|
||||
$headers['Content-Type'] = $value;
|
||||
break;
|
||||
case 'CONTENT_LENGTH':
|
||||
$headers['Content-Length'] = $value;
|
||||
break;
|
||||
|
||||
// mod_php on apache will put credentials in these variables.
|
||||
// (fast)cgi does not usually do this, however.
|
||||
case 'PHP_AUTH_USER':
|
||||
if (isset($serverArray['PHP_AUTH_PW'])) {
|
||||
$headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']);
|
||||
}
|
||||
break;
|
||||
|
||||
// Similarly, mod_php may also screw around with digest auth.
|
||||
case 'PHP_AUTH_DIGEST':
|
||||
$headers['Authorization'] = 'Digest '.$value;
|
||||
break;
|
||||
|
||||
// Apache may prefix the HTTP_AUTHORIZATION header with
|
||||
// REDIRECT_, if mod_rewrite was used.
|
||||
case 'REDIRECT_HTTP_AUTHORIZATION':
|
||||
$headers['Authorization'] = $value;
|
||||
break;
|
||||
|
||||
case 'HTTP_HOST':
|
||||
$hostName = $value;
|
||||
$headers['Host'] = $value;
|
||||
break;
|
||||
|
||||
case 'HTTPS':
|
||||
if (!empty($value) && 'off' !== $value) {
|
||||
$protocol = 'https';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if ('HTTP_' === substr($key, 0, 5)) {
|
||||
// It's a HTTP header
|
||||
|
||||
// Normalizing it to be prettier
|
||||
$header = strtolower(substr($key, 5));
|
||||
|
||||
// Transforming dashes into spaces, and upper-casing
|
||||
// every first letter.
|
||||
$header = ucwords(str_replace('_', ' ', $header));
|
||||
|
||||
// Turning spaces into dashes.
|
||||
$header = str_replace(' ', '-', $header);
|
||||
$headers[$header] = $value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $url) {
|
||||
throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_URI key');
|
||||
}
|
||||
|
||||
if (null === $method) {
|
||||
throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key');
|
||||
}
|
||||
$r = new Request($method, $url, $headers);
|
||||
$r->setHttpVersion($httpVersion);
|
||||
$r->setRawServerData($serverArray);
|
||||
$r->setAbsoluteUrl($protocol.'://'.$hostName.$url);
|
||||
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* This class contains the version number for the HTTP package.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Version
|
||||
{
|
||||
/**
|
||||
* Full version number.
|
||||
*/
|
||||
public const VERSION = '5.1.13';
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
/**
|
||||
* A collection of useful helpers for parsing or generating various HTTP
|
||||
* headers.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a HTTP date-string.
|
||||
*
|
||||
* This method returns false if the date is invalid.
|
||||
*
|
||||
* The following formats are supported:
|
||||
* Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
|
||||
* Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
|
||||
* Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
|
||||
*
|
||||
* See:
|
||||
* http://tools.ietf.org/html/rfc7231#section-7.1.1.1
|
||||
*
|
||||
* @return bool|\DateTime
|
||||
*/
|
||||
function parseDate(string $dateString)
|
||||
{
|
||||
// Only the format is checked, valid ranges are checked by strtotime below
|
||||
$month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
|
||||
$weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
|
||||
$wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
|
||||
$time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}';
|
||||
$date3 = $month.' ([12]\d|3[01]| [1-9])';
|
||||
$date2 = '(0[1-9]|[12]\d|3[01])\-'.$month.'\-\d{2}';
|
||||
// 4-digit year cannot begin with 0 - unix timestamp begins in 1970
|
||||
$date1 = '(0[1-9]|[12]\d|3[01]) '.$month.' [1-9]\d{3}';
|
||||
|
||||
// ANSI C's asctime() format
|
||||
// 4-digit year cannot begin with 0 - unix timestamp begins in 1970
|
||||
$asctime_date = $wkday.' '.$date3.' '.$time.' [1-9]\d{3}';
|
||||
// RFC 850, obsoleted by RFC 1036
|
||||
$rfc850_date = $weekday.', '.$date2.' '.$time.' GMT';
|
||||
// RFC 822, updated by RFC 1123
|
||||
$rfc1123_date = $wkday.', '.$date1.' '.$time.' GMT';
|
||||
// allowed date formats by RFC 2616
|
||||
$HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)";
|
||||
|
||||
// allow for space around the string and strip it
|
||||
$dateString = trim($dateString, ' ');
|
||||
if (!preg_match('/^'.$HTTP_date.'$/', $dateString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// append implicit GMT timezone to ANSI C time format
|
||||
if (false === strpos($dateString, ' GMT')) {
|
||||
$dateString .= ' GMT';
|
||||
}
|
||||
|
||||
try {
|
||||
return new \DateTime($dateString, new \DateTimeZone('UTC'));
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a DateTime object to a valid HTTP/1.1 Date header value.
|
||||
*/
|
||||
function toDate(\DateTime $dateTime): string
|
||||
{
|
||||
// We need to clone it, as we don't want to affect the existing
|
||||
// DateTime.
|
||||
$dateTime = clone $dateTime;
|
||||
$dateTime->setTimezone(new \DateTimeZone('GMT'));
|
||||
|
||||
return $dateTime->format('D, d M Y H:i:s \G\M\T');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function can be used to aid with content negotiation.
|
||||
*
|
||||
* It takes 2 arguments, the $acceptHeaderValue, which usually comes from
|
||||
* an Accept header, and $availableOptions, which contains an array of
|
||||
* items that the server can support.
|
||||
*
|
||||
* The result of this function will be the 'best possible option'. If no
|
||||
* best possible option could be found, null is returned.
|
||||
*
|
||||
* When it's null you can according to the spec either return a default, or
|
||||
* you can choose to emit 406 Not Acceptable.
|
||||
*
|
||||
* The method also accepts sending 'null' for the $acceptHeaderValue,
|
||||
* implying that no accept header was sent.
|
||||
*
|
||||
* @param string|null $acceptHeaderValue
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
function negotiateContentType($acceptHeaderValue, array $availableOptions)
|
||||
{
|
||||
if (!$acceptHeaderValue) {
|
||||
// Grabbing the first in the list.
|
||||
return reset($availableOptions);
|
||||
}
|
||||
|
||||
$proposals = array_map(
|
||||
'Sabre\HTTP\parseMimeType',
|
||||
explode(',', $acceptHeaderValue)
|
||||
);
|
||||
|
||||
// Ensuring array keys are reset.
|
||||
$availableOptions = array_values($availableOptions);
|
||||
|
||||
$options = array_map(
|
||||
'Sabre\HTTP\parseMimeType',
|
||||
$availableOptions
|
||||
);
|
||||
|
||||
$lastQuality = 0;
|
||||
$lastSpecificity = 0;
|
||||
$lastOptionIndex = 0;
|
||||
$lastChoice = null;
|
||||
|
||||
foreach ($proposals as $proposal) {
|
||||
// Ignoring broken values.
|
||||
if (null === $proposal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the quality is lower we don't have to bother comparing.
|
||||
if ($proposal['quality'] < $lastQuality) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($options as $optionIndex => $option) {
|
||||
if ('*' !== $proposal['type'] && $proposal['type'] !== $option['type']) {
|
||||
// no match on type.
|
||||
continue;
|
||||
}
|
||||
if ('*' !== $proposal['subType'] && $proposal['subType'] !== $option['subType']) {
|
||||
// no match on subtype.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any parameters appearing on the options must appear on
|
||||
// proposals.
|
||||
foreach ($option['parameters'] as $paramName => $paramValue) {
|
||||
if (!array_key_exists($paramName, $proposal['parameters'])) {
|
||||
continue 2;
|
||||
}
|
||||
if ($paramValue !== $proposal['parameters'][$paramName]) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we have a match on parameters, type and
|
||||
// subtype. We need to calculate a score for how specific the
|
||||
// match was.
|
||||
$specificity =
|
||||
('*' !== $proposal['type'] ? 20 : 0) +
|
||||
('*' !== $proposal['subType'] ? 10 : 0) +
|
||||
count($option['parameters']);
|
||||
|
||||
// Does this entry win?
|
||||
if (
|
||||
($proposal['quality'] > $lastQuality)
|
||||
|| ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity)
|
||||
|| ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex)
|
||||
) {
|
||||
$lastQuality = $proposal['quality'];
|
||||
$lastSpecificity = $specificity;
|
||||
$lastOptionIndex = $optionIndex;
|
||||
$lastChoice = $availableOptions[$optionIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $lastChoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Prefer header, as defined in RFC7240.
|
||||
*
|
||||
* Input can be given as a single header value (string) or multiple headers
|
||||
* (array of string).
|
||||
*
|
||||
* This method will return a key->value array with the various Prefer
|
||||
* parameters.
|
||||
*
|
||||
* Prefer: return=minimal will result in:
|
||||
*
|
||||
* [ 'return' => 'minimal' ]
|
||||
*
|
||||
* Prefer: foo, wait=10 will result in:
|
||||
*
|
||||
* [ 'foo' => true, 'wait' => '10']
|
||||
*
|
||||
* This method also supports the formats from older drafts of RFC7240, and
|
||||
* it will automatically map them to the new values, as the older values
|
||||
* are still pretty common.
|
||||
*
|
||||
* Parameters are currently discarded. There's no known prefer value that
|
||||
* uses them.
|
||||
*
|
||||
* @param string|string[] $input
|
||||
*/
|
||||
function parsePrefer($input): array
|
||||
{
|
||||
$token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+';
|
||||
|
||||
// Work in progress
|
||||
$word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )';
|
||||
|
||||
$regex = <<<REGEX
|
||||
/
|
||||
^
|
||||
(?<name> $token) # Prefer property name
|
||||
\s* # Optional space
|
||||
(?: = \s* # Prefer property value
|
||||
(?<value> $word)
|
||||
)?
|
||||
(?: \s* ; (?: .*))? # Prefer parameters (ignored)
|
||||
$
|
||||
/x
|
||||
REGEX;
|
||||
|
||||
$output = [];
|
||||
foreach (getHeaderValues($input) as $value) {
|
||||
if (!preg_match($regex, $value, $matches)) {
|
||||
// Ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mapping old values to their new counterparts
|
||||
switch ($matches['name']) {
|
||||
case 'return-asynch':
|
||||
$output['respond-async'] = true;
|
||||
break;
|
||||
case 'return-representation':
|
||||
$output['return'] = 'representation';
|
||||
break;
|
||||
case 'return-minimal':
|
||||
$output['return'] = 'minimal';
|
||||
break;
|
||||
case 'strict':
|
||||
$output['handling'] = 'strict';
|
||||
break;
|
||||
case 'lenient':
|
||||
$output['handling'] = 'lenient';
|
||||
break;
|
||||
default:
|
||||
if (isset($matches['value'])) {
|
||||
$value = trim($matches['value'], '"');
|
||||
} else {
|
||||
$value = true;
|
||||
}
|
||||
$output[strtolower($matches['name'])] = empty($value) ? true : $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method splits up headers into all their individual values.
|
||||
*
|
||||
* A HTTP header may have more than one header, such as this:
|
||||
* Cache-Control: private, no-store
|
||||
*
|
||||
* Header values are always split with a comma.
|
||||
*
|
||||
* You can pass either a string, or an array. The resulting value is always
|
||||
* an array with each spliced value.
|
||||
*
|
||||
* If the second headers argument is set, this value will simply be merged
|
||||
* in. This makes it quicker to merge an old list of values with a new set.
|
||||
*
|
||||
* @param string|string[] $values
|
||||
* @param string|string[] $values2
|
||||
*/
|
||||
function getHeaderValues($values, $values2 = null): array
|
||||
{
|
||||
$values = (array) $values;
|
||||
if ($values2) {
|
||||
$values = array_merge($values, (array) $values2);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($values as $l1) {
|
||||
foreach (explode(',', $l1) as $l2) {
|
||||
$result[] = trim($l2);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a mime-type and splits it into:.
|
||||
*
|
||||
* 1. type
|
||||
* 2. subtype
|
||||
* 3. quality
|
||||
* 4. parameters
|
||||
*/
|
||||
function parseMimeType(string $str): array
|
||||
{
|
||||
$parameters = [];
|
||||
// If no q= parameter appears, then quality = 1.
|
||||
$quality = 1;
|
||||
|
||||
$parts = explode(';', $str);
|
||||
|
||||
// The first part is the mime-type.
|
||||
$mimeType = trim(array_shift($parts));
|
||||
|
||||
if ('*' === $mimeType) {
|
||||
$mimeType = '*/*';
|
||||
}
|
||||
|
||||
$mimeType = explode('/', $mimeType);
|
||||
if (2 !== count($mimeType)) {
|
||||
// Illegal value
|
||||
var_dump($mimeType);
|
||||
exit;
|
||||
// throw new InvalidArgumentException('Not a valid mime-type: '.$str);
|
||||
}
|
||||
list($type, $subType) = $mimeType;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
if (strpos($part, '=')) {
|
||||
list($partName, $partValue) =
|
||||
explode('=', $part, 2);
|
||||
} else {
|
||||
$partName = $part;
|
||||
$partValue = null;
|
||||
}
|
||||
|
||||
// The quality parameter, if it appears, also marks the end of
|
||||
// the parameter list. Anything after the q= counts as an
|
||||
// 'accept extension' and could introduce new semantics in
|
||||
// content-negotiation.
|
||||
if ('q' !== $partName) {
|
||||
$parameters[$partName] = $part;
|
||||
} else {
|
||||
$quality = (float) $partValue;
|
||||
break; // Stop parsing parts
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'subType' => $subType,
|
||||
'quality' => $quality,
|
||||
'parameters' => $parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the path of a url.
|
||||
*
|
||||
* slashes (/) are treated as path-separators.
|
||||
*/
|
||||
function encodePath(string $path): string
|
||||
{
|
||||
return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function ($match) {
|
||||
return '%'.sprintf('%02x', ord($match[0]));
|
||||
}, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a 1 segment of a path.
|
||||
*
|
||||
* Slashes are considered part of the name, and are encoded as %2f
|
||||
*/
|
||||
function encodePathSegment(string $pathSegment): string
|
||||
{
|
||||
return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function ($match) {
|
||||
return '%'.sprintf('%02x', ord($match[0]));
|
||||
}, $pathSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a url-encoded path.
|
||||
*/
|
||||
function decodePath(string $path): string
|
||||
{
|
||||
return decodePathSegment($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a url-encoded path segment.
|
||||
*/
|
||||
function decodePathSegment(string $path): string
|
||||
{
|
||||
$path = rawurldecode($path);
|
||||
|
||||
if (!mb_check_encoding($path, 'UTF-8') && mb_check_encoding($path, 'ISO-8859-1')) {
|
||||
$path = mb_convert_encoding($path, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
parameters:
|
||||
level: 1
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\Request;
|
||||
use Sabre\HTTP\Response;
|
||||
|
||||
class AWSTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var Sabre\HTTP\Response
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* @var Sabre\HTTP\Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @var Sabre\HTTP\Auth\AWS
|
||||
*/
|
||||
private $auth;
|
||||
|
||||
public const REALM = 'SabreDAV unittest';
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->response = new Response();
|
||||
$this->request = new Request('GET', '/');
|
||||
$this->auth = new AWS(self::REALM, $this->request, $this->response);
|
||||
}
|
||||
|
||||
public function testNoHeader()
|
||||
{
|
||||
$this->request->setMethod('GET');
|
||||
$result = $this->auth->init();
|
||||
|
||||
$this->assertFalse($result, 'No AWS Authorization header was supplied, so we should have gotten false');
|
||||
$this->assertEquals(AWS::ERR_NOAWSHEADER, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testInvalidAuthorizationHeader()
|
||||
{
|
||||
$this->request->setMethod('GET');
|
||||
$this->request->setHeader('Authorization', 'Invalid Auth Header');
|
||||
|
||||
$this->assertFalse($this->auth->init(), 'The Invalid AWS authorization header');
|
||||
}
|
||||
|
||||
public function testIncorrectContentMD5()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
|
||||
$this->request->setMethod('GET');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:sig",
|
||||
'Content-MD5' => 'garbage',
|
||||
]);
|
||||
$this->request->setUrl('/');
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AWS::ERR_MD5CHECKSUMWRONG, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testNoDate()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
$content = 'thisisthebody';
|
||||
$contentMD5 = base64_encode(md5($content, true));
|
||||
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:sig",
|
||||
'Content-MD5' => $contentMD5,
|
||||
]);
|
||||
$this->request->setUrl('/');
|
||||
$this->request->setBody($content);
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AWS::ERR_INVALIDDATEFORMAT, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testFutureDate()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
$content = 'thisisthebody';
|
||||
$contentMD5 = base64_encode(md5($content, true));
|
||||
|
||||
$date = new \DateTime('@'.(time() + (60 * 20)));
|
||||
$date->setTimeZone(new \DateTimeZone('GMT'));
|
||||
$date = $date->format('D, d M Y H:i:s \\G\\M\\T');
|
||||
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:sig",
|
||||
'Content-MD5' => $contentMD5,
|
||||
'Date' => $date,
|
||||
]);
|
||||
|
||||
$this->request->setBody($content);
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testPastDate()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
$content = 'thisisthebody';
|
||||
$contentMD5 = base64_encode(md5($content, true));
|
||||
|
||||
$date = new \DateTime('@'.(time() - (60 * 20)));
|
||||
$date->setTimeZone(new \DateTimeZone('GMT'));
|
||||
$date = $date->format('D, d M Y H:i:s \\G\\M\\T');
|
||||
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:sig",
|
||||
'Content-MD5' => $contentMD5,
|
||||
'Date' => $date,
|
||||
]);
|
||||
|
||||
$this->request->setBody($content);
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testIncorrectSignature()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
$content = 'thisisthebody';
|
||||
|
||||
$contentMD5 = base64_encode(md5($content, true));
|
||||
|
||||
$date = new \DateTime('now');
|
||||
$date->setTimeZone(new \DateTimeZone('GMT'));
|
||||
$date = $date->format('D, d M Y H:i:s \\G\\M\\T');
|
||||
|
||||
$this->request->setUrl('/');
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:sig",
|
||||
'Content-MD5' => $contentMD5,
|
||||
'X-amz-date' => $date,
|
||||
]);
|
||||
$this->request->setBody($content);
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AWS::ERR_INVALIDSIGNATURE, $this->auth->errorCode);
|
||||
}
|
||||
|
||||
public function testValidRequest()
|
||||
{
|
||||
$accessKey = 'accessKey';
|
||||
$secretKey = 'secretKey';
|
||||
$content = 'thisisthebody';
|
||||
$contentMD5 = base64_encode(md5($content, true));
|
||||
|
||||
$date = new \DateTime('now');
|
||||
$date->setTimeZone(new \DateTimeZone('GMT'));
|
||||
$date = $date->format('D, d M Y H:i:s \\G\\M\\T');
|
||||
|
||||
$sig = base64_encode($this->hmacsha1($secretKey,
|
||||
"POST\n$contentMD5\n\n$date\nx-amz-date:$date\n/evert"
|
||||
));
|
||||
|
||||
$this->request->setUrl('/evert');
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeaders([
|
||||
'Authorization' => "AWS $accessKey:$sig",
|
||||
'Content-MD5' => $contentMD5,
|
||||
'X-amz-date' => $date,
|
||||
]);
|
||||
|
||||
$this->request->setBody($content);
|
||||
|
||||
$this->auth->init();
|
||||
$result = $this->auth->validate($secretKey);
|
||||
|
||||
$this->assertTrue($result, 'Signature did not validate, got errorcode '.$this->auth->errorCode);
|
||||
$this->assertEquals($accessKey, $this->auth->getAccessKey());
|
||||
}
|
||||
|
||||
public function test401()
|
||||
{
|
||||
$this->auth->requireLogin();
|
||||
$test = preg_match('/^AWS$/', $this->response->getHeader('WWW-Authenticate'), $matches);
|
||||
$this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an HMAC-SHA1 signature.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function hmacsha1($key, $message)
|
||||
{
|
||||
$blocksize = 64;
|
||||
if (strlen($key) > $blocksize) {
|
||||
$key = pack('H*', sha1($key));
|
||||
}
|
||||
$key = str_pad($key, $blocksize, chr(0x00));
|
||||
$ipad = str_repeat(chr(0x36), $blocksize);
|
||||
$opad = str_repeat(chr(0x5C), $blocksize);
|
||||
$hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message))));
|
||||
|
||||
return $hmac;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\Request;
|
||||
use Sabre\HTTP\Response;
|
||||
|
||||
class BasicTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetCredentials()
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'Authorization' => 'Basic '.base64_encode('user:pass:bla'),
|
||||
]);
|
||||
|
||||
$basic = new Basic('Dagger', $request, new Response());
|
||||
|
||||
$this->assertEquals([
|
||||
'user',
|
||||
'pass:bla',
|
||||
], $basic->getCredentials());
|
||||
}
|
||||
|
||||
public function testGetInvalidCredentialsColonMissing()
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'Authorization' => 'Basic '.base64_encode('userpass'),
|
||||
]);
|
||||
|
||||
$basic = new Basic('Dagger', $request, new Response());
|
||||
|
||||
$this->assertNull($basic->getCredentials());
|
||||
}
|
||||
|
||||
public function testGetCredentialsNoHeader()
|
||||
{
|
||||
$request = new Request('GET', '/', []);
|
||||
$basic = new Basic('Dagger', $request, new Response());
|
||||
|
||||
$this->assertNull($basic->getCredentials());
|
||||
}
|
||||
|
||||
public function testGetCredentialsNotBasic()
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'Authorization' => 'QBasic '.base64_encode('user:pass:bla'),
|
||||
]);
|
||||
$basic = new Basic('Dagger', $request, new Response());
|
||||
|
||||
$this->assertNull($basic->getCredentials());
|
||||
}
|
||||
|
||||
public function testRequireLogin()
|
||||
{
|
||||
$response = new Response();
|
||||
$request = new Request('GET', '/');
|
||||
|
||||
$basic = new Basic('Dagger', $request, $response);
|
||||
|
||||
$basic->requireLogin();
|
||||
|
||||
$this->assertEquals('Basic realm="Dagger", charset="UTF-8"', $response->getHeader('WWW-Authenticate'));
|
||||
$this->assertEquals(401, $response->getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\Request;
|
||||
use Sabre\HTTP\Response;
|
||||
|
||||
class BearerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetToken()
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'Authorization' => 'Bearer 12345',
|
||||
]);
|
||||
|
||||
$bearer = new Bearer('Dagger', $request, new Response());
|
||||
|
||||
$this->assertEquals(
|
||||
'12345',
|
||||
$bearer->getToken()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetCredentialsNoHeader()
|
||||
{
|
||||
$request = new Request('GET', '/', []);
|
||||
$bearer = new Bearer('Dagger', $request, new Response());
|
||||
|
||||
$this->assertNull($bearer->getToken());
|
||||
}
|
||||
|
||||
public function testGetCredentialsNotBearer()
|
||||
{
|
||||
$request = new Request('GET', '/', [
|
||||
'Authorization' => 'QBearer 12345',
|
||||
]);
|
||||
$bearer = new Bearer('Dagger', $request, new Response());
|
||||
|
||||
$this->assertNull($bearer->getToken());
|
||||
}
|
||||
|
||||
public function testRequireLogin()
|
||||
{
|
||||
$response = new Response();
|
||||
$request = new Request('GET', '/');
|
||||
$bearer = new Bearer('Dagger', $request, $response);
|
||||
|
||||
$bearer->requireLogin();
|
||||
|
||||
$this->assertEquals('Bearer realm="Dagger"', $response->getHeader('WWW-Authenticate'));
|
||||
$this->assertEquals(401, $response->getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP\Auth;
|
||||
|
||||
use Sabre\HTTP\Request;
|
||||
use Sabre\HTTP\Response;
|
||||
|
||||
class DigestTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var Sabre\HTTP\Response
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* request.
|
||||
*
|
||||
* @var Sabre\HTTP\Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @var Sabre\HTTP\Auth\Digest
|
||||
*/
|
||||
private $auth;
|
||||
|
||||
public const REALM = 'SabreDAV unittest';
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->response = new Response();
|
||||
$this->request = new Request('GET', '/');
|
||||
$this->auth = new Digest(self::REALM, $this->request, $this->response);
|
||||
}
|
||||
|
||||
public function testDigest()
|
||||
{
|
||||
list($nonce, $opaque) = $this->getServerTokens();
|
||||
|
||||
$username = 'admin';
|
||||
$password = '12345';
|
||||
$nc = '00002';
|
||||
$cnonce = uniqid();
|
||||
|
||||
$digestHash = md5(
|
||||
md5($username.':'.self::REALM.':'.$password).':'.
|
||||
$nonce.':'.
|
||||
$nc.':'.
|
||||
$cnonce.':'.
|
||||
'auth:'.
|
||||
md5('GET:/')
|
||||
);
|
||||
|
||||
$this->request->setMethod('GET');
|
||||
$this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"');
|
||||
|
||||
$this->auth->init();
|
||||
|
||||
$this->assertEquals($username, $this->auth->getUsername());
|
||||
$this->assertEquals(self::REALM, $this->auth->getRealm());
|
||||
$this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1');
|
||||
$this->assertTrue($this->auth->validatePassword($password), 'Authentication is deemed invalid through validatePassword');
|
||||
}
|
||||
|
||||
public function testInvalidDigest()
|
||||
{
|
||||
list($nonce, $opaque) = $this->getServerTokens();
|
||||
|
||||
$username = 'admin';
|
||||
$password = 12345;
|
||||
$nc = '00002';
|
||||
$cnonce = uniqid();
|
||||
|
||||
$digestHash = md5(
|
||||
md5($username.':'.self::REALM.':'.$password).':'.
|
||||
$nonce.':'.
|
||||
$nc.':'.
|
||||
$cnonce.':'.
|
||||
'auth:'.
|
||||
md5('GET:/')
|
||||
);
|
||||
|
||||
$this->request->setMethod('GET');
|
||||
$this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"');
|
||||
|
||||
$this->auth->init();
|
||||
|
||||
$this->assertFalse($this->auth->validateA1(md5($username.':'.self::REALM.':'.($password.'randomness'))), 'Authentication is deemed invalid through validateA1');
|
||||
}
|
||||
|
||||
public function testInvalidDigest2()
|
||||
{
|
||||
$this->request->setMethod('GET');
|
||||
$this->request->setHeader('Authorization', 'basic blablabla');
|
||||
|
||||
$this->auth->init();
|
||||
$this->assertFalse($this->auth->validateA1(md5('user:realm:password')));
|
||||
}
|
||||
|
||||
public function testDigestAuthInt()
|
||||
{
|
||||
$this->auth->setQOP(Digest::QOP_AUTHINT);
|
||||
list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT);
|
||||
|
||||
$username = 'admin';
|
||||
$password = 12345;
|
||||
$nc = '00003';
|
||||
$cnonce = uniqid();
|
||||
|
||||
$digestHash = md5(
|
||||
md5($username.':'.self::REALM.':'.$password).':'.
|
||||
$nonce.':'.
|
||||
$nc.':'.
|
||||
$cnonce.':'.
|
||||
'auth-int:'.
|
||||
md5('POST:/:'.md5('body'))
|
||||
);
|
||||
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"');
|
||||
$this->request->setBody('body');
|
||||
|
||||
$this->auth->init();
|
||||
|
||||
$this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1');
|
||||
}
|
||||
|
||||
public function testDigestAuthBoth()
|
||||
{
|
||||
$this->auth->setQOP(Digest::QOP_AUTHINT | Digest::QOP_AUTH);
|
||||
list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT | Digest::QOP_AUTH);
|
||||
|
||||
$username = 'admin';
|
||||
$password = 12345;
|
||||
$nc = '00003';
|
||||
$cnonce = uniqid();
|
||||
|
||||
$digestHash = md5(
|
||||
md5($username.':'.self::REALM.':'.$password).':'.
|
||||
$nonce.':'.
|
||||
$nc.':'.
|
||||
$cnonce.':'.
|
||||
'auth-int:'.
|
||||
md5('POST:/:'.md5('body'))
|
||||
);
|
||||
|
||||
$this->request->setMethod('POST');
|
||||
$this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"');
|
||||
$this->request->setBody('body');
|
||||
|
||||
$this->auth->init();
|
||||
|
||||
$this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1');
|
||||
}
|
||||
|
||||
private function getServerTokens($qop = Digest::QOP_AUTH)
|
||||
{
|
||||
$this->auth->requireLogin();
|
||||
|
||||
switch ($qop) {
|
||||
case Digest::QOP_AUTH: $qopstr = 'auth';
|
||||
break;
|
||||
case Digest::QOP_AUTHINT: $qopstr = 'auth-int';
|
||||
break;
|
||||
default: $qopstr = 'auth,auth-int';
|
||||
break;
|
||||
}
|
||||
|
||||
$test = preg_match('/Digest realm="'.self::REALM.'",qop="'.$qopstr.'",nonce="([0-9a-f]*)",opaque="([0-9a-f]*)"/',
|
||||
$this->response->getHeader('WWW-Authenticate'), $matches);
|
||||
|
||||
$this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern. We received: '.$this->response->getHeader('WWW-Authenticate'));
|
||||
|
||||
$nonce = $matches[1];
|
||||
$opaque = $matches[2];
|
||||
|
||||
// Reset our environment
|
||||
$this->setUp();
|
||||
$this->auth->setQOP($qop);
|
||||
|
||||
return [$nonce, $opaque];
|
||||
}
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class ClientTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testCreateCurlSettingsArrayGET()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$client->addCurlSetting(CURLOPT_POSTREDIR, 0);
|
||||
|
||||
$request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']);
|
||||
|
||||
$settings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_POSTREDIR => 0,
|
||||
CURLOPT_HTTPHEADER => ['X-Foo: bar'],
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
// FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
|
||||
// at least if this unit test fails in the future we know it is :)
|
||||
if (false === defined('HHVM_VERSION')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
$this->assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testCreateCurlSettingsHTTPHeader(): void
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$header = [
|
||||
'Authorization: Bearer 12345',
|
||||
];
|
||||
$client->addCurlSetting(CURLOPT_POSTREDIR, 0);
|
||||
$client->addCurlSetting(CURLOPT_HTTPHEADER, $header);
|
||||
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$settings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_POSTREDIR => 0,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer 12345'],
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
];
|
||||
|
||||
self::assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testCreateCurlSettingsArrayHEAD()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']);
|
||||
|
||||
$settings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'HEAD',
|
||||
CURLOPT_HTTPHEADER => ['X-Foo: bar'],
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
// FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
|
||||
// at least if this unit test fails in the future we know it is :)
|
||||
if (false === defined('HHVM_VERSION')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
$this->assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testCreateCurlSettingsArrayGETAfterHEAD()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']);
|
||||
|
||||
// Parsing the settings for this method, and discarding the result.
|
||||
// This will cause the client to automatically persist previous
|
||||
// settings and will help us detect problems.
|
||||
$client->createCurlSettingsArray($request);
|
||||
|
||||
// This is the real request.
|
||||
$request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']);
|
||||
|
||||
$settings = [
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_HTTPHEADER => ['X-Foo: bar'],
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
// FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
|
||||
// at least if this unit test fails in the future we know it is :)
|
||||
if (false === defined('HHVM_VERSION')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
$this->assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testCreateCurlSettingsArrayPUTStream()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
|
||||
$fileContent = 'booh';
|
||||
$h = fopen('php://memory', 'r+');
|
||||
fwrite($h, $fileContent);
|
||||
$request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], $h);
|
||||
|
||||
$settings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_PUT => true,
|
||||
CURLOPT_INFILE => $h,
|
||||
CURLOPT_INFILESIZE => strlen($fileContent),
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_HTTPHEADER => ['X-Foo: bar'],
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
// FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
|
||||
// at least if this unit test fails in the future we know it is :)
|
||||
if (false === defined('HHVM_VERSION')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
$this->assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testCreateCurlSettingsArrayPUTString()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], 'boo');
|
||||
|
||||
$settings = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_NOBODY => false,
|
||||
CURLOPT_POSTFIELDS => 'boo',
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_HTTPHEADER => ['X-Foo: bar'],
|
||||
CURLOPT_URL => 'http://example.org/',
|
||||
CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)',
|
||||
];
|
||||
|
||||
// FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
|
||||
// at least if this unit test fails in the future we know it is :)
|
||||
if (false === defined('HHVM_VERSION')) {
|
||||
$settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
$settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
|
||||
}
|
||||
|
||||
$this->assertEquals($settings, $client->createCurlSettingsArray($request));
|
||||
}
|
||||
|
||||
public function testIssue89MultiplePutInfileGivesWarning()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$tmpFile = tmpfile();
|
||||
$request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body');
|
||||
|
||||
$settings = $client->createCurlSettingsArray($request);
|
||||
$this->assertArrayNotHasKey(CURLOPT_PUT, $settings);
|
||||
$this->assertArrayNotHasKey(CURLOPT_INFILE, $settings);
|
||||
|
||||
$request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], $tmpFile);
|
||||
|
||||
$settings = $client->createCurlSettingsArray($request);
|
||||
$this->assertEquals(true, $settings[CURLOPT_PUT]);
|
||||
$this->assertEquals($tmpFile, $settings[CURLOPT_INFILE]);
|
||||
|
||||
$request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body');
|
||||
|
||||
$settings = $client->createCurlSettingsArray($request);
|
||||
$this->assertArrayNotHasKey(CURLOPT_PUT, $settings);
|
||||
$this->assertArrayNotHasKey(CURLOPT_INFILE, $settings);
|
||||
}
|
||||
|
||||
public function testSend()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$client->on('doRequest', function ($request, &$response) {
|
||||
$response = new Response(200);
|
||||
});
|
||||
|
||||
$response = $client->send($request);
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
}
|
||||
|
||||
protected function getAbsoluteUrl($path)
|
||||
{
|
||||
$baseUrl = getenv('BASEURL');
|
||||
if ($baseUrl) {
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
return "$baseUrl/$path";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @group ci
|
||||
*/
|
||||
public function testSendToGetLargeContent()
|
||||
{
|
||||
$url = $this->getAbsoluteUrl('/large.php');
|
||||
if (!$url) {
|
||||
$this->markTestSkipped('Set an environment value BASEURL to continue');
|
||||
}
|
||||
|
||||
// Allow the peak memory usage limit to be specified externally, if needed.
|
||||
// When running this test in different environments it may be appropriate to set a different limit.
|
||||
$maxPeakMemoryUsageEnvVariable = 'SABRE_HTTP_TEST_GET_LARGE_CONTENT_MAX_PEAK_MEMORY_USAGE';
|
||||
$maxPeakMemoryUsage = \getenv($maxPeakMemoryUsageEnvVariable);
|
||||
if (false === $maxPeakMemoryUsage) {
|
||||
$maxPeakMemoryUsage = 60 * pow(1024, 2);
|
||||
}
|
||||
|
||||
$request = new Request('GET', $url);
|
||||
$client = new Client();
|
||||
$response = $client->send($request);
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertLessThan(
|
||||
(int) $maxPeakMemoryUsage,
|
||||
memory_get_peak_usage(),
|
||||
"Hint: you can adjust the max peak memory usage allowed for this test by defining env variable $maxPeakMemoryUsageEnvVariable to be the desired max bytes"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group ci
|
||||
*/
|
||||
public function testSendAsync()
|
||||
{
|
||||
$url = $this->getAbsoluteUrl('/foo');
|
||||
if (!$url) {
|
||||
$this->markTestSkipped('Set an environment value BASEURL to continue');
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
|
||||
$request = new Request('GET', $url);
|
||||
$client->sendAsync($request, function (ResponseInterface $response) {
|
||||
$this->assertEquals("foo\n", $response->getBody());
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals(4, $response->getHeader('Content-Length'));
|
||||
}, function ($error) use ($request) {
|
||||
$url = $request->getUrl();
|
||||
$this->fail("Failed to GET $url");
|
||||
});
|
||||
|
||||
$client->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group ci
|
||||
*/
|
||||
public function testSendAsynConsecutively()
|
||||
{
|
||||
$url = $this->getAbsoluteUrl('/foo');
|
||||
if (!$url) {
|
||||
$this->markTestSkipped('Set an environment value BASEURL to continue');
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
|
||||
$request = new Request('GET', $url);
|
||||
$client->sendAsync($request, function (ResponseInterface $response) {
|
||||
$this->assertEquals("foo\n", $response->getBody());
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals(4, $response->getHeader('Content-Length'));
|
||||
}, function ($error) use ($request) {
|
||||
$url = $request->getUrl();
|
||||
$this->fail("Failed to get $url");
|
||||
});
|
||||
|
||||
$url = $this->getAbsoluteUrl('/bar.php');
|
||||
$request = new Request('GET', $url);
|
||||
$client->sendAsync($request, function (ResponseInterface $response) {
|
||||
$this->assertEquals("bar\n", $response->getBody());
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals('Bar', $response->getHeader('X-Test'));
|
||||
}, function ($error) use ($request) {
|
||||
$url = $request->getUrl();
|
||||
$this->fail("Failed to get $url");
|
||||
});
|
||||
|
||||
$client->wait();
|
||||
}
|
||||
|
||||
public function testSendClientError()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$client->on('doRequest', function ($request, &$response) {
|
||||
throw new ClientException('aaah', 1);
|
||||
});
|
||||
$called = false;
|
||||
$client->on('exception', function () use (&$called) {
|
||||
$called = true;
|
||||
});
|
||||
|
||||
try {
|
||||
$client->send($request);
|
||||
$this->fail('send() should have thrown an exception');
|
||||
} catch (ClientException $e) {
|
||||
}
|
||||
$this->assertTrue($called);
|
||||
}
|
||||
|
||||
public function testSendHttpError()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$client->on('doRequest', function ($request, &$response) {
|
||||
$response = new Response(404);
|
||||
});
|
||||
$called = 0;
|
||||
$client->on('error', function () use (&$called) {
|
||||
++$called;
|
||||
});
|
||||
$client->on('error:404', function () use (&$called) {
|
||||
++$called;
|
||||
});
|
||||
|
||||
$client->send($request);
|
||||
$this->assertEquals(2, $called);
|
||||
}
|
||||
|
||||
public function testSendRetry()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$called = 0;
|
||||
$client->on('doRequest', function ($request, &$response) use (&$called) {
|
||||
++$called;
|
||||
if ($called < 3) {
|
||||
$response = new Response(404);
|
||||
} else {
|
||||
$response = new Response(200);
|
||||
}
|
||||
});
|
||||
|
||||
$errorCalled = 0;
|
||||
$client->on('error', function ($request, $response, &$retry, $retryCount) use (&$errorCalled) {
|
||||
++$errorCalled;
|
||||
$retry = true;
|
||||
});
|
||||
|
||||
$response = $client->send($request);
|
||||
$this->assertEquals(3, $called);
|
||||
$this->assertEquals(2, $errorCalled);
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testHttpErrorException()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$client->setThrowExceptions(true);
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
|
||||
$client->on('doRequest', function ($request, &$response) {
|
||||
$response = new Response(404);
|
||||
});
|
||||
|
||||
try {
|
||||
$client->send($request);
|
||||
$this->fail('An exception should have been thrown');
|
||||
} catch (ClientHttpException $e) {
|
||||
$this->assertEquals(404, $e->getHttpStatus());
|
||||
$this->assertInstanceOf('Sabre\HTTP\Response', $e->getResponse());
|
||||
}
|
||||
}
|
||||
|
||||
public function testParseCurlResult()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$client->on('curlStuff', function (&$return) {
|
||||
$return = [
|
||||
[
|
||||
'header_size' => 33,
|
||||
'http_code' => 200,
|
||||
],
|
||||
0,
|
||||
'',
|
||||
];
|
||||
});
|
||||
|
||||
$body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
|
||||
$result = $client->parseCurlResult($body, 'foobar');
|
||||
|
||||
$this->assertEquals(Client::STATUS_SUCCESS, $result['status']);
|
||||
$this->assertEquals(200, $result['http_code']);
|
||||
$this->assertEquals(200, $result['response']->getStatus());
|
||||
$this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders());
|
||||
$this->assertEquals('Foo', $result['response']->getBodyAsString());
|
||||
}
|
||||
|
||||
public function testParseCurlResultEmptyBody()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$client->on('curlStuff', function (&$return) {
|
||||
$return = [
|
||||
[
|
||||
'header_size' => 33,
|
||||
'http_code' => 200,
|
||||
],
|
||||
0,
|
||||
'',
|
||||
];
|
||||
});
|
||||
|
||||
$body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\n";
|
||||
$result = $client->parseCurlResult($body, 'foobar');
|
||||
|
||||
$this->assertEquals(Client::STATUS_SUCCESS, $result['status']);
|
||||
$this->assertEquals(200, $result['http_code']);
|
||||
$this->assertEquals(200, $result['response']->getStatus());
|
||||
$this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders());
|
||||
$this->assertEquals('', $result['response']->getBodyAsString());
|
||||
}
|
||||
|
||||
public function testParseCurlError()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$client->on('curlStuff', function (&$return) {
|
||||
$return = [
|
||||
[],
|
||||
1,
|
||||
'Curl error',
|
||||
];
|
||||
});
|
||||
|
||||
$body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
|
||||
$result = $client->parseCurlResult($body, 'foobar');
|
||||
|
||||
$this->assertEquals(Client::STATUS_CURLERROR, $result['status']);
|
||||
$this->assertEquals(1, $result['curl_errno']);
|
||||
$this->assertEquals('Curl error', $result['curl_errmsg']);
|
||||
}
|
||||
|
||||
public function testDoRequest()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
$client->on('curlExec', function (&$return) {
|
||||
$return = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
|
||||
});
|
||||
$client->on('curlStuff', function (&$return) {
|
||||
$return = [
|
||||
[
|
||||
'header_size' => 33,
|
||||
'http_code' => 200,
|
||||
],
|
||||
0,
|
||||
'',
|
||||
];
|
||||
});
|
||||
$response = $client->doRequest($request);
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals(['Header1' => ['Val1']], $response->getHeaders());
|
||||
$this->assertEquals('Foo', $response->getBodyAsString());
|
||||
}
|
||||
|
||||
public function testDoRequestCurlError()
|
||||
{
|
||||
$client = new ClientMock();
|
||||
$request = new Request('GET', 'http://example.org/');
|
||||
$client->on('curlExec', function (&$return) {
|
||||
$return = '';
|
||||
});
|
||||
$client->on('curlStuff', function (&$return) {
|
||||
$return = [
|
||||
[],
|
||||
1,
|
||||
'Curl error',
|
||||
];
|
||||
});
|
||||
|
||||
try {
|
||||
$response = $client->doRequest($request);
|
||||
$this->fail('This should have thrown an exception');
|
||||
} catch (ClientException $e) {
|
||||
$this->assertEquals(1, $e->getCode());
|
||||
$this->assertEquals('Curl error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientMock extends Client
|
||||
{
|
||||
protected $persistedSettings = [];
|
||||
|
||||
/**
|
||||
* Making this method public.
|
||||
*/
|
||||
public function receiveCurlHeader($curlHandle, $headerLine)
|
||||
{
|
||||
return parent::receiveCurlHeader($curlHandle, $headerLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Making this method public.
|
||||
*/
|
||||
public function createCurlSettingsArray(RequestInterface $request): array
|
||||
{
|
||||
return parent::createCurlSettingsArray($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Making this method public.
|
||||
*/
|
||||
public function parseCurlResult(string $response, $curlHandle): array
|
||||
{
|
||||
return parent::parseCurlResult($response, $curlHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for performing a single request.
|
||||
*/
|
||||
public function doRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$response = null;
|
||||
$this->emit('doRequest', [$request, &$response]);
|
||||
|
||||
// If nothing modified $response, we're using the default behavior.
|
||||
if (is_null($response)) {
|
||||
return parent::doRequest($request);
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bunch of information about a curl request.
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function curlStuff($curlHandle): array
|
||||
{
|
||||
$return = null;
|
||||
$this->emit('curlStuff', [&$return]);
|
||||
|
||||
// If nothing modified $return, we're using the default behavior.
|
||||
if (is_null($return)) {
|
||||
return parent::curlStuff($curlHandle);
|
||||
} else {
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls curl_exec.
|
||||
*
|
||||
* This method exists so it can easily be overridden and mocked.
|
||||
*
|
||||
* @param resource $curlHandle
|
||||
*/
|
||||
protected function curlExec($curlHandle): string
|
||||
{
|
||||
$return = null;
|
||||
$this->emit('curlExec', [&$return]);
|
||||
|
||||
// If nothing modified $return, we're using the default behavior.
|
||||
if (is_null($return)) {
|
||||
return parent::curlExec($curlHandle);
|
||||
} else {
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class FunctionsTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getHeaderValuesDataOnValues2
|
||||
*/
|
||||
public function testGetHeaderValuesOnValues2($result, $values1, $values2)
|
||||
{
|
||||
$this->assertEquals($result, getHeaderValues($values1, $values2));
|
||||
}
|
||||
|
||||
public function getHeaderValuesDataOnValues2()
|
||||
{
|
||||
return [
|
||||
[
|
||||
['a', 'b'],
|
||||
['a'],
|
||||
['b'],
|
||||
],
|
||||
[
|
||||
['a', 'b', 'c', 'd', 'e'],
|
||||
['a', 'b', 'c'],
|
||||
['d', 'e'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getHeaderValuesData
|
||||
*/
|
||||
public function testGetHeaderValues($input, $output)
|
||||
{
|
||||
$this->assertEquals(
|
||||
$output,
|
||||
getHeaderValues($input)
|
||||
);
|
||||
}
|
||||
|
||||
public function getHeaderValuesData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'a',
|
||||
['a'],
|
||||
],
|
||||
[
|
||||
'a,b',
|
||||
['a', 'b'],
|
||||
],
|
||||
[
|
||||
'a, b',
|
||||
['a', 'b'],
|
||||
],
|
||||
[
|
||||
['a, b'],
|
||||
['a', 'b'],
|
||||
],
|
||||
[
|
||||
['a, b', 'c', 'd,e'],
|
||||
['a', 'b', 'c', 'd', 'e'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider preferData
|
||||
*/
|
||||
public function testPrefer($input, $output)
|
||||
{
|
||||
$this->assertEquals(
|
||||
$output,
|
||||
parsePrefer($input)
|
||||
);
|
||||
}
|
||||
|
||||
public function preferData()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'foo; bar',
|
||||
['foo' => true],
|
||||
],
|
||||
[
|
||||
'foo; bar=""',
|
||||
['foo' => true],
|
||||
],
|
||||
[
|
||||
'foo=""; bar',
|
||||
['foo' => true],
|
||||
],
|
||||
[
|
||||
'FOO',
|
||||
['foo' => true],
|
||||
],
|
||||
[
|
||||
'respond-async',
|
||||
['respond-async' => true],
|
||||
],
|
||||
[
|
||||
['respond-async, wait=100', 'handling=lenient'],
|
||||
['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'],
|
||||
],
|
||||
[
|
||||
['respond-async, wait=100, handling=lenient'],
|
||||
['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'],
|
||||
],
|
||||
// Old values
|
||||
[
|
||||
'return-asynch, return-representation',
|
||||
['respond-async' => true, 'return' => 'representation'],
|
||||
],
|
||||
[
|
||||
'return-minimal',
|
||||
['return' => 'minimal'],
|
||||
],
|
||||
[
|
||||
'strict',
|
||||
['handling' => 'strict'],
|
||||
],
|
||||
[
|
||||
'lenient',
|
||||
['handling' => 'lenient'],
|
||||
],
|
||||
// Invalid token
|
||||
[
|
||||
['foo=%bar%'],
|
||||
[],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testParseHTTPDate()
|
||||
{
|
||||
$times = [
|
||||
'Wed, 13 Oct 2010 10:26:00 GMT',
|
||||
'Wednesday, 13-Oct-10 10:26:00 GMT',
|
||||
'Wed Oct 13 10:26:00 2010',
|
||||
];
|
||||
|
||||
$expected = 1286965560;
|
||||
|
||||
foreach ($times as $time) {
|
||||
$result = parseDate($time);
|
||||
$this->assertEquals($expected, $result->format('U'));
|
||||
}
|
||||
|
||||
$result = parseDate('Wed Oct 6 10:26:00 2010');
|
||||
$this->assertEquals(1286360760, $result->format('U'));
|
||||
}
|
||||
|
||||
public function testParseHTTPDateFail()
|
||||
{
|
||||
$times = [
|
||||
// random string
|
||||
'NOW',
|
||||
// not-GMT timezone
|
||||
'Wednesday, 13-Oct-10 10:26:00 UTC',
|
||||
// No space before the 6
|
||||
'Wed Oct 6 10:26:00 2010',
|
||||
// Invalid day
|
||||
'Wed Oct 0 10:26:00 2010',
|
||||
'Wed Oct 32 10:26:00 2010',
|
||||
'Wed, 0 Oct 2010 10:26:00 GMT',
|
||||
'Wed, 32 Oct 2010 10:26:00 GMT',
|
||||
'Wednesday, 32-Oct-10 10:26:00 GMT',
|
||||
// Invalid hour
|
||||
'Wed, 13 Oct 2010 24:26:00 GMT',
|
||||
'Wednesday, 13-Oct-10 24:26:00 GMT',
|
||||
'Wed Oct 13 24:26:00 2010',
|
||||
];
|
||||
|
||||
foreach ($times as $time) {
|
||||
$this->assertFalse(parseDate($time), 'We used the string: '.$time);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTimezones()
|
||||
{
|
||||
$default = date_default_timezone_get();
|
||||
date_default_timezone_set('Europe/Amsterdam');
|
||||
|
||||
$this->testParseHTTPDate();
|
||||
|
||||
date_default_timezone_set($default);
|
||||
}
|
||||
|
||||
public function testToHTTPDate()
|
||||
{
|
||||
$dt = new \DateTime('2011-12-10 12:00:00 +0200');
|
||||
|
||||
$this->assertEquals(
|
||||
'Sat, 10 Dec 2011 10:00:00 GMT',
|
||||
toDate($dt)
|
||||
);
|
||||
}
|
||||
|
||||
public function testParseMimeTypeOnInvalidMimeType()
|
||||
{
|
||||
if (false === \getenv('EXECUTE_INVALID_MIME_TYPE_TEST')) {
|
||||
$this->markTestSkipped('Test skipped because parseMimeType with an invalid mime type will exit in 5.x');
|
||||
}
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Not a valid mime-type: invalid_mime_type');
|
||||
|
||||
parseMimeType('invalid_mime_type');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class MessageDecoratorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected $inner;
|
||||
protected $outer;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->inner = new Request('GET', '/');
|
||||
$this->outer = new RequestDecorator($this->inner);
|
||||
}
|
||||
|
||||
public function testBody()
|
||||
{
|
||||
$this->outer->setBody('foo');
|
||||
$this->assertEquals('foo', stream_get_contents($this->inner->getBodyAsStream()));
|
||||
$this->assertEquals('foo', stream_get_contents($this->outer->getBodyAsStream()));
|
||||
$this->assertEquals('foo', $this->inner->getBodyAsString());
|
||||
$this->assertEquals('foo', $this->outer->getBodyAsString());
|
||||
$this->assertEquals('foo', $this->inner->getBody());
|
||||
$this->assertEquals('foo', $this->outer->getBody());
|
||||
}
|
||||
|
||||
public function testHeaders()
|
||||
{
|
||||
$this->outer->setHeaders([
|
||||
'a' => 'b',
|
||||
]);
|
||||
|
||||
$this->assertEquals(['a' => ['b']], $this->inner->getHeaders());
|
||||
$this->assertEquals(['a' => ['b']], $this->outer->getHeaders());
|
||||
|
||||
$this->outer->setHeaders([
|
||||
'c' => 'd',
|
||||
]);
|
||||
|
||||
$this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->inner->getHeaders());
|
||||
$this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->outer->getHeaders());
|
||||
|
||||
$this->outer->addHeaders([
|
||||
'e' => 'f',
|
||||
]);
|
||||
|
||||
$this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->inner->getHeaders());
|
||||
$this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->outer->getHeaders());
|
||||
}
|
||||
|
||||
public function testHeader()
|
||||
{
|
||||
$this->assertFalse($this->outer->hasHeader('a'));
|
||||
$this->assertFalse($this->inner->hasHeader('a'));
|
||||
$this->outer->setHeader('a', 'c');
|
||||
$this->assertTrue($this->outer->hasHeader('a'));
|
||||
$this->assertTrue($this->inner->hasHeader('a'));
|
||||
|
||||
$this->assertEquals('c', $this->inner->getHeader('A'));
|
||||
$this->assertEquals('c', $this->outer->getHeader('A'));
|
||||
|
||||
$this->outer->addHeader('A', 'd');
|
||||
|
||||
$this->assertEquals(
|
||||
['c', 'd'],
|
||||
$this->inner->getHeaderAsArray('A')
|
||||
);
|
||||
$this->assertEquals(
|
||||
['c', 'd'],
|
||||
$this->outer->getHeaderAsArray('A')
|
||||
);
|
||||
|
||||
$success = $this->outer->removeHeader('a');
|
||||
|
||||
$this->assertTrue($success);
|
||||
$this->assertNull($this->inner->getHeader('A'));
|
||||
$this->assertNull($this->outer->getHeader('A'));
|
||||
|
||||
$this->assertFalse($this->outer->removeHeader('i-dont-exist'));
|
||||
}
|
||||
|
||||
public function testHttpVersion()
|
||||
{
|
||||
$this->outer->setHttpVersion('1.0');
|
||||
|
||||
$this->assertEquals('1.0', $this->inner->getHttpVersion());
|
||||
$this->assertEquals('1.0', $this->outer->getHttpVersion());
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class MessageTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
$this->assertInstanceOf('Sabre\HTTP\Message', $message);
|
||||
}
|
||||
|
||||
public function testStreamBody()
|
||||
{
|
||||
$body = 'foo';
|
||||
$h = fopen('php://memory', 'r+');
|
||||
fwrite($h, $body);
|
||||
rewind($h);
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($h);
|
||||
|
||||
$this->assertEquals($body, $message->getBodyAsString());
|
||||
rewind($h);
|
||||
$this->assertEquals($body, stream_get_contents($message->getBodyAsStream()));
|
||||
rewind($h);
|
||||
$this->assertEquals($body, stream_get_contents($message->getBody()));
|
||||
}
|
||||
|
||||
public function testStringBody()
|
||||
{
|
||||
$body = 'foo';
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($body);
|
||||
|
||||
$this->assertEquals($body, $message->getBodyAsString());
|
||||
$this->assertEquals($body, stream_get_contents($message->getBodyAsStream()));
|
||||
$this->assertEquals($body, $message->getBody());
|
||||
}
|
||||
|
||||
public function testCallbackBodyAsString()
|
||||
{
|
||||
$body = $this->createCallback('foo');
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($body);
|
||||
|
||||
$string = $message->getBodyAsString();
|
||||
|
||||
$this->assertSame('foo', $string);
|
||||
}
|
||||
|
||||
public function testCallbackBodyAsStream()
|
||||
{
|
||||
$body = $this->createCallback('foo');
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($body);
|
||||
|
||||
$stream = $message->getBodyAsStream();
|
||||
|
||||
$this->assertSame('foo', stream_get_contents($stream));
|
||||
}
|
||||
|
||||
public function testGetBodyWhenCallback()
|
||||
{
|
||||
$callback = $this->createCallback('foo');
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($callback);
|
||||
|
||||
$this->assertSame($callback, $message->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* It's possible that streams contains more data than the Content-Length.
|
||||
*
|
||||
* The request object should make sure to never emit more than
|
||||
* Content-Length, if Content-Length is set.
|
||||
*
|
||||
* This is in particular useful when responding to range requests with
|
||||
* streams that represent files on the filesystem, as it's possible to just
|
||||
* seek the stream to a certain point, set the content-length and let the
|
||||
* request object do the rest.
|
||||
*/
|
||||
public function testLongStreamToStringBody()
|
||||
{
|
||||
$body = fopen('php://memory', 'r+');
|
||||
fwrite($body, 'abcdefg');
|
||||
fseek($body, 2);
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($body);
|
||||
$message->setHeader('Content-Length', '4');
|
||||
|
||||
$this->assertEquals(
|
||||
'cdef',
|
||||
$message->getBodyAsString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Some clients include a content-length header, but the header is empty.
|
||||
* This is definitely broken behavior, but we should support it.
|
||||
*/
|
||||
public function testEmptyContentLengthHeader()
|
||||
{
|
||||
$body = fopen('php://memory', 'r+');
|
||||
fwrite($body, 'abcdefg');
|
||||
fseek($body, 2);
|
||||
|
||||
$message = new MessageMock();
|
||||
$message->setBody($body);
|
||||
$message->setHeader('Content-Length', '');
|
||||
|
||||
$this->assertEquals(
|
||||
'cdefg',
|
||||
$message->getBodyAsString()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetEmptyBodyStream()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
$body = $message->getBodyAsStream();
|
||||
|
||||
$this->assertEquals('', stream_get_contents($body));
|
||||
}
|
||||
|
||||
public function testGetEmptyBodyString()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
$body = $message->getBodyAsString();
|
||||
|
||||
$this->assertEquals('', $body);
|
||||
}
|
||||
|
||||
public function testHeaders()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
$message->setHeader('X-Foo', 'bar');
|
||||
|
||||
// Testing caselessness
|
||||
$this->assertEquals('bar', $message->getHeader('X-Foo'));
|
||||
$this->assertEquals('bar', $message->getHeader('x-fOO'));
|
||||
|
||||
$this->assertTrue(
|
||||
$message->removeHeader('X-FOO')
|
||||
);
|
||||
$this->assertNull($message->getHeader('X-Foo'));
|
||||
$this->assertFalse(
|
||||
$message->removeHeader('X-FOO')
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetHeaders()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
|
||||
$headers = [
|
||||
'X-Foo' => ['1'],
|
||||
'X-Bar' => ['2'],
|
||||
];
|
||||
|
||||
$message->setHeaders($headers);
|
||||
$this->assertEquals($headers, $message->getHeaders());
|
||||
|
||||
$message->setHeaders([
|
||||
'X-Foo' => ['3', '4'],
|
||||
'X-Bar' => '5',
|
||||
]);
|
||||
|
||||
$expected = [
|
||||
'X-Foo' => ['3', '4'],
|
||||
'X-Bar' => ['5'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $message->getHeaders());
|
||||
}
|
||||
|
||||
public function testAddHeaders()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
|
||||
$headers = [
|
||||
'X-Foo' => ['1'],
|
||||
'X-Bar' => ['2'],
|
||||
];
|
||||
|
||||
$message->addHeaders($headers);
|
||||
$this->assertEquals($headers, $message->getHeaders());
|
||||
|
||||
$message->addHeaders([
|
||||
'X-Foo' => ['3', '4'],
|
||||
'X-Bar' => '5',
|
||||
]);
|
||||
|
||||
$expected = [
|
||||
'X-Foo' => ['1', '3', '4'],
|
||||
'X-Bar' => ['2', '5'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $message->getHeaders());
|
||||
}
|
||||
|
||||
public function testSendBody()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
|
||||
// String
|
||||
$message->setBody('foo');
|
||||
|
||||
// Stream
|
||||
$h = fopen('php://memory', 'r+');
|
||||
fwrite($h, 'bar');
|
||||
rewind($h);
|
||||
$message->setBody($h);
|
||||
|
||||
$body = $message->getBody();
|
||||
rewind($body);
|
||||
|
||||
$this->assertEquals('bar', stream_get_contents($body));
|
||||
}
|
||||
|
||||
public function testMultipleHeaders()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
$message->setHeader('a', '1');
|
||||
$message->addHeader('A', '2');
|
||||
|
||||
$this->assertEquals(
|
||||
'1,2',
|
||||
$message->getHeader('A')
|
||||
);
|
||||
$this->assertEquals(
|
||||
'1,2',
|
||||
$message->getHeader('a')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
['1', '2'],
|
||||
$message->getHeaderAsArray('a')
|
||||
);
|
||||
$this->assertEquals(
|
||||
['1', '2'],
|
||||
$message->getHeaderAsArray('A')
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
$message->getHeaderAsArray('B')
|
||||
);
|
||||
}
|
||||
|
||||
public function testHasHeaders()
|
||||
{
|
||||
$message = new MessageMock();
|
||||
|
||||
$this->assertFalse($message->hasHeader('X-Foo'));
|
||||
$message->setHeader('X-Foo', 'Bar');
|
||||
$this->assertTrue($message->hasHeader('X-Foo'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*
|
||||
* @return \Closure Returns a callback printing $content to php://output stream
|
||||
*/
|
||||
private function createCallback($content)
|
||||
{
|
||||
return function () use ($content) {
|
||||
echo $content;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MessageMock extends Message
|
||||
{
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class NegotiateTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider negotiateData
|
||||
*/
|
||||
public function testNegotiate($acceptHeader, $available, $expected)
|
||||
{
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
negotiateContentType($acceptHeader, $available)
|
||||
);
|
||||
}
|
||||
|
||||
public function negotiateData()
|
||||
{
|
||||
return [
|
||||
[ // simple
|
||||
'application/xml',
|
||||
['application/xml'],
|
||||
'application/xml',
|
||||
],
|
||||
[ // no header
|
||||
null,
|
||||
['application/xml'],
|
||||
'application/xml',
|
||||
],
|
||||
[ // 2 options
|
||||
'application/json',
|
||||
['application/xml', 'application/json'],
|
||||
'application/json',
|
||||
],
|
||||
[ // 2 choices
|
||||
'application/json, application/xml',
|
||||
['application/xml'],
|
||||
'application/xml',
|
||||
],
|
||||
[ // quality
|
||||
'application/xml;q=0.2, application/json',
|
||||
['application/xml', 'application/json'],
|
||||
'application/json',
|
||||
],
|
||||
[ // wildcard
|
||||
'image/jpeg, image/png, */*',
|
||||
['application/xml', 'application/json'],
|
||||
'application/xml',
|
||||
],
|
||||
[ // wildcard + quality
|
||||
'image/jpeg, image/png; q=0.5, */*',
|
||||
['application/xml', 'application/json', 'image/png'],
|
||||
'application/xml',
|
||||
],
|
||||
[ // no match
|
||||
'image/jpeg',
|
||||
['application/xml'],
|
||||
null,
|
||||
],
|
||||
[ // This is used in sabre/dav
|
||||
'text/vcard; version=4.0',
|
||||
[
|
||||
// Most often used mime-type. Version 3
|
||||
'text/x-vcard',
|
||||
// The correct standard mime-type. Defaults to version 3 as
|
||||
// well.
|
||||
'text/vcard',
|
||||
// vCard 4
|
||||
'text/vcard; version=4.0',
|
||||
// vCard 3
|
||||
'text/vcard; version=3.0',
|
||||
// jCard
|
||||
'application/vcard+json',
|
||||
],
|
||||
'text/vcard; version=4.0',
|
||||
],
|
||||
[ // rfc7231 example 1
|
||||
'audio/*; q=0.2, audio/basic',
|
||||
[
|
||||
'audio/pcm',
|
||||
'audio/basic',
|
||||
],
|
||||
'audio/basic',
|
||||
],
|
||||
[ // Lower quality after
|
||||
'audio/pcm; q=0.2, audio/basic; q=0.1',
|
||||
[
|
||||
'audio/pcm',
|
||||
'audio/basic',
|
||||
],
|
||||
'audio/pcm',
|
||||
],
|
||||
[ // Random parameter, should be ignored
|
||||
'audio/pcm; hello; q=0.2, audio/basic; q=0.1',
|
||||
[
|
||||
'audio/pcm',
|
||||
'audio/basic',
|
||||
],
|
||||
'audio/pcm',
|
||||
],
|
||||
[ // No whitespace after type, should pick the one that is the most specific.
|
||||
'text/vcard;version=3.0, text/vcard',
|
||||
[
|
||||
'text/vcard',
|
||||
'text/vcard; version=3.0',
|
||||
],
|
||||
'text/vcard; version=3.0',
|
||||
],
|
||||
[ // Same as last one, but order is different
|
||||
'text/vcard, text/vcard;version=3.0',
|
||||
[
|
||||
'text/vcard; version=3.0',
|
||||
'text/vcard',
|
||||
],
|
||||
'text/vcard; version=3.0',
|
||||
],
|
||||
[ // Charset should be ignored here.
|
||||
'text/vcard; charset=utf-8; version=3.0, text/vcard',
|
||||
[
|
||||
'text/vcard',
|
||||
'text/vcard; version=3.0',
|
||||
],
|
||||
'text/vcard; version=3.0',
|
||||
],
|
||||
[ // Undefined offset issue.
|
||||
'text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2',
|
||||
['application/xml', 'application/json', 'image/png'],
|
||||
'application/xml',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class RequestDecoratorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected $inner;
|
||||
protected $outer;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->inner = new Request('GET', '/');
|
||||
$this->outer = new RequestDecorator($this->inner);
|
||||
}
|
||||
|
||||
public function testMethod()
|
||||
{
|
||||
$this->outer->setMethod('FOO');
|
||||
$this->assertEquals('FOO', $this->inner->getMethod());
|
||||
$this->assertEquals('FOO', $this->outer->getMethod());
|
||||
}
|
||||
|
||||
public function testUrl()
|
||||
{
|
||||
$this->outer->setUrl('/foo');
|
||||
$this->assertEquals('/foo', $this->inner->getUrl());
|
||||
$this->assertEquals('/foo', $this->outer->getUrl());
|
||||
}
|
||||
|
||||
public function testAbsoluteUrl()
|
||||
{
|
||||
$this->outer->setAbsoluteUrl('http://example.org/foo');
|
||||
$this->assertEquals('http://example.org/foo', $this->inner->getAbsoluteUrl());
|
||||
$this->assertEquals('http://example.org/foo', $this->outer->getAbsoluteUrl());
|
||||
}
|
||||
|
||||
public function testBaseUrl()
|
||||
{
|
||||
$this->outer->setBaseUrl('/foo');
|
||||
$this->assertEquals('/foo', $this->inner->getBaseUrl());
|
||||
$this->assertEquals('/foo', $this->outer->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testPath()
|
||||
{
|
||||
$this->outer->setBaseUrl('/foo');
|
||||
$this->outer->setUrl('/foo/bar');
|
||||
$this->assertEquals('bar', $this->inner->getPath());
|
||||
$this->assertEquals('bar', $this->outer->getPath());
|
||||
}
|
||||
|
||||
public function testQueryParams()
|
||||
{
|
||||
$this->outer->setUrl('/foo?a=b&c=d&e');
|
||||
$expected = [
|
||||
'a' => 'b',
|
||||
'c' => 'd',
|
||||
'e' => null,
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, $this->inner->getQueryParameters());
|
||||
$this->assertEquals($expected, $this->outer->getQueryParameters());
|
||||
}
|
||||
|
||||
public function testPostData()
|
||||
{
|
||||
$postData = [
|
||||
'a' => 'b',
|
||||
'c' => 'd',
|
||||
'e' => null,
|
||||
];
|
||||
|
||||
$this->outer->setPostData($postData);
|
||||
$this->assertEquals($postData, $this->inner->getPostData());
|
||||
$this->assertEquals($postData, $this->outer->getPostData());
|
||||
}
|
||||
|
||||
public function testServerData()
|
||||
{
|
||||
$serverData = [
|
||||
'HTTPS' => 'On',
|
||||
];
|
||||
|
||||
$this->outer->setRawServerData($serverData);
|
||||
$this->assertEquals('On', $this->inner->getRawServerValue('HTTPS'));
|
||||
$this->assertEquals('On', $this->outer->getRawServerValue('HTTPS'));
|
||||
|
||||
$this->assertNull($this->inner->getRawServerValue('FOO'));
|
||||
$this->assertNull($this->outer->getRawServerValue('FOO'));
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
{
|
||||
$this->inner->setMethod('POST');
|
||||
$this->inner->setUrl('/foo/bar/');
|
||||
$this->inner->setBody('foo');
|
||||
$this->inner->setHeader('foo', 'bar');
|
||||
|
||||
$this->assertEquals((string) $this->inner, (string) $this->outer);
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class RequestTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$request = new Request('GET', '/foo', [
|
||||
'User-Agent' => 'Evert',
|
||||
]);
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
$this->assertEquals('/foo', $request->getUrl());
|
||||
$this->assertEquals([
|
||||
'User-Agent' => ['Evert'],
|
||||
], $request->getHeaders());
|
||||
}
|
||||
|
||||
public function testGetQueryParameters()
|
||||
{
|
||||
$request = new Request('GET', '/foo?a=b&c&d=e');
|
||||
$this->assertEquals([
|
||||
'a' => 'b',
|
||||
'c' => null,
|
||||
'd' => 'e',
|
||||
], $request->getQueryParameters());
|
||||
}
|
||||
|
||||
public function testGetQueryParametersNoData()
|
||||
{
|
||||
$request = new Request('GET', '/foo');
|
||||
$this->assertEquals([], $request->getQueryParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* @backupGlobals
|
||||
*/
|
||||
public function testCreateFromPHPRequest()
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/';
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
|
||||
$request = Sapi::getRequest();
|
||||
$this->assertEquals('PUT', $request->getMethod());
|
||||
}
|
||||
|
||||
public function testGetAbsoluteUrl()
|
||||
{
|
||||
$r = new Request('GET', '/foo', [
|
||||
'Host' => 'sabredav.org',
|
||||
]);
|
||||
|
||||
$this->assertEquals('http://sabredav.org/foo', $r->getAbsoluteUrl());
|
||||
|
||||
$s = [
|
||||
'HTTP_HOST' => 'sabredav.org',
|
||||
'REQUEST_URI' => '/foo',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'HTTPS' => 'on',
|
||||
];
|
||||
|
||||
$r = Sapi::createFromServerArray($s);
|
||||
|
||||
$this->assertEquals('https://sabredav.org/foo', $r->getAbsoluteUrl());
|
||||
}
|
||||
|
||||
public function testGetPostData()
|
||||
{
|
||||
$post = [
|
||||
'bla' => 'foo',
|
||||
];
|
||||
$r = new Request('POST', '/');
|
||||
$r->setPostData($post);
|
||||
$this->assertEquals($post, $r->getPostData());
|
||||
}
|
||||
|
||||
public function testGetPath()
|
||||
{
|
||||
$request = new Request('GET', '/foo/bar/');
|
||||
$request->setBaseUrl('/foo');
|
||||
$request->setUrl('/foo/bar/');
|
||||
|
||||
$this->assertEquals('bar', $request->getPath());
|
||||
}
|
||||
|
||||
public function testGetPathStrippedQuery()
|
||||
{
|
||||
$request = new Request('GET', '/foo/bar?a=B');
|
||||
$request->setBaseUrl('/foo');
|
||||
|
||||
$this->assertEquals('bar', $request->getPath());
|
||||
}
|
||||
|
||||
public function testGetPathMissingSlash()
|
||||
{
|
||||
$request = new Request('GET', '/foo');
|
||||
$request->setBaseUrl('/foo/');
|
||||
|
||||
$this->assertEquals('', $request->getPath());
|
||||
}
|
||||
|
||||
public function testGetPathOutsideBaseUrl()
|
||||
{
|
||||
$this->expectException('LogicException');
|
||||
$request = new Request('GET', '/bar/');
|
||||
$request->setBaseUrl('/foo/');
|
||||
|
||||
$request->getPath();
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
{
|
||||
$request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml']);
|
||||
$request->setBody('foo');
|
||||
|
||||
$expected = "PUT /foo/bar HTTP/1.1\r\n"
|
||||
."Content-Type: text/xml\r\n"
|
||||
."\r\n"
|
||||
.'foo';
|
||||
$this->assertEquals($expected, (string) $request);
|
||||
}
|
||||
|
||||
public function testToStringAuthorization()
|
||||
{
|
||||
$request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml', 'Authorization' => 'Basic foobar']);
|
||||
$request->setBody('foo');
|
||||
|
||||
$expected = "PUT /foo/bar HTTP/1.1\r\n"
|
||||
."Content-Type: text/xml\r\n"
|
||||
."Authorization: Basic REDACTED\r\n"
|
||||
."\r\n"
|
||||
.'foo';
|
||||
$this->assertEquals($expected, (string) $request);
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlHttp(): void
|
||||
{
|
||||
$request = new Request('GET', 'http://example.com/foo/bar?a=b&c=d');
|
||||
self::assertEquals('http://example.com/foo/bar?a=b&c=d', $request->getAbsoluteUrl());
|
||||
}
|
||||
|
||||
public function testAbsoluteUrlHttpHostPrevalence(): void
|
||||
{
|
||||
$request = new Request('GET', 'http://example.com/foo/bar?a=b&c=d', [
|
||||
'Host' => 'example.org',
|
||||
]);
|
||||
self::assertEquals('http://example.com/foo/bar?a=b&c=d', $request->getAbsoluteUrl());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class ResponseDecoratorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected $inner;
|
||||
protected $outer;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->inner = new Response();
|
||||
$this->outer = new ResponseDecorator($this->inner);
|
||||
}
|
||||
|
||||
public function testStatus()
|
||||
{
|
||||
$this->outer->setStatus(201);
|
||||
$this->assertEquals(201, $this->inner->getStatus());
|
||||
$this->assertEquals(201, $this->outer->getStatus());
|
||||
$this->assertEquals('Created', $this->inner->getStatusText());
|
||||
$this->assertEquals('Created', $this->outer->getStatusText());
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
{
|
||||
$this->inner->setStatus(201);
|
||||
$this->inner->setBody('foo');
|
||||
$this->inner->setHeader('foo', 'bar');
|
||||
|
||||
$this->assertEquals((string) $this->inner, (string) $this->outer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class ResponseTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$response = new Response(200, ['Content-Type' => 'text/xml']);
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals('OK', $response->getStatusText());
|
||||
}
|
||||
|
||||
public function testSetStatus()
|
||||
{
|
||||
$response = new Response();
|
||||
$response->setStatus('402 Where\'s my money?');
|
||||
$this->assertEquals(402, $response->getStatus());
|
||||
$this->assertEquals('Where\'s my money?', $response->getStatusText());
|
||||
}
|
||||
|
||||
public function testInvalidStatus()
|
||||
{
|
||||
$this->expectException('InvalidArgumentException');
|
||||
$response = new Response(1000);
|
||||
}
|
||||
|
||||
public function testToString()
|
||||
{
|
||||
$response = new Response(200, ['Content-Type' => 'text/xml']);
|
||||
$response->setBody('foo');
|
||||
|
||||
$expected = "HTTP/1.1 200 OK\r\n"
|
||||
."Content-Type: text/xml\r\n"
|
||||
."\r\n"
|
||||
.'foo';
|
||||
$this->assertEquals($expected, (string) $response);
|
||||
}
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class SapiTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testConstructFromServerArray()
|
||||
{
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_URI' => '/foo',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'HTTP_USER_AGENT' => 'Evert',
|
||||
'CONTENT_TYPE' => 'text/xml',
|
||||
'CONTENT_LENGTH' => '400',
|
||||
'SERVER_PROTOCOL' => 'HTTP/1.0',
|
||||
]);
|
||||
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
$this->assertEquals('/foo', $request->getUrl());
|
||||
$this->assertEquals([
|
||||
'User-Agent' => ['Evert'],
|
||||
'Content-Type' => ['text/xml'],
|
||||
'Content-Length' => ['400'],
|
||||
], $request->getHeaders());
|
||||
|
||||
$this->assertEquals('1.0', $request->getHttpVersion());
|
||||
|
||||
$this->assertEquals('400', $request->getRawServerValue('CONTENT_LENGTH'));
|
||||
$this->assertNull($request->getRawServerValue('FOO'));
|
||||
}
|
||||
|
||||
public function testConstructFromServerArrayOnNullUrl()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The _SERVER array must have a REQUEST_URI key');
|
||||
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'HTTP_USER_AGENT' => 'Evert',
|
||||
'CONTENT_TYPE' => 'text/xml',
|
||||
'CONTENT_LENGTH' => '400',
|
||||
'SERVER_PROTOCOL' => 'HTTP/1.0',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConstructFromServerArrayOnNullMethod()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The _SERVER array must have a REQUEST_METHOD key');
|
||||
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_URI' => '/foo',
|
||||
'HTTP_USER_AGENT' => 'Evert',
|
||||
'CONTENT_TYPE' => 'text/xml',
|
||||
'CONTENT_LENGTH' => '400',
|
||||
'SERVER_PROTOCOL' => 'HTTP/1.0',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConstructPHPAuth()
|
||||
{
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_URI' => '/foo',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'PHP_AUTH_USER' => 'user',
|
||||
'PHP_AUTH_PW' => 'pass',
|
||||
]);
|
||||
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
$this->assertEquals('/foo', $request->getUrl());
|
||||
$this->assertEquals([
|
||||
'Authorization' => ['Basic '.base64_encode('user:pass')],
|
||||
], $request->getHeaders());
|
||||
}
|
||||
|
||||
public function testConstructPHPAuthDigest()
|
||||
{
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_URI' => '/foo',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'PHP_AUTH_DIGEST' => 'blabla',
|
||||
]);
|
||||
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
$this->assertEquals('/foo', $request->getUrl());
|
||||
$this->assertEquals([
|
||||
'Authorization' => ['Digest blabla'],
|
||||
], $request->getHeaders());
|
||||
}
|
||||
|
||||
public function testConstructRedirectAuth()
|
||||
{
|
||||
$request = Sapi::createFromServerArray([
|
||||
'REQUEST_URI' => '/foo',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'REDIRECT_HTTP_AUTHORIZATION' => 'Basic bla',
|
||||
]);
|
||||
|
||||
$this->assertEquals('GET', $request->getMethod());
|
||||
$this->assertEquals('/foo', $request->getUrl());
|
||||
$this->assertEquals([
|
||||
'Authorization' => ['Basic bla'],
|
||||
], $request->getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* Unfortunately we have no way of testing if the HTTP response code got
|
||||
* changed.
|
||||
*/
|
||||
public function testSend()
|
||||
{
|
||||
if (!function_exists('xdebug_get_headers')) {
|
||||
$this->markTestSkipped('XDebug needs to be installed for this test to run');
|
||||
}
|
||||
|
||||
$response = new Response(204, ['Content-Type' => 'text/xml;charset=UTF-8']);
|
||||
|
||||
// Second Content-Type header. Normally this doesn't make sense.
|
||||
$response->addHeader('Content-Type', 'application/xml');
|
||||
$response->setBody('foo');
|
||||
|
||||
ob_start();
|
||||
|
||||
Sapi::sendResponse($response);
|
||||
$headers = xdebug_get_headers();
|
||||
|
||||
$result = ob_get_clean();
|
||||
header_remove();
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'Content-Type: text/xml;charset=UTF-8',
|
||||
'Content-Type: application/xml',
|
||||
],
|
||||
$headers
|
||||
);
|
||||
|
||||
$this->assertEquals('foo', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @depends testSend
|
||||
*/
|
||||
public function testSendLimitedByContentLengthString()
|
||||
{
|
||||
$response = new Response(200);
|
||||
|
||||
$response->addHeader('Content-Length', 19);
|
||||
$response->setBody('Send this sentence. Ignore this one.');
|
||||
|
||||
ob_start();
|
||||
|
||||
Sapi::sendResponse($response);
|
||||
|
||||
$result = ob_get_clean();
|
||||
header_remove();
|
||||
|
||||
$this->assertEquals('Send this sentence.', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether http2 is recognized.
|
||||
*/
|
||||
public function testRecognizeHttp2()
|
||||
{
|
||||
$request = Sapi::createFromServerArray([
|
||||
'SERVER_PROTOCOL' => 'HTTP/2.0',
|
||||
'REQUEST_URI' => 'bla',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
]);
|
||||
|
||||
$this->assertEquals('2.0', $request->getHttpVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @depends testSend
|
||||
*/
|
||||
public function testSendLimitedByContentLengthStream()
|
||||
{
|
||||
$response = new Response(200, ['Content-Length' => 19]);
|
||||
|
||||
$body = fopen('php://memory', 'w');
|
||||
fwrite($body, 'Ignore this. Send this sentence. Ignore this too.');
|
||||
rewind($body);
|
||||
fread($body, 13);
|
||||
$response->setBody($body);
|
||||
|
||||
ob_start();
|
||||
|
||||
Sapi::sendResponse($response);
|
||||
|
||||
$result = ob_get_clean();
|
||||
header_remove();
|
||||
|
||||
$this->assertEquals('Send this sentence.', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @depends testSend
|
||||
*
|
||||
* @dataProvider sendContentRangeStreamData
|
||||
*/
|
||||
public function testSendContentRangeStream($ignoreAtStart, $sendText, $multiplier, $ignoreAtEnd, $contentLength)
|
||||
{
|
||||
$partial = str_repeat($sendText, $multiplier);
|
||||
$ignoreAtStartLength = strlen($ignoreAtStart);
|
||||
$ignoreAtEndLength = strlen($ignoreAtEnd);
|
||||
$body = fopen('php://memory', 'w');
|
||||
if (!$contentLength) {
|
||||
$contentLength = strlen($partial);
|
||||
}
|
||||
fwrite($body, $ignoreAtStart);
|
||||
fwrite($body, $partial);
|
||||
if ($ignoreAtEndLength > 0) {
|
||||
fwrite($body, $ignoreAtEnd);
|
||||
}
|
||||
rewind($body);
|
||||
if ($ignoreAtStartLength > 0) {
|
||||
fread($body, $ignoreAtStartLength);
|
||||
}
|
||||
$response = new Response(200, [
|
||||
'Content-Length' => $contentLength,
|
||||
'Content-Range' => sprintf('bytes %d-%d/%d', $ignoreAtStartLength, $ignoreAtStartLength + strlen($partial) - 1, $ignoreAtStartLength + strlen($partial) + $ignoreAtEndLength),
|
||||
]);
|
||||
$response->setBody($body);
|
||||
|
||||
ob_start();
|
||||
|
||||
Sapi::sendResponse($response);
|
||||
|
||||
$result = ob_get_clean();
|
||||
header_remove();
|
||||
|
||||
$this->assertEquals($partial, $result);
|
||||
}
|
||||
|
||||
public function sendContentRangeStreamData()
|
||||
{
|
||||
return [
|
||||
['Ignore this. ', 'Send this.', 10, ' Ignore this at end.'],
|
||||
['Ignore this. ', 'Send this.', 1000, ' Ignore this at end.'],
|
||||
['Ignore this. ', 'S', 4096, ' Ignore this at end.'],
|
||||
['I', 'S', 4094, 'E'],
|
||||
['', 'Send this.', 10, ' Ignore this at end.'],
|
||||
['', 'Send this.', 1000, ' Ignore this at end.'],
|
||||
['', 'S', 4096, ' Ignore this at end.'],
|
||||
['', 'S', 4094, 'En'],
|
||||
['Ignore this. ', 'Send this.', 10, ''],
|
||||
['Ignore this. ', 'Send this.', 1000, ''],
|
||||
['Ignore this. ', 'S', 4096, ''],
|
||||
['Ig', 'S', 4094, ''],
|
||||
|
||||
// Provide contentLength greater than the bytes remaining in the stream.
|
||||
['Ignore this. ', 'Send this.', 10, '', 101],
|
||||
['Ignore this. ', 'Send this.', 1000, '', 10001],
|
||||
['Ignore this. ', 'S', 4096, '', 5000000],
|
||||
['I', 'S', 4094, '', 8095],
|
||||
// Provide contentLength equal to the bytes remaining in the stream.
|
||||
['', 'Send this.', 10, '', 100],
|
||||
['Ignore this. ', 'Send this.', 1000, '', 10000],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @runInSeparateProcess
|
||||
*
|
||||
* @depends testSend
|
||||
*/
|
||||
public function testSendWorksWithCallbackAsBody()
|
||||
{
|
||||
$response = new Response(200, [], function () {
|
||||
$fd = fopen('php://output', 'r+');
|
||||
fwrite($fd, 'foo');
|
||||
fclose($fd);
|
||||
});
|
||||
|
||||
ob_start();
|
||||
|
||||
Sapi::sendResponse($response);
|
||||
|
||||
$result = ob_get_clean();
|
||||
|
||||
$this->assertEquals('foo', $result);
|
||||
}
|
||||
|
||||
public function testSendConnectionAborted(): void
|
||||
{
|
||||
$baseUrl = getenv('BASEURL');
|
||||
if (!$baseUrl) {
|
||||
$this->markTestSkipped('Set an environment value BASEURL to continue');
|
||||
}
|
||||
|
||||
$url = rtrim($baseUrl, '/').'/connection_aborted.php';
|
||||
$chunk_size = 4 * 1024 * 1024;
|
||||
$fetch_size = 6 * 1024 * 1024;
|
||||
|
||||
$stream = fopen($url, 'r');
|
||||
$size = 0;
|
||||
|
||||
while ($size <= $fetch_size) {
|
||||
$temp = fread($stream, 8192);
|
||||
if (false === $temp) {
|
||||
break;
|
||||
}
|
||||
$size += strlen($temp);
|
||||
}
|
||||
|
||||
fclose($stream);
|
||||
|
||||
sleep(5);
|
||||
|
||||
$bytes_read = file_get_contents(sys_get_temp_dir().'/dummy_stream_read_counter');
|
||||
$this->assertEquals($chunk_size * 2, $bytes_read);
|
||||
$this->assertGreaterThanOrEqual($fetch_size, $bytes_read);
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\HTTP;
|
||||
|
||||
class URLUtilTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testEncodePath()
|
||||
{
|
||||
$str = '';
|
||||
for ($i = 0; $i < 128; ++$i) {
|
||||
$str .= chr($i);
|
||||
}
|
||||
|
||||
$newStr = encodePath($str);
|
||||
|
||||
$this->assertEquals(
|
||||
'%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f'.
|
||||
'%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f'.
|
||||
'%20%21%22%23%24%25%26%27()%2a%2b%2c-./'.
|
||||
'0123456789:%3b%3c%3d%3e%3f'.
|
||||
'@ABCDEFGHIJKLMNO'.
|
||||
'PQRSTUVWXYZ%5b%5c%5d%5e_'.
|
||||
'%60abcdefghijklmno'.
|
||||
'pqrstuvwxyz%7b%7c%7d~%7f',
|
||||
$newStr);
|
||||
|
||||
$this->assertEquals($str, decodePath($newStr));
|
||||
}
|
||||
|
||||
public function testEncodePathSegment()
|
||||
{
|
||||
$str = '';
|
||||
for ($i = 0; $i < 128; ++$i) {
|
||||
$str .= chr($i);
|
||||
}
|
||||
|
||||
$newStr = encodePathSegment($str);
|
||||
|
||||
// Note: almost exactly the same as the last test, with the
|
||||
// exception of the encoding of / (ascii code 2f)
|
||||
$this->assertEquals(
|
||||
'%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f'.
|
||||
'%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f'.
|
||||
'%20%21%22%23%24%25%26%27()%2a%2b%2c-.%2f'.
|
||||
'0123456789:%3b%3c%3d%3e%3f'.
|
||||
'@ABCDEFGHIJKLMNO'.
|
||||
'PQRSTUVWXYZ%5b%5c%5d%5e_'.
|
||||
'%60abcdefghijklmno'.
|
||||
'pqrstuvwxyz%7b%7c%7d~%7f',
|
||||
$newStr);
|
||||
|
||||
$this->assertEquals($str, decodePathSegment($newStr));
|
||||
}
|
||||
|
||||
public function testDecode()
|
||||
{
|
||||
$str = 'Hello%20Test+Test2.txt';
|
||||
$newStr = decodePath($str);
|
||||
$this->assertEquals('Hello Test+Test2.txt', $newStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testDecode
|
||||
*/
|
||||
public function testDecodeUmlaut()
|
||||
{
|
||||
$str = 'Hello%C3%BC.txt';
|
||||
$newStr = decodePath($str);
|
||||
$this->assertEquals("Hello\xC3\xBC.txt", $newStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testDecode
|
||||
*/
|
||||
public function testDecodeSlavicWords()
|
||||
{
|
||||
$words = [
|
||||
'Ostroměr',
|
||||
'Šventaragis',
|
||||
'Świętopełk',
|
||||
'Dušan',
|
||||
'Živko',
|
||||
];
|
||||
foreach ($words as $word) {
|
||||
$str = rawurlencode($word);
|
||||
$newStr = decodePath($str);
|
||||
$this->assertEquals($word, $newStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testDecodeUmlaut
|
||||
*/
|
||||
public function testDecodeUmlautLatin1()
|
||||
{
|
||||
$str = 'Hello%FC.txt';
|
||||
$newStr = decodePath($str);
|
||||
$this->assertEquals("Hello\xC3\xBC.txt", $newStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* This testcase was sent by a bug reporter.
|
||||
*
|
||||
* @depends testDecode
|
||||
*/
|
||||
public function testDecodeAccentsWindows7()
|
||||
{
|
||||
$str = '/webdav/%C3%A0fo%C3%B3';
|
||||
$newStr = decodePath($str);
|
||||
$this->assertEquals(strtolower($str), encodePath($newStr));
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
ini_set('error_reporting', (string) (E_ALL | E_STRICT | E_DEPRECATED));
|
||||
|
||||
// Composer autoloader
|
||||
if (file_exists(__DIR__.'/../vendor/autoload.php')) {
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
} else {
|
||||
// We may be running as a dependency inside some other repo.
|
||||
// So we are probably in vendor/sabre/http/tests of that repo.
|
||||
// Go up 3 levels to find autoload.php
|
||||
include __DIR__.'/../../../autoload.php';
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<phpunit
|
||||
colors="true"
|
||||
beStrictAboutCoversAnnotation="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutTodoAnnotatedTests="true"
|
||||
bootstrap="bootstrap.php"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Sabre_HTTP">
|
||||
<directory>HTTP/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">../lib/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
<php>
|
||||
<env name="BASEURL" value="http://localhost:8000"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
header('X-Test: Bar');
|
||||
?>
|
||||
bar
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Sabre\HTTP;
|
||||
|
||||
include '../bootstrap.php';
|
||||
|
||||
class DummyStream
|
||||
{
|
||||
private $position;
|
||||
|
||||
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
||||
{
|
||||
$this->position = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function stream_read(int $count): string
|
||||
{
|
||||
$this->position += $count;
|
||||
|
||||
return random_bytes($count);
|
||||
}
|
||||
|
||||
public function stream_tell(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof(): bool
|
||||
{
|
||||
return $this->position > 25 * 1024 * 1024;
|
||||
}
|
||||
|
||||
public function stream_close(): void
|
||||
{
|
||||
file_put_contents(sys_get_temp_dir().'/dummy_stream_read_counter', $this->position);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The DummyStream wrapper has two functions:
|
||||
* - Provide dummy data.
|
||||
* - Count how many bytes have been read.
|
||||
*/
|
||||
stream_wrapper_register('dummy', DummyStream::class);
|
||||
|
||||
/*
|
||||
* Overwrite default connection handling.
|
||||
* The default behaviour is however for your script to be aborted when the remote client disconnects.
|
||||
*
|
||||
* Nextcloud/ownCloud set ignore_user_abort(true) on purpose to work around
|
||||
* some edge cases where the default behavior would end a script too early.
|
||||
*
|
||||
* https://github.com/owncloud/core/issues/22370
|
||||
* https://github.com/owncloud/core/pull/26775
|
||||
*/
|
||||
ignore_user_abort(true);
|
||||
|
||||
$body = fopen('dummy://hello', 'r');
|
||||
|
||||
$response = new HTTP\Response();
|
||||
$response->setStatus(200);
|
||||
$response->addHeader('Content-Length', 25 * 1024 * 1024);
|
||||
$response->setBody($body);
|
||||
|
||||
HTTP\Sapi::sendResponse($response);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
foo
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
$data = str_repeat('x', 32 * 1024 * 1024);
|
||||
header('Content-Length: '.strlen($data));
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
echo $data;
|
||||
Reference in New Issue
Block a user