Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,2 @@
coverage_clover: clover.xml
json_path: coveralls-upload.json

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,43 @@
# Contributor Code of Conduct
The Zend Framework project adheres to [The Code Manifesto](http://codemanifesto.com)
as its guidelines for contributor interactions.
## The Code Manifesto
We want to work in an ecosystem that empowers developers to reach their
potential — one that encourages growth and effective collaboration. A space that
is safe for all.
A space such as this benefits everyone that participates in it. It encourages
new developers to enter our field. It is through discussion and collaboration
that we grow, and through growth that we improve.
In the effort to create such a place, we hold to these values:
1. **Discrimination limits us.** This includes discrimination on the basis of
race, gender, sexual orientation, gender identity, age, nationality, technology
and any other arbitrary exclusion of a group of people.
2. **Boundaries honor us.** Your comfort levels are not everyones comfort
levels. Remember that, and if brought to your attention, heed it.
3. **We are our biggest assets.** None of us were born masters of our trade.
Each of us has been helped along the way. Return that favor, when and where
you can.
4. **We are resources for the future.** As an extension of #3, share what you
know. Make yourself a resource to help those that come after you.
5. **Respect defines us.** Treat others as you wish to be treated. Make your
discussions, criticisms and debates from a position of respectfulness. Ask
yourself, is it true? Is it necessary? Is it constructive? Anything less is
unacceptable.
6. **Reactions require grace.** Angry responses are valid, but abusive language
and vindictive actions are toxic. When something happens that offends you,
handle it assertively, but be respectful. Escalate reasonably, and try to
allow the offender an opportunity to explain themselves, and possibly correct
the issue.
7. **Opinions are just that: opinions.** Each and every one of us, due to our
background and upbringing, have varying opinions. The fact of the matter, is
that is perfectly acceptable. Remember this: if you respect your own
opinions, you should respect the opinions of others.
8. **To err is human.** You might not intend it, but mistakes do happen and
contribute to build experience. Tolerate honest mistakes, and don't hesitate
to apologize if you make one yourself.

View file

@ -0,0 +1,228 @@
# CONTRIBUTING
## RESOURCES
If you wish to contribute to Zend Framework, please be sure to
read/subscribe to the following resources:
- [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards)
- [Contributor's Guide](http://framework.zend.com/participate/contributor-guide)
- ZF Contributor's mailing list:
Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html
Subscribe: zf-contributors-subscribe@lists.zend.com
- ZF Contributor's IRC channel:
#zftalk.dev on Freenode.net
If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-diactoros/issues/new).
## Reporting Potential Security Issues
If you have encountered a potential security vulnerability, please **DO NOT** report it on the public
issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead.
We will work with you to verify the vulnerability and patch it as soon as possible.
When reporting issues, please provide the following information:
- Component(s) affected
- A description indicating how to reproduce the issue
- A summary of the security vulnerability and impact
We request that you contact us via the email address above and give the project
contributors a chance to resolve the vulnerability and issue a new release prior
to any public exposure; this helps protect users and provides them with a chance
to upgrade and/or update in order to protect their applications.
For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc).
## Documentation
Documentation is in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/),
and rendered using [bookdown](http://bookdown.io). Please read and follow the [general documentation
guidelines](https://github.com/zendframework/documentation/blob/master/CONTRIBUTING.md) when
providing documentation.
All new features **must** include documentation before they may be accepted and merged.
## RUNNING TESTS
To run tests:
- Clone the repository:
```console
$ git clone git@github.com:zendframework/zend-diactoros.git
$ cd
```
- Install dependencies via composer:
```console
$ curl -sS https://getcomposer.org/installer | php --
$ ./composer.phar install
```
If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/
- Run the tests via `phpunit` and the provided PHPUnit config, like in this example:
```console
$ ./vendor/bin/phpunit
```
## Running Coding Standards Checks
This component uses [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) for coding
standards checks, and provides configuration for our selected checks.
`phpcs` is installed by default via Composer.
To run checks only:
```console
$ composer cs-check
```
`phpcs` also installs a tool named `phpcbf` which can attempt to fix problems
for you:
```console
$ composer cs-fix
```
If you allow phpcbf to fix CS issues, please re-run the tests to ensure
they pass, and make sure you add and commit the changes after verification.
## Recommended Workflow for Contributions
Your first step is to establish a public repository from which we can
pull your work into the master repository. We recommend using
[GitHub](https://github.com), as that is where the component is already hosted.
1. Setup a [GitHub account](http://github.com/), if you haven't yet
2. Fork the repository (http://github.com/zendframework/zend-diactoros)
3. Clone the canonical repository locally and enter it.
```console
$ git clone git://github.com/zendframework/zend-diactoros.git
$ cd zend-diactoros
```
4. Add a remote to your fork; substitute your GitHub username in the command
below.
```console
$ git remote add {username} git@github.com:{username}/zend-diactoros.git
$ git fetch {username}
```
### Keeping Up-to-Date
Periodically, you should update your fork or personal repository to
match the canonical repository. Assuming you have setup your local repository
per the instructions above, you can do the following:
```console
$ git checkout master
$ git fetch origin
$ git rebase origin/master
# OPTIONALLY, to keep your remote up-to-date -
$ git push {username} master:master
```
If you're tracking other branches -- for example, the "develop" branch, where
new feature development occurs -- you'll want to do the same operations for that
branch; simply substitute "develop" for "master".
### Working on a patch
We recommend you do each new feature or bugfix in a new branch. This simplifies
the task of code review as well as the task of merging your changes into the
canonical repository.
A typical workflow will then consist of the following:
1. Create a new local branch based off either your master or develop branch.
2. Switch to your new local branch. (This step can be combined with the
previous step with the use of `git checkout -b`.)
3. Do some work, commit, repeat as necessary.
4. Push the local branch to your remote repository.
5. Send a pull request.
The mechanics of this process are actually quite trivial. Below, we will
create a branch for fixing an issue in the tracker.
```console
$ git checkout -b hotfix/9295
Switched to a new branch 'hotfix/9295'
```
... do some work ...
```console
$ git commit
```
... write your log message ...
```console
$ git push {username} hotfix/9295:hotfix/9295
Counting objects: 38, done.
Delta compression using up to 2 threads.
Compression objects: 100% (18/18), done.
Writing objects: 100% (20/20), 8.19KiB, done.
Total 20 (delta 12), reused 0 (delta 0)
To ssh://git@github.com/{username}/zend-diactoros.git
b5583aa..4f51698 HEAD -> master
```
To send a pull request, you have two options.
If using GitHub, you can do the pull request from there. Navigate to
your repository, select the branch you just created, and then select the
"Pull Request" button in the upper right. Select the user/organization
"zendframework" as the recipient.
If using your own repository - or even if using GitHub - you can use `git
format-patch` to create a patchset for us to apply; in fact, this is
**recommended** for security-related patches. If you use `format-patch`, please
send the patches as attachments to:
- zf-devteam@zend.com for patches without security implications
- zf-security@zend.com for security patches
#### What branch to issue the pull request against?
Which branch should you issue a pull request against?
- For fixes against the stable release, issue the pull request against the
"master" branch.
- For new features, or fixes that introduce new elements to the public API (such
as new public methods or properties), issue the pull request against the
"develop" branch.
### Branch Cleanup
As you might imagine, if you are a frequent contributor, you'll start to
get a ton of branches both locally and on your remote.
Once you know that your changes have been accepted to the master
repository, we suggest doing some cleanup of these branches.
- Local branch cleanup
```console
$ git branch -d <branchname>
```
- Remote branch removal
```console
$ git push {username} :<branchname>
```
## Conduct
Please see our [CONDUCT.md](CONDUCT.md) to understand expected behavior when interacting with others in the project.

View file

@ -0,0 +1,12 @@
Copyright (c) 2015-2016, Zend Technologies USA, Inc.
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 of Zend Technologies USA, Inc. 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.

View file

@ -0,0 +1,34 @@
# zend-diactoros
Master:
[![Build status][Master image]][Master]
[![Coverage Status][Master coverage image]][Master coverage]
Develop:
[![Build status][Develop image]][Develop]
[![Coverage Status][Develop coverage image]][Develop coverage]
> Diactoros (pronunciation: `/dɪʌktɒrɒs/`): an epithet for Hermes, meaning literally, "the messenger."
This package supercedes and replaces [phly/http](https://github.com/phly/http).
`zend-diactoros` is a PHP package containing implementations of the [accepted PSR-7 HTTP message interfaces](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md), as well as a "server" implementation similar to [node's http.Server](http://nodejs.org/api/http.html).
* File issues at https://github.com/zendframework/zend-diactoros/issues
* Issue patches to https://github.com/zendframework/zend-diactoros/pulls
## Documentation
Documentation is available at:
- https://zendframework.github.io/zend-diactoros/
Source files for documentation are [in the doc/ tree](doc/).
[Master]: https://travis-ci.org/zendframework/zend-diactoros
[Master image]: https://secure.travis-ci.org/zendframework/zend-diactoros.svg?branch=master
[Master coverage image]: https://img.shields.io/coveralls/zendframework/zend-diactoros/master.svg
[Master coverage]: https://coveralls.io/r/zendframework/zend-diactoros?branch=master
[Develop]: https://github.com/zendframework/zend-diactoros/tree/develop
[Develop image]: https://secure.travis-ci.org/zendframework/zend-diactoros.svg?branch=develop
[Develop coverage image]: https://coveralls.io/repos/zendframework/zend-diactoros/badge.svg?branch=develop
[Develop coverage]: https://coveralls.io/r/zendframework/zend-diactoros?branch=develop

View file

@ -0,0 +1,74 @@
{
"name": "zendframework/zend-diactoros",
"description": "PSR HTTP Message implementations",
"type": "library",
"license": "BSD-2-Clause",
"keywords": [
"http",
"psr",
"psr-7"
],
"homepage": "https://github.com/zendframework/zend-diactoros",
"support": {
"issues": "https://github.com/zendframework/zend-diactoros/issues",
"source": "https://github.com/zendframework/zend-diactoros"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev",
"dev-develop": "1.9.x-dev",
"dev-release-2.0": "2.0.x-dev"
}
},
"require": {
"php": "^5.6 || ^7.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"ext-dom": "*",
"ext-libxml": "*",
"php-http/psr7-integration-tests": "dev-master",
"phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
"zendframework/zend-coding-standard": "~1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"autoload": {
"files": [
"src/functions/create_uploaded_file.php",
"src/functions/marshal_headers_from_sapi.php",
"src/functions/marshal_method_from_sapi.php",
"src/functions/marshal_protocol_version_from_sapi.php",
"src/functions/marshal_uri_from_sapi.php",
"src/functions/normalize_server.php",
"src/functions/normalize_uploaded_files.php",
"src/functions/parse_cookie_header.php"
],
"psr-4": {
"Zend\\Diactoros\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ZendTest\\Diactoros\\": "test/"
},
"files": [
"test/TestAsset/Functions.php",
"test/TestAsset/SapiResponse.php"
]
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

1695
vendor/zendframework/zend-diactoros/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
docs_dir: doc/book
site_dir: doc/html
pages:
- index.md
- Overview: overview.md
- Installation: install.md
- Usage: usage.md
- Reference:
- "Custom Responses": custom-responses.md
- "Emitting Responses": emitting-responses.md
- Serialization: serialization.md
- API: api.md
site_name: zend-diactoros
site_description: 'zend-diactoros: PSR-7 HTTP message implementation'
repo_url: 'https://github.com/zendframework/zend-diactoros'
copyright: 'Copyright (c) 2016 <a href="http://www.zend.com/">Zend Technologies USA Inc.</a>'

View file

@ -0,0 +1,160 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use Psr\Http\Message\StreamInterface;
use UnexpectedValueException;
use function array_pop;
use function implode;
use function ltrim;
use function preg_match;
use function sprintf;
use function str_replace;
use function ucwords;
/**
* Provides base functionality for request and response de/serialization
* strategies, including functionality for retrieving a line at a time from
* the message, splitting headers from the body, and serializing headers.
*/
abstract class AbstractSerializer
{
const CR = "\r";
const EOL = "\r\n";
const LF = "\n";
/**
* Retrieve a single line from the stream.
*
* Retrieves a line from the stream; a line is defined as a sequence of
* characters ending in a CRLF sequence.
*
* @param StreamInterface $stream
* @return string
* @throws UnexpectedValueException if the sequence contains a CR or LF in
* isolation, or ends in a CR.
*/
protected static function getLine(StreamInterface $stream)
{
$line = '';
$crFound = false;
while (! $stream->eof()) {
$char = $stream->read(1);
if ($crFound && $char === self::LF) {
$crFound = false;
break;
}
// CR NOT followed by LF
if ($crFound && $char !== self::LF) {
throw new UnexpectedValueException('Unexpected carriage return detected');
}
// LF in isolation
if (! $crFound && $char === self::LF) {
throw new UnexpectedValueException('Unexpected line feed detected');
}
// CR found; do not append
if ($char === self::CR) {
$crFound = true;
continue;
}
// Any other character: append
$line .= $char;
}
// CR found at end of stream
if ($crFound) {
throw new UnexpectedValueException("Unexpected end of headers");
}
return $line;
}
/**
* Split the stream into headers and body content.
*
* Returns an array containing two elements
*
* - The first is an array of headers
* - The second is a StreamInterface containing the body content
*
* @param StreamInterface $stream
* @return array
* @throws UnexpectedValueException For invalid headers.
*/
protected static function splitStream(StreamInterface $stream)
{
$headers = [];
$currentHeader = false;
while ($line = self::getLine($stream)) {
if (preg_match(';^(?P<name>[!#$%&\'*+.^_`\|~0-9a-zA-Z-]+):(?P<value>.*)$;', $line, $matches)) {
$currentHeader = $matches['name'];
if (! isset($headers[$currentHeader])) {
$headers[$currentHeader] = [];
}
$headers[$currentHeader][] = ltrim($matches['value']);
continue;
}
if (! $currentHeader) {
throw new UnexpectedValueException('Invalid header detected');
}
if (! preg_match('#^[ \t]#', $line)) {
throw new UnexpectedValueException('Invalid header continuation');
}
// Append continuation to last header value found
$value = array_pop($headers[$currentHeader]);
$headers[$currentHeader][] = $value . ltrim($line);
}
// use RelativeStream to avoid copying initial stream into memory
return [$headers, new RelativeStream($stream, $stream->tell())];
}
/**
* Serialize headers to string values.
*
* @param array $headers
* @return string
*/
protected static function serializeHeaders(array $headers)
{
$lines = [];
foreach ($headers as $header => $values) {
$normalized = self::filterHeader($header);
foreach ($values as $value) {
$lines[] = sprintf('%s: %s', $normalized, $value);
}
}
return implode("\r\n", $lines);
}
/**
* Filter a header name to wordcase
*
* @param string $header
* @return string
*/
protected static function filterHeader($header)
{
$filtered = str_replace('-', ' ', $header);
$filtered = ucwords($filtered);
return str_replace(' ', '-', $filtered);
}
}

View file

@ -0,0 +1,185 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use function array_key_exists;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class CallbackStream implements StreamInterface
{
/**
* @var callable|null
*/
protected $callback;
/**
* @param callable $callback
* @throws InvalidArgumentException
*/
public function __construct(callable $callback)
{
$this->attach($callback);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->getContents();
}
/**
* {@inheritdoc}
*/
public function close()
{
$this->callback = null;
}
/**
* {@inheritdoc}
*/
public function detach()
{
$callback = $this->callback;
$this->callback = null;
return $callback;
}
/**
* Attach a new callback to the instance.
*
* @param callable $callback
* @throws InvalidArgumentException for callable callback
*/
public function attach(callable $callback)
{
$this->callback = $callback;
}
/**
* {@inheritdoc}
*/
public function getSize()
{
}
/**
* {@inheritdoc}
*/
public function tell()
{
throw new RuntimeException('Callback streams cannot tell position');
}
/**
* {@inheritdoc}
*/
public function eof()
{
return empty($this->callback);
}
/**
* {@inheritdoc}
*/
public function isSeekable()
{
return false;
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET)
{
throw new RuntimeException('Callback streams cannot seek position');
}
/**
* {@inheritdoc}
*/
public function rewind()
{
throw new RuntimeException('Callback streams cannot rewind position');
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
return false;
}
/**
* {@inheritdoc}
*/
public function write($string)
{
throw new RuntimeException('Callback streams cannot write');
}
/**
* {@inheritdoc}
*/
public function isReadable()
{
return false;
}
/**
* {@inheritdoc}
*/
public function read($length)
{
throw new RuntimeException('Callback streams cannot read');
}
/**
* {@inheritdoc}
*/
public function getContents()
{
$callback = $this->detach();
return $callback ? $callback() : '';
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
$metadata = [
'eof' => $this->eof(),
'stream_type' => 'callback',
'seekable' => false
];
if (null === $key) {
return $metadata;
}
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Exception;
use BadMethodCallException;
/**
* Exception indicating a deprecated method.
*/
class DeprecatedMethodException extends BadMethodCallException implements ExceptionInterface
{
}

View file

@ -0,0 +1,17 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Exception;
/**
* Marker interface for package-specific exceptions.
*/
interface ExceptionInterface
{
}

View file

@ -0,0 +1,177 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use function get_class;
use function gettype;
use function in_array;
use function is_numeric;
use function is_object;
use function is_string;
use function ord;
use function preg_match;
use function sprintf;
use function strlen;
/**
* Provide security tools around HTTP headers to prevent common injection vectors.
*
* Code is largely lifted from the Zend\Http\Header\HeaderValue implementation in
* Zend Framework, released with the copyright and license below.
*
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
final class HeaderSecurity
{
/**
* Private constructor; non-instantiable.
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Filter a header value
*
* Ensures CRLF header injection vectors are filtered.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* This method filters any values not allowed from the string, and is
* lossy.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
* @param string $value
* @return string
*/
public static function filter($value)
{
$value = (string) $value;
$length = strlen($value);
$string = '';
for ($i = 0; $i < $length; $i += 1) {
$ascii = ord($value[$i]);
// Detect continuation sequences
if ($ascii === 13) {
$lf = ord($value[$i + 1]);
$ws = ord($value[$i + 2]);
if ($lf === 10 && in_array($ws, [9, 32], true)) {
$string .= $value[$i] . $value[$i + 1];
$i += 1;
}
continue;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 32-126, 128-254 === visible
// 127 === DEL
// 255 === null byte
if (($ascii < 32 && $ascii !== 9)
|| $ascii === 127
|| $ascii > 254
) {
continue;
}
$string .= $value[$i];
}
return $string;
}
/**
* Validate a header value.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
* @param string $value
* @return bool
*/
public static function isValid($value)
{
$value = (string) $value;
// Look for:
// \n not preceded by \r, OR
// \r not followed by \n, OR
// \r\n not followed by space or horizontal tab; these are all CRLF attacks
if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
return false;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 10 === line feed
// 13 === carriage return
// 32-126, 128-254 === visible
// 127 === DEL (disallowed)
// 255 === null byte (disallowed)
if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
return false;
}
return true;
}
/**
* Assert a header value is valid.
*
* @param string $value
* @throws InvalidArgumentException for invalid values
*/
public static function assertValid($value)
{
if (! is_string($value) && ! is_numeric($value)) {
throw new InvalidArgumentException(sprintf(
'Invalid header value type; must be a string or numeric; received %s',
(is_object($value) ? get_class($value) : gettype($value))
));
}
if (! self::isValid($value)) {
throw new InvalidArgumentException(sprintf(
'"%s" is not valid header value',
$value
));
}
}
/**
* Assert whether or not a header name is valid.
*
* @see http://tools.ietf.org/html/rfc7230#section-3.2
* @param mixed $name
* @throws InvalidArgumentException
*/
public static function assertValidName($name)
{
if (! is_string($name)) {
throw new InvalidArgumentException(sprintf(
'Invalid header name type; expected string; received %s',
(is_object($name) ? get_class($name) : gettype($name))
));
}
if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
throw new InvalidArgumentException(sprintf(
'"%s" is not valid header name',
$name
));
}
}
}

View file

@ -0,0 +1,413 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use function array_map;
use function array_merge;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_object;
use function is_resource;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
/**
* Trait implementing the various methods defined in MessageInterface.
*
* @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
*/
trait MessageTrait
{
/**
* List of all registered headers, as key => array of values.
*
* @var array
*/
protected $headers = [];
/**
* Map of normalized header name to original name used to register header.
*
* @var array
*/
protected $headerNames = [];
/**
* @var string
*/
private $protocol = '1.1';
/**
* @var StreamInterface
*/
private $stream;
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion()
{
return $this->protocol;
}
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion($version)
{
$this->validateProtocolVersion($version);
$new = clone $this;
$new->protocol = $version;
return $new;
}
/**
* Retrieves all message headers.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* @return array Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings.
*/
public function getHeaders()
{
return $this->headers;
}
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $header Case-insensitive header name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader($header)
{
return isset($this->headerNames[strtolower($header)]);
}
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $header Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader($header)
{
if (! $this->hasHeader($header)) {
return [];
}
$header = $this->headerNames[strtolower($header)];
return $this->headers[$header];
}
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine($name)
{
$value = $this->getHeader($name);
if (empty($value)) {
return '';
}
return implode(',', $value);
}
/**
* Return an instance with the provided header, replacing any existing
* values of any headers with the same case-insensitive name.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $header Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader($header, $value)
{
$this->assertHeader($header);
$normalized = strtolower($header);
$new = clone $this;
if ($new->hasHeader($header)) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$value = $this->filterHeaderValue($value);
$new->headerNames[$normalized] = $header;
$new->headers[$header] = $value;
return $new;
}
/**
* Return an instance with the specified header appended with the
* given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $header Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader($header, $value)
{
$this->assertHeader($header);
if (! $this->hasHeader($header)) {
return $this->withHeader($header, $value);
}
$header = $this->headerNames[strtolower($header)];
$new = clone $this;
$value = $this->filterHeaderValue($value);
$new->headers[$header] = array_merge($this->headers[$header], $value);
return $new;
}
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $header Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader($header)
{
if (! $this->hasHeader($header)) {
return clone $this;
}
$normalized = strtolower($header);
$original = $this->headerNames[$normalized];
$new = clone $this;
unset($new->headers[$original], $new->headerNames[$normalized]);
return $new;
}
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody()
{
return $this->stream;
}
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @param StreamInterface $body Body.
* @return static
* @throws \InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body)
{
$new = clone $this;
$new->stream = $body;
return $new;
}
private function getStream($stream, $modeIfNotInstance)
{
if ($stream instanceof StreamInterface) {
return $stream;
}
if (! is_string($stream) && ! is_resource($stream)) {
throw new InvalidArgumentException(
'Stream must be a string stream resource identifier, '
. 'an actual stream resource, '
. 'or a Psr\Http\Message\StreamInterface implementation'
);
}
return new Stream($stream, $modeIfNotInstance);
}
/**
* Filter a set of headers to ensure they are in the correct internal format.
*
* Used by message constructors to allow setting all initial headers at once.
*
* @param array $originalHeaders Headers to filter.
*/
private function setHeaders(array $originalHeaders)
{
$headerNames = $headers = [];
foreach ($originalHeaders as $header => $value) {
$value = $this->filterHeaderValue($value);
$this->assertHeader($header);
$headerNames[strtolower($header)] = $header;
$headers[$header] = $value;
}
$this->headerNames = $headerNames;
$this->headers = $headers;
}
/**
* Validate the HTTP protocol version
*
* @param string $version
* @throws InvalidArgumentException on invalid HTTP protocol version
*/
private function validateProtocolVersion($version)
{
if (empty($version)) {
throw new InvalidArgumentException(
'HTTP protocol version can not be empty'
);
}
if (! is_string($version)) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP protocol version; must be a string, received %s',
(is_object($version) ? get_class($version) : gettype($version))
));
}
// HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
// versions of the protocol, while HTTP/2 does not.
if (! preg_match('#^(1\.[01]|2)$#', $version)) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP protocol version "%s" provided',
$version
));
}
}
/**
* @param mixed $values
* @return string[]
*/
private function filterHeaderValue($values)
{
if (! is_array($values)) {
$values = [$values];
}
if ([] === $values) {
throw new InvalidArgumentException(
'Invalid header value: must be a string or array of strings; '
. 'cannot be an empty array'
);
}
return array_map(function ($value) {
HeaderSecurity::assertValid($value);
return (string) $value;
}, array_values($values));
}
/**
* Ensure header name and values are valid.
*
* @param string $name
*
* @throws InvalidArgumentException
*/
private function assertHeader($name)
{
HeaderSecurity::assertValidName($name);
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use function stream_get_contents;
/**
* Caching version of php://input
*/
class PhpInputStream extends Stream
{
/**
* @var string
*/
private $cache = '';
/**
* @var bool
*/
private $reachedEof = false;
/**
* @param string|resource $stream
*/
public function __construct($stream = 'php://input')
{
parent::__construct($stream, 'r');
}
/**
* {@inheritdoc}
*/
public function __toString()
{
if ($this->reachedEof) {
return $this->cache;
}
$this->getContents();
return $this->cache;
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
return false;
}
/**
* {@inheritdoc}
*/
public function read($length)
{
$content = parent::read($length);
if (! $this->reachedEof) {
$this->cache .= $content;
}
if ($this->eof()) {
$this->reachedEof = true;
}
return $content;
}
/**
* {@inheritdoc}
*/
public function getContents($maxLength = -1)
{
if ($this->reachedEof) {
return $this->cache;
}
$contents = stream_get_contents($this->resource, $maxLength);
$this->cache .= $contents;
if ($maxLength === -1 || $this->eof()) {
$this->reachedEof = true;
}
return $contents;
}
}

View file

@ -0,0 +1,180 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use const SEEK_SET;
/**
* Class RelativeStream
*
* Wrapper for default Stream class, representing subpart (starting from given offset) of initial stream.
* It can be used to avoid copying full stream, conserving memory.
* @example see Zend\Diactoros\AbstractSerializer::splitStream()
*/
final class RelativeStream implements StreamInterface
{
/**
* @var StreamInterface
*/
private $decoratedStream;
/**
* @var int
*/
private $offset;
/**
* Class constructor
*
* @param StreamInterface $decoratedStream
* @param int $offset
*/
public function __construct(StreamInterface $decoratedStream, $offset)
{
$this->decoratedStream = $decoratedStream;
$this->offset = (int)$offset;
}
/**
* {@inheritdoc}
*/
public function __toString()
{
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
/**
* {@inheritdoc}
*/
public function close()
{
$this->decoratedStream->close();
}
/**
* {@inheritdoc}
*/
public function detach()
{
return $this->decoratedStream->detach();
}
/**
* {@inheritdoc}
*/
public function getSize()
{
return $this->decoratedStream->getSize() - $this->offset;
}
/**
* {@inheritdoc}
*/
public function tell()
{
return $this->decoratedStream->tell() - $this->offset;
}
/**
* {@inheritdoc}
*/
public function eof()
{
return $this->decoratedStream->eof();
}
/**
* {@inheritdoc}
*/
public function isSeekable()
{
return $this->decoratedStream->isSeekable();
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET)
{
if ($whence == SEEK_SET) {
return $this->decoratedStream->seek($offset + $this->offset, $whence);
}
return $this->decoratedStream->seek($offset, $whence);
}
/**
* {@inheritdoc}
*/
public function rewind()
{
return $this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
return $this->decoratedStream->isWritable();
}
/**
* {@inheritdoc}
*/
public function write($string)
{
if ($this->tell() < 0) {
throw new RuntimeException('Invalid pointer position');
}
return $this->decoratedStream->write($string);
}
/**
* {@inheritdoc}
*/
public function isReadable()
{
return $this->decoratedStream->isReadable();
}
/**
* {@inheritdoc}
*/
public function read($length)
{
if ($this->tell() < 0) {
throw new RuntimeException('Invalid pointer position');
}
return $this->decoratedStream->read($length);
}
/**
* {@inheritdoc}
*/
public function getContents()
{
if ($this->tell() < 0) {
throw new RuntimeException('Invalid pointer position');
}
return $this->decoratedStream->getContents();
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
return $this->decoratedStream->getMetadata($key);
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function strtolower;
/**
* HTTP Request encapsulation
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Request implements RequestInterface
{
use RequestTrait;
/**
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @throws \InvalidArgumentException for any invalid value.
*/
public function __construct($uri = null, $method = null, $body = 'php://temp', array $headers = [])
{
$this->initialize($uri, $method, $body, $headers);
}
/**
* {@inheritdoc}
*/
public function getHeaders()
{
$headers = $this->headers;
if (! $this->hasHeader('host')
&& $this->uri->getHost()
) {
$headers['Host'] = [$this->getHostFromUri()];
}
return $headers;
}
/**
* {@inheritdoc}
*/
public function getHeader($header)
{
if (! $this->hasHeader($header)) {
if (strtolower($header) === 'host'
&& $this->uri->getHost()
) {
return [$this->getHostFromUri()];
}
return [];
}
$header = $this->headerNames[strtolower($header)];
return $this->headers[$header];
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Request;
use Psr\Http\Message\RequestInterface;
use UnexpectedValueException;
use Zend\Diactoros\Request;
use Zend\Diactoros\Stream;
use function sprintf;
/**
* Serialize or deserialize request messages to/from arrays.
*
* This class provides functionality for serializing a RequestInterface instance
* to an array, as well as the reverse operation of creating a Request instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a request message to an array.
*
* @param RequestInterface $request
* @return array
*/
public static function toArray(RequestInterface $request)
{
return [
'method' => $request->getMethod(),
'request_target' => $request->getRequestTarget(),
'uri' => (string) $request->getUri(),
'protocol_version' => $request->getProtocolVersion(),
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
];
}
/**
* Deserialize a request array to a request instance.
*
* @param array $serializedRequest
* @return Request
* @throws UnexpectedValueException when cannot deserialize response
*/
public static function fromArray(array $serializedRequest)
{
try {
$uri = self::getValueFromKey($serializedRequest, 'uri');
$method = self::getValueFromKey($serializedRequest, 'method');
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedRequest, 'body'));
$headers = self::getValueFromKey($serializedRequest, 'headers');
$requestTarget = self::getValueFromKey($serializedRequest, 'request_target');
$protocolVersion = self::getValueFromKey($serializedRequest, 'protocol_version');
return (new Request($uri, $method, $body, $headers))
->withRequestTarget($requestTarget)
->withProtocolVersion($protocolVersion);
} catch (\Exception $exception) {
throw new UnexpectedValueException('Cannot deserialize request', null, $exception);
}
}
/**
* @param array $data
* @param string $key
* @param string $message
* @return mixed
* @throws UnexpectedValueException
*/
private static function getValueFromKey(array $data, $key, $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized request', $key);
}
throw new UnexpectedValueException($message);
}
}

View file

@ -0,0 +1,154 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Request;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use UnexpectedValueException;
use Zend\Diactoros\AbstractSerializer;
use Zend\Diactoros\Request;
use Zend\Diactoros\Stream;
use Zend\Diactoros\Uri;
use function preg_match;
use function sprintf;
/**
* Serialize (cast to string) or deserialize (cast string to Request) messages.
*
* This class provides functionality for serializing a RequestInterface instance
* to a string, as well as the reverse operation of creating a Request instance
* from a string/stream representing a message.
*/
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a request string to a request instance.
*
* Internally, casts the message to a stream and invokes fromStream().
*
* @param string $message
* @return Request
* @throws UnexpectedValueException when errors occur parsing the message.
*/
public static function fromString($message)
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return self::fromStream($stream);
}
/**
* Deserialize a request stream to a request instance.
*
* @param StreamInterface $stream
* @return Request
* @throws UnexpectedValueException when errors occur parsing the message.
*/
public static function fromStream(StreamInterface $stream)
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
list($method, $requestTarget, $version) = self::getRequestLine($stream);
$uri = self::createUriFromRequestTarget($requestTarget);
list($headers, $body) = self::splitStream($stream);
return (new Request($uri, $method, $body, $headers))
->withProtocolVersion($version)
->withRequestTarget($requestTarget);
}
/**
* Serialize a request message to a string.
*
* @param RequestInterface $request
* @return string
*/
public static function toString(RequestInterface $request)
{
$httpMethod = $request->getMethod();
if (empty($httpMethod)) {
throw new UnexpectedValueException('Object can not be serialized because HTTP method is empty');
}
$headers = self::serializeHeaders($request->getHeaders());
$body = (string) $request->getBody();
$format = '%s %s HTTP/%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
if (! empty($body)) {
$headers .= "\r\n\r\n";
}
return sprintf(
$format,
$httpMethod,
$request->getRequestTarget(),
$request->getProtocolVersion(),
$headers,
$body
);
}
/**
* Retrieve the components of the request line.
*
* Retrieves the first line of the stream and parses it, raising an
* exception if it does not follow specifications; if valid, returns a list
* with the method, target, and version, in that order.
*
* @param StreamInterface $stream
* @return array
*/
private static function getRequestLine(StreamInterface $stream)
{
$requestLine = self::getLine($stream);
if (! preg_match(
'#^(?P<method>[!\#$%&\'*+.^_`|~a-zA-Z0-9-]+) (?P<target>[^\s]+) HTTP/(?P<version>[1-9]\d*\.\d+)$#',
$requestLine,
$matches
)) {
throw new UnexpectedValueException('Invalid request line detected');
}
return [$matches['method'], $matches['target'], $matches['version']];
}
/**
* Create and return a Uri instance based on the provided request target.
*
* If the request target is of authority or asterisk form, an empty Uri
* instance is returned; otherwise, the value is used to create and return
* a new Uri instance.
*
* @param string $requestTarget
* @return Uri
*/
private static function createUriFromRequestTarget($requestTarget)
{
if (preg_match('#^https?://#', $requestTarget)) {
return new Uri($requestTarget);
}
if (preg_match('#^(\*|[^/])#', $requestTarget)) {
return new Uri();
}
return new Uri($requestTarget);
}
}

