gestione documentale avanzata 1 parte

This commit is contained in:
2026-05-28 09:34:28 +02:00
parent 3471befb1a
commit f2b0833b90
34482 changed files with 4312269 additions and 546 deletions
+68
View File
@@ -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'
+9
View File
@@ -0,0 +1,9 @@
# Composer
vendor/
composer.lock
# Tests
tests/cov/
.phpunit.result.cache
.php_cs.cache
.php-cs-fixer.cache
+17
View File
@@ -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;
+12
View File
@@ -0,0 +1,12 @@
<?php
$config = PhpCsFixer\Config::create();
$config->getFinder()
->exclude('vendor')
->in(__DIR__);
$config->setRules([
'@PSR1' => true,
'@Symfony' => true
]);
return $config;
+403
View File
@@ -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
+27
View File
@@ -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.
+747
View File
@@ -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 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=master)](https://travis-ci.org/sabre-io/http) |
| 4.2 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=4.2)](https://travis-ci.org/sabre-io/http) |
| 3.0 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=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
View File
+64
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+620
View File
@@ -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
View File
@@ -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
{
}
+50
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+240
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
parameters:
level: 1
+236
View File
@@ -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;
}
}
+67
View File
@@ -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());
}
}
+55
View File
@@ -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());
}
}
+185
View File
@@ -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
View File
@@ -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
View File
@@ -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');
}
}
+91
View File
@@ -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
View File
@@ -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
View File
@@ -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',
],
];
}
}
+103
View File
@@ -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
View File
@@ -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());
}
}
+35
View File
@@ -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);
}
}
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,5 @@
<?php
header('X-Test: Bar');
?>
bar
+69
View File
@@ -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);
+1
View File
@@ -0,0 +1 @@
foo
+7
View File
@@ -0,0 +1,7 @@
<?php
$data = str_repeat('x', 32 * 1024 * 1024);
header('Content-Length: '.strlen($data));
header('Content-Type: text/plain');
echo $data;