View file

@ -0,0 +1,324 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function array_keys;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
/**
* Trait with common request behaviors.
*
* Server and client-side requests differ slightly in how the Host header is
* handled; on client-side, it should be calculated on-the-fly from the
* composed URI (if present), while on server-side, it will be calculated from
* the environment. As such, this trait exists to provide the common code
* between both client-side and server-side requests, and each can then
* use the headers functionality required by their implementations.
*/
trait RequestTrait
{
use MessageTrait;
/**
* @var string
*/
private $method = '';
/**
* The request-target, if it has been provided or calculated.
*
* @var null|string
*/
private $requestTarget;
/**
* @var UriInterface
*/
private $uri;
/**
* Initialize request state.
*
* Used by constructors.
*
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @throws InvalidArgumentException for any invalid value.
*/
private function initialize($uri = null, $method = null, $body = 'php://memory', array $headers = [])
{
$this->validateMethod($method);
$this->method = $method ?: '';
$this->uri = $this->createUri($uri);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
// per PSR-7: attempt to set the Host header from a provided URI if no
// Host header is provided
if (! $this->hasHeader('Host') && $this->uri->getHost()) {
$this->headerNames['host'] = 'Host';
$this->headers['Host'] = [$this->getHostFromUri()];
}
}
/**
* Create and return a URI instance.
*
* If `$uri` is a already a `UriInterface` instance, returns it.
*
* If `$uri` is a string, passes it to the `Uri` constructor to return an
* instance.
*
* If `$uri is null, creates and returns an empty `Uri` instance.
*
* Otherwise, it raises an exception.
*
* @param null|string|UriInterface $uri
* @return UriInterface
* @throws InvalidArgumentException
*/
private function createUri($uri)
{
if ($uri instanceof UriInterface) {
return $uri;
}
if (is_string($uri)) {
return new Uri($uri);
}
if ($uri === null) {
return new Uri();
}
throw new InvalidArgumentException(
'Invalid URI provided; must be null, a string, or a Psr\Http\Message\UriInterface instance'
);
}
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*
* @return string
*/
public function getRequestTarget()
{
if (null !== $this->requestTarget) {
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($this->uri->getQuery()) {
$target .= '?' . $this->uri->getQuery();
}
if (empty($target)) {
$target = '/';
}
return $target;
}
/**
* Create a new instance with a specific request-target.
*
* If the request needs a non-origin-form request-target e.g., for
* specifying an absolute-form, authority-form, or asterisk-form
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
* request-target forms allowed in request messages)
* @param mixed $requestTarget
* @return static
* @throws InvalidArgumentException if the request target is invalid.
*/
public function withRequestTarget($requestTarget)
{
if (preg_match('#\s#', $requestTarget)) {
throw new InvalidArgumentException(
'Invalid request target provided; cannot contain whitespace'
);
}
$new = clone $this;
$new->requestTarget = $requestTarget;
return $new;
}
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod()
{
return $this->method;
}
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request method.
*
* @param string $method Case-insensitive method.
* @return static
* @throws InvalidArgumentException for invalid HTTP methods.
*/
public function withMethod($method)
{
$this->validateMethod($method);
$new = clone $this;
$new->method = $method;
return $new;
}
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request, if any.
*/
public function getUri()
{
return $this->uri;
}
/**
* Returns an instance with the provided URI.
*
* This method will update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header will be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, the returned request will not update the Host header of the
* returned message -- even if the message contains no Host header. This
* means that a call to `getHeader('Host')` on the original request MUST
* equal the return value of a call to `getHeader('Host')` on the returned
* request.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, $preserveHost = false)
{
$new = clone $this;
$new->uri = $uri;
if ($preserveHost && $this->hasHeader('Host')) {
return $new;
}
if (! $uri->getHost()) {
return $new;
}
$host = $uri->getHost();
if ($uri->getPort()) {
$host .= ':' . $uri->getPort();
}
$new->headerNames['host'] = 'Host';
// Remove an existing host header if present, regardless of current
// de-normalization of the header name.
// @see https://github.com/zendframework/zend-diactoros/issues/91
foreach (array_keys($new->headers) as $header) {
if (strtolower($header) === 'host') {
unset($new->headers[$header]);
}
}
$new->headers['Host'] = [$host];
return $new;
}
/**
* Validate the HTTP method
*
* @param null|string $method
* @throws InvalidArgumentException on invalid HTTP method.
*/
private function validateMethod($method)
{
if (null === $method) {
return;
}
if (! is_string($method)) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP method; must be a string, received %s',
(is_object($method) ? get_class($method) : gettype($method))
));
}
if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP method "%s" provided',
$method
));
}
}
/**
* Retrieve the host from the URI instance
*
* @return string
*/
private function getHostFromUri()
{
$host = $this->uri->getHost();
$host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
return $host;
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use function gettype;
use function is_float;
use function is_numeric;
use function is_scalar;
use function sprintf;
/**
* HTTP response encapsulation.
*
* Responses are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Response implements ResponseInterface
{
use MessageTrait;
const MIN_STATUS_CODE_VALUE = 100;
const MAX_STATUS_CODE_VALUE = 599;
/**
* Map of standard HTTP status code/reason phrases
*
* @var array
*/
private $phrases = [
// INFORMATIONAL CODES
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
// SUCCESS CODES
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
// REDIRECTION CODES
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
// CLIENT ERROR
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 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
444 => 'Connection Closed Without Response',
451 => 'Unavailable For Legal Reasons',
// SERVER ERROR
499 => 'Client Closed Request',
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',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
599 => 'Network Connect Timeout Error',
];
/**
* @var string
*/
private $reasonPhrase;
/**
* @var int
*/
private $statusCode;
/**
* @param string|resource|StreamInterface $body Stream identifier and/or actual stream resource
* @param int $status Status code for the response, if any.
* @param array $headers Headers for the response, if any.
* @throws InvalidArgumentException on any invalid element.
*/
public function __construct($body = 'php://memory', $status = 200, array $headers = [])
{
$this->setStatusCode($status);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
}
/**
* {@inheritdoc}
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* {@inheritdoc}
*/
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
/**
* {@inheritdoc}
*/
public function withStatus($code, $reasonPhrase = '')
{
$new = clone $this;
$new->setStatusCode($code, $reasonPhrase);
return $new;
}
/**
* Set a valid status code.
*
* @param int $code
* @param string $reasonPhrase
* @throws InvalidArgumentException on an invalid status code.
*/
private function setStatusCode($code, $reasonPhrase = '')
{
if (! is_numeric($code)
|| is_float($code)
|| $code < static::MIN_STATUS_CODE_VALUE
|| $code > static::MAX_STATUS_CODE_VALUE
) {
throw new InvalidArgumentException(sprintf(
'Invalid status code "%s"; must be an integer between %d and %d, inclusive',
is_scalar($code) ? $code : gettype($code),
static::MIN_STATUS_CODE_VALUE,
static::MAX_STATUS_CODE_VALUE
));
}
if (! is_string($reasonPhrase)) {
throw new InvalidArgumentException(sprintf(
'Unsupported response reason phrase; must be a string, received %s',
is_object($reasonPhrase) ? get_class($reasonPhrase) : gettype($reasonPhrase)
));
}
if ($reasonPhrase === '' && isset($this->phrases[$code])) {
$reasonPhrase = $this->phrases[$code];
}
$this->reasonPhrase = $reasonPhrase;
$this->statusCode = (int) $code;
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use UnexpectedValueException;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function sprintf;
/**
* Serialize or deserialize response messages to/from arrays.
*
* This class provides functionality for serializing a ResponseInterface instance
* to an array, as well as the reverse operation of creating a Response instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a response message to an array.
*
* @param ResponseInterface $response
* @return array
*/
public static function toArray(ResponseInterface $response)
{
return [
'status_code' => $response->getStatusCode(),
'reason_phrase' => $response->getReasonPhrase(),
'protocol_version' => $response->getProtocolVersion(),
'headers' => $response->getHeaders(),
'body' => (string) $response->getBody(),
];
}
/**
* Deserialize a response array to a response instance.
*
* @param array $serializedResponse
* @return Response
* @throws UnexpectedValueException when cannot deserialize response
*/
public static function fromArray(array $serializedResponse)
{
try {
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedResponse, 'body'));
$statusCode = self::getValueFromKey($serializedResponse, 'status_code');
$headers = self::getValueFromKey($serializedResponse, 'headers');
$protocolVersion = self::getValueFromKey($serializedResponse, 'protocol_version');
$reasonPhrase = self::getValueFromKey($serializedResponse, 'reason_phrase');
return (new Response($body, $statusCode, $headers))
->withProtocolVersion($protocolVersion)
->withStatus($statusCode, $reasonPhrase);
} catch (\Exception $exception) {
throw new UnexpectedValueException('Cannot deserialize response', null, $exception);
}
}
/**
* @param array $data
* @param string $key
* @param string $message
* @return mixed
* @throws UnexpectedValueException
*/
private static function getValueFromKey(array $data, $key, $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized request', $key);
}
throw new UnexpectedValueException($message);
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
/**
* @deprecated since 1.8.0. The package zendframework/zend-httphandlerrunner
* now provides this functionality.
*/
interface EmitterInterface
{
/**
* Emit a response.
*
* Emits a response, including status line, headers, and the message body,
* according to the environment.
*
* Implementations of this method may be written in such a way as to have
* side effects, such as usage of header() or pushing output to the
* output buffer.
*
* Implementations MAY raise exceptions if they are unable to emit the
* response; e.g., if headers have already been sent.
*
* @param ResponseInterface $response
*/
public function emit(ResponseInterface $response);
}

View file

@ -0,0 +1,42 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
/**
* A class representing empty HTTP responses.
*/
class EmptyResponse extends Response
{
/**
* Create an empty response with the given status code.
*
* @param int $status Status code for the response, if any.
* @param array $headers Headers for the response, if any.
*/
public function __construct($status = 204, array $headers = [])
{
$body = new Stream('php://temp', 'r');
parent::__construct($body, $status, $headers);
}
/**
* Create an empty response with the given headers.
*
* @param array $headers Headers for the response.
* @return EmptyResponse
*/
public static function withHeaders(array $headers)
{
return new static(204, $headers);
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* HTML response.
*
* Allows creating a response by passing an HTML string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/html.
*/
class HtmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an HTML response.
*
* Produces an HTML response with a Content-Type of text/html and a default
* status of 200.
*
* @param string|StreamInterface $html HTML or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws InvalidArgumentException if $html is neither a string or stream.
*/
public function __construct($html, $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($html),
$status,
$this->injectContentType('text/html; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $html
* @return StreamInterface
* @throws InvalidArgumentException if $html is neither a string or stream.
*/
private function createBody($html)
{
if ($html instanceof StreamInterface) {
return $html;
}
if (! is_string($html)) {
throw new InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
(is_object($html) ? get_class($html) : gettype($html)),
__CLASS__
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($html);
$body->rewind();
return $body;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use function array_keys;
use function array_reduce;
use function strtolower;
trait InjectContentTypeTrait
{
/**
* Inject the provided Content-Type, if none is already present.
*
* @param string $contentType
* @param array $headers
* @return array Headers with injected Content-Type
*/
private function injectContentType($contentType, array $headers)
{
$hasContentType = array_reduce(array_keys($headers), function ($carry, $item) {
return $carry ?: (strtolower($item) === 'content-type');
}, false);
if (! $hasContentType) {
$headers['content-type'] = [$contentType];
}
return $headers;
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function is_object;
use function is_resource;
use function json_encode;
use function json_last_error;
use function json_last_error_msg;
use function sprintf;
use const JSON_ERROR_NONE;
/**
* JSON response.
*
* Allows creating a response by passing data to the constructor; by default,
* serializes the data to JSON, sets a status code of 200 and sets the
* Content-Type header to application/json.
*/
class JsonResponse extends Response
{
use InjectContentTypeTrait;
/**
* Default flags for json_encode; value of:
*
* <code>
* JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
* </code>
*
* @const int
*/
const DEFAULT_JSON_FLAGS = 79;
/**
* @var mixed
*/
private $payload;
/**
* @var int
*/
private $encodingOptions;
/**
* Create a JSON response with the given data.
*
* Default JSON encoding is performed with the following options, which
* produces RFC4627-compliant JSON, capable of embedding into HTML.
*
* - JSON_HEX_TAG
* - JSON_HEX_APOS
* - JSON_HEX_AMP
* - JSON_HEX_QUOT
* - JSON_UNESCAPED_SLASHES
*
* @param mixed $data Data to convert to JSON.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @param int $encodingOptions JSON encoding options to use.
* @throws InvalidArgumentException if unable to encode the $data to JSON.
*/
public function __construct(
$data,
$status = 200,
array $headers = [],
$encodingOptions = self::DEFAULT_JSON_FLAGS
) {
$this->setPayload($data);
$this->encodingOptions = $encodingOptions;
$json = $this->jsonEncode($data, $this->encodingOptions);
$body = $this->createBodyFromJson($json);
$headers = $this->injectContentType('application/json', $headers);
parent::__construct($body, $status, $headers);
}
/**
* @return mixed
*/
public function getPayload()
{
return $this->payload;
}
/**
* @param $data
*
* @return JsonResponse
*/
public function withPayload($data)
{
$new = clone $this;
$new->setPayload($data);
return $this->updateBodyFor($new);
}
/**
* @return int
*/
public function getEncodingOptions()
{
return $this->encodingOptions;
}
/**
* @param int $encodingOptions
*
* @return JsonResponse
*/
public function withEncodingOptions($encodingOptions)
{
$new = clone $this;
$new->encodingOptions = $encodingOptions;
return $this->updateBodyFor($new);
}
/**
* @param string $json
*
* @return Stream
*/
private function createBodyFromJson($json)
{
$body = new Stream('php://temp', 'wb+');
$body->write($json);
$body->rewind();
return $body;
}
/**
* Encode the provided data to JSON.
*
* @param mixed $data
* @param int $encodingOptions
* @return string
* @throws InvalidArgumentException if unable to encode the $data to JSON.
*/
private function jsonEncode($data, $encodingOptions)
{
if (is_resource($data)) {
throw new InvalidArgumentException('Cannot JSON encode resources');
}
// Clear json_last_error()
json_encode(null);
$json = json_encode($data, $encodingOptions);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidArgumentException(sprintf(
'Unable to encode data to JSON in %s: %s',
__CLASS__,
json_last_error_msg()
));
}
return $json;
}
/**
* @param $data
*/
private function setPayload($data)
{
if (is_object($data)) {
$data = clone $data;
}
$this->payload = $data;
}
/**
* Update the response body for the given instance.
*
* @param self $toUpdate Instance to update.
* @return JsonResponse Returns a new instance with an updated body.
*/
private function updateBodyFor(self $toUpdate)
{
$json = $this->jsonEncode($toUpdate->payload, $toUpdate->encodingOptions);
$body = $this->createBodyFromJson($json);
return $toUpdate->withBody($body);
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use Zend\Diactoros\Response;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* Produce a redirect response.
*/
class RedirectResponse extends Response
{
/**
* Create a redirect response.
*
* Produces a redirect response with a Location header and the given status
* (302 by default).
*
* Note: this method overwrites the `location` $headers value.
*
* @param string|UriInterface $uri URI for the Location header.
* @param int $status Integer status code for the redirect; 302 by default.
* @param array $headers Array of headers to use at initialization.
*/
public function __construct($uri, $status = 302, array $headers = [])
{
if (! is_string($uri) && ! $uri instanceof UriInterface) {
throw new InvalidArgumentException(sprintf(
'Uri provided to %s MUST be a string or Psr\Http\Message\UriInterface instance; received "%s"',
__CLASS__,
(is_object($uri) ? get_class($uri) : gettype($uri))
));
}
$headers['location'] = [(string) $uri];
parent::__construct('php://temp', $status, $headers);
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
/**
* @deprecated since 1.8.0. The package zendframework/zend-httphandlerrunner
* now provides this functionality.
*/
class SapiEmitter implements EmitterInterface
{
use SapiEmitterTrait;
/**
* Emits a response for a PHP SAPI environment.
*
* Emits the status line and headers via the header() function, and the
* body content via the output buffer.
*
* @param ResponseInterface $response
*/
public function emit(ResponseInterface $response)
{
$this->assertNoPreviousOutput();
$this->emitHeaders($response);
$this->emitStatusLine($response);
$this->emitBody($response);
}
/**
* Emit the message body.
*
* @param ResponseInterface $response
*/
private function emitBody(ResponseInterface $response)
{
echo $response->getBody();
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use function ob_get_length;
use function ob_get_level;
use function sprintf;
use function str_replace;
use function ucwords;
/**
* @deprecated since 1.8.0. The package zendframework/zend-httphandlerrunner
* now provides this functionality.
*/
trait SapiEmitterTrait
{
/**
* Checks to see if content has previously been sent.
*
* If either headers have been sent or the output buffer contains content,
* raises an exception.
*
* @throws RuntimeException if headers have already been sent.
* @throws RuntimeException if output is present in the output buffer.
*/
private function assertNoPreviousOutput()
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}
if (ob_get_level() > 0 && ob_get_length() > 0) {
throw new RuntimeException('Output has been emitted previously; cannot emit response');
}
}
/**
* Emit the status line.
*
* Emits the status line using the protocol version and status code from
* the response; if a reason phrase is available, it, too, is emitted.
*
* It is important to mention that this method should be called after
* `emitHeaders()` in order to prevent PHP from changing the status code of
* the emitted response.
*
* @param ResponseInterface $response
*
* @see \Zend\Diactoros\Response\SapiEmitterTrait::emitHeaders()
*/
private function emitStatusLine(ResponseInterface $response)
{
$reasonPhrase = $response->getReasonPhrase();
$statusCode = $response->getStatusCode();
header(sprintf(
'HTTP/%s %d%s',
$response->getProtocolVersion(),
$statusCode,
($reasonPhrase ? ' ' . $reasonPhrase : '')
), true, $statusCode);
}
/**
* Emit response headers.
*
* Loops through each header, emitting each; if the header value
* is an array with multiple values, ensures that each is sent
* in such a way as to create aggregate headers (instead of replace
* the previous).
*
* @param ResponseInterface $response
*/
private function emitHeaders(ResponseInterface $response)
{
$statusCode = $response->getStatusCode();
foreach ($response->getHeaders() as $header => $values) {
$name = $this->filterHeader($header);
$first = $name === 'Set-Cookie' ? false : true;
foreach ($values as $value) {
header(sprintf(
'%s: %s',
$name,
$value
), $first, $statusCode);
$first = false;
}
}
}
/**
* Filter a header name to wordcase
*
* @param string $header
* @return string
*/
private function filterHeader($header)
{
$filtered = str_replace('-', ' ', $header);
$filtered = ucwords($filtered);
return str_replace(' ', '-', $filtered);
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use function is_array;
use function preg_match;
use function strlen;
use function substr;
/**
* @deprecated since 1.8.0. The package zendframework/zend-httphandlerrunner
* now provides this functionality.
*/
class SapiStreamEmitter implements EmitterInterface
{
use SapiEmitterTrait;
/**
* Emits a response for a PHP SAPI environment.
*
* Emits the status line and headers via the header() function, and the
* body content via the output buffer.
*
* @param ResponseInterface $response
* @param int $maxBufferLength Maximum output buffering size for each iteration
*/
public function emit(ResponseInterface $response, $maxBufferLength = 8192)
{
$this->assertNoPreviousOutput();
$this->emitHeaders($response);
$this->emitStatusLine($response);
$range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
if (is_array($range) && $range[0] === 'bytes') {
$this->emitBodyRange($range, $response, $maxBufferLength);
return;
}
$this->emitBody($response, $maxBufferLength);
}
/**
* Emit the message body.
*
* @param ResponseInterface $response
* @param int $maxBufferLength
*/
private function emitBody(ResponseInterface $response, $maxBufferLength)
{
$body = $response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
if (! $body->isReadable()) {
echo $body;
return;
}
while (! $body->eof()) {
echo $body->read($maxBufferLength);
}
}
/**
* Emit a range of the message body.
*
* @param array $range
* @param ResponseInterface $response
* @param int $maxBufferLength
*/
private function emitBodyRange(array $range, ResponseInterface $response, $maxBufferLength)
{
list($unit, $first, $last, $length) = $range;
$body = $response->getBody();
$length = $last - $first + 1;
if ($body->isSeekable()) {
$body->seek($first);
$first = 0;
}
if (! $body->isReadable()) {
echo substr($body->getContents(), $first, $length);
return;
}
$remaining = $length;
while ($remaining >= $maxBufferLength && ! $body->eof()) {
$contents = $body->read($maxBufferLength);
$remaining -= strlen($contents);
echo $contents;
}
if ($remaining > 0 && ! $body->eof()) {
echo $body->read($remaining);
}
}
/**
* Parse content-range header
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
*
* @param string $header
* @return false|array [unit, first, last, length]; returns false if no
* content range or an invalid content range is provided
*/
private function parseContentRange($header)
{
if (preg_match('/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches)) {
return [
$matches['unit'],
(int) $matches['first'],
(int) $matches['last'],
$matches['length'] === '*' ? '*' : (int) $matches['length'],
];
}
return false;
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use UnexpectedValueException;
use Zend\Diactoros\AbstractSerializer;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function preg_match;
use function sprintf;
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a response string to a response instance.
*
* @param string $message
* @return Response
* @throws UnexpectedValueException when errors occur parsing the message.
*/
public static function fromString($message)
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return static::fromStream($stream);
}
/**
* Parse a response from a stream.
*
* @param StreamInterface $stream
* @return Response
* @throws InvalidArgumentException when the stream is not readable.
* @throws UnexpectedValueException when errors occur parsing the message.
*/
public static function fromStream(StreamInterface $stream)
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
list($version, $status, $reasonPhrase) = self::getStatusLine($stream);
list($headers, $body) = self::splitStream($stream);
return (new Response($body, $status, $headers))
->withProtocolVersion($version)
->withStatus((int) $status, $reasonPhrase);
}
/**
* Create a string representation of a response.
*
* @param ResponseInterface $response
* @return string
*/
public static function toString(ResponseInterface $response)
{
$reasonPhrase = $response->getReasonPhrase();
$headers = self::serializeHeaders($response->getHeaders());
$body = (string) $response->getBody();
$format = 'HTTP/%s %d%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
$headers .= "\r\n\r\n";
return sprintf(
$format,
$response->getProtocolVersion(),
$response->getStatusCode(),
($reasonPhrase ? ' ' . $reasonPhrase : ''),
$headers,
$body
);
}
/**
* Retrieve the status line for the message.
*
* @param StreamInterface $stream
* @return array Array with three elements: 0 => version, 1 => status, 2 => reason
* @throws UnexpectedValueException if line is malformed
*/
private static function getStatusLine(StreamInterface $stream)
{
$line = self::getLine($stream);
if (! preg_match(
'#^HTTP/(?P<version>[1-9]\d*\.\d) (?P<status>[1-5]\d{2})(\s+(?P<reason>.+))?$#',
$line,
$matches
)) {
throw new UnexpectedValueException('No status line detected');
}
return [$matches['version'], $matches['status'], isset($matches['reason']) ? $matches['reason'] : ''];
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* Plain text response.
*
* Allows creating a response by passing a string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/plain.
*/
class TextResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create a plain text response.
*
* Produces a text response with a Content-Type of text/plain and a default
* status of 200.
*
* @param string|StreamInterface $text String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws InvalidArgumentException if $text is neither a string or stream.
*/
public function __construct($text, $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($text),
$status,
$this->injectContentType('text/plain; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $text
* @return StreamInterface
* @throws InvalidArgumentException if $html is neither a string or stream.
*/
private function createBody($text)
{
if ($text instanceof StreamInterface) {
return $text;
}
if (! is_string($text)) {
throw new InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
(is_object($text) ? get_class($text) : gettype($text)),
__CLASS__
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($text);
$body->rewind();
return $body;
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros\Response;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* XML response.
*
* Allows creating a response by passing an XML string to the constructor; by default,
* sets a status code of 200 and sets the Content-Type header to application/xml.
*/
class XmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an XML response.
*
* Produces an XML response with a Content-Type of application/xml and a default
* status of 200.
*
* @param string|StreamInterface $xml String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws InvalidArgumentException if $text is neither a string or stream.
*/
public function __construct(
$xml,
$status = 200,
array $headers = []
) {
parent::__construct(
$this->createBody($xml),
$status,
$this->injectContentType('application/xml; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $xml
* @return StreamInterface
* @throws InvalidArgumentException if $xml is neither a string or stream.
*/
private function createBody($xml)
{
if ($xml instanceof StreamInterface) {
return $xml;
}
if (! is_string($xml)) {
throw new InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
(is_object($xml) ? get_class($xml) : gettype($xml)),
__CLASS__
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($xml);
$body->rewind();
return $body;
}
}

View file

@ -0,0 +1,185 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2018 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use OutOfBoundsException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use function property_exists;
/**
* "Serve" incoming HTTP requests
*
* Given a callback, takes an incoming request, dispatches it to the
* callback, and then sends a response.
*
* @deprecated since 1.8.0. We recommend using the `RequestHandlerRunner` class
* from the zendframework/zend-httphandlerrunner package instead.
*/
class Server
{
/**
* @var callable
*/
private $callback;
/**
* Response emitter to use; by default, uses Response\SapiEmitter.
*
* @var Response\EmitterInterface
*/
private $emitter;
/**
* @var ServerRequestInterface
*/
private $request;
/**
* @var ResponseInterface
*/
private $response;
/**
* Constructor
*
* Given a callback, a request, and a response, we can create a server.
*
* @param callable $callback
* @param ServerRequestInterface $request
* @param ResponseInterface $response
*/
public function __construct(
callable $callback,
ServerRequestInterface $request,
ResponseInterface $response
) {
$this->callback = $callback;
$this->request = $request;
$this->response = $response;
}
/**
* Allow retrieving the request, response and callback as properties
*
* @param string $name
* @return mixed
* @throws OutOfBoundsException for invalid properties
*/
public function __get($name)
{
if (! property_exists($this, $name)) {
throw new OutOfBoundsException('Cannot retrieve arbitrary properties from server');
}
return $this->{$name};
}
/**
* Set alternate response emitter to use.
*
* @param Response\EmitterInterface $emitter
*/
public function setEmitter(Response\EmitterInterface $emitter)
{
$this->emitter = $emitter;
}
/**
* Create a Server instance
*
* Creates a server instance from the callback and the following
* PHP environmental values:
*
* - server; typically this will be the $_SERVER superglobal
* - query; typically this will be the $_GET superglobal
* - body; typically this will be the $_POST superglobal
* - cookies; typically this will be the $_COOKIE superglobal
* - files; typically this will be the $_FILES superglobal
*
* @param callable $callback
* @param array $server
* @param array $query
* @param array $body
* @param array $cookies
* @param array $files
* @return static
*/
public static function createServer(
callable $callback,
array $server,
array $query,
array $body,
array $cookies,
array $files
) {
$request = ServerRequestFactory::fromGlobals($server, $query, $body, $cookies, $files);
$response = new Response();
return new static($callback, $request, $response);
}
/**
* Create a Server instance from an existing request object
*
* Provided a callback, an existing request object, and optionally an
* existing response object, create and return the Server instance.
*
* If no Response object is provided, one will be created.
*
* @param callable $callback
* @param ServerRequestInterface $request
* @param null|ResponseInterface $response
* @return static
*/
public static function createServerFromRequest(
callable $callback,
ServerRequestInterface $request,
ResponseInterface $response = null
) {
if (! $response) {
$response = new Response();
}
return new static($callback, $request, $response);
}
/**
* "Listen" to an incoming request
*
* If provided a $finalHandler, that callable will be used for
* incomplete requests.
*
* @param null|callable $finalHandler
*/
public function listen(callable $finalHandler = null)
{
$callback = $this->callback;
$response = $callback($this->request, $this->response, $finalHandler);
if (! $response instanceof ResponseInterface) {
$response = $this->response;
}
$this->getEmitter()->emit($response);
}
/**
* Retrieve the current response emitter.
*
* If none has been registered, lazy-loads a Response\SapiEmitter.
*
* @return Response\EmitterInterface
*/
private function getEmitter()
{
if (! $this->emitter) {
$this->emitter = new Response\SapiEmitter();
}
return $this->emitter;
}
}

View file

@ -0,0 +1,291 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use function array_key_exists;
use function is_array;
/**
* Server-side HTTP request
*
* Extends the Request definition to add methods for accessing incoming data,
* specifically server parameters, cookies, matched path parameters, query
* string arguments, body parameters, and upload file information.
*
* "Attributes" are discovered via decomposing the request (and usually
* specifically the URI path), and typically will be injected by the application.
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class ServerRequest implements ServerRequestInterface
{
use RequestTrait;
/**
* @var array
*/
private $attributes = [];
/**
* @var array
*/
private $cookieParams = [];
/**
* @var null|array|object
*/
private $parsedBody;
/**
* @var array
*/
private $queryParams = [];
/**
* @var array
*/
private $serverParams;
/**
* @var array
*/
private $uploadedFiles;
/**
* @param array $serverParams Server parameters, typically from $_SERVER
* @param array $uploadedFiles Upload file information, a tree of UploadedFiles
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @param array $cookies Cookies for the message, if any.
* @param array $queryParams Query params for the message, if any.
* @param null|array|object $parsedBody The deserialized body parameters, if any.
* @param string $protocol HTTP protocol version.
* @throws InvalidArgumentException for any invalid value.
*/
public function __construct(
array $serverParams = [],
array $uploadedFiles = [],
$uri = null,
$method = null,
$body = 'php://input',
array $headers = [],
array $cookies = [],
array $queryParams = [],
$parsedBody = null,
$protocol = '1.1'
) {
$this->validateUploadedFiles($uploadedFiles);
if ($body === 'php://input') {
$body = new PhpInputStream();
}
$this->initialize($uri, $method, $body, $headers);
$this->serverParams = $serverParams;
$this->uploadedFiles = $uploadedFiles;
$this->cookieParams = $cookies;
$this->queryParams = $queryParams;
$this->parsedBody = $parsedBody;
$this->protocol = $protocol;
}
/**
* {@inheritdoc}
*/
public function getServerParams()
{
return $this->serverParams;
}
/**
* {@inheritdoc}
*/
public function getUploadedFiles()
{
return $this->uploadedFiles;
}
/**
* {@inheritdoc}
*/
public function withUploadedFiles(array $uploadedFiles)
{
$this->validateUploadedFiles($uploadedFiles);
$new = clone $this;
$new->uploadedFiles = $uploadedFiles;
return $new;
}
/**
* {@inheritdoc}
*/
public function getCookieParams()
{
return $this->cookieParams;
}
/**
* {@inheritdoc}
*/
public function withCookieParams(array $cookies)
{
$new = clone $this;
$new->cookieParams = $cookies;
return $new;
}
/**
* {@inheritdoc}
*/
public function getQueryParams()
{
return $this->queryParams;
}
/**
* {@inheritdoc}
*/
public function withQueryParams(array $query)
{
$new = clone $this;
$new->queryParams = $query;
return $new;
}
/**
* {@inheritdoc}
*/
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* {@inheritdoc}
*/
public function withParsedBody($data)
{
if (! is_array($data) && ! is_object($data) && null !== $data) {
throw new InvalidArgumentException(sprintf(
'%s expects a null, array, or object argument; received %s',
__METHOD__,
gettype($data)
));
}
$new = clone $this;
$new->parsedBody = $data;
return $new;
}
/**
* {@inheritdoc}
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
public function getAttribute($attribute, $default = null)
{
if (! array_key_exists($attribute, $this->attributes)) {
return $default;
}
return $this->attributes[$attribute];
}
/**
* {@inheritdoc}
*/
public function withAttribute($attribute, $value)
{
$new = clone $this;
$new->attributes[$attribute] = $value;
return $new;
}
/**
* {@inheritdoc}
*/
public function withoutAttribute($attribute)
{
$new = clone $this;
unset($new->attributes[$attribute]);
return $new;
}
/**
* Proxy to receive the request method.
*
* This overrides the parent functionality to ensure the method is never
* empty; if no method is present, it returns 'GET'.
*
* @return string
*/
public function getMethod()
{
if (empty($this->method)) {
return 'GET';
}
return $this->method;
}
/**
* Set the request method.
*
* Unlike the regular Request implementation, the server-side
* normalizes the method to uppercase to ensure consistency
* and make checking the method simpler.
*
* This methods returns a new instance.
*
* @param string $method
* @return self
*/
public function withMethod($method)
{
$this->validateMethod($method);
$new = clone $this;
$new->method = $method;
return $new;
}
/**
* Recursively validate the structure in an uploaded files array.
*
* @param array $uploadedFiles
* @throws InvalidArgumentException if any leaf is not an UploadedFileInterface instance.
*/
private function validateUploadedFiles(array $uploadedFiles)
{
foreach ($uploadedFiles as $file) {
if (is_array($file)) {
$this->validateUploadedFiles($file);
continue;
}
if (! $file instanceof UploadedFileInterface) {
throw new InvalidArgumentException('Invalid leaf in uploaded files structure');
}
}
}
}

View file

@ -0,0 +1,240 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\UploadedFileInterface;
use stdClass;
use UnexpectedValueException;
use function array_change_key_case;
use function array_key_exists;
use function explode;
use function implode;
use function is_array;
use function is_callable;
use function strtolower;
use const CASE_LOWER;
/**
* Class for marshaling a request object from the current PHP environment.
*
* Logic largely refactored from the ZF2 Zend\Http\PhpEnvironment\Request class.
*
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
abstract class ServerRequestFactory
{
/**
* Function to use to get apache request headers; present only to simplify mocking.
*
* @var callable
*/
private static $apacheRequestHeaders = 'apache_request_headers';
/**
* Create a request from the supplied superglobal values.
*
* If any argument is not supplied, the corresponding superglobal value will
* be used.
*
* The ServerRequest created is then passed to the fromServer() method in
* order to marshal the request URI and headers.
*
* @see fromServer()
* @param array $server $_SERVER superglobal
* @param array $query $_GET superglobal
* @param array $body $_POST superglobal
* @param array $cookies $_COOKIE superglobal
* @param array $files $_FILES superglobal
* @return ServerRequest
* @throws InvalidArgumentException for invalid file values
*/
public static function fromGlobals(
array $server = null,
array $query = null,
array $body = null,
array $cookies = null,
array $files = null
) {
$server = normalizeServer(
$server ?: $_SERVER,
is_callable(self::$apacheRequestHeaders) ? self::$apacheRequestHeaders : null
);
$files = normalizeUploadedFiles($files ?: $_FILES);
$headers = marshalHeadersFromSapi($server);
if (null === $cookies && array_key_exists('cookie', $headers)) {
$cookies = parseCookieHeader($headers['cookie']);
}
return new ServerRequest(
$server,
$files,
marshalUriFromSapi($server, $headers),
marshalMethodFromSapi($server),
'php://input',
$headers,
$cookies ?: $_COOKIE,
$query ?: $_GET,
$body ?: $_POST,
marshalProtocolVersionFromSapi($server)
);
}
/**
* Access a value in an array, returning a default value if not found
*
* @deprecated since 1.8.0; no longer used internally.
* @param string $key
* @param array $values
* @param mixed $default
* @return mixed
*/
public static function get($key, array $values, $default = null)
{
if (array_key_exists($key, $values)) {
return $values[$key];
}
return $default;
}
/**
* Search for a header value.
*
* Does a case-insensitive search for a matching header.
*
* If found, it is returned as a string, using comma concatenation.
*
* If not, the $default is returned.
*
* @deprecated since 1.8.0; no longer used internally.
* @param string $header
* @param array $headers
* @param mixed $default
* @return string
*/
public static function getHeader($header, array $headers, $default = null)
{
$header = strtolower($header);
$headers = array_change_key_case($headers, CASE_LOWER);
if (array_key_exists($header, $headers)) {
$value = is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header];
return $value;
}
return $default;
}
/**
* Marshal the $_SERVER array
*
* Pre-processes and returns the $_SERVER superglobal.
*
* @deprected since 1.8.0; use Zend\Diactoros\normalizeServer() instead.
* @param array $server
* @return array
*/
public static function normalizeServer(array $server)
{
return normalizeServer(
$server ?: $_SERVER,
is_callable(self::$apacheRequestHeaders) ? self::$apacheRequestHeaders : null
);
}
/**
* Normalize uploaded files
*
* Transforms each value into an UploadedFileInterface instance, and ensures
* that nested arrays are normalized.
*
* @deprecated since 1.8.0; use \Zend\Diactoros\normalizeUploadedFiles instead.
* @param array $files
* @return array
* @throws InvalidArgumentException for unrecognized values
*/
public static function normalizeFiles(array $files)
{
return normalizeUploadedFiles($files);
}
/**
* Marshal headers from $_SERVER
*
* @deprecated since 1.8.0; use Zend\Diactoros\marshalHeadersFromSapi().
* @param array $server
* @return array
*/
public static function marshalHeaders(array $server)
{
return marshalHeadersFromSapi($server);
}
/**
* Marshal the URI from the $_SERVER array and headers
*
* @deprecated since 1.8.0; use Zend\Diactoros\marshalUriFromSapi() instead.
* @param array $server
* @param array $headers
* @return Uri
*/
public static function marshalUriFromServer(array $server, array $headers)
{
return marshalUriFromSapi($server, $headers);
}
/**
* Marshal the host and port from HTTP headers and/or the PHP environment
*
* @deprecated since 1.8.0; use Zend\Diactoros\marshalUriFromSapi() instead,
* and pull the host and port from the Uri instance that function
* returns.
* @param stdClass $accumulator
* @param array $server
* @param array $headers
*/
public static function marshalHostAndPortFromHeaders(stdClass $accumulator, array $server, array $headers)
{
$uri = marshalUriFromSapi($server, $headers);
$accumulator->host = $uri->getHost();
$accumulator->port = $uri->getPort();
}
/**
* Detect the base URI for the request
*
* Looks at a variety of criteria in order to attempt to autodetect a base
* URI, including rewrite URIs, proxy URIs, etc.
*
* @deprecated since 1.8.0; use Zend\Diactoros\marshalUriFromSapi() instead,
* and pull the path from the Uri instance that function returns.
* @param array $server
* @return string
*/
public static function marshalRequestUri(array $server)
{
$uri = marshalUriFromSapi($server, []);
return $uri->getPath();
}
/**
* Strip the query string from a path
*
* @deprecated since 1.8.0; no longer used internally.
* @param mixed $path
* @return string
*/
public static function stripQueryString($path)
{
return explode('?', $path, 2)[0];
}
}

View file

@ -0,0 +1,359 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use function array_key_exists;
use function fclose;
use function feof;
use function fopen;
use function fread;
use function fseek;
use function fstat;
use function ftell;
use function fwrite;
use function get_resource_type;
use function is_int;
use function is_resource;
use function is_string;
use function restore_error_handler;
use function set_error_handler;
use function stream_get_contents;
use function stream_get_meta_data;
use function strstr;
use const E_WARNING;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class Stream implements StreamInterface
{
/**
* @var resource|null
*/
protected $resource;
/**
* @var string|resource
*/
protected $stream;
/**
* @param string|resource $stream
* @param string $mode Mode with which to open stream
* @throws InvalidArgumentException
*/
public function __construct($stream, $mode = 'r')
{
$this->setStream($stream, $mode);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
if (! $this->isReadable()) {
return '';
}
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (RuntimeException $e) {
return '';
}
}
/**
* {@inheritdoc}
*/
public function close()
{
if (! $this->resource) {
return;
}
$resource = $this->detach();
fclose($resource);
}
/**
* {@inheritdoc}
*/
public function detach()
{
$resource = $this->resource;
$this->resource = null;
return $resource;
}
/**
* Attach a new stream/resource to the instance.
*
* @param string|resource $resource
* @param string $mode
* @throws InvalidArgumentException for stream identifier that cannot be
* cast to a resource
* @throws InvalidArgumentException for non-resource stream
*/
public function attach($resource, $mode = 'r')
{
$this->setStream($resource, $mode);
}
/**
* {@inheritdoc}
*/
public function getSize()
{
if (null === $this->resource) {
return null;
}
$stats = fstat($this->resource);
if ($stats !== false) {
return $stats['size'];
}
return null;
}
/**
* {@inheritdoc}
*/
public function tell()
{
if (! $this->resource) {
throw new RuntimeException('No resource available; cannot tell position');
}
$result = ftell($this->resource);
if (! is_int($result)) {
throw new RuntimeException('Error occurred during tell operation');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function eof()
{
if (! $this->resource) {
return true;
}
return feof($this->resource);
}
/**
* {@inheritdoc}
*/
public function isSeekable()
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
return $meta['seekable'];
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET)
{
if (! $this->resource) {
throw new RuntimeException('No resource available; cannot seek position');
}
if (! $this->isSeekable()) {
throw new RuntimeException('Stream is not seekable');
}
$result = fseek($this->resource, $offset, $whence);
if (0 !== $result) {
throw new RuntimeException('Error seeking within stream');
}
return true;
}
/**
* {@inheritdoc}
*/
public function rewind()
{
return $this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return (
strstr($mode, 'x')
|| strstr($mode, 'w')
|| strstr($mode, 'c')
|| strstr($mode, 'a')
|| strstr($mode, '+')
);
}
/**
* {@inheritdoc}
*/
public function write($string)
{
if (! $this->resource) {
throw new RuntimeException('No resource available; cannot write');
}
if (! $this->isWritable()) {
throw new RuntimeException('Stream is not writable');
}
$result = fwrite($this->resource, $string);
if (false === $result) {
throw new RuntimeException('Error writing to stream');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function isReadable()
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return (strstr($mode, 'r') || strstr($mode, '+'));
}
/**
* {@inheritdoc}
*/
public function read($length)
{
if (! $this->resource) {
throw new RuntimeException('No resource available; cannot read');
}
if (! $this->isReadable()) {
throw new RuntimeException('Stream is not readable');
}
$result = fread($this->resource, $length);
if (false === $result) {
throw new RuntimeException('Error reading stream');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getContents()
{
if (! $this->isReadable()) {
throw new RuntimeException('Stream is not readable');
}
$result = stream_get_contents($this->resource);
if (false === $result) {
throw new RuntimeException('Error reading from stream');
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
if (null === $key) {
return stream_get_meta_data($this->resource);
}
$metadata = stream_get_meta_data($this->resource);
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
/**
* Set the internal stream resource.
*
* @param string|resource $stream String stream target or stream resource.
* @param string $mode Resource mode for stream target.
* @throws InvalidArgumentException for invalid streams or resources.
*/
private function setStream($stream, $mode = 'r')
{
$error = null;
$resource = $stream;
if (is_string($stream)) {
set_error_handler(function ($e) use (&$error) {
if ($e !== E_WARNING) {
return;
}
$error = $e;
});
$resource = fopen($stream, $mode);
restore_error_handler();
}
if ($error) {
throw new InvalidArgumentException('Invalid stream reference provided');
}
if (! is_resource($resource) || 'stream' !== get_resource_type($resource)) {
throw new InvalidArgumentException(
'Invalid stream provided; must be a string stream identifier or stream resource'
);
}
if ($stream !== $resource) {
$this->stream = $stream;
}
$this->resource = $resource;
}
}

View file

@ -0,0 +1,283 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;
use function dirname;
use function fclose;
use function fopen;
use function fwrite;
use function is_dir;
use function is_int;
use function is_resource;
use function is_string;
use function is_writable;
use function move_uploaded_file;
use function sprintf;
use function strpos;
use const PHP_SAPI;
use const UPLOAD_ERR_CANT_WRITE;
use const UPLOAD_ERR_EXTENSION;
use const UPLOAD_ERR_FORM_SIZE;
use const UPLOAD_ERR_INI_SIZE;
use const UPLOAD_ERR_NO_FILE;
use const UPLOAD_ERR_NO_TMP_DIR;
use const UPLOAD_ERR_OK;
use const UPLOAD_ERR_PARTIAL;
class UploadedFile implements UploadedFileInterface
{
const ERROR_MESSAGES = [
UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was '
. 'specified in the HTML form',
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
];
/**
* @var string|null
*/
private $clientFilename;
/**
* @var string|null
*/
private $clientMediaType;
/**
* @var int
*/
private $error;
/**
* @var null|string
*/
private $file;
/**
* @var bool
*/
private $moved = false;
/**
* @var int
*/
private $size;
/**
* @var null|StreamInterface
*/
private $stream;
/**
* @param string|resource $streamOrFile
* @param int $size
* @param int $errorStatus
* @param string|null $clientFilename
* @param string|null $clientMediaType
* @throws InvalidArgumentException
*/
public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
{
if ($errorStatus === UPLOAD_ERR_OK) {
if (is_string($streamOrFile)) {
$this->file = $streamOrFile;
}
if (is_resource($streamOrFile)) {
$this->stream = new Stream($streamOrFile);
}
if (! $this->file && ! $this->stream) {
if (! $streamOrFile instanceof StreamInterface) {
throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile');
}
$this->stream = $streamOrFile;
}
}
if (! is_int($size)) {
throw new InvalidArgumentException('Invalid size provided for UploadedFile; must be an int');
}
$this->size = $size;
if (! is_int($errorStatus)
|| 0 > $errorStatus
|| 8 < $errorStatus
) {
throw new InvalidArgumentException(
'Invalid error status for UploadedFile; must be an UPLOAD_ERR_* constant'
);
}
$this->error = $errorStatus;
if (null !== $clientFilename && ! is_string($clientFilename)) {
throw new InvalidArgumentException(
'Invalid client filename provided for UploadedFile; must be null or a string'
);
}
$this->clientFilename = $clientFilename;
if (null !== $clientMediaType && ! is_string($clientMediaType)) {
throw new InvalidArgumentException(
'Invalid client media type provided for UploadedFile; must be null or a string'
);
}
$this->clientMediaType = $clientMediaType;
}
/**
* {@inheritdoc}
* @throws \RuntimeException if the upload was not successful.
*/
public function getStream()
{
if ($this->error !== UPLOAD_ERR_OK) {
throw new RuntimeException(sprintf(
'Cannot retrieve stream due to upload error: %s',
self::ERROR_MESSAGES[$this->error]
));
}
if ($this->moved) {
throw new RuntimeException('Cannot retrieve stream after it has already been moved');
}
if ($this->stream instanceof StreamInterface) {
return $this->stream;
}
$this->stream = new Stream($this->file);
return $this->stream;
}
/**
* {@inheritdoc}
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @throws \RuntimeException if the upload was not successful.
* @throws \InvalidArgumentException if the $path specified is invalid.
* @throws \RuntimeException on any error during the move operation, or on
* the second or subsequent call to the method.
*/
public function moveTo($targetPath)
{
if ($this->moved) {
throw new RuntimeException('Cannot move file; already moved!');
}
if ($this->error !== UPLOAD_ERR_OK) {
throw new RuntimeException(sprintf(
'Cannot retrieve stream due to upload error: %s',
self::ERROR_MESSAGES[$this->error]
));
}
if (! is_string($targetPath) || empty($targetPath)) {
throw new InvalidArgumentException(
'Invalid path provided for move operation; must be a non-empty string'
);
}
$targetDirectory = dirname($targetPath);
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
throw new RuntimeException(sprintf(
'The target directory `%s` does not exists or is not writable',
$targetDirectory
));
}
$sapi = PHP_SAPI;
switch (true) {
case (empty($sapi) || 0 === strpos($sapi, 'cli') || ! $this->file):
// Non-SAPI environment, or no filename present
$this->writeFile($targetPath);
break;
default:
// SAPI environment, with file present
if (false === move_uploaded_file($this->file, $targetPath)) {
throw new RuntimeException('Error occurred while moving uploaded file');
}
break;
}
$this->moved = true;
}
/**
* {@inheritdoc}
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize()
{
return $this->size;
}
/**
* {@inheritdoc}
*
* @see http://php.net/manual/en/features.file-upload.errors.php
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError()
{
return $this->error;
}
/**
* {@inheritdoc}
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename()
{
return $this->clientFilename;
}
/**
* {@inheritdoc}
*/
public function getClientMediaType()
{
return $this->clientMediaType;
}
/**
* Write internal stream to given path
*
* @param string $path
*/
private function writeFile($path)
{
$handle = fopen($path, 'wb+');
if (false === $handle) {
throw new RuntimeException('Unable to write to designated path');
}
$stream = $this->getStream();
$stream->rewind();
while (! $stream->eof()) {
fwrite($handle, $stream->read(4096));
}
fclose($handle);
}
}

View file

@ -0,0 +1,706 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use function array_key_exists;
use function array_keys;
use function count;
use function explode;
use function get_class;
use function gettype;
use function implode;
use function is_numeric;
use function is_object;
use function is_string;
use function ltrim;
use function parse_url;
use function preg_replace;
use function preg_replace_callback;
use function rawurlencode;
use function sprintf;
use function strpos;
use function strtolower;
use function substr;
/**
* Implementation of Psr\Http\UriInterface.
*
* Provides a value object representing a URI for HTTP requests.
*
* Instances of this class are considered immutable; all methods that
* might change state are implemented such that they retain the internal
* state of the current instance and return a new instance that contains the
* changed state.
*/
class Uri implements UriInterface
{
/**
* Sub-delimiters used in user info, query strings and fragments.
*
* @const string
*/
const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters used in user info, paths, query strings, and fragments.
*
* @const string
*/
const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
/**
* @var int[] Array indexed by valid scheme names to their corresponding ports.
*/
protected $allowedSchemes = [
'http' => 80,
'https' => 443,
];
/**
* @var string
*/
private $scheme = '';
/**
* @var string
*/
private $userInfo = '';
/**
* @var string
*/
private $host = '';
/**
* @var int
*/
private $port;
/**
* @var string
*/
private $path = '';
/**
* @var string
*/
private $query = '';
/**
* @var string
*/
private $fragment = '';
/**
* generated uri string cache
* @var string|null
*/
private $uriString;
/**
* @param string $uri
* @throws InvalidArgumentException on non-string $uri argument
*/
public function __construct($uri = '')
{
if ('' === $uri) {
return;
}
if (! is_string($uri)) {
throw new InvalidArgumentException(sprintf(
'URI passed to constructor must be a string; received "%s"',
is_object($uri) ? get_class($uri) : gettype($uri)
));
}
$this->parseUri($uri);
}
/**
* Operations to perform on clone.
*
* Since cloning usually is for purposes of mutation, we reset the
* $uriString property so it will be re-calculated.
*/
public function __clone()
{
$this->uriString = null;
}
/**
* {@inheritdoc}
*/
public function __toString()
{
if (null !== $this->uriString) {
return $this->uriString;
}
$this->uriString = static::createUriString(
$this->scheme,
$this->getAuthority(),
$this->getPath(), // Absolute URIs should use a "/" for an empty path
$this->query,
$this->fragment
);
return $this->uriString;
}
/**
* {@inheritdoc}
*/
public function getScheme()
{
return $this->scheme;
}
/**
* {@inheritdoc}
*/
public function getAuthority()
{
if ('' === $this->host) {
return '';
}
$authority = $this->host;
if ('' !== $this->userInfo) {
$authority = $this->userInfo . '@' . $authority;
}
if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* Retrieve the user-info part of the URI.
*
* This value is percent-encoded, per RFC 3986 Section 3.2.1.
*
* {@inheritdoc}
*/
public function getUserInfo()
{
return $this->userInfo;
}
/**
* {@inheritdoc}
*/
public function getHost()
{
return $this->host;
}
/**
* {@inheritdoc}
*/
public function getPort()
{
return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
? $this->port
: null;
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->path;
}
/**
* {@inheritdoc}
*/
public function getQuery()
{
return $this->query;
}
/**
* {@inheritdoc}
*/
public function getFragment()
{
return $this->fragment;
}
/**
* {@inheritdoc}
*/
public function withScheme($scheme)
{
if (! is_string($scheme)) {
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($scheme) ? get_class($scheme) : gettype($scheme)
));
}
$scheme = $this->filterScheme($scheme);
if ($scheme === $this->scheme) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
return $new;
}
/**
* Create and return a new instance containing the provided user credentials.
*
* The value will be percent-encoded in the new instance, but with measures
* taken to prevent double-encoding.
*
* {@inheritdoc}
*/
public function withUserInfo($user, $password = null)
{
if (! is_string($user)) {
throw new InvalidArgumentException(sprintf(
'%s expects a string user argument; received %s',
__METHOD__,
is_object($user) ? get_class($user) : gettype($user)
));
}
if (null !== $password && ! is_string($password)) {
throw new InvalidArgumentException(sprintf(
'%s expects a string or null password argument; received %s',
__METHOD__,
is_object($password) ? get_class($password) : gettype($password)
));
}
$info = $this->filterUserInfoPart($user);
if (null !== $password) {
$info .= ':' . $this->filterUserInfoPart($password);
}
if ($info === $this->userInfo) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->userInfo = $info;
return $new;
}
/**
* {@inheritdoc}
*/
public function withHost($host)
{
if (! is_string($host)) {
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($host) ? get_class($host) : gettype($host)
));
}
if ($host === $this->host) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->host = strtolower($host);
return $new;
}
/**
* {@inheritdoc}
*/
public function withPort($port)
{
if ($port !== null) {
if (! is_numeric($port) || is_float($port)) {
throw new InvalidArgumentException(sprintf(
'Invalid port "%s" specified; must be an integer, an integer string, or null',
is_object($port) ? get_class($port) : gettype($port)
));
}
$port = (int) $port;
}
if ($port === $this->port) {
// Do nothing if no change was made.
return $this;
}
if ($port !== null && ($port < 1 || $port > 65535)) {
throw new InvalidArgumentException(sprintf(
'Invalid port "%d" specified; must be a valid TCP/UDP port',
$port
));
}
$new = clone $this;
$new->port = $port;
return $new;
}
/**
* {@inheritdoc}
*/
public function withPath($path)
{
if (! is_string($path)) {
throw new InvalidArgumentException(
'Invalid path provided; must be a string'
);
}
if (strpos($path, '?') !== false) {
throw new InvalidArgumentException(
'Invalid path provided; must not contain a query string'
);
}
if (strpos($path, '#') !== false) {
throw new InvalidArgumentException(
'Invalid path provided; must not contain a URI fragment'
);
}
$path = $this->filterPath($path);
if ($path === $this->path) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* {@inheritdoc}
*/
public function withQuery($query)
{
if (! is_string($query)) {
throw new InvalidArgumentException(
'Query string must be a string'
);
}
if (strpos($query, '#') !== false) {
throw new InvalidArgumentException(
'Query string must not include a URI fragment'
);
}
$query = $this->filterQuery($query);
if ($query === $this->query) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* {@inheritdoc}
*/
public function withFragment($fragment)
{
if (! is_string($fragment)) {
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($fragment) ? get_class($fragment) : gettype($fragment)
));
}
$fragment = $this->filterFragment($fragment);
if ($fragment === $this->fragment) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* Parse a URI into its parts, and set the properties
*
* @param string $uri
*/
private function parseUri($uri)
{
$parts = parse_url($uri);
if (false === $parts) {
throw new \InvalidArgumentException(
'The source URI string appears to be malformed'
);
}
$this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
$this->userInfo = isset($parts['user']) ? $this->filterUserInfoPart($parts['user']) : '';
$this->host = isset($parts['host']) ? strtolower($parts['host']) : '';
$this->port = isset($parts['port']) ? $parts['port'] : null;
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
}
}
/**
* Create a URI string from its various parts
*
* @param string $scheme
* @param string $authority
* @param string $path
* @param string $query
* @param string $fragment
* @return string
*/
private static function createUriString($scheme, $authority, $path, $query, $fragment)
{
$uri = '';
if ('' !== $scheme) {
$uri .= sprintf('%s:', $scheme);
}
if ('' !== $authority) {
$uri .= '//' . $authority;
}
if ('' !== $path && '/' !== substr($path, 0, 1)) {
$path = '/' . $path;
}
$uri .= $path;
if ('' !== $query) {
$uri .= sprintf('?%s', $query);
}
if ('' !== $fragment) {
$uri .= sprintf('#%s', $fragment);
}
return $uri;
}
/**
* Is a given port non-standard for the current scheme?
*
* @param string $scheme
* @param string $host
* @param int $port
* @return bool
*/
private function isNonStandardPort($scheme, $host, $port)
{
if ('' === $scheme) {
return '' === $host || null !== $port;
}
if ('' === $host || null === $port) {
return false;
}
return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
}
/**
* Filters the scheme to ensure it is a valid scheme.
*
* @param string $scheme Scheme name.
*
* @return string Filtered scheme.
*/
private function filterScheme($scheme)
{
$scheme = strtolower($scheme);
$scheme = preg_replace('#:(//)?$#', '', $scheme);
if ('' === $scheme) {
return '';
}
if (! isset($this->allowedSchemes[$scheme])) {
throw new InvalidArgumentException(sprintf(
'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
$scheme,
implode(', ', array_keys($this->allowedSchemes))
));
}
return $scheme;
}
/**
* Filters a part of user info in a URI to ensure it is properly encoded.
*
* @param string $part
* @return string
*/
private function filterUserInfoPart($part)
{
// Note the addition of `%` to initial charset; this allows `|` portion
// to match and thus prevent double-encoding.
return preg_replace_callback(
'/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$part
);
}
/**
* Filters the path of a URI to ensure it is properly encoded.
*
* @param string $path
* @return string
*/
private function filterPath($path)
{
$path = preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$path
);
if ('' === $path) {
// No path
return $path;
}
if ($path[0] !== '/') {
// Relative path
return $path;
}
// Ensure only one leading slash, to prevent XSS attempts.
return '/' . ltrim($path, '/');
}
/**
* Filter a query string to ensure it is propertly encoded.
*
* Ensures that the values in the query string are properly urlencoded.
*
* @param string $query
* @return string
*/
private function filterQuery($query)
{
if ('' !== $query && strpos($query, '?') === 0) {
$query = substr($query, 1);
}
$parts = explode('&', $query);
foreach ($parts as $index => $part) {
list($key, $value) = $this->splitQueryValue($part);
if ($value === null) {
$parts[$index] = $this->filterQueryOrFragment($key);
continue;
}
$parts[$index] = sprintf(
'%s=%s',
$this->filterQueryOrFragment($key),
$this->filterQueryOrFragment($value)
);
}
return implode('&', $parts);
}
/**
* Split a query value into a key/value tuple.
*
* @param string $value
* @return array A value with exactly two elements, key and value
*/
private function splitQueryValue($value)
{
$data = explode('=', $value, 2);
if (! isset($data[1])) {
$data[] = null;
}
return $data;
}
/**
* Filter a fragment value to ensure it is properly encoded.
*
* @param string $fragment
* @return string
*/
private function filterFragment($fragment)
{
if ('' !== $fragment && strpos($fragment, '#') === 0) {
$fragment = '%23' . substr($fragment, 1);
}
return $this->filterQueryOrFragment($fragment);
}
/**
* Filter a query string key or value, or a fragment.
*
* @param string $value
* @return string
*/
private function filterQueryOrFragment($value)
{
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$value
);
}
/**
* URL encode a character returned by a regex.
*
* @param array $matches
* @return string
*/
private function urlEncodeChar(array $matches)
{
return rawurlencode($matches[0]);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
/**
* Create an uploaded file instance from an array of values.
*
* @param array $spec A single $_FILES entry.
* @return UploadedFile
* @throws InvalidArgumentException if one or more of the tmp_name, size,
* or error keys are missing from $spec.
*/
function createUploadedFile(array $spec)
{
if (! isset($spec['tmp_name'])
|| ! isset($spec['size'])
|| ! isset($spec['error'])
) {
throw new InvalidArgumentException(sprintf(
'$spec provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error"; one or more were missing',
__FUNCTION__
));
}
return new UploadedFile(
$spec['tmp_name'],
$spec['size'],
$spec['error'],
isset($spec['name']) ? $spec['name'] : null,
isset($spec['type']) ? $spec['type'] : null
);
}

View file

@ -0,0 +1,50 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use function array_key_exists;
use function strpos;
use function strtolower;
use function strtr;
use function substr;
/**
* @param array $server Values obtained from the SAPI (generally `$_SERVER`).
* @return array Header/value pairs
*/
function marshalHeadersFromSapi(array $server)
{
$headers = [];
foreach ($server as $key => $value) {
// Apache prefixes environment variables with REDIRECT_
// if they are added by rewrite rules
if (strpos($key, 'REDIRECT_') === 0) {
$key = substr($key, 9);
// We will not overwrite existing variables with the
// prefixed versions, though
if (array_key_exists($key, $server)) {
continue;
}
}
if ($value && strpos($key, 'HTTP_') === 0) {
$name = strtr(strtolower(substr($key, 5)), '_', '-');
$headers[$name] = $value;
continue;
}
if ($value && strpos($key, 'CONTENT_') === 0) {
$name = 'content-' . strtolower(substr($key, 8));
$headers[$name] = $value;
continue;
}
}
return $headers;
}

View file

@ -0,0 +1,19 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
/**
* Retrieve the request method from the SAPI parameters.
*
* @param array $server
* @return string
*/
function marshalMethodFromSapi(array $server)
{
return isset($server['REQUEST_METHOD']) ? $server['REQUEST_METHOD'] : 'GET';
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use UnexpectedValueException;
use function preg_match;
/**
* Return HTTP protocol version (X.Y) as discovered within a `$_SERVER` array.
*
* @param array $server
* @return string
* @throws UnexpectedValueException if the $server['SERVER_PROTOCOL'] value is
* malformed.
*/
function marshalProtocolVersionFromSapi(array $server)
{
if (! isset($server['SERVER_PROTOCOL'])) {
return '1.1';
}
if (! preg_match('#^(HTTP/)?(?P<version>[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) {
throw new UnexpectedValueException(sprintf(
'Unrecognized protocol version (%s)',
$server['SERVER_PROTOCOL']
));
}
return $matches['version'];
}

View file

@ -0,0 +1,212 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use function array_change_key_case;
use function array_key_exists;
use function explode;
use function implode;
use function is_array;
use function ltrim;
use function preg_match;
use function preg_replace;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
/**
* Marshal a Uri instance based on the values presnt in the $_SERVER array and headers.
*
* @param array $server SAPI parameters
* @param array $headers HTTP request headers
* @return Uri
*/
function marshalUriFromSapi(array $server, array $headers)
{
/**
* Retrieve a header value from an array of headers using a case-insensitive lookup.
*
* @param string $name
* @param array $headers Key/value header pairs
* @param mixed $default Default value to return if header not found
* @return mixed
*/
$getHeaderFromArray = function ($name, array $headers, $default = null) {
$header = strtolower($name);
$headers = array_change_key_case($headers, CASE_LOWER);
if (array_key_exists($header, $headers)) {
$value = is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header];
return $value;
}
return $default;
};
/**
* Marshal the host and port from HTTP headers and/or the PHP environment.
*
* @param array $headers
* @param array $server
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalHostAndPort = function (array $headers, array $server) use ($getHeaderFromArray) {
/**
* @param string|array $host
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalHostAndPortFromHeader = function ($host) {
if (is_array($host)) {
$host = implode(', ', $host);
}
$port = null;
// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $host, $matches)) {
$host = substr($host, 0, -1 * (strlen($matches[1]) + 1));
$port = (int) $matches[1];
}
return [$host, $port];
};
/**
* @param array $server
* @param string $host
* @param null|int $port
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalIpv6HostAndPort = function (array $server, $host, $port) {
$host = '[' . $server['SERVER_ADDR'] . ']';
$port = $port ?: 80;
if ($port . ']' === substr($host, strrpos($host, ':') + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$port = null;
}
return [$host, $port];
};
static $defaults = ['', null];
if ($getHeaderFromArray('host', $headers, false)) {
return $marshalHostAndPortFromHeader($getHeaderFromArray('host', $headers));
}
if (! isset($server['SERVER_NAME'])) {
return $defaults;
}
$host = $server['SERVER_NAME'];
$port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null;
if (! isset($server['SERVER_ADDR'])
|| ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host)
) {
return [$host, $port];
}
// Misinterpreted IPv6-Address
// Reported for Safari on Windows
return $marshalIpv6HostAndPort($server, $host, $port);
};
/**
* Detect the path for the request
*
* Looks at a variety of criteria in order to attempt to autodetect the base
* request path, including:
*
* - IIS7 UrlRewrite environment
* - REQUEST_URI
* - ORIG_PATH_INFO
*
* From ZF2's Zend\Http\PhpEnvironment\Request class
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*
* @param array $server SAPI environment array (typically `$_SERVER`)
* @return string Discovered path
*/
$marshalRequestPath = function (array $server) {
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = array_key_exists('IIS_WasUrlRewritten', $server) ? $server['IIS_WasUrlRewritten'] : null;
$unencodedUrl = array_key_exists('UNENCODED_URL', $server) ? $server['UNENCODED_URL'] : '';
if ('1' === $iisUrlRewritten && ! empty($unencodedUrl)) {
return $unencodedUrl;
}
$requestUri = array_key_exists('REQUEST_URI', $server) ? $server['REQUEST_URI'] : null;
if ($requestUri !== null) {
return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
}
$origPathInfo = array_key_exists('ORIG_PATH_INFO', $server) ? $server['ORIG_PATH_INFO'] : null;
if (empty($origPathInfo)) {
return '/';
}
return $origPathInfo;
};
$uri = new Uri('');
// URI scheme
$scheme = 'http';
if (array_key_exists('HTTPS', $server)) {
$https = $server['HTTPS'];
} elseif (array_key_exists('https', $server)) {
$https = $server['https'];
} else {
$https = false;
}
if (($https && 'off' !== strtolower($https))
|| strtolower($getHeaderFromArray('x-forwarded-proto', $headers, false)) === 'https'
) {
$scheme = 'https';
}
$uri = $uri->withScheme($scheme);
// Set the host
list($host, $port) = $marshalHostAndPort($headers, $server);
if (! empty($host)) {
$uri = $uri->withHost($host);
if (! empty($port)) {
$uri = $uri->withPort($port);
}
}
// URI path
$path = $marshalRequestPath($server);
// Strip query string
$path = explode('?', $path, 2)[0];
// URI query
$query = '';
if (isset($server['QUERY_STRING'])) {
$query = ltrim($server['QUERY_STRING'], '?');
}
// URI fragment
$fragment = '';
if (strpos($path, '#') !== false) {
list($path, $fragment) = explode('#', $path, 2);
}
return $uri
->withPath($path)
->withFragment($fragment)
->withQuery($query);
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use function is_callable;
/**
* Marshal the $_SERVER array
*
* Pre-processes and returns the $_SERVER superglobal. In particularly, it
* attempts to detect the Authorization header, which is often not aggregated
* correctly under various SAPI/httpd combinations.
*
* @param array $server
* @param null|callable $apacheRequestHeaderCallback Callback that can be used to
* retrieve Apache request headers. This defaults to
* `apache_request_headers` under the Apache mod_php.
* @return array Either $server verbatim, or with an added HTTP_AUTHORIZATION header.
*/
function normalizeServer(array $server, callable $apacheRequestHeaderCallback = null)
{
if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) {
$apacheRequestHeaderCallback = 'apache_request_headers';
}
// If the HTTP_AUTHORIZATION value is already set, or the callback is not
// callable, we return verbatim
if (isset($server['HTTP_AUTHORIZATION'])
|| ! is_callable($apacheRequestHeaderCallback)
) {
return $server;
}
$apacheRequestHeaders = $apacheRequestHeaderCallback();
if (isset($apacheRequestHeaders['Authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization'];
return $server;
}
if (isset($apacheRequestHeaders['authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization'];
return $server;
}
return $server;
}

View file

@ -0,0 +1,129 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use InvalidArgumentException;
use Psr\Http\Message\UploadedFileInterface;
use function is_array;
/**
* Normalize uploaded files
*
* Transforms each value into an UploadedFile instance, and ensures that nested
* arrays are normalized.
*
* @param array $files
* @return UploadedFileInterface[]
* @throws InvalidArgumentException for unrecognized values
*/
function normalizeUploadedFiles(array $files)
{
/**
* Traverse a nested tree of uploaded file specifications.
*
* @param string[]|array[] $tmpNameTree
* @param int[]|array[] $sizeTree
* @param int[]|array[] $errorTree
* @param string[]|array[]|null $nameTree
* @param string[]|array[]|null $typeTree
* @return UploadedFile[]|array[]
*/
$recursiveNormalize = function (
array $tmpNameTree,
array $sizeTree,
array $errorTree,
array $nameTree = null,
array $typeTree = null
) use (&$recursiveNormalize) {
$normalized = [];
foreach ($tmpNameTree as $key => $value) {
if (is_array($value)) {
// Traverse
$normalized[$key] = $recursiveNormalize(
$tmpNameTree[$key],
$sizeTree[$key],
$errorTree[$key],
isset($nameTree[$key]) ? $nameTree[$key] : null,
isset($typeTree[$key]) ? $typeTree[$key] : null
);
continue;
}
$normalized[$key] = createUploadedFile([
'tmp_name' => $tmpNameTree[$key],
'size' => $sizeTree[$key],
'error' => $errorTree[$key],
'name' => isset($nameTree[$key]) ? $nameTree[$key] : null,
'type' => isset($typeTree[$key]) ? $typeTree[$key] : null
]);
}
return $normalized;
};
/**
* Normalize an array of file specifications.
*
* Loops through all nested files (as determined by receiving an array to the
* `tmp_name` key of a `$_FILES` specification) and returns a normalized array
* of UploadedFile instances.
*
* This function normalizes a `$_FILES` array representing a nested set of
* uploaded files as produced by the php-fpm SAPI, CGI SAPI, or mod_php
* SAPI.
*
* @param array $files
* @return UploadedFile[]
*/
$normalizeUploadedFileSpecification = function (array $files = []) use (&$recursiveNormalize) {
if (! isset($files['tmp_name']) || ! is_array($files['tmp_name'])
|| ! isset($files['size']) || ! is_array($files['size'])
|| ! isset($files['error']) || ! is_array($files['error'])
) {
throw new InvalidArgumentException(sprintf(
'$files provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error", with each represented as an array;'
. ' one or more were missing or non-array values',
__FUNCTION__
));
}
return $recursiveNormalize(
$files['tmp_name'],
$files['size'],
$files['error'],
isset($files['name']) ? $files['name'] : null,
isset($files['type']) ? $files['type'] : null
);
};
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
continue;
}
if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) {
$normalized[$key] = $normalizeUploadedFileSpecification($value);
continue;
}
if (is_array($value) && isset($value['tmp_name'])) {
$normalized[$key] = createUploadedFile($value);
continue;
}
if (is_array($value)) {
$normalized[$key] = normalizeUploadedFiles($value);
continue;
}
throw new InvalidArgumentException('Invalid value in files specification');
}
return $normalized;
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @see https://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Diactoros;
use function preg_match_all;
use function urldecode;
/**
* Parse a cookie header according to RFC 6265.
*
* PHP will replace special characters in cookie names, which results in other cookies not being available due to
* overwriting. Thus, the server request should take the cookies from the request header instead.
*
* @param string $cookieHeader A string cookie header value.
* @return array key/value cookie pairs.
*/
function parseCookieHeader($cookieHeader)
{
preg_match_all('(
(?:^\\n?[ \t]*|;[ ])
(?P<name>[!#$%&\'*+-.0-9A-Z^_`a-z|~]+)
=
(?P<DQUOTE>"?)
(?P<value>[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*)
(?P=DQUOTE)
(?=\\n?[ \t]*$|;[ ])
)x', $cookieHeader, $matches, PREG_SET_ORDER);
$cookies = [];
foreach ($matches as $match) {
$cookies[$match['name']] = urldecode($match['value']);
}
return $cookies;
}

View file

@ -0,0 +1,51 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 2.6.0 - 2018-04-25
### Added
- [#28](https://github.com/zendframework/zend-escaper/pull/28) adds support for PHP 7.1 and 7.2.
### Changed
- [#25](https://github.com/zendframework/zend-escaper/pull/25) changes the behavior of the `Escaper` constructor; it now raises an
exception for non-null, non-string `$encoding` arguments.
### Deprecated
- Nothing.
### Removed
- [#28](https://github.com/zendframework/zend-escaper/pull/28) removes support for PHP 5.5.
- [#28](https://github.com/zendframework/zend-escaper/pull/28) removes support for HHVM.
### Fixed
- Nothing.
## 2.5.2 - 2016-06-30
### Added
- [#11](https://github.com/zendframework/zend-escaper/pull/11),
[#12](https://github.com/zendframework/zend-escaper/pull/12), and
[#13](https://github.com/zendframework/zend-escaper/pull/13) prepare and
publish documentation to https://zendframework.github.io/zend-escaper/
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#3](https://github.com/zendframework/zend-escaper/pull/3) updates the
the escaping mechanism to add support for escaping characters outside the Basic
Multilingual Plane when escaping for JS, CSS, or HTML attributes.

View file

@ -0,0 +1,27 @@
Copyright (c) 2005-2018, Zend Technologies USA, Inc.
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 of Zend Technologies USA, Inc. 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.

View file

@ -0,0 +1,13 @@
# zend-escaper
[![Build Status](https://secure.travis-ci.org/zendframework/zend-escaper.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-escaper)
[![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-escaper/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-escaper?branch=master)
The OWASP Top 10 web security risks study lists Cross-Site Scripting (XSS) in
second place. PHPs sole functionality against XSS is limited to two functions
of which one is commonly misapplied. Thus, the zend-escaper component was written.
It offers developers a way to escape output and defend from XSS and related
vulnerabilities by introducing contextual escaping based on peer-reviewed rules.
- File issues at https://github.com/zendframework/zend-escaper/issues
- Documentation is at https://docs.zendframework.com/zend-escaper/

View file

@ -0,0 +1,54 @@
{
"name": "zendframework/zend-escaper",
"description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
"license": "BSD-3-Clause",
"keywords": [
"zf",
"zendframework",
"escaper"
],
"support": {
"docs": "https://docs.zendframework.com/zend-escaper/",
"issues": "https://github.com/zendframework/zend-escaper/issues",
"source": "https://github.com/zendframework/zend-escaper",
"rss": "https://github.com/zendframework/zend-escaper/releases.atom",
"chat": "https://zendframework-slack.herokuapp.com",
"forum": "https://discourse.zendframework.com/c/questions/components"
},
"require": {
"php": "^5.6 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
"zendframework/zend-coding-standard": "~1.0.0"
},
"autoload": {
"psr-4": {
"Zend\\Escaper\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ZendTest\\Escaper\\": "test/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.6.x-dev",
"dev-develop": "2.7.x-dev"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

View file

@ -0,0 +1,392 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Escaper;
/**
* Context specific methods for use in secure output escaping
*/
class Escaper
{
/**
* Entity Map mapping Unicode codepoints to any available named HTML entities.
*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*
* @var array
*/
protected static $htmlNamedEntityMap = [
34 => 'quot', // quotation mark
38 => 'amp', // ampersand
60 => 'lt', // less-than sign
62 => 'gt', // greater-than sign
];
/**
* Current encoding for escaping. If not UTF-8, we convert strings from this encoding
* pre-escaping and back to this encoding post-escaping.
*
* @var string
*/
protected $encoding = 'utf-8';
/**
* Holds the value of the special flags passed as second parameter to
* htmlspecialchars().
*
* @var int
*/
protected $htmlSpecialCharsFlags;
/**
* Static Matcher which escapes characters for HTML Attribute contexts
*
* @var callable
*/
protected $htmlAttrMatcher;
/**
* Static Matcher which escapes characters for Javascript contexts
*
* @var callable
*/
protected $jsMatcher;
/**
* Static Matcher which escapes characters for CSS Attribute contexts
*
* @var callable
*/
protected $cssMatcher;
/**
* List of all encoding supported by this class
*
* @var array
*/
protected $supportedEncodings = [
'iso-8859-1', 'iso8859-1', 'iso-8859-5', 'iso8859-5',
'iso-8859-15', 'iso8859-15', 'utf-8', 'cp866',
'ibm866', '866', 'cp1251', 'windows-1251',
'win-1251', '1251', 'cp1252', 'windows-1252',
'1252', 'koi8-r', 'koi8-ru', 'koi8r',
'big5', '950', 'gb2312', '936',
'big5-hkscs', 'shift_jis', 'sjis', 'sjis-win',
'cp932', '932', 'euc-jp', 'eucjp',
'eucjp-win', 'macroman'
];
/**
* Constructor: Single parameter allows setting of global encoding for use by
* the current object.
*
* @param string $encoding
* @throws Exception\InvalidArgumentException
*/
public function __construct($encoding = null)
{
if ($encoding !== null) {
if (! is_string($encoding)) {
throw new Exception\InvalidArgumentException(
get_class($this) . ' constructor parameter must be a string, received ' . gettype($encoding)
);
}
if ($encoding === '') {
throw new Exception\InvalidArgumentException(
get_class($this) . ' constructor parameter does not allow a blank value'
);
}
$encoding = strtolower($encoding);
if (! in_array($encoding, $this->supportedEncodings)) {
throw new Exception\InvalidArgumentException(
'Value of \'' . $encoding . '\' passed to ' . get_class($this)
. ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()'
);
}
$this->encoding = $encoding;
}
// We take advantage of ENT_SUBSTITUTE flag to correctly deal with invalid UTF-8 sequences.
$this->htmlSpecialCharsFlags = ENT_QUOTES | ENT_SUBSTITUTE;
// set matcher callbacks
$this->htmlAttrMatcher = [$this, 'htmlAttrMatcher'];
$this->jsMatcher = [$this, 'jsMatcher'];
$this->cssMatcher = [$this, 'cssMatcher'];
}
/**
* Return the encoding that all output/input is expected to be encoded in.
*
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Escape a string for the HTML Body context where there are very few characters
* of special meaning. Internally this will use htmlspecialchars().
*
* @param string $string
* @return string
*/
public function escapeHtml($string)
{
return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding);
}
/**
* Escape a string for the HTML Attribute context. We use an extended set of characters
* to escape that are not covered by htmlspecialchars() to cover cases where an attribute
* might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE).
*
* @param string $string
* @return string
*/
public function escapeHtmlAttr($string)
{
$string = $this->toUtf8($string);
if ($string === '' || ctype_digit($string)) {
return $string;
}
$result = preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', $this->htmlAttrMatcher, $string);
return $this->fromUtf8($result);
}
/**
* Escape a string for the Javascript context. This does not use json_encode(). An extended
* set of characters are escaped beyond ECMAScript's rules for Javascript literal string
* escaping in order to prevent misinterpretation of Javascript as HTML leading to the
* injection of special characters and entities. The escaping used should be tolerant
* of cases where HTML escaping was not applied on top of Javascript escaping correctly.
* Backslash escaping is not used as it still leaves the escaped character as-is and so
* is not useful in a HTML context.
*
* @param string $string
* @return string
*/
public function escapeJs($string)
{
$string = $this->toUtf8($string);
if ($string === '' || ctype_digit($string)) {
return $string;
}
$result = preg_replace_callback('/[^a-z0-9,\._]/iSu', $this->jsMatcher, $string);
return $this->fromUtf8($result);
}
/**
* Escape a string for the URI or Parameter contexts. This should not be used to escape
* an entire URI - only a subcomponent being inserted. The function is a simple proxy
* to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely.
*
* @param string $string
* @return string
*/
public function escapeUrl($string)
{
return rawurlencode($string);
}
/**
* Escape a string for the CSS context. CSS escaping can be applied to any string being
* inserted into CSS and escapes everything except alphanumerics.
*
* @param string $string
* @return string
*/
public function escapeCss($string)
{
$string = $this->toUtf8($string);
if ($string === '' || ctype_digit($string)) {
return $string;
}
$result = preg_replace_callback('/[^a-z0-9]/iSu', $this->cssMatcher, $string);
return $this->fromUtf8($result);
}
/**
* Callback function for preg_replace_callback that applies HTML Attribute
* escaping to all matches.
*
* @param array $matches
* @return string
*/
protected function htmlAttrMatcher($matches)
{
$chr = $matches[0];
$ord = ord($chr);
/**
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r")
|| ($ord >= 0x7f && $ord <= 0x9f)
) {
return '&#xFFFD;';
}
/**
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the integer value of the character.
*/
if (strlen($chr) > 1) {
$chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8');
}
$hex = bin2hex($chr);
$ord = hexdec($hex);
if (isset(static::$htmlNamedEntityMap[$ord])) {
return '&' . static::$htmlNamedEntityMap[$ord] . ';';
}
/**
* Per OWASP recommendations, we'll use upper hex entities
* for any other characters where a named entity does not exist.
*/
if ($ord > 255) {
return sprintf('&#x%04X;', $ord);
}
return sprintf('&#x%02X;', $ord);
}
/**
* Callback function for preg_replace_callback that applies Javascript
* escaping to all matches.
*
* @param array $matches
* @return string
*/
protected function jsMatcher($matches)
{
$chr = $matches[0];
if (strlen($chr) == 1) {
return sprintf('\\x%02X', ord($chr));
}
$chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8');
$hex = strtoupper(bin2hex($chr));
if (strlen($hex) <= 4) {
return sprintf('\\u%04s', $hex);
}
$highSurrogate = substr($hex, 0, 4);
$lowSurrogate = substr($hex, 4, 4);
return sprintf('\\u%04s\\u%04s', $highSurrogate, $lowSurrogate);
}
/**
* Callback function for preg_replace_callback that applies CSS
* escaping to all matches.
*
* @param array $matches
* @return string
*/
protected function cssMatcher($matches)
{
$chr = $matches[0];
if (strlen($chr) == 1) {
$ord = ord($chr);
} else {
$chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8');
$ord = hexdec(bin2hex($chr));
}
return sprintf('\\%X ', $ord);
}
/**
* Converts a string to UTF-8 from the base encoding. The base encoding is set via this
* class' constructor.
*
* @param string $string
* @throws Exception\RuntimeException
* @return string
*/
protected function toUtf8($string)
{
if ($this->getEncoding() === 'utf-8') {
$result = $string;
} else {
$result = $this->convertEncoding($string, 'UTF-8', $this->getEncoding());
}
if (! $this->isUtf8($result)) {
throw new Exception\RuntimeException(
sprintf('String to be escaped was not valid UTF-8 or could not be converted: %s', $result)
);
}
return $result;
}
/**
* Converts a string from UTF-8 to the base encoding. The base encoding is set via this
* class' constructor.
* @param string $string
* @return string
*/
protected function fromUtf8($string)
{
if ($this->getEncoding() === 'utf-8') {
return $string;
}
return $this->convertEncoding($string, $this->getEncoding(), 'UTF-8');
}
/**
* Checks if a given string appears to be valid UTF-8 or not.
*
* @param string $string
* @return bool
*/
protected function isUtf8($string)
{
return ($string === '' || preg_match('/^./su', $string));
}
/**
* Encoding conversion helper which wraps iconv and mbstring where they exist or throws
* and exception where neither is available.
*
* @param string $string
* @param string $to
* @param array|string $from
* @throws Exception\RuntimeException
* @return string
*/
protected function convertEncoding($string, $to, $from)
{
if (function_exists('iconv')) {
$result = iconv($from, $to, $string);
} elseif (function_exists('mb_convert_encoding')) {
$result = mb_convert_encoding($string, $to, $from);
} else {
throw new Exception\RuntimeException(
get_class($this)
. ' requires either the iconv or mbstring extension to be installed'
. ' when escaping for non UTF-8 strings.'
);
}
if ($result === false) {
return ''; // return non-fatal blank string on encoding errors from users
}
return $result;
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Escaper\Exception;
interface ExceptionInterface
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Escaper\Exception;
/**
* Invalid argument exception
*/
class InvalidArgumentException extends \InvalidArgumentException implements
ExceptionInterface
{
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Escaper\Exception;
/**
* Invalid argument exception
*/
class RuntimeException extends \RuntimeException implements
ExceptionInterface
{
}

View file

@ -0,0 +1,393 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 2.10.3 - 2018-08-01
### Added
- Nothing.
### Changed
- This release modifies how `Zend\Feed\Pubsubhubbub\AbstractCallback::_detectCallbackUrl()`
marshals the request URI. In prior releases, we would attempt to inspect the
`X-Rewrite-Url` and `X-Original-Url` headers, using their values, if present.
These headers are issued by the ISAPI_Rewrite module for IIS (developed by
HeliconTech). However, we have no way of guaranteeing that the module is what
issued the headers, making it an unreliable source for discovering the URI. As
such, we have removed this feature in this release.
The method is not called internally. If you are calling the method from your
own extension and need support for ISAPI_Rewrite, you will need to override
the method as follows:
```php
protected function _detectCallbackUrl()
{
$callbackUrl = null;
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_REWRITE_URL'];
}
if (isset($_SERVER['HTTP_X_ORIGINAL_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_ORIGINAL_URL'];
}
return $callbackUrl ?: parent::__detectCallbackUrl();
}
```
If you use an approach such as the above, make sure you also instruct your web
server to strip any incoming headers of the same name so that you can
guarantee they are issued by the ISAPI_Rewrite module.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 2.10.2 - 2018-06-18
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#81](https://github.com/zendframework/zend-feed/pull/81) updates the `Zend\Feed\Reader\Reader` and `Zend\Feed\Writer\Writer` classes to
conditionally register their respective "GooglePlayPodcast" extensions only if
their extension managers are aware of it. This is done due to the fact that
existing `ExtensionManagerInterface` implementations may not register it by
default as the extension did not exist in releases prior to 2.10.0. By having
the registration conditional, we prevent an exception from being raised; users
are not impacted by its absence, as the extension features were not exposed
previously.
Both `Reader` and `Writer` emit an `E_USER_NOTICE` when the extension is not
found in the extension manager, indicating that the
`ExtensionManagerInterface` implementation should be updated to add entries
for the "GooglePlayPodcast" entry, feed, and/or renderer classes.
## 2.10.1 - 2018-06-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#79](https://github.com/zendframework/zend-feed/pull/79) fixes an issue in the `setType()` method of the iTunes feed renderer whereby it was setting
the DOM content with an uninitialized variable.
## 2.10.0 - 2018-05-24
### Added
- [#78](https://github.com/zendframework/zend-feed/pull/78) adds support for the Google Play Podcasts 1.0 DTD in both the Reader and
Writer subcomponents. The following new classes provide the support:
- `Zend\Feed\Reader\Extension\GooglePlayPodcast\Entry`
- `Zend\Feed\Reader\Extension\GooglePlayPodcast\Feed`
- `Zend\Feed\Writer\Extension\GooglePlayPodcast\Entry`
- `Zend\Feed\Writer\Extension\GooglePlayPodcast\Feed`
- `Zend\Feed\Writer\Extension\GooglePlayPodcast\Renderer\Entry`
- `Zend\Feed\Writer\Extension\GooglePlayPodcast\Renderer\Feed`
The extensions are registered by default with both `Zend\Feed\Reader\Reader`
and `Zend\Feed\Writer\Writer`.
- [#77](https://github.com/zendframework/zend-feed/pull/77) adds support for `itunes:image` for each of:
- `Zend\Feed\Reader\Extension\Podcast\Entry`, via `getItunesImage()`; previously only the `Feed` supported it.
- `Zend\Feed\Writer\Extension\ITunes\Entry`, via `setItunesImage()`; previously only the `Feed` supported it.
- `Zend\Feed\Writer\Extension\ITunes\Renderer\Entry`; previously on the `Feed` supported it.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Entry::setItunesSeason()`, corresponding to the
`itunes:season` tag, and allowing setting the season number of the episode the
entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Entry::setItunesIsClosedCaptioned()`, corresponding to the
`itunes:isClosedCaptioned` tag, and allowing setting the status of closed
captioning support in the episode the entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Entry::setItunesEpisodeType()`, corresponding to the
`itunes:episodeType` tag, and allowing setting the type of episode the entry represents
(one of "full", "trailer", or "bonus", and defaulting to "full").
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Entry::setEpisode()`, corresponding to the
`itunes:episode` tag, and allowing setting the number of the episode the entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Feed::setItunesComplete()`, corresponding to the
`itunes:complete` tag. It allows setting a boolean flag, indicating whether or not the
podcast is complete (will not air new episodes).
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Writer\Extension\ITunes\Feed::setItunesType()`, corresponding to the
`itunes:type` tag, and allowing setting the podcast type (one of "serial" or "episodic").
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Entry::getEpisodeType()`, corresponding to the
`itunes:episodeType` tag, and returning the type of episode the entry represents
(one of "full", "trailer", or "bonus", and defaulting to "full").
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Entry::getSeason()`, corresponding to the
`itunes:season` tag, and returning the season number of the episode the entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Entry::isClsoedCaptioned()`, corresponding to the
`itunes:isClosedCaptioned` tag, and returning the status of closed captioning
in the episode the entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Entry::getEpisode()`, corresponding to the
`itunes:episode` tag, and returning the number of the episode the entry represents.
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Feed::isComplete()`, corresponding to the
`itunes:complete` tag. It returns a boolean, indicating whether or not the podcast is
complete (will not air new episodes).
- [#75](https://github.com/zendframework/zend-feed/pull/75) adds `Zend\Feed\Reader\Extension\Podcast\Feed::getPodcastType()`, corresponding to the
`itunes:type` tag, and providing the podcast type (one of "serial" or "episodic", defaulting
to the latter).
### Changed
- [#77](https://github.com/zendframework/zend-feed/pull/77) updates URI validation for `Zend\Feed\Writer\Extension\ITunes\Feed::setItunesImage()` to
first check that we have received a string value before proceeding.
### Deprecated
- [#75](https://github.com/zendframework/zend-feed/pull/75) deprecates each of:
- `Zend\Feed\Reader\Extension\Podcast\Entry::getKeywords()`
- `Zend\Feed\Reader\Extension\Podcast\Feed::getKeywords()`
- `Zend\Feed\Writer\Extension\ITunes\Entry::setKeywords()`
- `Zend\Feed\Writer\Extension\ITunes\Feed::setKeywords()`
as the iTunes Podcast RSS specification no longer supports keywords.
### Removed
- Nothing.
### Fixed
- Nothing.
## 2.9.1 - 2018-05-14
### Added
- Nothing.
### Changed
- [#16](https://github.com/zendframework/zend-feed/pull/16) updates the `Zend\Feed\Pubsubhubbub\AbstractCallback` to no longer use the
`$GLOBALS['HTTP_RAW_POST_DATA']` value as a fallback when `php://input` is
empty. The fallback existed because, prior to PHP 5.6, `php://input` could
only be read once. As we now require PHP 5.6, the fallback is unnecessary,
and best removed as the globals value is deprecated.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#68](https://github.com/zendframework/zend-feed/pull/68) fixes the behavior of `Zend\Feed\Writer\AbstractFeed::setTitle()` and
`Zend\Feed\Writer\Entry::setTitle()` to accept the string `"0"`.
- [#68](https://github.com/zendframework/zend-feed/pull/68) updates both `Zend\Feed\Writer\AbstractFeed` and `Zend\Feed\Writer\Entry`
to no longer throw an exception for entry titles which have a string value of `0`.
## 2.9.0 - 2017-12-04
### Added
- [#52](https://github.com/zendframework/zend-feed/pull/52) adds support for PHP
7.2
- [#53](https://github.com/zendframework/zend-feed/pull/53) adds a number of
additional aliases to the `Writer\ExtensionPluginManager` to ensure plugins
will be pulled as expected.
- [#63](https://github.com/zendframework/zend-feed/pull/63) adds the feed title
to the attributes incorporated in the `FeedSet` instance, per what was already
documented.
- [#55](https://github.com/zendframework/zend-feed/pull/55) makes two API
additions to the `StandaloneExtensionManager` implementations of both the reader
and writer subcomponents:
- `$manager->add($name, $class)` will add an extension class using the
provided name.
- `$manager->remove($name)` will remove an existing extension by the provided
name.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- [#52](https://github.com/zendframework/zend-feed/pull/52) removes support for
HHVM.
### Fixed
- [#50](https://github.com/zendframework/zend-feed/pull/50) fixes a few issues
in the PubSubHubbub `Subscription` model where counting was being performed on
uncountable data; this ensures the subcomponent will work correctly under PHP
7.2.
## 2.8.0 - 2017-04-02
### Added
- [#27](https://github.com/zendframework/zend-feed/pull/27) adds a documentation
chapter demonstrating wrapping a PSR-7 client to use with `Zend\Feed\Reader`.
- [#22](https://github.com/zendframework/zend-feed/pull/22) adds missing
ExtensionManagerInterface on Writer\ExtensionPluginManager.
- [#32](https://github.com/zendframework/zend-feed/pull/32) adds missing
ExtensionManagerInterface on Reader\ExtensionPluginManager.
### Deprecated
- Nothing.
### Removed
- [#38](https://github.com/zendframework/zend-feed/pull/38) dropped php 5.5
support
### Fixed
- [#35](https://github.com/zendframework/zend-feed/pull/35) fixed
"A non-numeric value encountered" in php 7.1
- [#39](https://github.com/zendframework/zend-feed/pull/39) fixed protocol
relative link absolutisation
- [#40](https://github.com/zendframework/zend-feed/pull/40) fixed service
manager v3 compatibility aliases in extension plugin managers
## 2.7.0 - 2016-02-11
### Added
- [#21](https://github.com/zendframework/zend-feed/pull/21) edits, revises, and
prepares the documentation for publication at https://zendframework.github.io/zend-feed/
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#20](https://github.com/zendframework/zend-feed/pull/20) makes the two
zend-servicemanager extension manager implementations forwards compatible
with version 3, and the overall code base forwards compatible with zend-stdlib
v3.
## 2.6.0 - 2015-11-24
### Added
- [#13](https://github.com/zendframework/zend-feed/pull/13) introduces
`Zend\Feed\Writer\StandaloneExtensionManager`, an implementation of
`Zend\Feed\Writer\ExtensionManagerInterface` that has no dependencies.
`Zend\Feed\Writer\ExtensionManager` now composes this by default, instead of
`Zend\Feed\Writer\ExtensionPluginManager`, for managing the various feed and
entry extensions. If you relied on `ExtensionPluginManager` previously, you
will need to create an instance manually and inject it into the `Writer`
instance.
- [#14](https://github.com/zendframework/zend-feed/pull/14) introduces:
- `Zend\Feed\Reader\Http\HeaderAwareClientInterface`, which extends
`ClientInterface` and adds an optional argument to the `get()` method,
`array $headers = []`; this argument allows specifying request headers for
the client to send. `$headers` should have header names for keys, and the
values should be arrays of strings/numbers representing the header values
(if only a single value is necessary, it should be represented as an single
value array).
- `Zend\Feed\Reader\Http\HeaderAwareResponseInterface`, which extends
`ResponseInterface` and adds the method `getHeader($name, $default = null)`.
Clients may return either a `ResponseInterface` or
`HeaderAwareResponseInterface` instance.
- `Zend\Feed\Reader\Http\Response`, which is an implementation of
`HeaderAwareResponseInterface`. Its constructor accepts the status code,
body, and, optionally, headers.
- `Zend\Feed\Reader\Http\Psr7ResponseDecorator`, which is an implementation of
`HeaderAwareResponseInterface`. Its constructor accepts a PSR-7 response
instance, and the various methdos then proxy to those methods. This should
make creating wrappers for PSR-7 HTTP clients trivial.
- `Zend\Feed\Reader\Http\ZendHttpClientDecorator`, which decorates a
`Zend\Http\Client` instance, implements `HeaderAwareClientInterface`, and
returns a `Response` instance seeded from the zend-http response upon
calling `get()`. The class exposes a `getDecoratedClient()` method to allow
retrieval of the decorated zend-http client instance.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#5](https://github.com/zendframework/zend-feed/pull/5) fixes the enclosure
length check to allow zero and integer strings.
- [#2](https://github.com/zendframework/zend-feed/pull/2) ensures that the
routine for "absolutising" a link in `Reader\FeedSet` always generates a URI
with a scheme.
- [#14](https://github.com/zendframework/zend-feed/pull/14) makes the following
changes to fix behavior around HTTP clients used within
`Zend\Feed\Reader\Reader`:
- `setHttpClient()` now ensures that the passed client is either a
`Zend\Feed\Reader\Http\ClientInterface` or `Zend\Http\Client`, raising an
`InvalidArgumentException` if neither. If a `Zend\Http\Client` is passed, it
is passed to the constructor of `Zend\Feed\Reader\Http\ZendHttpClientDecorator`,
and the decorator instance is used.
- `getHttpClient()` now *always* returns a `Zend\Feed\Reader\Http\ClientInterface`
instance. If no instance is currently registered, it lazy loads a
`ZendHttpClientDecorator` instance.
- `import()` was updated to consume a `ClientInterface` instance; when caches
are in play, it checks the client against `HeaderAwareClientInterface` to
determine if it can check for HTTP caching headers, and, if so, to retrieve
them.
- `findFeedLinks()` was updated to consume a `ClientInterface`.

View file

@ -0,0 +1,27 @@
Copyright (c) 2005-2017, Zend Technologies USA, Inc.
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 of Zend Technologies USA, Inc. 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.

View file

@ -0,0 +1,12 @@
# zend-feed
[![Build Status](https://secure.travis-ci.org/zendframework/zend-feed.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-feed)
[![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-feed/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-feed?branch=master)
`Zend\Feed` provides functionality for consuming RSS and Atom feeds. It provides
a natural syntax for accessing elements of feeds, feed attributes, and entry
attributes. `Zend\Feed` also has extensive support for modifying feed and entry
structure with the same natural syntax, and turning the result back into XML.
- File issues at https://github.com/zendframework/zend-feed/issues
- Documentation is at https://docs.zendframework.com/zend-feed/

View file

@ -0,0 +1,70 @@
{
"name": "zendframework/zend-feed",
"description": "provides functionality for consuming RSS and Atom feeds",
"license": "BSD-3-Clause",
"keywords": [
"zf",
"zendframework",
"feed"
],
"support": {
"docs": "https://docs.zendframework.com/zend-feed/",
"issues": "https://github.com/zendframework/zend-feed/issues",
"source": "https://github.com/zendframework/zend-feed",
"rss": "https://github.com/zendframework/zend-feed/releases.atom",
"slack": "https://zendframework-slack.herokuapp.com",
"forum": "https://discourse.zendframework.com/c/questions/components"
},
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-escaper": "^2.5.2",
"zendframework/zend-stdlib": "^2.7.7 || ^3.1"
},
"require-dev": {
"phpunit/phpunit": "^5.7.23 || ^6.4.3",
"psr/http-message": "^1.0.1",
"zendframework/zend-cache": "^2.7.2",
"zendframework/zend-coding-standard": "~1.0.0",
"zendframework/zend-db": "^2.8.2",
"zendframework/zend-http": "^2.7",
"zendframework/zend-servicemanager": "^2.7.8 || ^3.3",
"zendframework/zend-validator": "^2.10.1"
},
"suggest": {
"psr/http-message": "PSR-7 ^1.0.1, if you wish to use Zend\\Feed\\Reader\\Http\\Psr7ResponseDecorator",
"zendframework/zend-cache": "Zend\\Cache component, for optionally caching feeds between requests",
"zendframework/zend-db": "Zend\\Db component, for use with PubSubHubbub",
"zendframework/zend-http": "Zend\\Http for PubSubHubbub, and optionally for use with Zend\\Feed\\Reader",
"zendframework/zend-servicemanager": "Zend\\ServiceManager component, for easily extending ExtensionManager implementations",
"zendframework/zend-validator": "Zend\\Validator component, for validating email addresses used in Atom feeds and entries when using the Writer subcomponent"
},
"autoload": {
"psr-4": {
"Zend\\Feed\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ZendTest\\Feed\\": "test/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.10.x-dev",
"dev-develop": "2.11.x-dev"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Exception;
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}

View file

@ -0,0 +1,14 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Exception;
interface ExceptionInterface
{
}

View file

@ -0,0 +1,14 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,14 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,344 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
use Traversable;
use Zend\Http\PhpEnvironment\Response as PhpResponse;
use Zend\Stdlib\ArrayUtils;
abstract class AbstractCallback implements CallbackInterface
{
/**
* An instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistenceInterface
* used to background save any verification tokens associated with a subscription
* or other.
*
* @var Model\SubscriptionPersistenceInterface
*/
protected $storage = null;
/**
* An instance of a class handling Http Responses. This is implemented in
* Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Zend\Controller\Response\Http.
*
* @var HttpResponse|PhpResponse
*/
protected $httpResponse = null;
/**
* The input stream to use when retrieving the request body. Defaults to
* php://input, but can be set to another value in order to force usage
* of another input method. This should primarily be used for testing
* purposes.
*
* @var string|resource String indicates a filename or stream to open;
* resource indicates an already created stream to use.
*/
protected $inputStream = 'php://input';
/**
* The number of Subscribers for which any updates are on behalf of.
*
* @var int
*/
protected $subscriberCount = 1;
/**
* Constructor; accepts an array or Traversable object to preset
* options for the Subscriber without calling all supported setter
* methods in turn.
*
* @param array|Traversable $options Options array or Traversable object
*/
public function __construct($options = null)
{
if ($options !== null) {
$this->setOptions($options);
}
}
/**
* Process any injected configuration options
*
* @param array|Traversable $options Options array or Traversable object
* @return AbstractCallback
* @throws Exception\InvalidArgumentException
*/
public function setOptions($options)
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}
if (! is_array($options)) {
throw new Exception\InvalidArgumentException('Array or Traversable object'
. 'expected, got ' . gettype($options));
}
if (is_array($options)) {
$this->setOptions($options);
}
if (array_key_exists('storage', $options)) {
$this->setStorage($options['storage']);
}
return $this;
}
/**
* Send the response, including all headers.
* If you wish to handle this via Zend\Http, use the getter methods
* to retrieve any data needed to be set on your HTTP Response object, or
* simply give this object the HTTP Response instance to work with for you!
*
* @return void
*/
public function sendResponse()
{
$this->getHttpResponse()->send();
}
/**
* Sets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used
* to background save any verification tokens associated with a subscription
* or other.
*
* @param Model\SubscriptionPersistenceInterface $storage
* @return AbstractCallback
*/
public function setStorage(Model\SubscriptionPersistenceInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* Gets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used
* to background save any verification tokens associated with a subscription
* or other.
*
* @return Model\SubscriptionPersistenceInterface
* @throws Exception\RuntimeException
*/
public function getStorage()
{
if ($this->storage === null) {
throw new Exception\RuntimeException('No storage object has been'
. ' set that subclasses Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence');
}
return $this->storage;
}
/**
* An instance of a class handling Http Responses. This is implemented in
* Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Zend\Controller\Response\Http.
*
* @param HttpResponse|PhpResponse $httpResponse
* @return AbstractCallback
* @throws Exception\InvalidArgumentException
*/
public function setHttpResponse($httpResponse)
{
if (! $httpResponse instanceof HttpResponse && ! $httpResponse instanceof PhpResponse) {
throw new Exception\InvalidArgumentException('HTTP Response object must'
. ' implement one of Zend\Feed\Pubsubhubbub\HttpResponse or'
. ' Zend\Http\PhpEnvironment\Response');
}
$this->httpResponse = $httpResponse;
return $this;
}
/**
* An instance of a class handling Http Responses. This is implemented in
* Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Zend\Controller\Response\Http.
*
* @return HttpResponse|PhpResponse
*/
public function getHttpResponse()
{
if ($this->httpResponse === null) {
$this->httpResponse = new HttpResponse;
}
return $this->httpResponse;
}
/**
* Sets the number of Subscribers for which any updates are on behalf of.
* In other words, is this class serving one or more subscribers? How many?
* Defaults to 1 if left unchanged.
*
* @param string|int $count
* @return AbstractCallback
* @throws Exception\InvalidArgumentException
*/
public function setSubscriberCount($count)
{
$count = intval($count);
if ($count <= 0) {
throw new Exception\InvalidArgumentException('Subscriber count must be'
. ' greater than zero');
}
$this->subscriberCount = $count;
return $this;
}
/**
* Gets the number of Subscribers for which any updates are on behalf of.
* In other words, is this class serving one or more subscribers? How many?
*
* @return int
*/
public function getSubscriberCount()
{
return $this->subscriberCount;
}
/**
* Attempt to detect the callback URL (specifically the path forward)
* @return string
*/
// @codingStandardsIgnoreStart
protected function _detectCallbackUrl()
{
// @codingStandardsIgnoreEnd
$callbackUrl = null;
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = isset($_SERVER['IIS_WasUrlRewritten']) ? $_SERVER['IIS_WasUrlRewritten'] : null;
$unencodedUrl = isset($_SERVER['UNENCODED_URL']) ? $_SERVER['UNENCODED_URL'] : null;
if ('1' == $iisUrlRewritten && ! empty($unencodedUrl)) {
return $unencodedUrl;
}
// HTTP proxy requests setup request URI with scheme and host [and port]
// + the URL path, only use URL path.
if (isset($_SERVER['REQUEST_URI'])) {
$callbackUrl = $this->buildCallbackUrlFromRequestUri();
}
if (null !== $callbackUrl) {
return $callbackUrl;
}
if (isset($_SERVER['ORIG_PATH_INFO'])) {
return $this->buildCallbackUrlFromOrigPathInfo();
}
return '';
}
/**
* Get the HTTP host
*
* @return string
*/
// @codingStandardsIgnoreStart
protected function _getHttpHost()
{
// @codingStandardsIgnoreEnd
if (! empty($_SERVER['HTTP_HOST'])) {
return $_SERVER['HTTP_HOST'];
}
$https = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : null;
$scheme = $https === 'on' ? 'https' : 'http';
$name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
$port = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80;
if (($scheme === 'http' && $port === 80)
|| ($scheme === 'https' && $port === 443)
) {
return $name;
}
return sprintf('%s:%d', $name, $port);
}
/**
* Retrieve a Header value from either $_SERVER or Apache
*
* @param string $header
* @return bool|string
*/
// @codingStandardsIgnoreStart
protected function _getHeader($header)
{
// @codingStandardsIgnoreEnd
$temp = strtoupper(str_replace('-', '_', $header));
if (! empty($_SERVER[$temp])) {
return $_SERVER[$temp];
}
$temp = 'HTTP_' . strtoupper(str_replace('-', '_', $header));
if (! empty($_SERVER[$temp])) {
return $_SERVER[$temp];
}
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (! empty($headers[$header])) {
return $headers[$header];
}
}
return false;
}
/**
* Return the raw body of the request
*
* @return string|false Raw body, or false if not present
*/
// @codingStandardsIgnoreStart
protected function _getRawBody()
{
// @codingStandardsIgnoreEnd
$body = is_resource($this->inputStream)
? stream_get_contents($this->inputStream)
: file_get_contents($this->inputStream);
return strlen(trim($body)) > 0 ? $body : false;
}
/**
* Build the callback URL from the REQUEST_URI server parameter.
*
* @return string
*/
private function buildCallbackUrlFromRequestUri()
{
$callbackUrl = $_SERVER['REQUEST_URI'];
$https = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : null;
$scheme = $https === 'on' ? 'https' : 'http';
if ($https === 'on') {
$scheme = 'https';
}
$schemeAndHttpHost = $scheme . '://' . $this->_getHttpHost();
if (strpos($callbackUrl, $schemeAndHttpHost) === 0) {
$callbackUrl = substr($callbackUrl, strlen($schemeAndHttpHost));
}
return $callbackUrl;
}
/**
* Build the callback URL from the ORIG_PATH_INFO server parameter.
*
* @return string
*/
private function buildCallbackUrlFromOrigPathInfo()
{
$callbackUrl = $_SERVER['ORIG_PATH_INFO'];
if (! empty($_SERVER['QUERY_STRING'])) {
$callbackUrl .= '?' . $_SERVER['QUERY_STRING'];
}
return $callbackUrl;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
interface CallbackInterface
{
/**
* Handle any callback from a Hub Server responding to a subscription or
* unsubscription request. This should be the Hub Server confirming the
* the request prior to taking action on it.
*
* @param array $httpData GET/POST data if available and not in $_GET/POST
* @param bool $sendResponseNow Whether to send response now or when asked
*/
public function handle(array $httpData = null, $sendResponseNow = false);
/**
* Send the response, including all headers.
* If you wish to handle this via Zend\Mvc\Controller, use the getter methods
* to retrieve any data needed to be set on your HTTP Response object, or
* simply give this object the HTTP Response instance to work with for you!
*
* @return void
*/
public function sendResponse();
/**
* An instance of a class handling Http Responses. This is implemented in
* Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Zend\Feed\Pubsubhubbub\AbstractCallback.
*
* @param HttpResponse|\Zend\Http\PhpEnvironment\Response $httpResponse
*/
public function setHttpResponse($httpResponse);
/**
* An instance of a class handling Http Responses. This is implemented in
* Zend\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Zend\Feed\Pubsubhubbub\AbstractCallback.
*
* @return HttpResponse|\Zend\Http\PhpEnvironment\Response
*/
public function getHttpResponse();
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Exception;
use Zend\Feed\Exception\ExceptionInterface as Exception;
interface ExceptionInterface extends Exception
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Exception;
use Zend\Feed\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Exception;
use Zend\Feed\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,215 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
class HttpResponse
{
/**
* The body of any response to the current callback request
*
* @var string
*/
protected $content = '';
/**
* Array of headers. Each header is an array with keys 'name' and 'value'
*
* @var array
*/
protected $headers = [];
/**
* HTTP response code to use in headers
*
* @var int
*/
protected $statusCode = 200;
/**
* Send the response, including all headers
*
* @return void
*/
public function send()
{
$this->sendHeaders();
echo $this->getContent();
}
/**
* Send all headers
*
* Sends any headers specified. If an {@link setHttpResponseCode() HTTP response code}
* has been specified, it is sent with the first header.
*
* @return void
*/
public function sendHeaders()
{
if (count($this->headers) || (200 != $this->statusCode)) {
$this->canSendHeaders(true);
} elseif (200 == $this->statusCode) {
return;
}
$httpCodeSent = false;
foreach ($this->headers as $header) {
if (! $httpCodeSent && $this->statusCode) {
header($header['name'] . ': ' . $header['value'], $header['replace'], $this->statusCode);
$httpCodeSent = true;
} else {
header($header['name'] . ': ' . $header['value'], $header['replace']);
}
}
if (! $httpCodeSent) {
header('HTTP/1.1 ' . $this->statusCode);
}
}
/**
* Set a header
*
* If $replace is true, replaces any headers already defined with that
* $name.
*
* @param string $name
* @param string $value
* @param bool $replace
* @return \Zend\Feed\PubSubHubbub\HttpResponse
*/
public function setHeader($name, $value, $replace = false)
{
$name = $this->_normalizeHeader($name);
$value = (string) $value;
if ($replace) {
foreach ($this->headers as $key => $header) {
if ($name == $header['name']) {
unset($this->headers[$key]);
}
}
}
$this->headers[] = [
'name' => $name,
'value' => $value,
'replace' => $replace,
];
return $this;
}
/**
* Check if a specific Header is set and return its value
*
* @param string $name
* @return string|null
*/
public function getHeader($name)
{
$name = $this->_normalizeHeader($name);
foreach ($this->headers as $header) {
if ($header['name'] == $name) {
return $header['value'];
}
}
}
/**
* Return array of headers; see {@link $headers} for format
*
* @return array
*/
public function getHeaders()
{
return $this->headers;
}
/**
* Can we send headers?
*
* @param bool $throw Whether or not to throw an exception if headers have been sent; defaults to false
* @return HttpResponse
* @throws Exception\RuntimeException
*/
public function canSendHeaders($throw = false)
{
$ok = headers_sent($file, $line);
if ($ok && $throw) {
throw new Exception\RuntimeException(
'Cannot send headers; headers already sent in ' . $file . ', line ' . $line
);
}
return ! $ok;
}
/**
* Set HTTP response code to use with headers
*
* @param int $code
* @return HttpResponse
* @throws Exception\InvalidArgumentException
*/
public function setStatusCode($code)
{
if (! is_int($code) || (100 > $code) || (599 < $code)) {
throw new Exception\InvalidArgumentException('Invalid HTTP response'
. ' code:' . $code);
}
$this->statusCode = $code;
return $this;
}
/**
* Retrieve HTTP response code
*
* @return int
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Set body content
*
* @param string $content
* @return \Zend\Feed\PubSubHubbub\HttpResponse
*/
public function setContent($content)
{
$this->content = (string) $content;
$this->setHeader('content-length', strlen($content));
return $this;
}
/**
* Return the body content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Normalizes a header name to X-Capitalized-Names
*
* @param string $name
* @return string
*/
// @codingStandardsIgnoreStart
protected function _normalizeHeader($name)
{
// @codingStandardsIgnoreEnd
$filtered = str_replace(['-', '_'], ' ', (string) $name);
$filtered = ucwords(strtolower($filtered));
$filtered = str_replace(' ', '-', $filtered);
return $filtered;
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Model;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\TableGateway\TableGatewayInterface;
class AbstractModel
{
/**
* Zend\Db\TableGateway\TableGatewayInterface instance to host database methods
*
* @var TableGatewayInterface
*/
protected $db = null;
/**
* Constructor
*
* @param null|TableGatewayInterface $tableGateway
*/
public function __construct(TableGatewayInterface $tableGateway = null)
{
if ($tableGateway === null) {
$parts = explode('\\', get_class($this));
$table = strtolower(array_pop($parts));
$this->db = new TableGateway($table, null);
} else {
$this->db = $tableGateway;
}
}
}

View file

@ -0,0 +1,142 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Model;
use DateInterval;
use DateTime;
use Zend\Feed\PubSubHubbub;
class Subscription extends AbstractModel implements SubscriptionPersistenceInterface
{
/**
* Common DateTime object to assist with unit testing
*
* @var DateTime
*/
protected $now;
/**
* Save subscription to RDMBS
*
* @param array $data
* @return bool
* @throws PubSubHubbub\Exception\InvalidArgumentException
*/
public function setSubscription(array $data)
{
if (! isset($data['id'])) {
throw new PubSubHubbub\Exception\InvalidArgumentException(
'ID must be set before attempting a save'
);
}
$result = $this->db->select(['id' => $data['id']]);
if ($result && (0 < count($result))) {
$data['created_time'] = $result->current()->created_time;
$now = $this->getNow();
if (array_key_exists('lease_seconds', $data)
&& $data['lease_seconds']
) {
$data['expiration_time'] = $now->add(new DateInterval('PT' . $data['lease_seconds'] . 'S'))
->format('Y-m-d H:i:s');
}
$this->db->update(
$data,
['id' => $data['id']]
);
return false;
}
$this->db->insert($data);
return true;
}
/**
* Get subscription by ID/key
*
* @param string $key
* @return array
* @throws PubSubHubbub\Exception\InvalidArgumentException
*/
public function getSubscription($key)
{
if (empty($key) || ! is_string($key)) {
throw new PubSubHubbub\Exception\InvalidArgumentException('Invalid parameter "key"'
.' of "' . $key . '" must be a non-empty string');
}
$result = $this->db->select(['id' => $key]);
if ($result && count($result)) {
return $result->current()->getArrayCopy();
}
return false;
}
/**
* Determine if a subscription matching the key exists
*
* @param string $key
* @return bool
* @throws PubSubHubbub\Exception\InvalidArgumentException
*/
public function hasSubscription($key)
{
if (empty($key) || ! is_string($key)) {
throw new PubSubHubbub\Exception\InvalidArgumentException('Invalid parameter "key"'
.' of "' . $key . '" must be a non-empty string');
}
$result = $this->db->select(['id' => $key]);
if ($result && count($result)) {
return true;
}
return false;
}
/**
* Delete a subscription
*
* @param string $key
* @return bool
*/
public function deleteSubscription($key)
{
$result = $this->db->select(['id' => $key]);
if ($result && count($result)) {
$this->db->delete(
['id' => $key]
);
return true;
}
return false;
}
/**
* Get a new DateTime or the one injected for testing
*
* @return DateTime
*/
public function getNow()
{
if (null === $this->now) {
return new DateTime();
}
return $this->now;
}
/**
* Set a DateTime instance for assisting with unit testing
*
* @param DateTime $now
* @return Subscription
*/
public function setNow(DateTime $now)
{
$this->now = $now;
return $this;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Model;
interface SubscriptionPersistenceInterface
{
/**
* Save subscription to RDMBS
*
* @param array $data The key must be stored here as a $data['id'] entry
* @return bool
*/
public function setSubscription(array $data);
/**
* Get subscription by ID/key
*
* @param string $key
* @return array
*/
public function getSubscription($key);
/**
* Determine if a subscription matching the key exists
*
* @param string $key
* @return bool
*/
public function hasSubscription($key);
/**
* Delete a subscription
*
* @param string $key
* @return bool
*/
public function deleteSubscription($key);
}

View file

@ -0,0 +1,147 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
use Zend\Escaper\Escaper;
use Zend\Feed\Reader;
use Zend\Http;
class PubSubHubbub
{
/**
* Verification Modes
*/
const VERIFICATION_MODE_SYNC = 'sync';
const VERIFICATION_MODE_ASYNC = 'async';
/**
* Subscription States
*/
const SUBSCRIPTION_VERIFIED = 'verified';
const SUBSCRIPTION_NOTVERIFIED = 'not_verified';
const SUBSCRIPTION_TODELETE = 'to_delete';
/**
* @var Escaper
*/
protected static $escaper;
/**
* Singleton instance if required of the HTTP client
*
* @var Http\Client
*/
protected static $httpClient = null;
/**
* Simple utility function which imports any feed URL and
* determines the existence of Hub Server endpoints. This works
* best if directly given an instance of Zend\Feed\Reader\Atom|Rss
* to leverage off.
*
* @param \Zend\Feed\Reader\Feed\AbstractFeed|string $source
* @return array
* @throws Exception\InvalidArgumentException
*/
public static function detectHubs($source)
{
if (is_string($source)) {
$feed = Reader\Reader::import($source);
} elseif ($source instanceof Reader\Feed\AbstractFeed) {
$feed = $source;
} else {
throw new Exception\InvalidArgumentException('The source parameter was'
. ' invalid, i.e. not a URL string or an instance of type'
. ' Zend\Feed\Reader\Feed\AbstractFeed');
}
return $feed->getHubs();
}
/**
* Allows the external environment to make ZendOAuth use a specific
* Client instance.
*
* @param Http\Client $httpClient
* @return void
*/
public static function setHttpClient(Http\Client $httpClient)
{
static::$httpClient = $httpClient;
}
/**
* Return the singleton instance of the HTTP Client. Note that
* the instance is reset and cleared of previous parameters GET/POST.
* Headers are NOT reset but handled by this component if applicable.
*
* @return Http\Client
*/
public static function getHttpClient()
{
if (! isset(static::$httpClient)) {
static::$httpClient = new Http\Client;
} else {
static::$httpClient->resetParameters();
}
return static::$httpClient;
}
/**
* Simple mechanism to delete the entire singleton HTTP Client instance
* which forces a new instantiation for subsequent requests.
*
* @return void
*/
public static function clearHttpClient()
{
static::$httpClient = null;
}
/**
* Set the Escaper instance
*
* If null, resets the instance
*
* @param null|Escaper $escaper
*/
public static function setEscaper(Escaper $escaper = null)
{
static::$escaper = $escaper;
}
/**
* Get the Escaper instance
*
* If none registered, lazy-loads an instance.
*
* @return Escaper
*/
public static function getEscaper()
{
if (null === static::$escaper) {
static::setEscaper(new Escaper());
}
return static::$escaper;
}
/**
* RFC 3986 safe url encoding method
*
* @param string $string
* @return string
*/
public static function urlencode($string)
{
$escaper = static::getEscaper();
$rawencoded = $escaper->escapeUrl($string);
$rfcencoded = str_replace('%7E', '~', $rawencoded);
return $rfcencoded;
}
}

View file

@ -0,0 +1,399 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
use Traversable;
use Zend\Feed\Uri;
use Zend\Http\Request as HttpRequest;
use Zend\Stdlib\ArrayUtils;
class Publisher
{
/**
* An array of URLs for all Hub Servers used by the Publisher, and to
* which all topic update notifications will be sent.
*
* @var array
*/
protected $hubUrls = [];
/**
* An array of topic (Atom or RSS feed) URLs which have been updated and
* whose updated status will be notified to all Hub Servers.
*
* @var array
*/
protected $updatedTopicUrls = [];
/**
* An array of any errors including keys for 'response', 'hubUrl'.
* The response is the actual Zend\Http\Response object.
*
* @var array
*/
protected $errors = [];
/**
* An array of topic (Atom or RSS feed) URLs which have been updated and
* whose updated status will be notified to all Hub Servers.
*
* @var array
*/
protected $parameters = [];
/**
* Constructor; accepts an array or Zend\Config\Config instance to preset
* options for the Publisher without calling all supported setter
* methods in turn.
*
* @param array|Traversable $options
*/
public function __construct($options = null)
{
if ($options !== null) {
$this->setOptions($options);
}
}
/**
* Process any injected configuration options
*
* @param array|Traversable $options Options array or Traversable object
* @return Publisher
* @throws Exception\InvalidArgumentException
*/
public function setOptions($options)
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}
if (! is_array($options)) {
throw new Exception\InvalidArgumentException('Array or Traversable object'
. 'expected, got ' . gettype($options));
}
if (array_key_exists('hubUrls', $options)) {
$this->addHubUrls($options['hubUrls']);
}
if (array_key_exists('updatedTopicUrls', $options)) {
$this->addUpdatedTopicUrls($options['updatedTopicUrls']);
}
if (array_key_exists('parameters', $options)) {
$this->setParameters($options['parameters']);
}
return $this;
}
/**
* Add a Hub Server URL supported by Publisher
*
* @param string $url
* @return Publisher
* @throws Exception\InvalidArgumentException
*/
public function addHubUrl($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. 'URL');
}
$this->hubUrls[] = $url;
return $this;
}
/**
* Add an array of Hub Server URLs supported by Publisher
*
* @param array $urls
* @return Publisher
*/
public function addHubUrls(array $urls)
{
foreach ($urls as $url) {
$this->addHubUrl($url);
}
return $this;
}
/**
* Remove a Hub Server URL
*
* @param string $url
* @return Publisher
*/
public function removeHubUrl($url)
{
if (! in_array($url, $this->getHubUrls())) {
return $this;
}
$key = array_search($url, $this->hubUrls);
unset($this->hubUrls[$key]);
return $this;
}
/**
* Return an array of unique Hub Server URLs currently available
*
* @return array
*/
public function getHubUrls()
{
$this->hubUrls = array_unique($this->hubUrls);
return $this->hubUrls;
}
/**
* Add a URL to a topic (Atom or RSS feed) which has been updated
*
* @param string $url
* @return Publisher
* @throws Exception\InvalidArgumentException
*/
public function addUpdatedTopicUrl($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. 'URL');
}
$this->updatedTopicUrls[] = $url;
return $this;
}
/**
* Add an array of Topic URLs which have been updated
*
* @param array $urls
* @return Publisher
*/
public function addUpdatedTopicUrls(array $urls)
{
foreach ($urls as $url) {
$this->addUpdatedTopicUrl($url);
}
return $this;
}
/**
* Remove an updated topic URL
*
* @param string $url
* @return Publisher
*/
public function removeUpdatedTopicUrl($url)
{
if (! in_array($url, $this->getUpdatedTopicUrls())) {
return $this;
}
$key = array_search($url, $this->updatedTopicUrls);
unset($this->updatedTopicUrls[$key]);
return $this;
}
/**
* Return an array of unique updated topic URLs currently available
*
* @return array
*/
public function getUpdatedTopicUrls()
{
$this->updatedTopicUrls = array_unique($this->updatedTopicUrls);
return $this->updatedTopicUrls;
}
/**
* Notifies a single Hub Server URL of changes
*
* @param string $url The Hub Server's URL
* @return void
* @throws Exception\InvalidArgumentException
* @throws Exception\RuntimeException
*/
public function notifyHub($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. 'URL');
}
$client = $this->_getHttpClient();
$client->setUri($url);
$response = $client->getResponse();
if ($response->getStatusCode() !== 204) {
throw new Exception\RuntimeException('Notification to Hub Server '
. 'at "' . $url . '" appears to have failed with a status code of "'
. $response->getStatusCode() . '" and message "'
. $response->getContent() . '"');
}
}
/**
* Notifies all Hub Server URLs of changes
*
* If a Hub notification fails, certain data will be retained in an
* an array retrieved using getErrors(), if a failure occurs for any Hubs
* the isSuccess() check will return FALSE. This method is designed not
* to needlessly fail with an Exception/Error unless from Zend\Http\Client.
*
* @return void
* @throws Exception\RuntimeException
*/
public function notifyAll()
{
$client = $this->_getHttpClient();
$hubs = $this->getHubUrls();
if (empty($hubs)) {
throw new Exception\RuntimeException('No Hub Server URLs'
. ' have been set so no notifications can be sent');
}
$this->errors = [];
foreach ($hubs as $url) {
$client->setUri($url);
$response = $client->getResponse();
if ($response->getStatusCode() !== 204) {
$this->errors[] = [
'response' => $response,
'hubUrl' => $url
];
}
}
}
/**
* Add an optional parameter to the update notification requests
*
* @param string $name
* @param string|null $value
* @return Publisher
* @throws Exception\InvalidArgumentException
*/
public function setParameter($name, $value = null)
{
if (is_array($name)) {
$this->setParameters($name);
return $this;
}
if (empty($name) || ! is_string($name)) {
throw new Exception\InvalidArgumentException('Invalid parameter "name"'
. ' of "' . $name . '" must be a non-empty string');
}
if ($value === null) {
$this->removeParameter($name);
return $this;
}
if (empty($value) || (! is_string($value) && $value !== null)) {
throw new Exception\InvalidArgumentException('Invalid parameter "value"'
. ' of "' . $value . '" must be a non-empty string');
}
$this->parameters[$name] = $value;
return $this;
}
/**
* Add an optional parameter to the update notification requests
*
* @param array $parameters
* @return Publisher
*/
public function setParameters(array $parameters)
{
foreach ($parameters as $name => $value) {
$this->setParameter($name, $value);
}
return $this;
}
/**
* Remove an optional parameter for the notification requests
*
* @param string $name
* @return Publisher
* @throws Exception\InvalidArgumentException
*/
public function removeParameter($name)
{
if (empty($name) || ! is_string($name)) {
throw new Exception\InvalidArgumentException('Invalid parameter "name"'
. ' of "' . $name . '" must be a non-empty string');
}
if (array_key_exists($name, $this->parameters)) {
unset($this->parameters[$name]);
}
return $this;
}
/**
* Return an array of optional parameters for notification requests
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Returns a boolean indicator of whether the notifications to Hub
* Servers were ALL successful. If even one failed, FALSE is returned.
*
* @return bool
*/
public function isSuccess()
{
return ! (count($this->errors) != 0);
}
/**
* Return an array of errors met from any failures, including keys:
* 'response' => the Zend\Http\Response object from the failure
* 'hubUrl' => the URL of the Hub Server whose notification failed
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Get a basic prepared HTTP client for use
*
* @return \Zend\Http\Client
* @throws Exception\RuntimeException
*/
// @codingStandardsIgnoreStart
protected function _getHttpClient()
{
// @codingStandardsIgnoreEnd
$client = PubSubHubbub::getHttpClient();
$client->setMethod(HttpRequest::METHOD_POST);
$client->setOptions([
'useragent' => 'Zend_Feed_Pubsubhubbub_Publisher/' . Version::VERSION,
]);
$params = [];
$params[] = 'hub.mode=publish';
$topics = $this->getUpdatedTopicUrls();
if (empty($topics)) {
throw new Exception\RuntimeException('No updated topic URLs'
. ' have been set');
}
foreach ($topics as $topicUrl) {
$params[] = 'hub.url=' . urlencode($topicUrl);
}
$optParams = $this->getParameters();
foreach ($optParams as $name => $value) {
$params[] = urlencode($name) . '=' . urlencode($value);
}
$paramString = implode('&', $params);
$client->setRawBody($paramString);
return $client;
}
}

View file

@ -0,0 +1,853 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
use DateInterval;
use DateTime;
use Traversable;
use Zend\Feed\Uri;
use Zend\Http\Request as HttpRequest;
use Zend\Stdlib\ArrayUtils;
class Subscriber
{
/**
* An array of URLs for all Hub Servers to subscribe/unsubscribe.
*
* @var array
*/
protected $hubUrls = [];
/**
* An array of optional parameters to be included in any
* (un)subscribe requests.
*
* @var array
*/
protected $parameters = [];
/**
* The URL of the topic (Rss or Atom feed) which is the subject of
* our current intent to subscribe to/unsubscribe from updates from
* the currently configured Hub Servers.
*
* @var string
*/
protected $topicUrl = '';
/**
* The URL Hub Servers must use when communicating with this Subscriber
*
* @var string
*/
protected $callbackUrl = '';
/**
* The number of seconds for which the subscriber would like to have the
* subscription active. Defaults to null, i.e. not sent, to setup a
* permanent subscription if possible.
*
* @var int
*/
protected $leaseSeconds = null;
/**
* The preferred verification mode (sync or async). By default, this
* Subscriber prefers synchronous verification, but is considered
* desirable to support asynchronous verification if possible.
*
* Zend\Feed\Pubsubhubbub\Subscriber will always send both modes, whose
* order of occurrence in the parameter list determines this preference.
*
* @var string
*/
protected $preferredVerificationMode = PubSubHubbub::VERIFICATION_MODE_SYNC;
/**
* An array of any errors including keys for 'response', 'hubUrl'.
* The response is the actual Zend\Http\Response object.
*
* @var array
*/
protected $errors = [];
/**
* An array of Hub Server URLs for Hubs operating at this time in
* asynchronous verification mode.
*
* @var array
*/
protected $asyncHubs = [];
/**
* An instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
* save any verification tokens associated with a subscription or other.
*
* @var \Zend\Feed\PubSubHubbub\Model\SubscriptionPersistenceInterface
*/
protected $storage = null;
/**
* An array of authentication credentials for HTTP Basic Authentication
* if required by specific Hubs. The array is indexed by Hub Endpoint URI
* and the value is a simple array of the username and password to apply.
*
* @var array
*/
protected $authentications = [];
/**
* Tells the Subscriber to append any subscription identifier to the path
* of the base Callback URL. E.g. an identifier "subkey1" would be added
* to the callback URL "http://www.example.com/callback" to create a subscription
* specific Callback URL of "http://www.example.com/callback/subkey1".
*
* This is required for all Hubs using the Pubsubhubbub 0.1 Specification.
* It should be manually intercepted and passed to the Callback class using
* Zend\Feed\Pubsubhubbub\Subscriber\Callback::setSubscriptionKey(). Will
* require a route in the form "callback/:subkey" to allow the parameter be
* retrieved from an action using the Zend\Controller\Action::\getParam()
* method.
*
* @var string
*/
protected $usePathParameter = false;
/**
* Constructor; accepts an array or Traversable instance to preset
* options for the Subscriber without calling all supported setter
* methods in turn.
*
* @param array|Traversable $options
*/
public function __construct($options = null)
{
if ($options !== null) {
$this->setOptions($options);
}
}
/**
* Process any injected configuration options
*
* @param array|Traversable $options
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setOptions($options)
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}
if (! is_array($options)) {
throw new Exception\InvalidArgumentException('Array or Traversable object'
. 'expected, got ' . gettype($options));
}
if (array_key_exists('hubUrls', $options)) {
$this->addHubUrls($options['hubUrls']);
}
if (array_key_exists('callbackUrl', $options)) {
$this->setCallbackUrl($options['callbackUrl']);
}
if (array_key_exists('topicUrl', $options)) {
$this->setTopicUrl($options['topicUrl']);
}
if (array_key_exists('storage', $options)) {
$this->setStorage($options['storage']);
}
if (array_key_exists('leaseSeconds', $options)) {
$this->setLeaseSeconds($options['leaseSeconds']);
}
if (array_key_exists('parameters', $options)) {
$this->setParameters($options['parameters']);
}
if (array_key_exists('authentications', $options)) {
$this->addAuthentications($options['authentications']);
}
if (array_key_exists('usePathParameter', $options)) {
$this->usePathParameter($options['usePathParameter']);
}
if (array_key_exists('preferredVerificationMode', $options)) {
$this->setPreferredVerificationMode(
$options['preferredVerificationMode']
);
}
return $this;
}
/**
* Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
* event will relate
*
* @param string $url
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setTopicUrl($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
.' of "' . $url . '" must be a non-empty string and a valid'
.' URL');
}
$this->topicUrl = $url;
return $this;
}
/**
* Set the topic URL (RSS or Atom feed) to which the intended (un)subscribe
* event will relate
*
* @return string
* @throws Exception\RuntimeException
*/
public function getTopicUrl()
{
if (empty($this->topicUrl)) {
throw new Exception\RuntimeException('A valid Topic (RSS or Atom'
. ' feed) URL MUST be set before attempting any operation');
}
return $this->topicUrl;
}
/**
* Set the number of seconds for which any subscription will remain valid
*
* @param int $seconds
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setLeaseSeconds($seconds)
{
$seconds = intval($seconds);
if ($seconds <= 0) {
throw new Exception\InvalidArgumentException('Expected lease seconds'
. ' must be an integer greater than zero');
}
$this->leaseSeconds = $seconds;
return $this;
}
/**
* Get the number of lease seconds on subscriptions
*
* @return int
*/
public function getLeaseSeconds()
{
return $this->leaseSeconds;
}
/**
* Set the callback URL to be used by Hub Servers when communicating with
* this Subscriber
*
* @param string $url
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setCallbackUrl($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. ' URL');
}
$this->callbackUrl = $url;
return $this;
}
/**
* Get the callback URL to be used by Hub Servers when communicating with
* this Subscriber
*
* @return string
* @throws Exception\RuntimeException
*/
public function getCallbackUrl()
{
if (empty($this->callbackUrl)) {
throw new Exception\RuntimeException('A valid Callback URL MUST be'
. ' set before attempting any operation');
}
return $this->callbackUrl;
}
/**
* Set preferred verification mode (sync or async). By default, this
* Subscriber prefers synchronous verification, but does support
* asynchronous if that's the Hub Server's utilised mode.
*
* Zend\Feed\Pubsubhubbub\Subscriber will always send both modes, whose
* order of occurrence in the parameter list determines this preference.
*
* @param string $mode Should be 'sync' or 'async'
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setPreferredVerificationMode($mode)
{
if ($mode !== PubSubHubbub::VERIFICATION_MODE_SYNC
&& $mode !== PubSubHubbub::VERIFICATION_MODE_ASYNC
) {
throw new Exception\InvalidArgumentException('Invalid preferred'
. ' mode specified: "' . $mode . '" but should be one of'
. ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_SYNC or'
. ' Zend\Feed\Pubsubhubbub::VERIFICATION_MODE_ASYNC');
}
$this->preferredVerificationMode = $mode;
return $this;
}
/**
* Get preferred verification mode (sync or async).
*
* @return string
*/
public function getPreferredVerificationMode()
{
return $this->preferredVerificationMode;
}
/**
* Add a Hub Server URL supported by Publisher
*
* @param string $url
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function addHubUrl($url)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. ' URL');
}
$this->hubUrls[] = $url;
return $this;
}
/**
* Add an array of Hub Server URLs supported by Publisher
*
* @param array $urls
* @return Subscriber
*/
public function addHubUrls(array $urls)
{
foreach ($urls as $url) {
$this->addHubUrl($url);
}
return $this;
}
/**
* Remove a Hub Server URL
*
* @param string $url
* @return Subscriber
*/
public function removeHubUrl($url)
{
if (! in_array($url, $this->getHubUrls())) {
return $this;
}
$key = array_search($url, $this->hubUrls);
unset($this->hubUrls[$key]);
return $this;
}
/**
* Return an array of unique Hub Server URLs currently available
*
* @return array
*/
public function getHubUrls()
{
$this->hubUrls = array_unique($this->hubUrls);
return $this->hubUrls;
}
/**
* Add authentication credentials for a given URL
*
* @param string $url
* @param array $authentication
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function addAuthentication($url, array $authentication)
{
if (empty($url) || ! is_string($url) || ! Uri::factory($url)->isValid()) {
throw new Exception\InvalidArgumentException('Invalid parameter "url"'
. ' of "' . $url . '" must be a non-empty string and a valid'
. ' URL');
}
$this->authentications[$url] = $authentication;
return $this;
}
/**
* Add authentication credentials for hub URLs
*
* @param array $authentications
* @return Subscriber
*/
public function addAuthentications(array $authentications)
{
foreach ($authentications as $url => $authentication) {
$this->addAuthentication($url, $authentication);
}
return $this;
}
/**
* Get all hub URL authentication credentials
*
* @return array
*/
public function getAuthentications()
{
return $this->authentications;
}
/**
* Set flag indicating whether or not to use a path parameter
*
* @param bool $bool
* @return Subscriber
*/
public function usePathParameter($bool = true)
{
$this->usePathParameter = $bool;
return $this;
}
/**
* Add an optional parameter to the (un)subscribe requests
*
* @param string $name
* @param string|null $value
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function setParameter($name, $value = null)
{
if (is_array($name)) {
$this->setParameters($name);
return $this;
}
if (empty($name) || ! is_string($name)) {
throw new Exception\InvalidArgumentException('Invalid parameter "name"'
. ' of "' . $name . '" must be a non-empty string');
}
if ($value === null) {
$this->removeParameter($name);
return $this;
}
if (empty($value) || (! is_string($value) && $value !== null)) {
throw new Exception\InvalidArgumentException('Invalid parameter "value"'
. ' of "' . $value . '" must be a non-empty string');
}
$this->parameters[$name] = $value;
return $this;
}
/**
* Add an optional parameter to the (un)subscribe requests
*
* @param array $parameters
* @return Subscriber
*/
public function setParameters(array $parameters)
{
foreach ($parameters as $name => $value) {
$this->setParameter($name, $value);
}
return $this;
}
/**
* Remove an optional parameter for the (un)subscribe requests
*
* @param string $name
* @return Subscriber
* @throws Exception\InvalidArgumentException
*/
public function removeParameter($name)
{
if (empty($name) || ! is_string($name)) {
throw new Exception\InvalidArgumentException('Invalid parameter "name"'
. ' of "' . $name . '" must be a non-empty string');
}
if (array_key_exists($name, $this->parameters)) {
unset($this->parameters[$name]);
}
return $this;
}
/**
* Return an array of optional parameters for (un)subscribe requests
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Sets an instance of Zend\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
* save any verification tokens associated with a subscription or other.
*
* @param Model\SubscriptionPersistenceInterface $storage
* @return Subscriber
*/
public function setStorage(Model\SubscriptionPersistenceInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* Gets an instance of Zend\Feed\Pubsubhubbub\Storage\StoragePersistence used
* to background save any verification tokens associated with a subscription
* or other.
*
* @return Model\SubscriptionPersistenceInterface
* @throws Exception\RuntimeException
*/
public function getStorage()
{
if ($this->storage === null) {
throw new Exception\RuntimeException('No storage vehicle '
. 'has been set.');
}
return $this->storage;
}
/**
* Subscribe to one or more Hub Servers using the stored Hub URLs
* for the given Topic URL (RSS or Atom feed)
*
* @return void
*/
public function subscribeAll()
{
$this->_doRequest('subscribe');
}
/**
* Unsubscribe from one or more Hub Servers using the stored Hub URLs
* for the given Topic URL (RSS or Atom feed)
*
* @return void
*/
public function unsubscribeAll()
{
$this->_doRequest('unsubscribe');
}
/**
* Returns a boolean indicator of whether the notifications to Hub
* Servers were ALL successful. If even one failed, FALSE is returned.
*
* @return bool
*/
public function isSuccess()
{
if (count($this->errors) > 0) {
return false;
}
return true;
}
/**
* Return an array of errors met from any failures, including keys:
* 'response' => the Zend\Http\Response object from the failure
* 'hubUrl' => the URL of the Hub Server whose notification failed
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Return an array of Hub Server URLs who returned a response indicating
* operation in Asynchronous Verification Mode, i.e. they will not confirm
* any (un)subscription immediately but at a later time (Hubs may be
* doing this as a batch process when load balancing)
*
* @return array
*/
public function getAsyncHubs()
{
return $this->asyncHubs;
}
/**
* Executes an (un)subscribe request
*
* @param string $mode
* @return void
* @throws Exception\RuntimeException
*/
// @codingStandardsIgnoreStart
protected function _doRequest($mode)
{
// @codingStandardsIgnoreEnd
$client = $this->_getHttpClient();
$hubs = $this->getHubUrls();
if (empty($hubs)) {
throw new Exception\RuntimeException('No Hub Server URLs'
. ' have been set so no subscriptions can be attempted');
}
$this->errors = [];
$this->asyncHubs = [];
foreach ($hubs as $url) {
if (array_key_exists($url, $this->authentications)) {
$auth = $this->authentications[$url];
$client->setAuth($auth[0], $auth[1]);
}
$client->setUri($url);
$client->setRawBody($params = $this->_getRequestParameters($url, $mode));
$response = $client->send();
if ($response->getStatusCode() !== 204
&& $response->getStatusCode() !== 202
) {
$this->errors[] = [
'response' => $response,
'hubUrl' => $url,
];
/**
* At first I thought it was needed, but the backend storage will
* allow tracking async without any user interference. It's left
* here in case the user is interested in knowing what Hubs
* are using async verification modes so they may update Models and
* move these to asynchronous processes.
*/
} elseif ($response->getStatusCode() == 202) {
$this->asyncHubs[] = [
'response' => $response,
'hubUrl' => $url,
];
}
}
}
/**
* Get a basic prepared HTTP client for use
*
* @return \Zend\Http\Client
*/
// @codingStandardsIgnoreStart
protected function _getHttpClient()
{
// @codingStandardsIgnoreEnd
$client = PubSubHubbub::getHttpClient();
$client->setMethod(HttpRequest::METHOD_POST);
$client->setOptions(['useragent' => 'Zend_Feed_Pubsubhubbub_Subscriber/'
. Version::VERSION]);
return $client;
}
/**
* Return a list of standard protocol/optional parameters for addition to
* client's POST body that are specific to the current Hub Server URL
*
* @param string $hubUrl
* @param string $mode
* @return string
* @throws Exception\InvalidArgumentException
*/
// @codingStandardsIgnoreStart
protected function _getRequestParameters($hubUrl, $mode)
{
// @codingStandardsIgnoreEnd
if (! in_array($mode, ['subscribe', 'unsubscribe'])) {
throw new Exception\InvalidArgumentException('Invalid mode specified: "'
. $mode . '" which should have been "subscribe" or "unsubscribe"');
}
$params = [
'hub.mode' => $mode,
'hub.topic' => $this->getTopicUrl(),
];
if ($this->getPreferredVerificationMode()
== PubSubHubbub::VERIFICATION_MODE_SYNC
) {
$vmodes = [
PubSubHubbub::VERIFICATION_MODE_SYNC,
PubSubHubbub::VERIFICATION_MODE_ASYNC,
];
} else {
$vmodes = [
PubSubHubbub::VERIFICATION_MODE_ASYNC,
PubSubHubbub::VERIFICATION_MODE_SYNC,
];
}
$params['hub.verify'] = [];
foreach ($vmodes as $vmode) {
$params['hub.verify'][] = $vmode;
}
/**
* Establish a persistent verify_token and attach key to callback
* URL's path/query_string
*/
$key = $this->_generateSubscriptionKey($params, $hubUrl);
$token = $this->_generateVerifyToken();
$params['hub.verify_token'] = $token;
// Note: query string only usable with PuSH 0.2 Hubs
if (! $this->usePathParameter) {
$params['hub.callback'] = $this->getCallbackUrl()
. '?xhub.subscription=' . PubSubHubbub::urlencode($key);
} else {
$params['hub.callback'] = rtrim($this->getCallbackUrl(), '/')
. '/' . PubSubHubbub::urlencode($key);
}
if ($mode == 'subscribe' && $this->getLeaseSeconds() !== null) {
$params['hub.lease_seconds'] = $this->getLeaseSeconds();
}
// hub.secret not currently supported
$optParams = $this->getParameters();
foreach ($optParams as $name => $value) {
$params[$name] = $value;
}
// store subscription to storage
$now = new DateTime();
$expires = null;
if (isset($params['hub.lease_seconds'])) {
$expires = $now->add(new DateInterval('PT' . $params['hub.lease_seconds'] . 'S'))
->format('Y-m-d H:i:s');
}
$data = [
'id' => $key,
'topic_url' => $params['hub.topic'],
'hub_url' => $hubUrl,
'created_time' => $now->format('Y-m-d H:i:s'),
'lease_seconds' => $params['hub.lease_seconds'],
'verify_token' => hash('sha256', $params['hub.verify_token']),
'secret' => null,
'expiration_time' => $expires,
// @codingStandardsIgnoreStart
'subscription_state' => ($mode == 'unsubscribe') ? PubSubHubbub::SUBSCRIPTION_TODELETE : PubSubHubbub::SUBSCRIPTION_NOTVERIFIED,
// @codingStandardsIgnoreEnd
];
$this->getStorage()->setSubscription($data);
return $this->_toByteValueOrderedString(
$this->_urlEncode($params)
);
}
/**
* Simple helper to generate a verification token used in (un)subscribe
* requests to a Hub Server. Follows no particular method, which means
* it might be improved/changed in future.
*
* @return string
*/
// @codingStandardsIgnoreStart
protected function _generateVerifyToken()
{
// @codingStandardsIgnoreEnd
if (! empty($this->testStaticToken)) {
return $this->testStaticToken;
}
return uniqid(rand(), true) . time();
}
/**
* Simple helper to generate a verification token used in (un)subscribe
* requests to a Hub Server.
*
* @param array $params
* @param string $hubUrl The Hub Server URL for which this token will apply
* @return string
*/
// @codingStandardsIgnoreStart
protected function _generateSubscriptionKey(array $params, $hubUrl)
{
// @codingStandardsIgnoreEnd
$keyBase = $params['hub.topic'] . $hubUrl;
$key = md5($keyBase);
return $key;
}
/**
* URL Encode an array of parameters
*
* @param array $params
* @return array
*/
// @codingStandardsIgnoreStart
protected function _urlEncode(array $params)
{
// @codingStandardsIgnoreEnd
$encoded = [];
foreach ($params as $key => $value) {
if (is_array($value)) {
$ekey = PubSubHubbub::urlencode($key);
$encoded[$ekey] = [];
foreach ($value as $duplicateKey) {
$encoded[$ekey][]
= PubSubHubbub::urlencode($duplicateKey);
}
} else {
$encoded[PubSubHubbub::urlencode($key)]
= PubSubHubbub::urlencode($value);
}
}
return $encoded;
}
/**
* Order outgoing parameters
*
* @param array $params
* @return array
*/
// @codingStandardsIgnoreStart
protected function _toByteValueOrderedString(array $params)
{
// @codingStandardsIgnoreEnd
$return = [];
uksort($params, 'strnatcmp');
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $keyduplicate) {
$return[] = $key . '=' . $keyduplicate;
}
} else {
$return[] = $key . '=' . $value;
}
}
return implode('&', $return);
}
/**
* This is STRICTLY for testing purposes only...
*/
protected $testStaticToken = null;
final public function setTestStaticToken($token)
{
$this->testStaticToken = (string) $token;
}
}

View file

@ -0,0 +1,322 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub\Subscriber;
use Zend\Feed\PubSubHubbub;
use Zend\Feed\PubSubHubbub\Exception;
use Zend\Feed\Uri;
class Callback extends PubSubHubbub\AbstractCallback
{
/**
* Contains the content of any feeds sent as updates to the Callback URL
*
* @var string
*/
protected $feedUpdate = null;
/**
* Holds a manually set subscription key (i.e. identifies a unique
* subscription) which is typical when it is not passed in the query string
* but is part of the Callback URL path, requiring manual retrieval e.g.
* using a route and the \Zend\Mvc\Router\RouteMatch::getParam() method.
*
* @var string
*/
protected $subscriptionKey = null;
/**
* After verification, this is set to the verified subscription's data.
*
* @var array
*/
protected $currentSubscriptionData = null;
/**
* Set a subscription key to use for the current callback request manually.
* Required if usePathParameter is enabled for the Subscriber.
*
* @param string $key
* @return \Zend\Feed\PubSubHubbub\Subscriber\Callback
*/
public function setSubscriptionKey($key)
{
$this->subscriptionKey = $key;
return $this;
}
/**
* Handle any callback from a Hub Server responding to a subscription or
* unsubscription request. This should be the Hub Server confirming the
* the request prior to taking action on it.
*
* @param array $httpGetData GET data if available and not in $_GET
* @param bool $sendResponseNow Whether to send response now or when asked
* @return void
*/
public function handle(array $httpGetData = null, $sendResponseNow = false)
{
if ($httpGetData === null) {
$httpGetData = $_GET;
}
/**
* Handle any feed updates (sorry for the mess :P)
*
* This DOES NOT attempt to process a feed update. Feed updates
* SHOULD be validated/processed by an asynchronous process so as
* to avoid holding up responses to the Hub.
*/
$contentType = $this->_getHeader('Content-Type');
if (strtolower($_SERVER['REQUEST_METHOD']) == 'post'
&& $this->_hasValidVerifyToken(null, false)
&& (stripos($contentType, 'application/atom+xml') === 0
|| stripos($contentType, 'application/rss+xml') === 0
|| stripos($contentType, 'application/xml') === 0
|| stripos($contentType, 'text/xml') === 0
|| stripos($contentType, 'application/rdf+xml') === 0)
) {
$this->setFeedUpdate($this->_getRawBody());
$this->getHttpResponse()->setHeader('X-Hub-On-Behalf-Of', $this->getSubscriberCount());
/**
* Handle any (un)subscribe confirmation requests
*/
} elseif ($this->isValidHubVerification($httpGetData)) {
$this->getHttpResponse()->setContent($httpGetData['hub_challenge']);
switch (strtolower($httpGetData['hub_mode'])) {
case 'subscribe':
$data = $this->currentSubscriptionData;
$data['subscription_state'] = PubSubHubbub\PubSubHubbub::SUBSCRIPTION_VERIFIED;
if (isset($httpGetData['hub_lease_seconds'])) {
$data['lease_seconds'] = $httpGetData['hub_lease_seconds'];
}
$this->getStorage()->setSubscription($data);
break;
case 'unsubscribe':
$verifyTokenKey = $this->_detectVerifyTokenKey($httpGetData);
$this->getStorage()->deleteSubscription($verifyTokenKey);
break;
default:
throw new Exception\RuntimeException(sprintf(
'Invalid hub_mode ("%s") provided',
$httpGetData['hub_mode']
));
}
/**
* Hey, C'mon! We tried everything else!
*/
} else {
$this->getHttpResponse()->setStatusCode(404);
}
if ($sendResponseNow) {
$this->sendResponse();
}
}
/**
* Checks validity of the request simply by making a quick pass and
* confirming the presence of all REQUIRED parameters.
*
* @param array $httpGetData
* @return bool
*/
public function isValidHubVerification(array $httpGetData)
{
/**
* As per the specification, the hub.verify_token is OPTIONAL. This
* implementation of Pubsubhubbub considers it REQUIRED and will
* always send a hub.verify_token parameter to be echoed back
* by the Hub Server. Therefore, its absence is considered invalid.
*/
if (strtolower($_SERVER['REQUEST_METHOD']) !== 'get') {
return false;
}
$required = [
'hub_mode',
'hub_topic',
'hub_challenge',
'hub_verify_token',
];
foreach ($required as $key) {
if (! array_key_exists($key, $httpGetData)) {
return false;
}
}
if ($httpGetData['hub_mode'] !== 'subscribe'
&& $httpGetData['hub_mode'] !== 'unsubscribe'
) {
return false;
}
if ($httpGetData['hub_mode'] == 'subscribe'
&& ! array_key_exists('hub_lease_seconds', $httpGetData)
) {
return false;
}
if (! Uri::factory($httpGetData['hub_topic'])->isValid()) {
return false;
}
/**
* Attempt to retrieve any Verification Token Key attached to Callback
* URL's path by our Subscriber implementation
*/
if (! $this->_hasValidVerifyToken($httpGetData)) {
return false;
}
return true;
}
/**
* Sets a newly received feed (Atom/RSS) sent by a Hub as an update to a
* Topic we've subscribed to.
*
* @param string $feed
* @return \Zend\Feed\PubSubHubbub\Subscriber\Callback
*/
public function setFeedUpdate($feed)
{
$this->feedUpdate = $feed;
return $this;
}
/**
* Check if any newly received feed (Atom/RSS) update was received
*
* @return bool
*/
public function hasFeedUpdate()
{
if ($this->feedUpdate === null) {
return false;
}
return true;
}
/**
* Gets a newly received feed (Atom/RSS) sent by a Hub as an update to a
* Topic we've subscribed to.
*
* @return string
*/
public function getFeedUpdate()
{
return $this->feedUpdate;
}
/**
* Check for a valid verify_token. By default attempts to compare values
* with that sent from Hub, otherwise merely ascertains its existence.
*
* @param array $httpGetData
* @param bool $checkValue
* @return bool
*/
// @codingStandardsIgnoreStart
protected function _hasValidVerifyToken(array $httpGetData = null, $checkValue = true)
{
// @codingStandardsIgnoreEnd
$verifyTokenKey = $this->_detectVerifyTokenKey($httpGetData);
if (empty($verifyTokenKey)) {
return false;
}
$verifyTokenExists = $this->getStorage()->hasSubscription($verifyTokenKey);
if (! $verifyTokenExists) {
return false;
}
if ($checkValue) {
$data = $this->getStorage()->getSubscription($verifyTokenKey);
$verifyToken = $data['verify_token'];
if ($verifyToken !== hash('sha256', $httpGetData['hub_verify_token'])) {
return false;
}
$this->currentSubscriptionData = $data;
return true;
}
return true;
}
/**
* Attempt to detect the verification token key. This would be passed in
* the Callback URL (which we are handling with this class!) as a URI
* path part (the last part by convention).
*
* @param null|array $httpGetData
* @return false|string
*/
// @codingStandardsIgnoreStart
protected function _detectVerifyTokenKey(array $httpGetData = null)
{
// @codingStandardsIgnoreEnd
/**
* Available when sub keys encoding in Callback URL path
*/
if (isset($this->subscriptionKey)) {
return $this->subscriptionKey;
}
/**
* Available only if allowed by PuSH 0.2 Hubs
*/
if (is_array($httpGetData)
&& isset($httpGetData['xhub_subscription'])
) {
return $httpGetData['xhub_subscription'];
}
/**
* Available (possibly) if corrupted in transit and not part of $_GET
*/
$params = $this->_parseQueryString();
if (isset($params['xhub.subscription'])) {
return rawurldecode($params['xhub.subscription']);
}
return false;
}
/**
* Build an array of Query String parameters.
* This bypasses $_GET which munges parameter names and cannot accept
* multiple parameters with the same key.
*
* @return array|void
*/
// @codingStandardsIgnoreStart
protected function _parseQueryString()
{
// @codingStandardsIgnoreEnd
$params = [];
$queryString = '';
if (isset($_SERVER['QUERY_STRING'])) {
$queryString = $_SERVER['QUERY_STRING'];
}
if (empty($queryString)) {
return [];
}
$parts = explode('&', $queryString);
foreach ($parts as $kvpair) {
$pair = explode('=', $kvpair);
$key = rawurldecode($pair[0]);
$value = rawurldecode($pair[1]);
if (isset($params[$key])) {
if (is_array($params[$key])) {
$params[$key][] = $value;
} else {
$params[$key] = [$params[$key], $value];
}
} else {
$params[$key] = $value;
}
}
return $params;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\PubSubHubbub;
abstract class Version
{
const VERSION = '2';
}

View file

@ -0,0 +1,226 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader;
use DOMDocument;
use DOMElement;
use DOMXPath;
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument = null;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry = null;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath = null;
/**
* Registered extensions
*
* @var array
*/
protected $extensions = [];
/**
* Constructor
*
* @param DOMElement $entry
* @param int $entryKey
* @param null|string $type
*/
public function __construct(DOMElement $entry, $entryKey, $type = null)
{
$this->entry = $entry;
$this->entryKey = $entryKey;
$this->domDocument = $entry->ownerDocument;
if ($type !== null) {
$this->data['type'] = $type;
} else {
$this->data['type'] = Reader::detectType($entry);
}
$this->_loadExtensions();
}
/**
* Get the DOM
*
* @return DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Get the entry element
*
* @return DOMElement
*/
public function getElement()
{
return $this->entry;
}
/**
* Get the Entry's encoding
*
* @return string
*/
public function getEncoding()
{
$assumed = $this->getDomDocument()->encoding;
if (empty($assumed)) {
$assumed = 'UTF-8';
}
return $assumed;
}
/**
* Get entry as xml
*
* @return string
*/
public function saveXml()
{
$dom = new DOMDocument('1.0', $this->getEncoding());
$entry = $dom->importNode($this->getElement(), true);
$dom->appendChild($entry);
return $dom->saveXML();
}
/**
* Get the entry type
*
* @return string
*/
public function getType()
{
return $this->data['type'];
}
/**
* Get the XPath query object
*
* @return DOMXPath
*/
public function getXpath()
{
if (! $this->xpath) {
$this->setXpath(new DOMXPath($this->getDomDocument()));
}
return $this->xpath;
}
/**
* Set the XPath query
*
* @param DOMXPath $xpath
* @return \Zend\Feed\Reader\AbstractEntry
*/
public function setXpath(DOMXPath $xpath)
{
$this->xpath = $xpath;
return $this;
}
/**
* Get registered extensions
*
* @return array
*/
public function getExtensions()
{
return $this->extensions;
}
/**
* Return an Extension object with the matching name (postfixed with _Entry)
*
* @param string $name
* @return \Zend\Feed\Reader\Extension\AbstractEntry
*/
public function getExtension($name)
{
if (array_key_exists($name . '\Entry', $this->extensions)) {
return $this->extensions[$name . '\Entry'];
}
return;
}
/**
* Method overloading: call given method on first extension implementing it
*
* @param string $method
* @param array $args
* @return mixed
* @throws Exception\BadMethodCallException if no extensions implements the method
*/
public function __call($method, $args)
{
foreach ($this->extensions as $extension) {
if (method_exists($extension, $method)) {
return call_user_func_array([$extension, $method], $args);
}
}
throw new Exception\BadMethodCallException('Method: ' . $method
. 'does not exist and could not be located on a registered Extension');
}
/**
* Load extensions from Zend\Feed\Reader\Reader
*
* @return void
*/
// @codingStandardsIgnoreStart
protected function _loadExtensions()
{
// @codingStandardsIgnoreEnd
$all = Reader::getExtensions();
$feed = $all['entry'];
foreach ($feed as $extension) {
if (in_array($extension, $all['core'])) {
continue;
}
$className = Reader::getPluginLoader()->getClassName($extension);
$this->extensions[$extension] = new $className(
$this->getElement(), $this->entryKey, $this->data['type']
);
}
}
}

View file

@ -0,0 +1,300 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader;
use DOMDocument;
use DOMElement;
use DOMXPath;
abstract class AbstractFeed implements Feed\FeedInterface
{
/**
* Parsed feed data
*
* @var array
*/
protected $data = [];
/**
* Parsed feed data in the shape of a DOMDocument
*
* @var DOMDocument
*/
protected $domDocument = null;
/**
* An array of parsed feed entries
*
* @var array
*/
protected $entries = [];
/**
* A pointer for the iterator to keep track of the entries array
*
* @var int
*/
protected $entriesKey = 0;
/**
* The base XPath query used to retrieve feed data
*
* @var DOMXPath
*/
protected $xpath = null;
/**
* Array of loaded extensions
*
* @var array
*/
protected $extensions = [];
/**
* Original Source URI (set if imported from a URI)
*
* @var string
*/
protected $originalSourceUri = null;
/**
* Constructor
*
* @param DomDocument $domDocument The DOM object for the feed's XML
* @param string $type Feed type
*/
public function __construct(DOMDocument $domDocument, $type = null)
{
$this->domDocument = $domDocument;
$this->xpath = new DOMXPath($this->domDocument);
if ($type !== null) {
$this->data['type'] = $type;
} else {
$this->data['type'] = Reader::detectType($this->domDocument);
}
$this->registerNamespaces();
$this->indexEntries();
$this->loadExtensions();
}
/**
* Set an original source URI for the feed being parsed. This value
* is returned from getFeedLink() method if the feed does not carry
* a self-referencing URI.
*
* @param string $uri
*/
public function setOriginalSourceUri($uri)
{
$this->originalSourceUri = $uri;
}
/**
* Get an original source URI for the feed being parsed. Returns null if
* unset or the feed was not imported from a URI.
*
* @return string|null
*/
public function getOriginalSourceUri()
{
return $this->originalSourceUri;
}
/**
* Get the number of feed entries.
* Required by the Iterator interface.
*
* @return int
*/
public function count()
{
return count($this->entries);
}
/**
* Return the current entry
*
* @return \Zend\Feed\Reader\AbstractEntry
*/
public function current()
{
if (substr($this->getType(), 0, 3) == 'rss') {
$reader = new Entry\RSS($this->entries[$this->key()], $this->key(), $this->getType());
} else {
$reader = new Entry\Atom($this->entries[$this->key()], $this->key(), $this->getType());
}
$reader->setXpath($this->xpath);
return $reader;
}
/**
* Get the DOM
*
* @return DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Get the Feed's encoding
*
* @return string
*/
public function getEncoding()
{
$assumed = $this->getDomDocument()->encoding;
if (empty($assumed)) {
$assumed = 'UTF-8';
}
return $assumed;
}
/**
* Get feed as xml
*
* @return string
*/
public function saveXml()
{
return $this->getDomDocument()->saveXML();
}
/**
* Get the DOMElement representing the items/feed element
*
* @return DOMElement
*/
public function getElement()
{
return $this->getDomDocument()->documentElement;
}
/**
* Get the DOMXPath object for this feed
*
* @return DOMXPath
*/
public function getXpath()
{
return $this->xpath;
}
/**
* Get the feed type
*
* @return string
*/
public function getType()
{
return $this->data['type'];
}
/**
* Return the current feed key
*
* @return int
*/
public function key()
{
return $this->entriesKey;
}
/**
* Move the feed pointer forward
*
*/
public function next()
{
++$this->entriesKey;
}
/**
* Reset the pointer in the feed object
*
*/
public function rewind()
{
$this->entriesKey = 0;
}
/**
* Check to see if the iterator is still valid
*
* @return bool
*/
public function valid()
{
return 0 <= $this->entriesKey && $this->entriesKey < $this->count();
}
public function getExtensions()
{
return $this->extensions;
}
public function __call($method, $args)
{
foreach ($this->extensions as $extension) {
if (method_exists($extension, $method)) {
return call_user_func_array([$extension, $method], $args);
}
}
throw new Exception\BadMethodCallException('Method: ' . $method
. 'does not exist and could not be located on a registered Extension');
}
/**
* Return an Extension object with the matching name (postfixed with _Feed)
*
* @param string $name
* @return \Zend\Feed\Reader\Extension\AbstractFeed
*/
public function getExtension($name)
{
if (array_key_exists($name . '\Feed', $this->extensions)) {
return $this->extensions[$name . '\Feed'];
}
return;
}
protected function loadExtensions()
{
$all = Reader::getExtensions();
$manager = Reader::getExtensionManager();
$feed = $all['feed'];
foreach ($feed as $extension) {
if (in_array($extension, $all['core'])) {
continue;
}
$plugin = $manager->get($extension);
$plugin->setDomDocument($this->getDomDocument());
$plugin->setType($this->data['type']);
$plugin->setXpath($this->xpath);
$this->extensions[$extension] = $plugin;
}
}
/**
* Read all entries to the internal entries array
*
*/
abstract protected function indexEntries();
/**
* Register the default namespaces for the current feed format
*
*/
abstract protected function registerNamespaces();
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader;
use ArrayObject;
class Collection extends ArrayObject
{
}

View file

@ -0,0 +1,25 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Collection;
use ArrayObject;
abstract class AbstractCollection extends ArrayObject
{
/**
* Return a simple array of the most relevant slice of
* the collection values. For example, feed categories contain
* the category name, domain/URI, and other data. This method would
* merely return the most useful data - i.e. the category names.
*
* @return array
*/
abstract public function getValues();
}

View file

@ -0,0 +1,28 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Collection;
class Author extends AbstractCollection
{
/**
* Return a simple array of the most relevant slice of
* the author values, i.e. all author names.
*
* @return array
*/
public function getValues()
{
$authors = [];
foreach ($this->getIterator() as $element) {
$authors[] = $element['name'];
}
return array_unique($authors);
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Collection;
class Category extends AbstractCollection
{
/**
* Return a simple array of the most relevant slice of
* the collection values. For example, feed categories contain
* the category name, domain/URI, and other data. This method would
* merely return the most useful data - i.e. the category names.
*
* @return array
*/
public function getValues()
{
$categories = [];
foreach ($this->getIterator() as $element) {
if (isset($element['label']) && ! empty($element['label'])) {
$categories[] = $element['label'];
} else {
$categories[] = $element['term'];
}
}
return array_unique($categories);
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Collection;
use ArrayObject;
class Collection extends ArrayObject
{
}

View file

@ -0,0 +1,233 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Entry;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Zend\Feed\Reader;
use Zend\Feed\Reader\Exception;
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument = null;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry = null;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath = null;
/**
* Registered extensions
*
* @var array
*/
protected $extensions = [];
/**
* Constructor
*
* @param DOMElement $entry
* @param int $entryKey
* @param string $type
*/
public function __construct(DOMElement $entry, $entryKey, $type = null)
{
$this->entry = $entry;
$this->entryKey = $entryKey;
$this->domDocument = $entry->ownerDocument;
if ($type !== null) {
$this->data['type'] = $type;
} elseif ($this->domDocument !== null) {
$this->data['type'] = Reader\Reader::detectType($this->domDocument);
} else {
$this->data['type'] = Reader\Reader::TYPE_ANY;
}
$this->loadExtensions();
}
/**
* Get the DOM
*
* @return DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Get the entry element
*
* @return DOMElement
*/
public function getElement()
{
return $this->entry;
}
/**
* Get the Entry's encoding
*
* @return string
*/
public function getEncoding()
{
$assumed = $this->getDomDocument()->encoding;
if (empty($assumed)) {
$assumed = 'UTF-8';
}
return $assumed;
}
/**
* Get entry as xml
*
* @return string
*/
public function saveXml()
{
$dom = new DOMDocument('1.0', $this->getEncoding());
$deep = version_compare(PHP_VERSION, '7', 'ge') ? 1 : true;
$entry = $dom->importNode($this->getElement(), $deep);
$dom->appendChild($entry);
return $dom->saveXML();
}
/**
* Get the entry type
*
* @return string
*/
public function getType()
{
return $this->data['type'];
}
/**
* Get the XPath query object
*
* @return DOMXPath
*/
public function getXpath()
{
if (! $this->xpath) {
$this->setXpath(new DOMXPath($this->getDomDocument()));
}
return $this->xpath;
}
/**
* Set the XPath query
*
* @param DOMXPath $xpath
* @return AbstractEntry
*/
public function setXpath(DOMXPath $xpath)
{
$this->xpath = $xpath;
return $this;
}
/**
* Get registered extensions
*
* @return array
*/
public function getExtensions()
{
return $this->extensions;
}
/**
* Return an Extension object with the matching name (postfixed with _Entry)
*
* @param string $name
* @return Reader\Extension\AbstractEntry
*/
public function getExtension($name)
{
if (array_key_exists($name . '\\Entry', $this->extensions)) {
return $this->extensions[$name . '\\Entry'];
}
return;
}
/**
* Method overloading: call given method on first extension implementing it
*
* @param string $method
* @param array $args
* @return mixed
* @throws Exception\RuntimeException if no extensions implements the method
*/
public function __call($method, $args)
{
foreach ($this->extensions as $extension) {
if (method_exists($extension, $method)) {
return call_user_func_array([$extension, $method], $args);
}
}
throw new Exception\RuntimeException(sprintf(
'Method: %s does not exist and could not be located on a registered Extension',
$method
));
}
/**
* Load extensions from Zend\Feed\Reader\Reader
*
* @return void
*/
protected function loadExtensions()
{
$all = Reader\Reader::getExtensions();
$manager = Reader\Reader::getExtensionManager();
$feed = $all['entry'];
foreach ($feed as $extension) {
if (in_array($extension, $all['core'])) {
continue;
}
$plugin = $manager->get($extension);
$plugin->setEntryElement($this->getElement());
$plugin->setEntryKey($this->entryKey);
$plugin->setType($this->data['type']);
$this->extensions[$extension] = $plugin;
}
}
}

View file

@ -0,0 +1,370 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Entry;
use DOMElement;
use DOMXPath;
use Zend\Feed\Reader;
class Atom extends AbstractEntry implements EntryInterface
{
/**
* XPath query
*
* @var string
*/
protected $xpathQuery = '';
/**
* Constructor
*
* @param DOMElement $entry
* @param int $entryKey
* @param string $type
*/
public function __construct(DOMElement $entry, $entryKey, $type = null)
{
parent::__construct($entry, $entryKey, $type);
// Everyone by now should know XPath indices start from 1 not 0
$this->xpathQuery = '//atom:entry[' . ($this->entryKey + 1) . ']';
$manager = Reader\Reader::getExtensionManager();
$extensions = ['Atom\Entry', 'Thread\Entry', 'DublinCore\Entry'];
foreach ($extensions as $name) {
$extension = $manager->get($name);
$extension->setEntryElement($entry);
$extension->setEntryKey($entryKey);
$extension->setType($type);
$this->extensions[$name] = $extension;
}
}
/**
* Get the specified author
*
* @param int $index
* @return string|null
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index])) {
return $authors[$index];
}
return;
}
/**
* Get an array with feed authors
*
* @return array
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$people = $this->getExtension('Atom')->getAuthors();
$this->data['authors'] = $people;
return $this->data['authors'];
}
/**
* Get the entry content
*
* @return string
*/
public function getContent()
{
if (array_key_exists('content', $this->data)) {
return $this->data['content'];
}
$content = $this->getExtension('Atom')->getContent();
$this->data['content'] = $content;
return $this->data['content'];
}
/**
* Get the entry creation date
*
* @return \DateTime
*/
public function getDateCreated()
{
if (array_key_exists('datecreated', $this->data)) {
return $this->data['datecreated'];
}
$dateCreated = $this->getExtension('Atom')->getDateCreated();
$this->data['datecreated'] = $dateCreated;
return $this->data['datecreated'];
}
/**
* Get the entry modification date
*
* @return \DateTime
*/
public function getDateModified()
{
if (array_key_exists('datemodified', $this->data)) {
return $this->data['datemodified'];
}
$dateModified = $this->getExtension('Atom')->getDateModified();
$this->data['datemodified'] = $dateModified;
return $this->data['datemodified'];
}
/**
* Get the entry description
*
* @return string
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$description = $this->getExtension('Atom')->getDescription();
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Get the entry enclosure
*
* @return string
*/
public function getEnclosure()
{
if (array_key_exists('enclosure', $this->data)) {
return $this->data['enclosure'];
}
$enclosure = $this->getExtension('Atom')->getEnclosure();
$this->data['enclosure'] = $enclosure;
return $this->data['enclosure'];
}
/**
* Get the entry ID
*
* @return string
*/
public function getId()
{
if (array_key_exists('id', $this->data)) {
return $this->data['id'];
}
$id = $this->getExtension('Atom')->getId();
$this->data['id'] = $id;
return $this->data['id'];
}
/**
* Get a specific link
*
* @param int $index
* @return string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
if (isset($this->data['links'][$index])) {
return $this->data['links'][$index];
}
return;
}
/**
* Get all links
*
* @return array
*/
public function getLinks()
{
if (array_key_exists('links', $this->data)) {
return $this->data['links'];
}
$links = $this->getExtension('Atom')->getLinks();
$this->data['links'] = $links;
return $this->data['links'];
}
/**
* Get a permalink to the entry
*
* @return string
*/
public function getPermalink()
{
return $this->getLink(0);
}
/**
* Get the entry title
*
* @return string
*/
public function getTitle()
{
if (array_key_exists('title', $this->data)) {
return $this->data['title'];
}
$title = $this->getExtension('Atom')->getTitle();
$this->data['title'] = $title;
return $this->data['title'];
}
/**
* Get the number of comments/replies for current entry
*
* @return int
*/
public function getCommentCount()
{
if (array_key_exists('commentcount', $this->data)) {
return $this->data['commentcount'];
}
$commentcount = $this->getExtension('Thread')->getCommentCount();
if (! $commentcount) {
$commentcount = $this->getExtension('Atom')->getCommentCount();
}
$this->data['commentcount'] = $commentcount;
return $this->data['commentcount'];
}
/**
* Returns a URI pointing to the HTML page where comments can be made on this entry
*
* @return string
*/
public function getCommentLink()
{
if (array_key_exists('commentlink', $this->data)) {
return $this->data['commentlink'];
}
$commentlink = $this->getExtension('Atom')->getCommentLink();
$this->data['commentlink'] = $commentlink;
return $this->data['commentlink'];
}
/**
* Returns a URI pointing to a feed of all comments for this entry
*
* @return string
*/
public function getCommentFeedLink()
{
if (array_key_exists('commentfeedlink', $this->data)) {
return $this->data['commentfeedlink'];
}
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink();
$this->data['commentfeedlink'] = $commentfeedlink;
return $this->data['commentfeedlink'];
}
/**
* Get category data as a Reader\Reader_Collection_Category object
*
* @return Reader\Collection\Category
*/
public function getCategories()
{
if (array_key_exists('categories', $this->data)) {
return $this->data['categories'];
}
$categoryCollection = $this->getExtension('Atom')->getCategories();
if (count($categoryCollection) == 0) {
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
}
$this->data['categories'] = $categoryCollection;
return $this->data['categories'];
}
/**
* Get source feed metadata from the entry
*
* @return Reader\Feed\Atom\Source|null
*/
public function getSource()
{
if (array_key_exists('source', $this->data)) {
return $this->data['source'];
}
$source = $this->getExtension('Atom')->getSource();
$this->data['source'] = $source;
return $this->data['source'];
}
/**
* Set the XPath query (incl. on all Extensions)
*
* @param DOMXPath $xpath
* @return void
*/
public function setXpath(DOMXPath $xpath)
{
parent::setXpath($xpath);
foreach ($this->extensions as $extension) {
$extension->setXpath($this->xpath);
}
}
}

View file

@ -0,0 +1,129 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Entry;
use Zend\Feed\Reader\Collection\Category;
interface EntryInterface
{
/**
* Get the specified author
*
* @param int $index
* @return string|null
*/
public function getAuthor($index = 0);
/**
* Get an array with feed authors
*
* @return array
*/
public function getAuthors();
/**
* Get the entry content
*
* @return string
*/
public function getContent();
/**
* Get the entry creation date
*
* @return \DateTime
*/
public function getDateCreated();
/**
* Get the entry modification date
*
* @return \DateTime
*/
public function getDateModified();
/**
* Get the entry description
*
* @return string
*/
public function getDescription();
/**
* Get the entry enclosure
*
* @return \stdClass
*/
public function getEnclosure();
/**
* Get the entry ID
*
* @return string
*/
public function getId();
/**
* Get a specific link
*
* @param int $index
* @return string
*/
public function getLink($index = 0);
/**
* Get all links
*
* @return array
*/
public function getLinks();
/**
* Get a permalink to the entry
*
* @return string
*/
public function getPermalink();
/**
* Get the entry title
*
* @return string
*/
public function getTitle();
/**
* Get the number of comments/replies for current entry
*
* @return int
*/
public function getCommentCount();
/**
* Returns a URI pointing to the HTML page where comments can be made on this entry
*
* @return string
*/
public function getCommentLink();
/**
* Returns a URI pointing to a feed of all comments for this entry
*
* @return string
*/
public function getCommentFeedLink();
/**
* Get all categories
*
* @return Category
*/
public function getCategories();
}

View file

@ -0,0 +1,596 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Entry;
use DateTime;
use DOMElement;
use DOMXPath;
use Zend\Feed\Reader;
use Zend\Feed\Reader\Exception;
class Rss extends AbstractEntry implements EntryInterface
{
/**
* XPath query for RDF
*
* @var string
*/
protected $xpathQueryRdf = '';
/**
* XPath query for RSS
*
* @var string
*/
protected $xpathQueryRss = '';
/**
* Constructor
*
* @param DOMElement $entry
* @param string $entryKey
* @param string $type
*/
public function __construct(DOMElement $entry, $entryKey, $type = null)
{
parent::__construct($entry, $entryKey, $type);
$this->xpathQueryRss = '//item[' . ($this->entryKey + 1) . ']';
$this->xpathQueryRdf = '//rss:item[' . ($this->entryKey + 1) . ']';
$manager = Reader\Reader::getExtensionManager();
$extensions = [
'DublinCore\Entry',
'Content\Entry',
'Atom\Entry',
'WellFormedWeb\Entry',
'Slash\Entry',
'Thread\Entry',
];
foreach ($extensions as $name) {
$extension = $manager->get($name);
$extension->setEntryElement($entry);
$extension->setEntryKey($entryKey);
$extension->setType($type);
$this->extensions[$name] = $extension;
}
}
/**
* Get an author entry
*
* @param int $index
* @return string
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index])) {
return $authors[$index];
}
return;
}
/**
* Get an array with feed authors
*
* @return array
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$authors = [];
$authorsDc = $this->getExtension('DublinCore')->getAuthors();
if (! empty($authorsDc)) {
foreach ($authorsDc as $author) {
$authors[] = [
'name' => $author['name']
];
}
}
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090) {
$list = $this->xpath->query($this->xpathQueryRss . '//author');
} else {
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:author');
}
if ($list->length) {
foreach ($list as $author) {
$string = trim($author->nodeValue);
$data = [];
// Pretty rough parsing - but it's a catchall
if (preg_match("/^.*@[^ ]*/", $string, $matches)) {
$data['email'] = trim($matches[0]);
if (preg_match("/\((.*)\)$/", $string, $matches)) {
$data['name'] = $matches[1];
}
$authors[] = $data;
}
}
}
if (count($authors) == 0) {
$authors = $this->getExtension('Atom')->getAuthors();
} else {
$authors = new Reader\Collection\Author(
Reader\Reader::arrayUnique($authors)
);
}
if (count($authors) == 0) {
$authors = null;
}
$this->data['authors'] = $authors;
return $this->data['authors'];
}
/**
* Get the entry content
*
* @return string
*/
public function getContent()
{
if (array_key_exists('content', $this->data)) {
return $this->data['content'];
}
$content = $this->getExtension('Content')->getContent();
if (! $content) {
$content = $this->getDescription();
}
if (empty($content)) {
$content = $this->getExtension('Atom')->getContent();
}
$this->data['content'] = $content;
return $this->data['content'];
}
/**
* Get the entry's date of creation
*
* @return \DateTime
*/
public function getDateCreated()
{
return $this->getDateModified();
}
/**
* Get the entry's date of modification
*
* @throws Exception\RuntimeException
* @return \DateTime
*/
public function getDateModified()
{
if (array_key_exists('datemodified', $this->data)) {
return $this->data['datemodified'];
}
$date = null;
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$dateModified = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/pubDate)');
if ($dateModified) {
$dateModifiedParsed = strtotime($dateModified);
if ($dateModifiedParsed) {
$date = new DateTime('@' . $dateModifiedParsed);
} else {
$dateStandards = [DateTime::RSS, DateTime::RFC822,
DateTime::RFC2822, null];
foreach ($dateStandards as $standard) {
try {
$date = date_create_from_format($standard, $dateModified);
break;
} catch (\Exception $e) {
if ($standard === null) {
throw new Exception\RuntimeException(
'Could not load date due to unrecognised'
.' format (should follow RFC 822 or 2822):'
. $e->getMessage(),
0,
$e
);
}
}
}
}
}
}
if (! $date) {
$date = $this->getExtension('DublinCore')->getDate();
}
if (! $date) {
$date = $this->getExtension('Atom')->getDateModified();
}
if (! $date) {
$date = null;
}
$this->data['datemodified'] = $date;
return $this->data['datemodified'];
}
/**
* Get the entry description
*
* @return string
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$description = null;
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$description = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/description)');
} else {
$description = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:description)');
}
if (! $description) {
$description = $this->getExtension('DublinCore')->getDescription();
}
if (empty($description)) {
$description = $this->getExtension('Atom')->getDescription();
}
if (! $description) {
$description = null;
}
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Get the entry enclosure
* @return string
*/
public function getEnclosure()
{
if (array_key_exists('enclosure', $this->data)) {
return $this->data['enclosure'];
}
$enclosure = null;
if ($this->getType() == Reader\Reader::TYPE_RSS_20) {
$nodeList = $this->xpath->query($this->xpathQueryRss . '/enclosure');
if ($nodeList->length > 0) {
$enclosure = new \stdClass();
$enclosure->url = $nodeList->item(0)->getAttribute('url');
$enclosure->length = $nodeList->item(0)->getAttribute('length');
$enclosure->type = $nodeList->item(0)->getAttribute('type');
}
}
if (! $enclosure) {
$enclosure = $this->getExtension('Atom')->getEnclosure();
}
$this->data['enclosure'] = $enclosure;
return $this->data['enclosure'];
}
/**
* Get the entry ID
*
* @return string
*/
public function getId()
{
if (array_key_exists('id', $this->data)) {
return $this->data['id'];
}
$id = null;
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$id = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/guid)');
}
if (! $id) {
$id = $this->getExtension('DublinCore')->getId();
}
if (empty($id)) {
$id = $this->getExtension('Atom')->getId();
}
if (! $id) {
if ($this->getPermalink()) {
$id = $this->getPermalink();
} elseif ($this->getTitle()) {
$id = $this->getTitle();
} else {
$id = null;
}
}
$this->data['id'] = $id;
return $this->data['id'];
}
/**
* Get a specific link
*
* @param int $index
* @return string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
if (isset($this->data['links'][$index])) {
return $this->data['links'][$index];
}
return;
}
/**
* Get all links
*
* @return array
*/
public function getLinks()
{
if (array_key_exists('links', $this->data)) {
return $this->data['links'];
}
$links = [];
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
$list = $this->xpath->query($this->xpathQueryRss . '//link');
} else {
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:link');
}
if (! $list->length) {
$links = $this->getExtension('Atom')->getLinks();
} else {
foreach ($list as $link) {
$links[] = $link->nodeValue;
}
}
$this->data['links'] = $links;
return $this->data['links'];
}
/**
* Get all categories
*
* @return Reader\Collection\Category
*/
public function getCategories()
{
if (array_key_exists('categories', $this->data)) {
return $this->data['categories'];
}
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
$list = $this->xpath->query($this->xpathQueryRss . '//category');
} else {
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:category');
}
if ($list->length) {
$categoryCollection = new Reader\Collection\Category;
foreach ($list as $category) {
$categoryCollection[] = [
'term' => $category->nodeValue,
'scheme' => $category->getAttribute('domain'),
'label' => $category->nodeValue,
];
}
} else {
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
}
if (count($categoryCollection) == 0) {
$categoryCollection = $this->getExtension('Atom')->getCategories();
}
$this->data['categories'] = $categoryCollection;
return $this->data['categories'];
}
/**
* Get a permalink to the entry
*
* @return string
*/
public function getPermalink()
{
return $this->getLink(0);
}
/**
* Get the entry title
*
* @return string
*/
public function getTitle()
{
if (array_key_exists('title', $this->data)) {
return $this->data['title'];
}
$title = null;
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$title = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/title)');
} else {
$title = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:title)');
}
if (! $title) {
$title = $this->getExtension('DublinCore')->getTitle();
}
if (! $title) {
$title = $this->getExtension('Atom')->getTitle();
}
if (! $title) {
$title = null;
}
$this->data['title'] = $title;
return $this->data['title'];
}
/**
* Get the number of comments/replies for current entry
*
* @return string|null
*/
public function getCommentCount()
{
if (array_key_exists('commentcount', $this->data)) {
return $this->data['commentcount'];
}
$commentcount = $this->getExtension('Slash')->getCommentCount();
if (! $commentcount) {
$commentcount = $this->getExtension('Thread')->getCommentCount();
}
if (! $commentcount) {
$commentcount = $this->getExtension('Atom')->getCommentCount();
}
if (! $commentcount) {
$commentcount = null;
}
$this->data['commentcount'] = $commentcount;
return $this->data['commentcount'];
}
/**
* Returns a URI pointing to the HTML page where comments can be made on this entry
*
* @return string
*/
public function getCommentLink()
{
if (array_key_exists('commentlink', $this->data)) {
return $this->data['commentlink'];
}
$commentlink = null;
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$commentlink = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/comments)');
}
if (! $commentlink) {
$commentlink = $this->getExtension('Atom')->getCommentLink();
}
if (! $commentlink) {
$commentlink = null;
}
$this->data['commentlink'] = $commentlink;
return $this->data['commentlink'];
}
/**
* Returns a URI pointing to a feed of all comments for this entry
*
* @return string
*/
public function getCommentFeedLink()
{
if (array_key_exists('commentfeedlink', $this->data)) {
return $this->data['commentfeedlink'];
}
$commentfeedlink = $this->getExtension('WellFormedWeb')->getCommentFeedLink();
if (! $commentfeedlink) {
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rss');
}
if (! $commentfeedlink) {
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rdf');
}
if (! $commentfeedlink) {
$commentfeedlink = null;
}
$this->data['commentfeedlink'] = $commentfeedlink;
return $this->data['commentfeedlink'];
}
/**
* Set the XPath query (incl. on all Extensions)
*
* @param DOMXPath $xpath
* @return void
*/
public function setXpath(DOMXPath $xpath)
{
parent::setXpath($xpath);
foreach ($this->extensions as $extension) {
$extension->setXpath($this->xpath);
}
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Exception;
use Zend\Feed\Exception;
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Exception;
use Zend\Feed\Exception\ExceptionInterface as Exception;
interface ExceptionInterface extends Exception
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Exception;
use Zend\Feed\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Exception;
use Zend\Feed\Exception;
class InvalidHttpClientException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,16 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Exception;
use Zend\Feed\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,233 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Extension;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Zend\Feed\Reader;
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument = null;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry = null;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath = null;
/**
* XPath query
*
* @var string
*/
protected $xpathPrefix = '';
/**
* Set the entry DOMElement
*
* Has side effect of setting the DOMDocument for the entry.
*
* @param DOMElement $entry
* @return AbstractEntry
*/
public function setEntryElement(DOMElement $entry)
{
$this->entry = $entry;
$this->domDocument = $entry->ownerDocument;
return $this;
}
/**
* Get the entry DOMElement
*
* @return DOMElement
*/
public function getEntryElement()
{
return $this->entry;
}
/**
* Set the entry key
*
* @param string $entryKey
* @return AbstractEntry
*/
public function setEntryKey($entryKey)
{
$this->entryKey = $entryKey;
return $this;
}
/**
* Get the DOM
*
* @return DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Get the Entry's encoding
*
* @return string
*/
public function getEncoding()
{
$assumed = $this->getDomDocument()->encoding;
return $assumed;
}
/**
* Set the entry type
*
* Has side effect of setting xpath prefix
*
* @param string $type
* @return AbstractEntry
*/
public function setType($type)
{
if (null === $type) {
$this->data['type'] = null;
return $this;
}
$this->data['type'] = $type;
if ($type === Reader\Reader::TYPE_RSS_10
|| $type === Reader\Reader::TYPE_RSS_090
) {
$this->setXpathPrefix('//rss:item[' . ((int)$this->entryKey + 1) . ']');
return $this;
}
if ($type === Reader\Reader::TYPE_ATOM_10
|| $type === Reader\Reader::TYPE_ATOM_03
) {
$this->setXpathPrefix('//atom:entry[' . ((int)$this->entryKey + 1) . ']');
return $this;
}
$this->setXpathPrefix('//item[' . ((int)$this->entryKey + 1) . ']');
return $this;
}
/**
* Get the entry type
*
* @return string
*/
public function getType()
{
$type = $this->data['type'];
if ($type === null) {
$type = Reader\Reader::detectType($this->getEntryElement(), true);
$this->setType($type);
}
return $type;
}
/**
* Set the XPath query
*
* @param DOMXPath $xpath
* @return AbstractEntry
*/
public function setXpath(DOMXPath $xpath)
{
$this->xpath = $xpath;
$this->registerNamespaces();
return $this;
}
/**
* Get the XPath query object
*
* @return DOMXPath
*/
public function getXpath()
{
if (! $this->xpath) {
$this->setXpath(new DOMXPath($this->getDomDocument()));
}
return $this->xpath;
}
/**
* Serialize the entry to an array
*
* @return array
*/
public function toArray()
{
return $this->data;
}
/**
* Get the XPath prefix
*
* @return string
*/
public function getXpathPrefix()
{
return $this->xpathPrefix;
}
/**
* Set the XPath prefix
*
* @param string $prefix
* @return AbstractEntry
*/
public function setXpathPrefix($prefix)
{
$this->xpathPrefix = $prefix;
return $this;
}
/**
* Register XML namespaces
*
* @return void
*/
abstract protected function registerNamespaces();
}

View file

@ -0,0 +1,175 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Extension;
use DOMDocument;
use DOMXPath;
use Zend\Feed\Reader;
abstract class AbstractFeed
{
/**
* Parsed feed data
*
* @var array
*/
protected $data = [];
/**
* Parsed feed data in the shape of a DOMDocument
*
* @var DOMDocument
*/
protected $domDocument = null;
/**
* The base XPath query used to retrieve feed data
*
* @var DOMXPath
*/
protected $xpath = null;
/**
* The XPath prefix
*
* @var string
*/
protected $xpathPrefix = '';
/**
* Set the DOM document
*
* @param DOMDocument $dom
* @return AbstractFeed
*/
public function setDomDocument(DOMDocument $dom)
{
$this->domDocument = $dom;
return $this;
}
/**
* Get the DOM
*
* @return DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Get the Feed's encoding
*
* @return string
*/
public function getEncoding()
{
$assumed = $this->getDomDocument()->encoding;
return $assumed;
}
/**
* Set the feed type
*
* @param string $type
* @return AbstractFeed
*/
public function setType($type)
{
$this->data['type'] = $type;
return $this;
}
/**
* Get the feed type
*
* If null, it will attempt to autodetect the type.
*
* @return string
*/
public function getType()
{
$type = $this->data['type'];
if (null === $type) {
$type = Reader\Reader::detectType($this->getDomDocument());
$this->setType($type);
}
return $type;
}
/**
* Return the feed as an array
*
* @return array
*/
public function toArray() // untested
{
return $this->data;
}
/**
* Set the XPath query
*
* @param DOMXPath $xpath
* @return AbstractEntry
*/
public function setXpath(DOMXPath $xpath = null)
{
if (null === $xpath) {
$this->xpath = null;
return $this;
}
$this->xpath = $xpath;
$this->registerNamespaces();
return $this;
}
/**
* Get the DOMXPath object
*
* @return string
*/
public function getXpath()
{
if (null === $this->xpath) {
$this->setXpath(new DOMXPath($this->getDomDocument()));
}
return $this->xpath;
}
/**
* Get the XPath prefix
*
* @return string
*/
public function getXpathPrefix()
{
return $this->xpathPrefix;
}
/**
* Set the XPath prefix
*
* @param string $prefix
* @return void
*/
public function setXpathPrefix($prefix)
{
$this->xpathPrefix = $prefix;
}
/**
* Register the default namespaces for the current feed format
*/
abstract protected function registerNamespaces();
}

View file

@ -0,0 +1,634 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Extension\Atom;
use DateTime;
use DOMDocument;
use DOMElement;
use stdClass;
use Zend\Feed\Reader;
use Zend\Feed\Reader\Collection;
use Zend\Feed\Reader\Extension;
use Zend\Feed\Uri;
class Entry extends Extension\AbstractEntry
{
/**
* Get the specified author
*
* @param int $index
* @return string|null
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index])) {
return $authors[$index];
}
return;
}
/**
* Get an array with feed authors
*
* @return Collection\Author
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$authors = [];
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:author');
if (! $list->length) {
/**
* TODO: Limit query to feed level els only!
*/
$list = $this->getXpath()->query('//atom:author');
}
if ($list->length) {
foreach ($list as $author) {
$author = $this->getAuthorFromElement($author);
if (! empty($author)) {
$authors[] = $author;
}
}
}
if (count($authors) == 0) {
$authors = new Collection\Author();
} else {
$authors = new Collection\Author(
Reader\Reader::arrayUnique($authors)
);
}
$this->data['authors'] = $authors;
return $this->data['authors'];
}
/**
* Get the entry content
*
* @return string
*/
public function getContent()
{
if (array_key_exists('content', $this->data)) {
return $this->data['content'];
}
$content = null;
$el = $this->getXpath()->query($this->getXpathPrefix() . '/atom:content');
if ($el->length > 0) {
$el = $el->item(0);
$type = $el->getAttribute('type');
switch ($type) {
case '':
case 'text':
case 'text/plain':
case 'html':
case 'text/html':
$content = $el->nodeValue;
break;
case 'xhtml':
$this->getXpath()->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
$xhtml = $this->getXpath()->query(
$this->getXpathPrefix() . '/atom:content/xhtml:div'
)->item(0);
$d = new DOMDocument('1.0', $this->getEncoding());
$deep = version_compare(PHP_VERSION, '7', 'ge') ? 1 : true;
$xhtmls = $d->importNode($xhtml, $deep);
$d->appendChild($xhtmls);
$content = $this->collectXhtml(
$d->saveXML(),
$d->lookupPrefix('http://www.w3.org/1999/xhtml')
);
break;
}
}
if (! $content) {
$content = $this->getDescription();
}
$this->data['content'] = trim($content);
return $this->data['content'];
}
/**
* Parse out XHTML to remove the namespacing
*
* @param $xhtml
* @param $prefix
* @return mixed
*/
protected function collectXhtml($xhtml, $prefix)
{
if (! empty($prefix)) {
$prefix = $prefix . ':';
}
$matches = [
"/<\?xml[^<]*>[^<]*<" . $prefix . "div[^<]*/",
"/<\/" . $prefix . "div>\s*$/"
];
$xhtml = preg_replace($matches, '', $xhtml);
if (! empty($prefix)) {
$xhtml = preg_replace("/(<[\/]?)" . $prefix . "([a-zA-Z]+)/", '$1$2', $xhtml);
}
return $xhtml;
}
/**
* Get the entry creation date
*
* @return string
*/
public function getDateCreated()
{
if (array_key_exists('datecreated', $this->data)) {
return $this->data['datecreated'];
}
$date = null;
if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) {
$dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)');
} else {
$dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)');
}
if ($dateCreated) {
$date = new DateTime($dateCreated);
}
$this->data['datecreated'] = $date;
return $this->data['datecreated'];
}
/**
* Get the entry modification date
*
* @return string
*/
public function getDateModified()
{
if (array_key_exists('datemodified', $this->data)) {
return $this->data['datemodified'];
}
$date = null;
if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) {
$dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)');
} else {
$dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)');
}
if ($dateModified) {
$date = new DateTime($dateModified);
}
$this->data['datemodified'] = $date;
return $this->data['datemodified'];
}
/**
* Get the entry description
*
* @return string
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:summary)');
if (! $description) {
$description = null;
}
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Get the entry enclosure
*
* @return string
*/
public function getEnclosure()
{
if (array_key_exists('enclosure', $this->data)) {
return $this->data['enclosure'];
}
$enclosure = null;
$nodeList = $this->getXpath()->query($this->getXpathPrefix() . '/atom:link[@rel="enclosure"]');
if ($nodeList->length > 0) {
$enclosure = new stdClass();
$enclosure->url = $nodeList->item(0)->getAttribute('href');
$enclosure->length = $nodeList->item(0)->getAttribute('length');
$enclosure->type = $nodeList->item(0)->getAttribute('type');
}
$this->data['enclosure'] = $enclosure;
return $this->data['enclosure'];
}
/**
* Get the entry ID
*
* @return string
*/
public function getId()
{
if (array_key_exists('id', $this->data)) {
return $this->data['id'];
}
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)');
if (! $id) {
if ($this->getPermalink()) {
$id = $this->getPermalink();
} elseif ($this->getTitle()) {
$id = $this->getTitle();
} else {
$id = null;
}
}
$this->data['id'] = $id;
return $this->data['id'];
}
/**
* Get the base URI of the feed (if set).
*
* @return string|null
*/
public function getBaseUrl()
{
if (array_key_exists('baseUrl', $this->data)) {
return $this->data['baseUrl'];
}
$baseUrl = $this->getXpath()->evaluate(
'string('
. $this->getXpathPrefix()
. '/@xml:base[1]'
. ')'
);
if (! $baseUrl) {
$baseUrl = $this->getXpath()->evaluate('string(//@xml:base[1])');
}
if (! $baseUrl) {
$baseUrl = null;
}
$this->data['baseUrl'] = $baseUrl;
return $this->data['baseUrl'];
}
/**
* Get a specific link
*
* @param int $index
* @return string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
if (isset($this->data['links'][$index])) {
return $this->data['links'][$index];
}
return;
}
/**
* Get all links
*
* @return array
*/
public function getLinks()
{
if (array_key_exists('links', $this->data)) {
return $this->data['links'];
}
$links = [];
$list = $this->getXpath()->query(
$this->getXpathPrefix() . '//atom:link[@rel="alternate"]/@href' . '|' .
$this->getXpathPrefix() . '//atom:link[not(@rel)]/@href'
);
if ($list->length) {
foreach ($list as $link) {
$links[] = $this->absolutiseUri($link->value);
}
}
$this->data['links'] = $links;
return $this->data['links'];
}
/**
* Get a permalink to the entry
*
* @return string
*/
public function getPermalink()
{
return $this->getLink(0);
}
/**
* Get the entry title
*
* @return string
*/
public function getTitle()
{
if (array_key_exists('title', $this->data)) {
return $this->data['title'];
}
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)');
if (! $title) {
$title = null;
}
$this->data['title'] = $title;
return $this->data['title'];
}
/**
* Get the number of comments/replies for current entry
*
* @return int
*/
public function getCommentCount()
{
if (array_key_exists('commentcount', $this->data)) {
return $this->data['commentcount'];
}
$count = null;
$this->getXpath()->registerNamespace('thread10', 'http://purl.org/syndication/thread/1.0');
$list = $this->getXpath()->query(
$this->getXpathPrefix() . '//atom:link[@rel="replies"]/@thread10:count'
);
if ($list->length) {
$count = $list->item(0)->value;
}
$this->data['commentcount'] = $count;
return $this->data['commentcount'];
}
/**
* Returns a URI pointing to the HTML page where comments can be made on this entry
*
* @return string
*/
public function getCommentLink()
{
if (array_key_exists('commentlink', $this->data)) {
return $this->data['commentlink'];
}
$link = null;
$list = $this->getXpath()->query(
$this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="text/html"]/@href'
);
if ($list->length) {
$link = $list->item(0)->value;
$link = $this->absolutiseUri($link);
}
$this->data['commentlink'] = $link;
return $this->data['commentlink'];
}
/**
* Returns a URI pointing to a feed of all comments for this entry
*
* @param string $type
* @return string
*/
public function getCommentFeedLink($type = 'atom')
{
if (array_key_exists('commentfeedlink', $this->data)) {
return $this->data['commentfeedlink'];
}
$link = null;
$list = $this->getXpath()->query(
$this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="application/' . $type.'+xml"]/@href'
);
if ($list->length) {
$link = $list->item(0)->value;
$link = $this->absolutiseUri($link);
}
$this->data['commentfeedlink'] = $link;
return $this->data['commentfeedlink'];
}
/**
* Get all categories
*
* @return Collection\Category
*/
public function getCategories()
{
if (array_key_exists('categories', $this->data)) {
return $this->data['categories'];
}
if ($this->getAtomType() == Reader\Reader::TYPE_ATOM_10) {
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:category');
} else {
/**
* Since Atom 0.3 did not support categories, it would have used the
* Dublin Core extension. However there is a small possibility Atom 0.3
* may have been retrofitted to use Atom 1.0 instead.
*/
$this->getXpath()->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10);
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom10:category');
}
if ($list->length) {
$categoryCollection = new Collection\Category;
foreach ($list as $category) {
$categoryCollection[] = [
'term' => $category->getAttribute('term'),
'scheme' => $category->getAttribute('scheme'),
'label' => $category->getAttribute('label')
];
}
} else {
return new Collection\Category;
}
$this->data['categories'] = $categoryCollection;
return $this->data['categories'];
}
/**
* Get source feed metadata from the entry
*
* @return Reader\Feed\Atom\Source|null
*/
public function getSource()
{
if (array_key_exists('source', $this->data)) {
return $this->data['source'];
}
$source = null;
// TODO: Investigate why _getAtomType() fails here. Is it even needed?
if ($this->getType() == Reader\Reader::TYPE_ATOM_10) {
$list = $this->getXpath()->query($this->getXpathPrefix() . '/atom:source[1]');
if ($list->length) {
$element = $list->item(0);
$source = new Reader\Feed\Atom\Source($element, $this->getXpathPrefix());
}
}
$this->data['source'] = $source;
return $this->data['source'];
}
/**
* Attempt to absolutise the URI, i.e. if a relative URI apply the
* xml:base value as a prefix to turn into an absolute URI.
*
* @param $link
* @return string
*/
protected function absolutiseUri($link)
{
if (! Uri::factory($link)->isAbsolute()) {
if ($this->getBaseUrl() !== null) {
$link = $this->getBaseUrl() . $link;
if (! Uri::factory($link)->isValid()) {
$link = null;
}
}
}
return $link;
}
/**
* Get an author entry
*
* @param DOMElement $element
* @return string
*/
protected function getAuthorFromElement(DOMElement $element)
{
$author = [];
$emailNode = $element->getElementsByTagName('email');
$nameNode = $element->getElementsByTagName('name');
$uriNode = $element->getElementsByTagName('uri');
if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) {
$author['email'] = $emailNode->item(0)->nodeValue;
}
if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) {
$author['name'] = $nameNode->item(0)->nodeValue;
}
if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) {
$author['uri'] = $uriNode->item(0)->nodeValue;
}
if (empty($author)) {
return;
}
return $author;
}
/**
* Register the default namespaces for the current feed format
*/
protected function registerNamespaces()
{
switch ($this->getAtomType()) {
case Reader\Reader::TYPE_ATOM_03:
$this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03);
break;
default:
$this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10);
break;
}
}
/**
* Detect the presence of any Atom namespaces in use
*
* @return string
*/
protected function getAtomType()
{
$dom = $this->getDomDocument();
$prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03);
$prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10);
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03)
|| ! empty($prefixAtom03)) {
return Reader\Reader::TYPE_ATOM_03;
}
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10)
|| ! empty($prefixAtom10)) {
return Reader\Reader::TYPE_ATOM_10;
}
}
}

View file

@ -0,0 +1,536 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Extension\Atom;
use DateTime;
use DOMElement;
use Zend\Feed\Reader;
use Zend\Feed\Reader\Collection;
use Zend\Feed\Reader\Extension;
use Zend\Feed\Uri;
class Feed extends Extension\AbstractFeed
{
/**
* Get a single author
*
* @param int $index
* @return string|null
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index])) {
return $authors[$index];
}
return;
}
/**
* Get an array with feed authors
*
* @return Collection\Author
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$list = $this->xpath->query('//atom:author');
$authors = [];
if ($list->length) {
foreach ($list as $author) {
$author = $this->getAuthorFromElement($author);
if (! empty($author)) {
$authors[] = $author;
}
}
}
if (count($authors) == 0) {
$authors = new Collection\Author();
} else {
$authors = new Collection\Author(
Reader\Reader::arrayUnique($authors)
);
}
$this->data['authors'] = $authors;
return $this->data['authors'];
}
/**
* Get the copyright entry
*
* @return string|null
*/
public function getCopyright()
{
if (array_key_exists('copyright', $this->data)) {
return $this->data['copyright'];
}
$copyright = null;
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
$copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:copyright)');
} else {
$copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:rights)');
}
if (! $copyright) {
$copyright = null;
}
$this->data['copyright'] = $copyright;
return $this->data['copyright'];
}
/**
* Get the feed creation date
*
* @return DateTime|null
*/
public function getDateCreated()
{
if (array_key_exists('datecreated', $this->data)) {
return $this->data['datecreated'];
}
$date = null;
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
$dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)');
} else {
$dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)');
}
if ($dateCreated) {
$date = new DateTime($dateCreated);
}
$this->data['datecreated'] = $date;
return $this->data['datecreated'];
}
/**
* Get the feed modification date
*
* @return DateTime|null
*/
public function getDateModified()
{
if (array_key_exists('datemodified', $this->data)) {
return $this->data['datemodified'];
}
$date = null;
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
$dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)');
} else {
$dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)');
}
if ($dateModified) {
$date = new DateTime($dateModified);
}
$this->data['datemodified'] = $date;
return $this->data['datemodified'];
}
/**
* Get the feed description
*
* @return string|null
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$description = null;
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:tagline)');
} else {
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:subtitle)');
}
if (! $description) {
$description = null;
}
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Get the feed generator entry
*
* @return string|null
*/
public function getGenerator()
{
if (array_key_exists('generator', $this->data)) {
return $this->data['generator'];
}
// TODO: Add uri support
$generator = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:generator)');
if (! $generator) {
$generator = null;
}
$this->data['generator'] = $generator;
return $this->data['generator'];
}
/**
* Get the feed ID
*
* @return string|null
*/
public function getId()
{
if (array_key_exists('id', $this->data)) {
return $this->data['id'];
}
$id = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)');
if (! $id) {
if ($this->getLink()) {
$id = $this->getLink();
} elseif ($this->getTitle()) {
$id = $this->getTitle();
} else {
$id = null;
}
}
$this->data['id'] = $id;
return $this->data['id'];
}
/**
* Get the feed language
*
* @return string|null
*/
public function getLanguage()
{
if (array_key_exists('language', $this->data)) {
return $this->data['language'];
}
$language = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:lang)');
if (! $language) {
$language = $this->xpath->evaluate('string(//@xml:lang[1])');
}
if (! $language) {
$language = null;
}
$this->data['language'] = $language;
return $this->data['language'];
}
/**
* Get the feed image
*
* @return array|null
*/
public function getImage()
{
if (array_key_exists('image', $this->data)) {
return $this->data['image'];
}
$imageUrl = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:logo)');
if (! $imageUrl) {
$image = null;
} else {
$image = ['uri' => $imageUrl];
}
$this->data['image'] = $image;
return $this->data['image'];
}
/**
* Get the base URI of the feed (if set).
*
* @return string|null
*/
public function getBaseUrl()
{
if (array_key_exists('baseUrl', $this->data)) {
return $this->data['baseUrl'];
}
$baseUrl = $this->xpath->evaluate('string(//@xml:base[1])');
if (! $baseUrl) {
$baseUrl = null;
}
$this->data['baseUrl'] = $baseUrl;
return $this->data['baseUrl'];
}
/**
* Get a link to the source website
*
* @return string|null
*/
public function getLink()
{
if (array_key_exists('link', $this->data)) {
return $this->data['link'];
}
$link = null;
$list = $this->xpath->query(
$this->getXpathPrefix() . '/atom:link[@rel="alternate"]/@href' . '|' .
$this->getXpathPrefix() . '/atom:link[not(@rel)]/@href'
);
if ($list->length) {
$link = $list->item(0)->nodeValue;
$link = $this->absolutiseUri($link);
}
$this->data['link'] = $link;
return $this->data['link'];
}
/**
* Get a link to the feed's XML Url
*
* @return string|null
*/
public function getFeedLink()
{
if (array_key_exists('feedlink', $this->data)) {
return $this->data['feedlink'];
}
$link = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:link[@rel="self"]/@href)');
$link = $this->absolutiseUri($link);
$this->data['feedlink'] = $link;
return $this->data['feedlink'];
}
/**
* Get an array of any supported Pusubhubbub endpoints
*
* @return array|null
*/
public function getHubs()
{
if (array_key_exists('hubs', $this->data)) {
return $this->data['hubs'];
}
$hubs = [];
$list = $this->xpath->query($this->getXpathPrefix()
. '//atom:link[@rel="hub"]/@href');
if ($list->length) {
foreach ($list as $uri) {
$hubs[] = $this->absolutiseUri($uri->nodeValue);
}
} else {
$hubs = null;
}
$this->data['hubs'] = $hubs;
return $this->data['hubs'];
}
/**
* Get the feed title
*
* @return string|null
*/
public function getTitle()
{
if (array_key_exists('title', $this->data)) {
return $this->data['title'];
}
$title = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)');
if (! $title) {
$title = null;
}
$this->data['title'] = $title;
return $this->data['title'];
}
/**
* Get all categories
*
* @return Collection\Category
*/
public function getCategories()
{
if (array_key_exists('categories', $this->data)) {
return $this->data['categories'];
}
if ($this->getType() == Reader\Reader::TYPE_ATOM_10) {
$list = $this->xpath->query($this->getXpathPrefix() . '/atom:category');
} else {
/**
* Since Atom 0.3 did not support categories, it would have used the
* Dublin Core extension. However there is a small possibility Atom 0.3
* may have been retrofittied to use Atom 1.0 instead.
*/
$this->xpath->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10);
$list = $this->xpath->query($this->getXpathPrefix() . '/atom10:category');
}
if ($list->length) {
$categoryCollection = new Collection\Category;
foreach ($list as $category) {
$categoryCollection[] = [
'term' => $category->getAttribute('term'),
'scheme' => $category->getAttribute('scheme'),
'label' => $category->getAttribute('label')
];
}
} else {
return new Collection\Category;
}
$this->data['categories'] = $categoryCollection;
return $this->data['categories'];
}
/**
* Get an author entry in RSS format
*
* @param DOMElement $element
* @return string
*/
protected function getAuthorFromElement(DOMElement $element)
{
$author = [];
$emailNode = $element->getElementsByTagName('email');
$nameNode = $element->getElementsByTagName('name');
$uriNode = $element->getElementsByTagName('uri');
if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) {
$author['email'] = $emailNode->item(0)->nodeValue;
}
if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) {
$author['name'] = $nameNode->item(0)->nodeValue;
}
if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) {
$author['uri'] = $uriNode->item(0)->nodeValue;
}
if (empty($author)) {
return;
}
return $author;
}
/**
* Attempt to absolutise the URI, i.e. if a relative URI apply the
* xml:base value as a prefix to turn into an absolute URI.
*/
protected function absolutiseUri($link)
{
if (! Uri::factory($link)->isAbsolute()) {
if ($this->getBaseUrl() !== null) {
$link = $this->getBaseUrl() . $link;
if (! Uri::factory($link)->isValid()) {
$link = null;
}
}
}
return $link;
}
/**
* Register the default namespaces for the current feed format
*/
protected function registerNamespaces()
{
if ($this->getType() == Reader\Reader::TYPE_ATOM_10
|| $this->getType() == Reader\Reader::TYPE_ATOM_03
) {
return; // pre-registered at Feed level
}
$atomDetected = $this->getAtomType();
switch ($atomDetected) {
case Reader\Reader::TYPE_ATOM_03:
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03);
break;
default:
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10);
break;
}
}
/**
* Detect the presence of any Atom namespaces in use
*/
protected function getAtomType()
{
$dom = $this->getDomDocument();
$prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03);
$prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10);
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10)
|| ! empty($prefixAtom10)
) {
return Reader\Reader::TYPE_ATOM_10;
}
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03)
|| ! empty($prefixAtom03)
) {
return Reader\Reader::TYPE_ATOM_03;
}
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Feed\Reader\Extension\Content;
use Zend\Feed\Reader;
use Zend\Feed\Reader\Extension;
class Entry extends Extension\AbstractEntry
{
public function getContent()
{
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
$content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
} else {
$content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
}
return $content;
}
/**
* Register RSS Content Module namespace
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
}
}

Some files were not shown because too many files have changed in this diff Show more