first commit

This commit is contained in:
2018-04-23 21:08:22 +02:00
commit c68c1dd9b0
12994 changed files with 1500824 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
coverage_clover: clover.xml
json_path: coveralls-upload.json
+699
View File
@@ -0,0 +1,699 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.3.11 - TBD
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.3.10 - 2017-01-23
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#226](https://github.com/zendframework/zend-diactoros/pull/226) fixed an
issue with the `SapiStreamEmitter` causing the response body to be cast
to `(string)` and also be read as a readable stream, potentially producing
double output.
## 1.3.9 - 2017-01-17
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#223](https://github.com/zendframework/zend-diactoros/issues/223)
[#224](https://github.com/zendframework/zend-diactoros/pull/224) fixed an issue
with the `SapiStreamEmitter` consuming too much memory when producing output
for readable bodies.
## 1.3.8 - 2017-01-05
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#222](https://github.com/zendframework/zend-diactoros/pull/222) fixes the
`SapiStreamEmitter`'s handling of the `Content-Range` header to properly only
emit a range of bytes if the header value is in the form `bytes {first-last}/length`.
This allows using other range units, such as `items`, without incorrectly
emitting truncated content.
## 1.3.7 - 2016-10-11
### Added
- [#208](https://github.com/zendframework/zend-diactoros/pull/208) adds several
missing response codes to `Zend\Diactoros\Response`, including:
- 226 ('IM used')
- 308 ('Permanent Redirect')
- 444 ('Connection Closed Without Response')
- 499 ('Client Closed Request')
- 510 ('Not Extended')
- 599 ('Network Connect Timeout Error')
- [#211](https://github.com/zendframework/zend-diactoros/pull/211) adds support
for UTF-8 characters in query strings handled by `Zend\Diactoros\Uri`.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.3.6 - 2016-09-07
### Added
- [#170](https://github.com/zendframework/zend-diactoros/pull/170) prepared
documentation for publication at https://zendframework.github.io/zend-diactoros/
- [#165](https://github.com/zendframework/zend-diactoros/pull/165) adds support
for Apache `REDIRECT_HTTP_*` header detection in the `ServerRequestFactory`.
- [#166](https://github.com/zendframework/zend-diactoros/pull/166) adds support
for UTF-8 characters in URI paths.
- [#204](https://github.com/zendframework/zend-diactoros/pull/204) adds testing
against PHP 7.1 release-candidate builds.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#186](https://github.com/zendframework/zend-diactoros/pull/186) fixes a typo
in a variable name within the `SapiStreamEmitter`.
- [#200](https://github.com/zendframework/zend-diactoros/pull/200) updates the
`SapiStreamEmitter` to implement a check for `isSeekable()` prior to attempts
to rewind; this allows it to work with non-seekable streams such as the
`CallbackStream`.
- [#169](https://github.com/zendframework/zend-diactoros/pull/169) ensures that
response serialization always provides a `\r\n\r\n` sequence following the
headers, even when no message body is present, to ensure it conforms with RFC
7230.
- [#175](https://github.com/zendframework/zend-diactoros/pull/175) updates the
`Request` class to set the `Host` header from the URI host if no header is
already present. (Ensures conformity with PSR-7 specification.)
- [#197](https://github.com/zendframework/zend-diactoros/pull/197) updates the
`Uri` class to ensure that string serialization does not include a colon after
the host name if no port is present in the instance.
## 1.3.5 - 2016-03-17
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#160](https://github.com/zendframework/zend-diactoros/pull/160) fixes HTTP
protocol detection in the `ServerRequestFactory` to work correctly with HTTP/2.
## 1.3.4 - 2016-03-17
### Added
- [#119](https://github.com/zendframework/zend-diactoros/pull/119) adds the 451
(Unavailable for Legal Reasons) status code to the `Response` class.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#117](https://github.com/zendframework/zend-diactoros/pull/117) provides
validation of the HTTP protocol version.
- [#127](https://github.com/zendframework/zend-diactoros/pull/127) now properly
removes attributes with `null` values when calling `withoutAttribute()`.
- [#132](https://github.com/zendframework/zend-diactoros/pull/132) updates the
`ServerRequestFactory` to marshal the request path fragment, if present.
- [#142](https://github.com/zendframework/zend-diactoros/pull/142) updates the
exceptions thrown by `HeaderSecurity` to include the header name and/or
value.
- [#148](https://github.com/zendframework/zend-diactoros/pull/148) fixes several
stream operations to ensure they raise exceptions when the internal pointer
is at an invalid position.
- [#151](https://github.com/zendframework/zend-diactoros/pull/151) ensures
URI fragments are properly encoded.
## 1.3.3 - 2016-01-04
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#135](https://github.com/zendframework/zend-diactoros/pull/135) fixes the
behavior of `ServerRequestFactory::marshalHeaders()` to no longer omit
`Cookie` headers from the aggregated headers. While the values are parsed and
injected into the cookie params, it's useful to have access to the raw headers
as well.
## 1.3.2 - 2015-12-22
### Added
- [#124](https://github.com/zendframework/zend-diactoros/pull/124) adds four
more optional arguments to the `ServerRequest` constructor:
- `array $cookies`
- `array $queryParams`
- `null|array|object $parsedBody`
- `string $protocolVersion`
`ServerRequestFactory` was updated to pass values for each of these parameters
when creating an instance, instead of using the related `with*()` methods on
an instance.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#122](https://github.com/zendframework/zend-diactoros/pull/122) updates the
`ServerRequestFactory` to retrieve the HTTP protocol version and inject it in
the generated `ServerRequest`, which previously was not performed.
## 1.3.1 - 2015-12-16
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#113](https://github.com/zendframework/zend-diactoros/pull/113) fixes an
issue in the response serializer, ensuring that the status code in the
deserialized response is an integer.
- [#115](https://github.com/zendframework/zend-diactoros/pull/115) fixes an
issue in the various text-basd response types (`TextResponse`, `HtmlResponse`,
and `JsonResponse`); due to the fact that the constructor was not
rewinding the message body stream, `getContents()` was thus returning `null`,
as the pointer was at the end of the stream. The constructor now rewinds the
stream after populating it in the constructor.
## 1.3.0 - 2015-12-15
### Added
- [#110](https://github.com/zendframework/zend-diactoros/pull/110) adds
`Zend\Diactoros\Response\SapiEmitterTrait`, which provides the following
private method definitions:
- `injectContentLength()`
- `emitStatusLine()`
- `emitHeaders()`
- `flush()`
- `filterHeader()`
The `SapiEmitter` implementation has been updated to remove those methods and
instead compose the trait.
- [#111](https://github.com/zendframework/zend-diactoros/pull/111) adds
a new emitter implementation, `SapiStreamEmitter`; this emitter type will
loop through the stream instead of emitting it in one go, and supports content
ranges.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.2.1 - 2015-12-15
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#101](https://github.com/zendframework/zend-diactoros/pull/101) fixes the
`withHeader()` implementation to ensure that if the header existed previously
but using a different casing strategy, the previous version will be removed
in the cloned instance.
- [#103](https://github.com/zendframework/zend-diactoros/pull/103) fixes the
constructor of `Response` to ensure that null status codes are not possible.
- [#99](https://github.com/zendframework/zend-diactoros/pull/99) fixes
validation of header values submitted via request and response constructors as
follows:
- numeric (integer and float) values are now properly allowed (this solves
some reported issues with setting Content-Length headers)
- invalid header names (non-string values or empty strings) now raise an
exception.
- invalid individual header values (non-string, non-numeric) now raise an
exception.
## 1.2.0 - 2015-11-24
### Added
- [#88](https://github.com/zendframework/zend-diactoros/pull/88) updates the
`SapiEmitter` to emit a `Content-Length` header with the content length as
reported by the response body stream, assuming that
`StreamInterface::getSize()` returns an integer.
- [#77](https://github.com/zendframework/zend-diactoros/pull/77) adds a new
response type, `Zend\Diactoros\Response\TextResponse`, for returning plain
text responses. By default, it sets the content type to `text/plain;
charset=utf-8`; per the other response types, the signature is `new
TextResponse($text, $status = 200, array $headers = [])`.
- [#90](https://github.com/zendframework/zend-diactoros/pull/90) adds a new
`Zend\Diactoros\CallbackStream`, allowing you to back a stream with a PHP
callable (such as a generator) to generate the message content. Its
constructor accepts the callable: `$stream = new CallbackStream($callable);`
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#77](https://github.com/zendframework/zend-diactoros/pull/77) updates the
`HtmlResponse` to set the charset to utf-8 by default (if no content type
header is provided at instantiation).
## 1.1.4 - 2015-10-16
### Added
- [#98](https://github.com/zendframework/zend-diactoros/pull/98) adds
`JSON_UNESCAPED_SLASHES` to the default `json_encode` flags used by
`Zend\Diactoros\Response\JsonResponse`.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#96](https://github.com/zendframework/zend-diactoros/pull/96) updates
`withPort()` to allow `null` port values (indicating usage of default for
the given scheme).
- [#91](https://github.com/zendframework/zend-diactoros/pull/91) fixes the
logic of `withUri()` to do a case-insensitive check for an existing `Host`
header, replacing it with the new one.
## 1.1.3 - 2015-08-10
### Added
- [#73](https://github.com/zendframework/zend-diactoros/pull/73) adds caching of
the vendor directory to the Travis-CI configuration, to speed up builds.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#71](https://github.com/zendframework/zend-diactoros/pull/71) fixes the
docblock of the `JsonResponse` constructor to typehint the `$data` argument
as `mixed`.
- [#73](https://github.com/zendframework/zend-diactoros/pull/73) changes the
behavior in `Request` such that if it marshals a stream during instantiation,
the stream is marked as writeable (specifically, mode `wb+`).
- [#85](https://github.com/zendframework/zend-diactoros/pull/85) updates the
behavior of `Zend\Diactoros\Uri`'s various `with*()` methods that are
documented as accepting strings to raise exceptions on non-string input.
Previously, several simply passed non-string input on verbatim, others
normalized the input, and a few correctly raised the exceptions. Behavior is
now consistent across each.
- [#87](https://github.com/zendframework/zend-diactoros/pull/87) fixes
`UploadedFile` to ensure that `moveTo()` works correctly in non-SAPI
environments when the file provided to the constructor is a path.
## 1.1.2 - 2015-07-12
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#67](https://github.com/zendframework/zend-diactoros/pull/67) ensures that
the `Stream` class only accepts `stream` resources, not any resource.
## 1.1.1 - 2015-06-25
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#64](https://github.com/zendframework/zend-diactoros/pull/64) fixes the
behavior of `JsonResponse` with regards to serialization of `null` and scalar
values; the new behavior is to serialize them verbatim, without any casting.
## 1.1.0 - 2015-06-24
### Added
- [#52](https://github.com/zendframework/zend-diactoros/pull/52),
[#58](https://github.com/zendframework/zend-diactoros/pull/58),
[#59](https://github.com/zendframework/zend-diactoros/pull/59), and
[#61](https://github.com/zendframework/zend-diactoros/pull/61) create several
custom response types for simplifying response creation:
- `Zend\Diactoros\Response\HtmlResponse` accepts HTML content via its
constructor, and sets the `Content-Type` to `text/html`.
- `Zend\Diactoros\Response\JsonResponse` accepts data to serialize to JSON via
its constructor, and sets the `Content-Type` to `application/json`.
- `Zend\Diactoros\Response\EmptyResponse` allows creating empty, read-only
responses, with a default status code of 204.
- `Zend\Diactoros\Response\RedirectResponse` allows specifying a URI for the
`Location` header in the constructor, with a default status code of 302.
Each also accepts an optional status code, and optional headers (which can
also be used to provide an alternate `Content-Type` in the case of the HTML
and JSON responses).
### Deprecated
- Nothing.
### Removed
- [#43](https://github.com/zendframework/zend-diactoros/pull/43) removed both
`ServerRequestFactory::marshalUri()` and `ServerRequestFactory::marshalHostAndPort()`,
which were deprecated prior to the 1.0 release.
### Fixed
- [#29](https://github.com/zendframework/zend-diactoros/pull/29) fixes request
method validation to allow any valid token as defined by [RFC
7230](http://tools.ietf.org/html/rfc7230#appendix-B). This allows usage of
custom request methods, vs a static, hard-coded list.
## 1.0.5 - 2015-06-24
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#60](https://github.com/zendframework/zend-diactoros/pull/60) fixes
the behavior of `UploadedFile` when the `$errorStatus` provided at
instantiation is not `UPLOAD_ERR_OK`. Prior to the fix, an
`InvalidArgumentException` would occur at instantiation due to the fact that
the upload file was missing or invalid. With the fix, no exception is raised
until a call to `moveTo()` or `getStream()` is made.
## 1.0.4 - 2015-06-23
This is a security release.
A patch has been applied to `Zend\Diactoros\Uri::filterPath()` that ensures that
paths can only begin with a single leading slash. This prevents the following
potential security issues:
- XSS vectors. If the URI path is used for links or form targets, this prevents
cases where the first segment of the path resembles a domain name, thus
creating scheme-relative links such as `//example.com/foo`. With the patch,
the leading double slash is reduced to a single slash, preventing the XSS
vector.
- Open redirects. If the URI path is used for `Location` or `Link` headers,
without a scheme and authority, potential for open redirects exist if clients
do not prepend the scheme and authority. Again, preventing a double slash
corrects the vector.
If you are using `Zend\Diactoros\Uri` for creating links, form targets, or
redirect paths, and only using the path segment, we recommend upgrading
immediately.
### Added
- [#25](https://github.com/zendframework/zend-diactoros/pull/25) adds
documentation. Documentation is written in markdown, and can be converted to
HTML using [bookdown](http://bookdown.io). New features now MUST include
documentation for acceptance.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#51](https://github.com/zendframework/zend-diactoros/pull/51) fixes
`MessageTrait::getHeaderLine()` to return an empty string instead of `null` if
the header is undefined (which is the behavior specified in PSR-7).
- [#57](https://github.com/zendframework/zend-diactoros/pull/57) fixes the
behavior of how the `ServerRequestFactory` marshals upload files when they are
represented as a nested associative array.
- [#49](https://github.com/zendframework/zend-diactoros/pull/49) provides several
fixes that ensure that Diactoros complies with the PSR-7 specification:
- `MessageInterface::getHeaderLine()` MUST return a string (that string CAN be
empty). Previously, Diactoros would return `null`.
- If no `Host` header is set, the `$preserveHost` flag MUST be ignored when
calling `withUri()` (previously, Diactoros would not set the `Host` header
if `$preserveHost` was `true`, but no `Host` header was present).
- The request method MUST be a string; it CAN be empty. Previously, Diactoros
would return `null`.
- The request MUST return a `UriInterface` instance from `getUri()`; that
instance CAN be empty. Previously, Diactoros would return `null`; now it
lazy-instantiates an empty `Uri` instance on initialization.
- [ZF2015-05](http://framework.zend.com/security/advisory/ZF2015-05) was
addressed by altering `Uri::filterPath()` to prevent emitting a path prepended
with multiple slashes.
## 1.0.3 - 2015-06-04
### Added
- [#48](https://github.com/zendframework/zend-diactoros/pull/48) drops the
minimum supported PHP version to 5.4, to allow an easier upgrade path for
Symfony 2.7 users, and potential Drupal 8 usage.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 1.0.2 - 2015-06-04
### Added
- [#27](https://github.com/zendframework/zend-diactoros/pull/27) adds phonetic
pronunciation of "Diactoros" to the README file.
- [#36](https://github.com/zendframework/zend-diactoros/pull/36) adds property
annotations to the class-level docblock of `Zend\Diactoros\RequestTrait` to
ensure properties inherited from the `MessageTrait` are inherited by
implementations.
### Deprecated
- Nothing.
### Removed
- Nothing.
-
### Fixed
- [#41](https://github.com/zendframework/zend-diactoros/pull/41) fixes the
namespace for test files to begin with `ZendTest` instead of `Zend`.
- [#46](https://github.com/zendframework/zend-diactoros/pull/46) ensures that
the cookie and query params for the `ServerRequest` implementation are
initialized as arrays.
- [#47](https://github.com/zendframework/zend-diactoros/pull/47) modifies the
internal logic in `HeaderSecurity::isValid()` to use a regular expression
instead of character-by-character comparisons, improving performance.
## 1.0.1 - 2015-05-26
### Added
- [#10](https://github.com/zendframework/zend-diactoros/pull/10) adds
`Zend\Diactoros\RelativeStream`, which will return stream contents relative to
a given offset (i.e., a subset of the stream). `AbstractSerializer` was
updated to create a `RelativeStream` when creating the body of a message,
which will prevent duplication of the stream in-memory.
- [#21](https://github.com/zendframework/zend-diactoros/pull/21) adds a
`.gitattributes` file that excludes directories and files not needed for
production; this will further minify the package for production use cases.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#9](https://github.com/zendframework/zend-diactoros/pull/9) ensures that
attributes are initialized to an empty array, ensuring that attempts to
retrieve single attributes when none are defined will not produce errors.
- [#14](https://github.com/zendframework/zend-diactoros/pull/14) updates
`Zend\Diactoros\Request` to use a `php://temp` stream by default instead of
`php://memory`, to ensure requests do not create an out-of-memory condition.
- [#15](https://github.com/zendframework/zend-diactoros/pull/15) updates
`Zend\Diactoros\Stream` to ensure that write operations trigger an exception
if the stream is not writeable. Additionally, it adds more robust logic for
determining if a stream is writeable.
## 1.0.0 - 2015-05-21
First stable release, and first release as `zend-diactoros`.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
+43
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.
+228
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.
+12
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.
+34
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
+58
View File
@@ -0,0 +1,58 @@
{
"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"
},
"extra": {
"branch-alias": {
"dev-master": "1.3-dev",
"dev-develop": "1.4-dev"
}
},
"require": {
"php": "^5.4 || ^7.0",
"psr/http-message": "~1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.6 || ^5.5",
"zendframework/zend-coding-standard": "~1.0.0"
},
"provide": {
"psr/http-message-implementation": "~1.0.0"
},
"autoload": {
"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"
],
"upload-coverage": "coveralls -v",
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}
+1136
View File
File diff suppressed because it is too large Load Diff
+16
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>'
@@ -0,0 +1,152 @@
<?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;
/**
* 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);
}
}
@@ -0,0 +1,181 @@
<?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 RuntimeException;
use Psr\Http\Message\StreamInterface;
/**
* 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];
}
}
@@ -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
{
}
@@ -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
{
}
@@ -0,0 +1,156 @@
<?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;
/**
* 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 (! 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 (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
throw new InvalidArgumentException(sprintf(
'"%s" is not valid header name',
$name
));
}
}
}
+451
View File
@@ -0,0 +1,451 @@
<?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;
/**
* 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 array_key_exists(strtolower($header), $this->headerNames);
}
/**
* 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)];
$value = $this->headers[$header];
$value = is_array($value) ? $value : [$value];
return $value;
}
/**
* 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)
{
if (is_string($value)) {
$value = [$value];
}
if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) {
throw new InvalidArgumentException(
'Invalid header value; must be a string or array of strings'
);
}
HeaderSecurity::assertValidName($header);
self::assertValidHeaderValue($value);
$normalized = strtolower($header);
$new = clone $this;
if ($new->hasHeader($header)) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$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)
{
if (is_string($value)) {
$value = [ $value ];
}
if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) {
throw new InvalidArgumentException(
'Invalid header value; must be a string or array of strings'
);
}
HeaderSecurity::assertValidName($header);
self::assertValidHeaderValue($value);
if (! $this->hasHeader($header)) {
return $this->withHeader($header, $value);
}
$normalized = strtolower($header);
$header = $this->headerNames[$normalized];
$new = clone $this;
$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);
}
/**
* Test that an array contains only strings
*
* @param array $array
* @return bool
*/
private function arrayContainsOnlyStrings(array $array)
{
return array_reduce($array, [__CLASS__, 'filterStringValue'], true);
}
/**
* 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.
* @return array Filtered headers and names.
*/
private function filterHeaders(array $originalHeaders)
{
$headerNames = $headers = [];
foreach ($originalHeaders as $header => $value) {
if (! is_string($header)) {
throw new InvalidArgumentException(sprintf(
'Invalid header name; expected non-empty string, received %s',
gettype($header)
));
}
if (! is_array($value) && ! is_string($value) && ! is_numeric($value)) {
throw new InvalidArgumentException(sprintf(
'Invalid header value type; expected number, string, or array; received %s',
(is_object($value) ? get_class($value) : gettype($value))
));
}
if (is_array($value)) {
array_walk($value, function ($item) {
if (! is_string($item) && ! is_numeric($item)) {
throw new InvalidArgumentException(sprintf(
'Invalid header value type; expected number, string, or array; received %s',
(is_object($item) ? get_class($item) : gettype($item))
));
}
});
}
if (! is_array($value)) {
$value = [ $value ];
}
$headerNames[strtolower($header)] = $header;
$headers[$header] = $value;
}
return [$headerNames, $headers];
}
/**
* Test if a value is a string
*
* Used with array_reduce.
*
* @param bool $carry
* @param mixed $item
* @return bool
*/
private static function filterStringValue($carry, $item)
{
if (! is_string($item)) {
return false;
}
return $carry;
}
/**
* Assert that the provided header values are valid.
*
* @see http://tools.ietf.org/html/rfc7230#section-3.2
* @param string[] $values
* @throws InvalidArgumentException
*/
private static function assertValidHeaderValue(array $values)
{
array_walk($values, __NAMESPACE__ . '\HeaderSecurity::assertValid');
}
/**
* 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(sprintf(
'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
));
}
}
}
@@ -0,0 +1,91 @@
<?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;
/**
* 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 ($content && ! $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;
}
}
@@ -0,0 +1,178 @@
<?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 RuntimeException;
/**
* 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()
{
$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);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?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\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
/**
* 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 MessageTrait, 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)];
$value = $this->headers[$header];
$value = is_array($value) ? $value : [$value];
return $value;
}
}
@@ -0,0 +1,151 @@
<?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;
/**
* 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);
}
}
+336
View File
@@ -0,0 +1,336 @@
<?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 Psr\Http\Message\UriInterface;
/**
* 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.
*
* @property array $headers
* @property array $headerNames
* @property StreamInterface $stream
* @method bool hasHeader(string $header)
*/
trait RequestTrait
{
/**
* @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+');
list($this->headerNames, $headers) = $this->filterHeaders($headers);
$this->assertHeaders($headers);
$this->headers = $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;
}
/**
* Ensure header names and values are valid.
*
* @param array $headers
* @throws InvalidArgumentException
*/
private function assertHeaders(array $headers)
{
foreach ($headers as $name => $headerValues) {
HeaderSecurity::assertValidName($name);
array_walk($headerValues, __NAMESPACE__ . '\HeaderSecurity::assertValid');
}
}
}
+197
View File
@@ -0,0 +1,197 @@
<?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\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* 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;
/**
* Map of standard HTTP status code/reason phrases
*
* @var array
*/
private $phrases = [
// INFORMATIONAL CODES
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
// 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 Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Unordered Collection',
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 Time-out',
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+');
list($this->headerNames, $headers) = $this->filterHeaders($headers);
$this->assertHeaders($headers);
$this->headers = $headers;
}
/**
* {@inheritdoc}
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* {@inheritdoc}
*/
public function getReasonPhrase()
{
if (! $this->reasonPhrase
&& isset($this->phrases[$this->statusCode])
) {
$this->reasonPhrase = $this->phrases[$this->statusCode];
}
return $this->reasonPhrase;
}
/**
* {@inheritdoc}
*/
public function withStatus($code, $reasonPhrase = '')
{
$new = clone $this;
$new->setStatusCode($code);
$new->reasonPhrase = $reasonPhrase;
return $new;
}
/**
* Validate a status code.
*
* @param int|string $code
* @throws InvalidArgumentException on an invalid status code.
*/
private function setStatusCode($code)
{
if (! is_numeric($code)
|| is_float($code)
|| $code < 100
|| $code >= 600
) {
throw new InvalidArgumentException(sprintf(
'Invalid status code "%s"; must be an integer between 100 and 599, inclusive',
(is_scalar($code) ? $code : gettype($code))
));
}
$this->statusCode = $code;
}
/**
* Ensure header names and values are valid.
*
* @param array $headers
* @throws InvalidArgumentException
*/
private function assertHeaders(array $headers)
{
foreach ($headers as $name => $headerValues) {
HeaderSecurity::assertValidName($name);
array_walk($headerValues, __NAMESPACE__ . '\HeaderSecurity::assertValid');
}
}
}
@@ -0,0 +1,32 @@
<?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 Psr\Http\Message\ResponseInterface;
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);
}
@@ -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);
}
}
@@ -0,0 +1,74 @@
<?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;
/**
* 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;
}
}
@@ -0,0 +1,33 @@
<?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;
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;
}
}
@@ -0,0 +1,100 @@
<?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 Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
/**
* 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;
/**
* 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
) {
$body = new Stream('php://temp', 'wb+');
$body->write($this->jsonEncode($data, $encodingOptions));
$body->rewind();
$headers = $this->injectContentType('application/json', $headers);
parent::__construct($body, $status, $headers);
}
/**
* 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;
}
}
@@ -0,0 +1,46 @@
<?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;
/**
* 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);
}
}
@@ -0,0 +1,51 @@
<?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 Psr\Http\Message\ResponseInterface;
use RuntimeException;
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
* @param null|int $maxBufferLevel Maximum output buffering level to unwrap.
*/
public function emit(ResponseInterface $response, $maxBufferLevel = null)
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}
$response = $this->injectContentLength($response);
$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush($maxBufferLevel);
$this->emitBody($response);
}
/**
* Emit the message body.
*
* @param ResponseInterface $response
*/
private function emitBody(ResponseInterface $response)
{
echo $response->getBody();
}
}
@@ -0,0 +1,109 @@
<?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 Psr\Http\Message\ResponseInterface;
trait SapiEmitterTrait
{
/**
* Inject the Content-Length header if is not already present.
*
* @param ResponseInterface $response
* @return ResponseInterface
*/
private function injectContentLength(ResponseInterface $response)
{
if (! $response->hasHeader('Content-Length')) {
// PSR-7 indicates int OR null for the stream size; for null values,
// we will not auto-inject the Content-Length.
if (null !== $response->getBody()->getSize()) {
return $response->withHeader('Content-Length', (string) $response->getBody()->getSize());
}
}
return $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.
*
* @param ResponseInterface $response
*/
private function emitStatusLine(ResponseInterface $response)
{
$reasonPhrase = $response->getReasonPhrase();
header(sprintf(
'HTTP/%s %d%s',
$response->getProtocolVersion(),
$response->getStatusCode(),
($reasonPhrase ? ' ' . $reasonPhrase : '')
));
}
/**
* 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)
{
foreach ($response->getHeaders() as $header => $values) {
$name = $this->filterHeader($header);
$first = true;
foreach ($values as $value) {
header(sprintf(
'%s: %s',
$name,
$value
), $first);
$first = false;
}
}
}
/**
* Loops through the output buffer, flushing each, before emitting
* the response.
*
* @param int|null $maxBufferLevel Flush up to this buffer level.
*/
private function flush($maxBufferLevel = null)
{
if (null === $maxBufferLevel) {
$maxBufferLevel = ob_get_level();
}
while (ob_get_level() > $maxBufferLevel) {
ob_end_flush();
}
}
/**
* 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);
}
}
@@ -0,0 +1,135 @@
<?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 Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Zend\Diactoros\RelativeStream;
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)
{
if (headers_sent()) {
throw new RuntimeException('Unable to emit response; headers already sent');
}
$response = $this->injectContentLength($response);
$this->emitStatusLine($response);
$this->emitHeaders($response);
$this->flush();
$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;
}
}
@@ -0,0 +1,110 @@
<?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\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use UnexpectedValueException;
use Zend\Diactoros\AbstractSerializer;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
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 ResponseInterface
* @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'] : ''];
}
}
@@ -0,0 +1,74 @@
<?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;
/**
* 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;
}
}
+188
View File
@@ -0,0 +1,188 @@
<?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 OutOfBoundsException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* "Serve" incoming HTTP requests
*
* Given a callback, takes an incoming request, dispatches it to the
* callback, and then sends a response.
*/
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.
*
* Output buffering is enabled prior to invoking the attached
* callback; any output buffered will be sent prior to any
* response body content.
*
* @param null|callable $finalHandler
*/
public function listen(callable $finalHandler = null)
{
$callback = $this->callback;
ob_start();
$bufferLevel = ob_get_level();
$response = $callback($this->request, $this->response, $finalHandler);
if (! $response instanceof ResponseInterface) {
$response = $this->response;
}
$this->getEmitter()->emit($response, $bufferLevel);
}
/**
* 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;
}
}
@@ -0,0 +1,282 @@
<?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\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
/**
* 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 MessageTrait, 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)
{
$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');
}
}
}
}
@@ -0,0 +1,488 @@
<?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\UploadedFileInterface;
use stdClass;
use UnexpectedValueException;
/**
* 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 = static::normalizeServer($server ?: $_SERVER);
$files = static::normalizeFiles($files ?: $_FILES);
$headers = static::marshalHeaders($server);
return new ServerRequest(
$server,
$files,
static::marshalUriFromServer($server, $headers),
static::get('REQUEST_METHOD', $server, 'GET'),
'php://input',
$headers,
$cookies ?: $_COOKIE,
$query ?: $_GET,
$body ?: $_POST,
static::marshalProtocolVersion($server)
);
}
/**
* Access a value in an array, returning a default value if not found
*
* Will also do a case-insensitive search if a case sensitive search fails.
*
* @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.
*
* @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.
*
* @param array $server
* @return array
*/
public static function normalizeServer(array $server)
{
// This seems to be the only way to get the Authorization header on Apache
$apacheRequestHeaders = self::$apacheRequestHeaders;
if (isset($server['HTTP_AUTHORIZATION'])
|| ! is_callable($apacheRequestHeaders)
) {
return $server;
}
$apacheRequestHeaders = $apacheRequestHeaders();
if (isset($apacheRequestHeaders['Authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization'];
return $server;
}
if (isset($apacheRequestHeaders['authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization'];
return $server;
}
return $server;
}
/**
* Normalize uploaded files
*
* Transforms each value into an UploadedFileInterface instance, and ensures
* that nested arrays are normalized.
*
* @param array $files
* @return array
* @throws InvalidArgumentException for unrecognized values
*/
public static function normalizeFiles(array $files)
{
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
continue;
}
if (is_array($value) && isset($value['tmp_name'])) {
$normalized[$key] = self::createUploadedFileFromSpec($value);
continue;
}
if (is_array($value)) {
$normalized[$key] = self::normalizeFiles($value);
continue;
}
throw new InvalidArgumentException('Invalid value in files specification');
}
return $normalized;
}
/**
* Marshal headers from $_SERVER
*
* @param array $server
* @return array
*/
public static function marshalHeaders(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;
}
/**
* Marshal the URI from the $_SERVER array and headers
*
* @param array $server
* @param array $headers
* @return Uri
*/
public static function marshalUriFromServer(array $server, array $headers)
{
$uri = new Uri('');
// URI scheme
$scheme = 'http';
$https = self::get('HTTPS', $server);
if (($https && 'off' !== $https)
|| self::getHeader('x-forwarded-proto', $headers, false) === 'https'
) {
$scheme = 'https';
}
if (! empty($scheme)) {
$uri = $uri->withScheme($scheme);
}
// Set the host
$accumulator = (object) ['host' => '', 'port' => null];
self::marshalHostAndPortFromHeaders($accumulator, $server, $headers);
$host = $accumulator->host;
$port = $accumulator->port;
if (! empty($host)) {
$uri = $uri->withHost($host);
if (! empty($port)) {
$uri = $uri->withPort($port);
}
}
// URI path
$path = self::marshalRequestUri($server);
$path = self::stripQueryString($path);
// 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);
}
/**
* Marshal the host and port from HTTP headers and/or the PHP environment
*
* @param stdClass $accumulator
* @param array $server
* @param array $headers
*/
public static function marshalHostAndPortFromHeaders(stdClass $accumulator, array $server, array $headers)
{
if (self::getHeader('host', $headers, false)) {
self::marshalHostAndPortFromHeader($accumulator, self::getHeader('host', $headers));
return;
}
if (! isset($server['SERVER_NAME'])) {
return;
}
$accumulator->host = $server['SERVER_NAME'];
if (isset($server['SERVER_PORT'])) {
$accumulator->port = (int) $server['SERVER_PORT'];
}
if (! isset($server['SERVER_ADDR']) || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $accumulator->host)) {
return;
}
// Misinterpreted IPv6-Address
// Reported for Safari on Windows
self::marshalIpv6HostAndPort($accumulator, $server);
}
/**
* 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.
*
* 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
* @return string
*/
public static function marshalRequestUri(array $server)
{
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = self::get('IIS_WasUrlRewritten', $server);
$unencodedUrl = self::get('UNENCODED_URL', $server, '');
if ('1' == $iisUrlRewritten && ! empty($unencodedUrl)) {
return $unencodedUrl;
}
$requestUri = self::get('REQUEST_URI', $server);
// Check this first so IIS will catch.
$httpXRewriteUrl = self::get('HTTP_X_REWRITE_URL', $server);
if ($httpXRewriteUrl !== null) {
$requestUri = $httpXRewriteUrl;
}
// Check for IIS 7.0 or later with ISAPI_Rewrite
$httpXOriginalUrl = self::get('HTTP_X_ORIGINAL_URL', $server);
if ($httpXOriginalUrl !== null) {
$requestUri = $httpXOriginalUrl;
}
if ($requestUri !== null) {
return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
}
$origPathInfo = self::get('ORIG_PATH_INFO', $server);
if (empty($origPathInfo)) {
return '/';
}
return $origPathInfo;
}
/**
* Strip the query string from a path
*
* @param mixed $path
* @return string
*/
public static function stripQueryString($path)
{
if (($qpos = strpos($path, '?')) !== false) {
return substr($path, 0, $qpos);
}
return $path;
}
/**
* Marshal the host and port from the request header
*
* @param stdClass $accumulator
* @param string|array $host
* @return void
*/
private static function marshalHostAndPortFromHeader(stdClass $accumulator, $host)
{
if (is_array($host)) {
$host = implode(', ', $host);
}
$accumulator->host = $host;
$accumulator->port = null;
// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $accumulator->host, $matches)) {
$accumulator->host = substr($accumulator->host, 0, -1 * (strlen($matches[1]) + 1));
$accumulator->port = (int) $matches[1];
}
}
/**
* Marshal host/port from misinterpreted IPv6 address
*
* @param stdClass $accumulator
* @param array $server
*/
private static function marshalIpv6HostAndPort(stdClass $accumulator, array $server)
{
$accumulator->host = '[' . $server['SERVER_ADDR'] . ']';
$accumulator->port = $accumulator->port ?: 80;
if ($accumulator->port . ']' === substr($accumulator->host, strrpos($accumulator->host, ':') + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$accumulator->port = null;
}
}
/**
* Create and return an UploadedFile instance from a $_FILES specification.
*
* If the specification represents an array of values, this method will
* delegate to normalizeNestedFileSpec() and return that return value.
*
* @param array $value $_FILES struct
* @return array|UploadedFileInterface
*/
private static function createUploadedFileFromSpec(array $value)
{
if (is_array($value['tmp_name'])) {
return self::normalizeNestedFileSpec($value);
}
return new UploadedFile(
$value['tmp_name'],
$value['size'],
$value['error'],
$value['name'],
$value['type']
);
}
/**
* Normalize an array of file specifications.
*
* Loops through all nested files and returns a normalized array of
* UploadedFileInterface instances.
*
* @param array $files
* @return UploadedFileInterface[]
*/
private static function normalizeNestedFileSpec(array $files = [])
{
$normalizedFiles = [];
foreach (array_keys($files['tmp_name']) as $key) {
$spec = [
'tmp_name' => $files['tmp_name'][$key],
'size' => $files['size'][$key],
'error' => $files['error'][$key],
'name' => $files['name'][$key],
'type' => $files['type'][$key],
];
$normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
}
return $normalizedFiles;
}
/**
* Return HTTP protocol version (X.Y)
*
* @param array $server
* @return string
*/
private static function marshalProtocolVersion(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'];
}
}
+328
View File
@@ -0,0 +1,328 @@
<?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 RuntimeException;
use Psr\Http\Message\StreamInterface;
/**
* Implementation of PSR HTTP streams
*/
class Stream implements StreamInterface
{
/**
* @var resource
*/
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 {
$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);
return $stats['size'];
}
/**
* {@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) {
$error = $e;
}, E_WARNING);
$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;
}
}
+244
View File
@@ -0,0 +1,244 @@
<?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 Psr\Http\Message\UploadedFileInterface;
use RuntimeException;
class UploadedFile implements UploadedFileInterface
{
/**
* @var string
*/
private $clientFilename;
/**
* @var string
*/
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('Cannot retrieve stream due to upload 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('Cannot retrieve stream due to upload 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);
}
}
+664
View File
@@ -0,0 +1,664 @@
<?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\UriInterface;
/**
* 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 query strings and fragments.
*
* @const string
*/
const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters used in 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 (! 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))
));
}
if (! empty($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 (empty($this->host)) {
return '';
}
$authority = $this->host;
if (! empty($this->userInfo)) {
$authority = $this->userInfo . '@' . $authority;
}
if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* {@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 clone $this;
}
$new = clone $this;
$new->scheme = $scheme;
return $new;
}
/**
* {@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 password argument; received %s',
__METHOD__,
(is_object($password) ? get_class($password) : gettype($password))
));
}
$info = $user;
if ($password) {
$info .= ':' . $password;
}
if ($info === $this->userInfo) {
// Do nothing if no change was made.
return clone $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 clone $this;
}
$new = clone $this;
$new->host = $host;
return $new;
}
/**
* {@inheritdoc}
*/
public function withPort($port)
{
if (! is_numeric($port) && $port !== null) {
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))
));
}
if ($port !== null) {
$port = (int) $port;
}
if ($port === $this->port) {
// Do nothing if no change was made.
return clone $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 clone $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 clone $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 clone $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']) ? $parts['user'] : '';
$this->host = isset($parts['host']) ? $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 (! empty($scheme)) {
$uri .= sprintf('%s://', $scheme);
}
if (! empty($authority)) {
$uri .= $authority;
}
if ($path) {
if (empty($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) {
if ($host && ! $port) {
return false;
}
return true;
}
if (! $host || ! $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 (empty($scheme)) {
return '';
}
if (! array_key_exists($scheme, $this->allowedSchemes)) {
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 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 (empty($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 (! empty($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 (1 === count($data)) {
$data[] = null;
}
return $data;
}
/**
* Filter a fragment value to ensure it is properly encoded.
*
* @param null|string $fragment
* @return string
*/
private function filterFragment($fragment)
{
if (! empty($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]);
}
}
+26
View File
@@ -0,0 +1,26 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 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.
+43
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.
+234
View File
@@ -0,0 +1,234 @@
# 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-escaper/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).
## RUNNING TESTS
> ### Note: testing versions prior to 2.4
>
> This component originates with Zend Framework 2. During the lifetime of ZF2,
> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no
> changes were necessary. However, due to the migration, tests may not run on
> versions < 2.4. As such, you may need to change the PHPUnit dependency if
> attempting a fix on such a version.
To run tests:
- Clone the repository:
```console
$ git clone git@github.com:zendframework/zend-escaper.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
```
You can turn on conditional tests with the phpunit.xml file.
To do so:
- Copy `phpunit.xml.dist` file to `phpunit.xml`
- Edit `phpunit.xml` to enable any specific functionality you
want to test, as well as to provide test values to utilize.
## Running Coding Standards Checks
This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding
standards checks, and provides configuration for our selected checks.
`php-cs-fixer` is installed by default via Composer.
To run checks only:
```console
$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs
```
To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run`
flag:
```console
$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs
```
If you allow php-cs-fixer 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-escaper)
3. Clone the canonical repository locally and enter it.
```console
$ git clone git://github.com:zendframework/zend-escaper.git
$ cd zend-escaper
```
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-escaper.git
$ git fetch {username}
```
### Keeping Up-to-Date
Periodically, you should update your fork or personal repository to
match the canonical ZF 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-escaper.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.
+28
View File
@@ -0,0 +1,28 @@
Copyright (c) 2005-2015, 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.
+13
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/zendframework/zend-escaper/badge.svg?branch=master)](https://coveralls.io/r/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://zendframework.github.io/zend-escaper/
+35
View File
@@ -0,0 +1,35 @@
{
"name": "zendframework/zend-escaper",
"description": " ",
"license": "BSD-3-Clause",
"keywords": [
"zf2",
"escaper"
],
"homepage": "https://github.com/zendframework/zend-escaper",
"autoload": {
"psr-4": {
"Zend\\Escaper\\": "src/"
}
},
"require": {
"php": ">=5.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-master": "2.5-dev",
"dev-develop": "2.6-dev"
}
},
"autoload-dev": {
"psr-4": {
"ZendTest\\Escaper\\": "test/"
}
},
"require-dev": {
"fabpot/php-cs-fixer": "1.7.*",
"phpunit/PHPUnit": "~4.0"
}
}
@@ -0,0 +1,21 @@
# Configuration
`Zend\Escaper\Escaper` has only one configuration option available, and that is
the encoding to be used by the `Escaper` instance.
The default encoding is **utf-8**. Other supported encodings are:
- iso-8859-1
- iso-8859-5
- iso-8859-15
- cp866, ibm866, 866
- cp1251, windows-1251
- cp1252, windows-1252
- koi8-r, koi8-ru
- big5, big5-hkscs, 950, gb2312, 936
- shift\_jis, sjis, sjis-win, cp932
- eucjp, eucjp-win
- macroman
If an unsupported encoding is passed to `Zend\Escaper\Escaper`, a
`Zend\Escaper\Exception\InvalidArgumentException` will be thrown.
@@ -0,0 +1,74 @@
# Escaping Cascading Style Sheets
CSS is similar to [escaping Javascript](escaping-javascript.md). CSS escaping
excludes only basic alphanumeric characters and escapes all other characters
into valid CSS hexadecimal escapes.
## Example of Bad CSS Escaping
In most cases developers forget to escape CSS completely:
```php
<?php header('Content-Type: application/xhtml+xml; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
body {
background-image: url('http://example.com/foo.jpg?</style><script>alert(1)</script>');
}
INPUT;
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Unescaped CSS</title>
<meta charset="UTF-8"/>
<style>
<?= $input ?>
</style>
</head>
<body>
<p>User controlled CSS needs to be properly escaped!</p>
</body>
</html>
```
In the above example, by failing to escape the user provided CSS, an attacker
can execute an XSS attack fairly easily.
## Example of Good CSS Escaping
By using `escapeCss()` method in the CSS context, such attacks can be prevented:
```php
<?php header('Content-Type: application/xhtml+xml; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
body {
background-image: url('http://example.com/foo.jpg?</style><script>alert(1)</script>');
}
INPUT;
$escaper = new Zend\Escaper\Escaper('utf-8');
$output = $escaper->escapeCss($input);
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Escaped CSS</title>
<meta charset="UTF-8"/>
<style>
<?php
// output will look something like
// body\20 \7B \A \20 \20 \20 \20 background\2D image\3A \20 url\28 ...
echo $output;
?>
</style>
</head>
<body>
<p>User controlled CSS needs to be properly escaped!</p>
</body>
</html>
```
By properly escaping user controlled CSS, we can prevent XSS attacks in our web
applications.
@@ -0,0 +1,128 @@
# Escaping HTML Attributes
Escaping data in **HTML Attribute** contexts is most often done incorrectly, if
not overlooked completely by developers. Regular [HTML
escaping](escaping-html.md) can be used for escaping HTML attributes *only* if
the attribute value can be **guaranteed as being properly quoted**! To avoid
confusion, we recommend always using the HTML Attribute escaper method when
dealing with HTTP attributes specifically.
To escape data for an HTML Attribute, use `Zend\Escaper\Escaper`'s
`escapeHtmlAttr()` method. Internally it will convert the data to UTF-8, check
for its validity, and use an extended set of characters to escape that are not
covered by `htmlspecialchars()` to cover the cases where an attribute might be
unquoted or quoted illegally.
## Examples of Bad HTML Attribute Escaping
An example of incorrect HTML attribute escaping:
```php
<?php header('Content-Type: text/html; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
' onmouseover='alert(/ZF2!/);
INPUT;
/**
* NOTE: This is equivalent to using htmlspecialchars($input, ENT_COMPAT)
*/
$output = htmlspecialchars($input);
?>
<html>
<head>
<title>Single Quoted Attribute</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div>
<?php
// the span tag will look like:
// <span title='' onmouseover='alert(/ZF2!/);'>
?>
<span title='<?= $output ?>'>
What framework are you using?
</span>
</div>
</body>
</html>
```
In the above example, the default `ENT_COMPAT` flag is being used, which does
not escape single quotes, thus resulting in an alert box popping up when the
`onmouseover` event happens on the `span` element.
Another example of incorrect HTML attribute escaping can happen when unquoted
attributes are used (which is, by the way, perfectly valid HTML5):
```php
<?php header('Content-Type: text/html; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
faketitle onmouseover=alert(/ZF2!/);
INPUT;
// Tough luck using proper flags when the title attribute is unquoted!
$output = htmlspecialchars($input, ENT_QUOTES);
?>
<html>
<head>
<title>Quoteless Attribute</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div>
<?php
// the span tag will look like:
// <span title=faketitle onmouseover=alert(/ZF2!/);>
?>
<span title=<?= $output ?>>
What framework are you using?
</span>
</div>
</body>
</html>
```
The above example shows how it is easy to break out from unquoted attributes in
HTML5.
## Example of Good HTML Attribute Escaping
Both of the previous examples can be avoided by simply using the
`escapeHtmlAttr()` method:
```php
<?php header('Content-Type: text/html; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
faketitle onmouseover=alert(/ZF2!/);
INPUT;
$escaper = new Zend\Escaper\Escaper('utf-8');
$output = $escaper->escapeHtmlAttr($input);
?>
<html>
<head>
<title>Quoteless Attribute</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div>
<?php
// the span tag will look like:
// <span title=faketitle&#x20;onmouseover&#x3D;alert&#x28;&#x2F;ZF2&#x21;&#x2F;&#x29;&#x3B;>
?>
<span title=<?= $output ?>>
What framework are you using?
</span>
</div>
</body>
</html>
```
In the above example, the malicious input from the attacker becomes completely
harmless as we used proper HTML attribute escaping!
@@ -0,0 +1,74 @@
# Escaping HTML
Probably the most common escaping happens for **HTML body** contexts. There are
very few characters with special meaning in this context, yet it is quite common
to escape data incorrectly, namely by setting the wrong flags and character
encoding.
For escaping data to use within an HTML body context, use
`Zend\Escaper\Escaper`'s `escapeHtml()` method. Internally it uses PHP's
`htmlspecialchars()`, correctly setting the flags and encoding for you.
```php
// Outputting this without escaping would be a bad idea!
$input = '<script>alert("zf2")</script>';
$escaper = new Zend\Escaper\Escaper('utf-8');
// somewhere in an HTML template
<div class="user-provided-input">
<?= $escaper->escapeHtml($input) // all safe! ?>
</div>
```
One thing a developer needs to pay special attention to is the encoding in which
the document is served to the client, as it **must be the same** as the encoding
used for escaping!
## Example of Bad HTML Escaping
An example of incorrect usage:
```php
<?php
$input = '<script>alert("zf2")</script>';
$escaper = new Zend\Escaper\Escaper('utf-8');
?>
<?php header('Content-Type: text/html; charset=ISO-8859-1'); ?>
<!DOCTYPE html>
<html>
<head>
<title>Encodings set incorrectly!</title>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
</head>
<body>
<?php
// Bad! The escaper's and the document's encodings are different!
echo $escaper->escapeHtml($input);
?>
</body>
```
## Example of Good HTML Escaping
An example of correct usage:
```php
<?php
$input = '<script>alert("zf2")</script>';
$escaper = new Zend\Escaper\Escaper('utf-8');
?>
<?php header('Content-Type: text/html; charset=UTF-8'); ?>
<!DOCTYPE html>
<html>
<head>
<title>Encodings set correctly!</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<?php
// Good! The escaper's and the document's encodings are same!
echo $escaper->escapeHtml($input);
?>
</body>
```
@@ -0,0 +1,93 @@
# Escaping Javascript
Javascript string literals in HTML are subject to significant restrictions due
to the potential for unquoted attributes and uncertainty as to whether
Javascript will be viewed as being `CDATA` or `PCDATA` by the browser. To
eliminate any possible XSS vulnerabilities, Javascript escaping for HTML extends
the escaping rules of both ECMAScript and JSON to include any potentially
dangerous character. Very similar to HTML attribute value escaping, this means
escaping everything except basic alphanumeric characters and the comma, period,
and underscore characters as hexadecimal or unicode escapes.
Javascript escaping applies to all literal strings and digits. It is not
possible to safely escape other Javascript markup.
To escape data in the **Javascript context**, use `Zend\Escaper\Escaper`'s
`escapeJs()` method. 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.
## Example of Bad Javascript Escaping
An example of incorrect Javascript escaping:
```php
<?php header('Content-Type: application/xhtml+xml; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
bar&quot;; alert(&quot;Meow!&quot;); var xss=&quot;true
INPUT;
$output = json_encode($input);
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Unescaped Entities</title>
<meta charset="UTF-8"/>
<script type="text/javascript">
<?php
// this will result in
// var foo = "bar&quot;; alert(&quot;Meow!&quot;); var xss=&quot;true";
?>
var foo = <?= $output ?>;
</script>
</head>
<body>
<p>json_encode() is not good for escaping javascript!</p>
</body>
</html>
```
The above example will show an alert popup box as soon as the page is loaded,
because the data is not properly escaped for the Javascript context.
## Example of Good Javascript Escaping
By using the `escapeJs()` method in the Javascript context, such attacks can be
prevented:
```php
<?php header('Content-Type: text/html; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
bar&quot;; alert(&quot;Meow!&quot;); var xss=&quot;true
INPUT;
$escaper = new Zend\Escaper\Escaper('utf-8');
$output = $escaper->escapeJs($input);
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Escaped Entities</title>
<meta charset="UTF-8"/>
<script type="text/javascript">
<?php
// this will look like
// var foo =
bar\x26quot\x3B\x3B\x20alert\x28\x26quot\x3BMeow\x21\x26quot\x3B\x29\x3B\x20var\x20xss\x3D\x26quot\x3Btrue;
?>
var foo = <?= $output ?>;
</script>
</head>
<body>
<p>Zend\Escaper\Escaper::escapeJs() is good for escaping javascript!</p>
</body>
</html>
```
In the above example, the Javascript parser will most likely report a
`SyntaxError`, but at least the targeted application remains safe from such
attacks.
@@ -0,0 +1,57 @@
# Escaping URLs
This method is basically an alias for PHP's `rawurlencode()` which has applied
RFC 3986 since PHP 5.3. It is included primarily for consistency.
URL escaping applies to data being inserted into a URL and not to the whole URL
itself.
## Example of Bad URL Escaping
XSS attacks are easy if data inserted into URLs is not escaped properly:
```php
<?php header('Content-Type: application/xhtml+xml; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
" onmouseover="alert('zf2')
INPUT;
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Unescaped URL data</title>
<meta charset="UTF-8"/>
</head>
<body>
<a href="http://example.com/?name=<?= $input ?>">Click here!</a>
</body>
</html>
```
## Example of Good URL Escaping
By properly escaping data in URLs by using `escapeUrl()`, we can prevent XSS
attacks:
```php
<?php header('Content-Type: application/xhtml+xml; charset=UTF-8'); ?>
<!DOCTYPE html>
<?php
$input = <<<INPUT
" onmouseover="alert('zf2')
INPUT;
$escaper = new Zend\Escaper\Escaper('utf-8');
$output = $escaper->escapeUrl($input);
?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Unescaped URL data</title>
<meta charset="UTF-8"/>
</head>
<body>
<a href="http://example.com/?name=<?= $output ?>">Click here!</a>
</body>
</html>
```
+10
View File
@@ -0,0 +1,10 @@
<div class="container">
<div class="jumbotron">
<h1>zend-escaper</h1>
<p>Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs.</p>
<pre><code class="language-bash">$ composer require zendframework/zend-escaper</code></pre>
</div>
</div>
+51
View File
@@ -0,0 +1,51 @@
# Introduction
The [OWASP Top 10 web security risks](https://www.owasp.org/index.php/Top_10_2010-Main)
study lists Cross-Site Scripting (XSS) in second place. PHP's 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**.
zend-escaper was written with ease of use in mind, so it can be used completely stand-alone from
the rest of the framework, and as such can be installed with Composer:
```bash
$ composer install zendframework/zend-escaper
```
Several Zend Framework components provide integrations for consuming
zend-escaper, including [zend-view](https://github.com/zendframework/zend-view),
which provides a set of helpers that consume it.
> ### Security
>
> zend-escaper is a security related component. As such, if you believe you have
> found an issue, we ask that you follow our [Security Policy](http://framework.zend.com/security/)
> and report security issues accordingly. The Zend Framework team and the
> contributors thank you in advance.
## Overview
zend-escaper provides one class, `Zend\Escaper\Escaper`, which in turn provides
five methods for escaping output. Which method to use depends on the context in
which the output is used. It is up to the developer to use the right methods in
the right context.
`Zend\Escaper\Escaper` has the following escaping methods available for each context:
- `escapeHtml`: escape a string for an HTML body context.
- `escapeHtmlAttr`: escape a string for an HTML attribute context.
- `escapeJs`: escape a string for a Javascript context.
- `escapeCss`: escape a string for a CSS context.
- `escapeUrl`: escape a string for a URI or URI parameter context.
Usage of each method will be discussed in detail in later chapters.
## What zend-Escaper is not
zend-escaper is meant to be used only for *escaping data for output*, and as
such should not be misused for *filtering input data*. For such tasks, use
[zend-filter](https://zendframework.github.io/zend-filter/),
[HTMLPurifier](http://htmlpurifier.org/) or PHP's
[Filter](http://php.net/filter) functionality should be used.
@@ -0,0 +1,147 @@
# Theory of Operation
zend-escaper provides methods for escaping output data, dependent on the context
in which the data will be used. Each method is based on peer-reviewed rules and
is in compliance with the current OWASP recommendations.
The escaping follows a well-known and fixed set of encoding rules defined by
OWASP for each key HTML context. These rules cannot be impacted or negated by
browser quirks or edge-case HTML parsing unless the browser suffers a
catastrophic bug in its HTML parser or Javascript interpreter &mdash; both of
these are unlikely.
The contexts in which zend-escaper should be used are **HTML Body**, **HTML
Attribute**, **Javascript**, **CSS**, and **URL/URI** contexts.
Every escaper method will take the data to be escaped, make sure it is utf-8
encoded data (or try to convert it to utf-8), perform context-based escaping,
encode the escaped data back to its original encoding, and return the data to
the caller.
The actual escaping of the data differs between each method; they all have their
own set of rules according to which escaping is performed. An example will allow
us to clearly demonstrate the difference, and how the same characters are being
escaped differently between contexts:
```php
$escaper = new Zend\Escaper\Escaper('utf-8');
// &lt;script&gt;alert(&quot;zf2&quot;)&lt;/script&gt;
echo $escaper->escapeHtml('<script>alert("zf2")</script>');
// &lt;script&gt;alert&#x28;&quot;zf2&quot;&#x29;&lt;&#x2F;script&gt;
echo $escaper->escapeHtmlAttr('<script>alert("zf2")</script>');
// \x3Cscript\x3Ealert\x28\x22zf2\x22\x29\x3C\x2Fscript\x3E
echo $escaper->escapeJs('<script>alert("zf2")</script>');
// \3C script\3E alert\28 \22 zf2\22 \29 \3C \2F script\3E
echo $escaper->escapeCss('<script>alert("zf2")</script>');
// %3Cscript%3Ealert%28%22zf2%22%29%3C%2Fscript%3E
echo $escaper->escapeUrl('<script>alert("zf2")</script>');
```
More detailed examples will be given in later chapters.
## The Problem with Inconsistent Functionality
At present, programmers orient towards the following PHP functions for each
common HTML context:
- **HTML Body**: `htmlspecialchars()` or `htmlentities()`
- **HTML Attribute**: `htmlspecialchars()` or `htmlentities()`
- **Javascript**: `addslashes()` or `json_encode()`
- **CSS**: n/a
- **URL/URI**: `rawurlencode()` or `urlencode()`
In practice, these decisions appear to depend more on what PHP offers, and if it
can be interpreted as offering sufficient escaping safety, than it does on what
is recommended in reality to defend against XSS. While these functions can
prevent some forms of XSS, they do not cover all use cases or risks and are
therefore insufficient defenses.
Using `htmlspecialchars()` in a perfectly valid HTML5 unquoted attribute value,
for example, is completely useless since the value can be terminated by a space
(among other things), which is never escaped. Thus, in this instance, we have a
conflict between a widely used HTML escaper and a modern HTML specification,
with no specific function available to cover this use case. While it's tempting
to blame users, or the HTML specification authors, escaping just needs to deal
with whatever HTML and browsers allow.
Using `addslashes()`, custom backslash escaping, or `json_encode()` will
typically ignore HTML special characters such as ampersands, which may be used
to inject entities into Javascript. Under the right circumstances, the browser
will convert these entities into their literal equivalents before interpreting
Javascript, thus allowing attackers to inject arbitrary code.
Inconsistencies with valid HTML, insecure default parameters, lack of character
encoding awareness, and misrepresentations of what functions are capable of by
some programmers &mdash; these all make escaping in PHP an unnecessarily
convoluted quest.
To circumvent the lack of escaping methods in PHP, zend-escaper addresses the
need to apply context-specific escaping in web applications. It implements
methods that specifically target XSS and offers programmers a tool to secure
their applications without misusing other inadequate methods, or using, most
likely incomplete, home-grown solutions.
## Why Contextual Escaping?
To understand why multiple standardised escaping methods are needed, what
follows are several quick points; they are by no means a complete set of
reasons, however!
### HTML escaping of unquoted HTML attribute values still allows XSS
This is probably the best known way to defeat `htmlspecialchars()` when used on
attribute values, since any space (or character interpreted as a space &mdash;
there are a lot) lets you inject new attributes whose content can't be
neutralised by HTML escaping. The solution (where this is possible) is
additional escaping as defined by the OWASP ESAPI codecs. The point here can be
extended further &mdash; escaping only works if a programmer or designer knows
what they're doing. In many contexts, there are additional practices and gotchas
that need to be carefully monitored since escaping sometimes needs a little
extra help to protect against XSS &mdash; even if that means ensuring all
attribute values are properly double quoted despite this not being required for
valid HTML.
### HTML escaping of CSS, Javascript or URIs is often reversed when passed to non-HTML interpreters by the browser
HTML escaping is just that &mdsash; it's designed to escape a string for HTML
(i.e. prevent tag or attribute insertion), but not alter the underlying meaning
of the content, whether it be text, Javascript, CSS, or URIs. For that purpose,
a fully HTML-escaped version of any other context may still have its unescaped
form extracted before it's interpreted or executed. For this reason we need
separate escapers for Javascript, CSS, and URIs, and developers or designers
writing templates **must** know which escaper to apply to which context. Of
course, this means you need to be able to identify the correct context before
selecting the right escaper!
### DOM-based XSS requires a defence using at least two levels of different escaping in many cases
DOM-based XSS has become increasingly common as Javascript has taken off in
popularity for large scale client-side coding. A simple example is Javascript
defined in a template which inserts a new piece of HTML text into the DOM. If
the string is only HTML escaped, it may still contain Javascript that will
execute in that context. If the string is only Javascript-escaped, it may
contain HTML markup (new tags and attributes) which will be injected into the
DOM and parsed once the inserting Javascript executes. Damned either way? The
solution is to escape twice &mdash; first escape the string for HTML (make it
safe for DOM insertion), and then for Javascript (make it safe for the current
Javascript context). Nested contexts are a common means of bypassing naive
escaping habits (e.g. you can inject Javascript into a CSS expression within an
HTML attribute).
### PHP has no known anti-XSS escape functions (only those kidnapped from their original purposes)
A simple example, widely used, is when you see `json_encode()` used to escape
Javascript, or worse, some kind of mutant `addslashes()` implementation. These
were never designed to eliminate XSS, yet PHP programmers use them as such. For
example, `json_encode()` does not escape the ampersand or semi-colon characters
by default. That means you can easily inject HTML entities which could then be
decoded before the Javascript is evaluated in a HTML document. This lets you
break out of strings, add new JS statements, close tags, etc. In other words,
using `json_encode()` is insufficient and naive. The same, arguably, could be
said for `htmlspecialchars()` which has its own well known limitations that make
a singular reliance on it a questionable practice.
+17
View File
@@ -0,0 +1,17 @@
docs_dir: doc/book
site_dir: doc/html
pages:
- index.md
- Intro: intro.md
- Reference:
- "Theory of Operation": theory-of-operation.md
- Configuration: configuration.md
- "Escaping HTML": escaping-html.md
- "Escaping HTML Attributes": escaping-html-attributes.md
- "Escaping Javascript": escaping-javascript.md
- "Escaping CSS": escaping-css.md
- "Escaping URLs": escaping-url.md
site_name: zend-escaper
site_description: zend-escaper
repo_url: 'https://github.com/zendframework/zend-escaper'
copyright: 'Copyright (c) 2016 <a href="http://www.zend.com/">Zend Technologies USA Inc.</a>'
+388
View File
@@ -0,0 +1,388 @@
<?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) {
$encoding = (string) $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;
}
}
@@ -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
{
}
@@ -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
{
}
@@ -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
{
}
+94
View File
@@ -0,0 +1,94 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 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`.
+229
View File
@@ -0,0 +1,229 @@
# 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-feed/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).
## RUNNING TESTS
> ### Note: testing versions prior to 2.4
>
> This component originates with Zend Framework 2. During the lifetime of ZF2,
> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no
> changes were necessary. However, due to the migration, tests may not run on
> versions < 2.4. As such, you may need to change the PHPUnit dependency if
> attempting a fix on such a version.
To run tests:
- Clone the repository:
```console
$ git clone git@github.com:zendframework/zend-feed.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
```
You can turn on conditional tests with the phpunit.xml file.
To do so:
- Copy `phpunit.xml.dist` file to `phpunit.xml`
- Edit `phpunit.xml` to enable any specific functionality you
want to test, as well as to provide test values to utilize.
## Running Coding Standards Checks
This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding
standards checks, and provides configuration for our selected checks.
`php-cs-fixer` is installed by default via Composer.
To run checks only:
```console
$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs
```
To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run`
flag:
```console
$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs
```
If you allow php-cs-fixer 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-feed)
3. Clone the canonical repository locally and enter it.
```console
$ git clone git://github.com:zendframework/zend-feed.git
$ cd zend-feed
```
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-feed.git
$ git fetch {username}
```
### Keeping Up-to-Date
Periodically, you should update your fork or personal repository to
match the canonical ZF 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-feed.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>
```
+28
View File
@@ -0,0 +1,28 @@
Copyright (c) 2005-2015, 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.
+13
View File
@@ -0,0 +1,13 @@
# 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/zendframework/zend-feed/badge.svg?branch=master)](https://coveralls.io/r/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://zendframework.github.io/zend-feed/
+51
View File
@@ -0,0 +1,51 @@
{
"name": "zendframework/zend-feed",
"description": "provides functionality for consuming RSS and Atom feeds",
"license": "BSD-3-Clause",
"keywords": [
"zf2",
"feed"
],
"homepage": "https://github.com/zendframework/zend-feed",
"autoload": {
"psr-4": {
"Zend\\Feed\\": "src/"
}
},
"require": {
"php": "^5.5 || ^7.0",
"zendframework/zend-escaper": "^2.5",
"zendframework/zend-stdlib": "^2.7 || ^3.0"
},
"require-dev": {
"zendframework/zend-db": "^2.5",
"zendframework/zend-cache": "^2.5",
"zendframework/zend-http": "^2.5",
"zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3",
"zendframework/zend-validator": "^2.5",
"fabpot/php-cs-fixer": "1.7.*",
"phpunit/PHPUnit": "~4.0",
"psr/http-message": "^1.0"
},
"suggest": {
"psr/http-message": "PSR-7 ^1.0, 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 ehen using the Writer subcomponent"
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"branch-alias": {
"dev-master": "2.7-dev",
"dev-develop": "2.8-dev"
}
},
"autoload-dev": {
"psr-4": {
"ZendTest\\Feed\\": "test/"
}
}
}
@@ -0,0 +1,23 @@
# Consuming a Single Atom Entry
Single Atom `<entry>` elements are also valid by themselves. Usually the URL for
an entry is the feed's URL followed by `/<entryId>`, such as
`http://atom.example.com/feed/1`, using the example URL we used above. This
pattern may exist for some web services which use Atom as a container syntax.
If you read a single entry, you will have a `Zend\Feed\Reader\Entry\Atom` object.
## Reading a Single-Entry Atom Feed
```php
$entry = Zend\Feed\Reader\Reader::import('http://atom.example.com/feed/1');
echo 'Entry title: ' . $entry->getTitle();
```
> ## Importing requires an HTTP client
>
> To import a feed, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
@@ -0,0 +1,62 @@
# Consuming Atom Feeds
`Zend\Feed\Reader\Feed\Atom` is used in much the same way as
`Zend\Feed\Reader\Feed\Rss`. It provides the same access to feed-level
properties and iteration over entries in the feed. The main difference is in the
structure of the Atom protocol itself. Atom is a successor to RSS; it is a
more generalized protocol and it is designed to deal more easily with feeds that
provide their full content inside the feed, splitting RSS' `description` tag
into two elements, `summary` and `content`, for that purpose.
## Basic Use of an Atom Feed
Read an Atom feed and print the `title` and `summary` of each entry:
```php
$feed = Zend\Feed\Reader\Reader::import('http://atom.example.com/feed/');
echo 'The feed contains ' . $feed->count() . ' entries.' . "\n\n";
foreach ($feed as $entry) {
echo 'Title: ' . $entry->getTitle() . "\n";
echo 'Description: ' . $entry->getDescription() . "\n";
echo 'URL: ' . $entry->getLink() . "\n\n";
}
```
> ## Importing requires an HTTP client
>
> To import a feed, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
In an Atom feed, you can expect to find the following feed properties:
- `title`: The feed's title, same as RSS' channel title.
- `id`: Every feed and entry in Atom has a unique identifier.
- `link`: Feeds can have multiple links, which are distinguished by a `type`
attribute. The equivalent to RSS's channel link would be `type="text/html"`.
If the link is to an alternate version of the same content that's in the feed,
it would have a `rel="alternate"` attribute.
- `subtitle`: The feed's description, equivalent to RSS' channel description.
- `author`: The feed's author, with `name` and `email` sub-tags.
Atom entries commonly have the following properties:
- `id`: The entry's unique identifier.
- `title`: The entry's title, same as RSS item titles.
- `link`: A link to another format or an alternate view of this entry.
The link property of an atom entry typically has an `href` attribute.
- `summary`: A summary of this entry's content.
- `content`: The full content of the entry; can be skipped if the feed just
contains summaries.
- `author`: with `name` and `email` sub-tags like feeds have.
- `published`: the date the entry was published, in RFC 3339 format.
- `updated`: the date the entry was last updated, in RFC 3339 format.
Where relevant, `Zend\Feed` supports a number of common RSS extensions including
Dublin Core; Content, Slash, Syndication, and Syndication/Thread; and several
others in common use on blogs.
For more information on Atom and plenty of resources, see
[http://www.atomenabled.org/](http://www.atomenabled.org/).
@@ -0,0 +1,99 @@
# Consuming RSS Feeds
## Reading a feed
To read an RSS feed, pass its URL to `Zend\Feed\Reader\Reader::import()`:
```php
$channel = Zend\Feed\Reader\Reader::import('http://rss.example.com/channelName');
```
> ## Importing requires an HTTP client
>
> To import a feed, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
If any errors occur fetching the feed, a
`Zend\Feed\Reader\Exception\RuntimeException` will be thrown.
## Get properties
Once you have a feed object, you can access any of the standard RSS channel
properties via the various instance getter methods:
```php
echo $channel->getTitle();
echo $channel->getAuthor();
// etc.
```
If channel properties have attributes, the getter method will return a key/value
pair, where the key is the attribute name, and the value is the attribute value.
```php
$author = $channel->getAuthor();
echo $author['name'];
```
Most commonly, you'll want to loop through the feed and do something with its
entries. `Zend\Feed\Reader\Feed\Rss` internally converts all entries to a
`Zend\Feed\Reader\Entry\Rss` instance. Entry properties, similarly to channel
properties, can be accessed via getter methods, such as `getTitle`,
`getDescription`, etc.
An example of printing all titles of articles in a channel is:
```php
foreach ($channel as $item) {
echo $item->getTitle() . "\n";
}
```
If you are not familiar with RSS, here are the standard elements you can expect
to be available in an RSS channel and in individual RSS items (entries).
Required channel elements:
- `title`: The name of the channel.
- `link`: The URL of the web site corresponding to the channel.
- `description`: A sentence (or more) describing the channel.
Common optional channel elements:
- `pubDate`: The publication date of this set of content, in RFC 822 date
format.
- `language`: The language the channel is written in.
- `category`: One or more (specified by multiple tags) categories the channel
belongs to.
RSS `<item>` elements do not have any strictly required elements. However,
either `title` or `description` must be present.
Common item elements:
- `title`: The title of the item.
- `link`: The URL of the item.
- `description`: A synopsis of the item.
- `author`: The author's email address.
- `category`: One more categories that the item belongs to.
- `comments`: URL of comments relating to this item.
- `pubDate`: The date the item was published, in RFC 822 date format.
In your code you can always test to see if an element is non-empty by calling
the getter:
```php
if ($item->getPropname()) {
// ... proceed.
}
```
Where relevant, `Zend\Feed` supports a number of common RSS extensions including
Dublin Core, Atom (inside RSS); the Content, Slash, Syndication,
Syndication/Thread extensions; as well as several others.
Please see the official [RSS 2.0 specification](http://cyber.law.harvard.edu/rss/rss.html)
for further information.
+48
View File
@@ -0,0 +1,48 @@
# Feed Discovery from Web Pages
Web pages often contain `<link>` tags that refer to feeds with content relevant
to the particular page. `Zend\Feed\Reader\Reader` enables you to retrieve all
feeds referenced by a web page with one method call:
```php
$feedLinks = Zend\Feed\Reader\Reader::findFeedLinks('http://www.example.com/news.html');
```
> ## Finding feed links requires an HTTP client
>
> To find feed links, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
Here the `findFeedLinks()` method returns a `Zend\Feed\Reader\FeedSet` object,
which is in turn a collection of other `Zend\Feed\Reader\FeedSet` objects, each
referenced by `<link>` tags on the `news.html` web page.
`Zend\Feed\Reader\Reader` will throw a
`Zend\Feed\Reader\Exception\RuntimeException` upon failure, such as an HTTP
404 response code or a malformed feed.
You can examine all feed links located by iterating across the collection:
```php
$rssFeed = null;
$feedLinks = Zend\Feed\Reader\Reader::findFeedLinks('http://www.example.com/news.html');
foreach ($feedLinks as $link) {
if (stripos($link['type'], 'application/rss+xml') !== false) {
$rssFeed = $link['href'];
break;
}
```
Each `Zend\Feed\Reader\FeedSet` object will expose the `rel`, `href`, `type`,
and `title` properties of detected links for all RSS, Atom, or RDF feeds. You
can always select the first encountered link of each type by using a shortcut:
the first encountered link of a given type is assigned to a property named after
the feed type.
```php
$rssFeed = null;
$feedLinks = Zend\Feed\Reader\Reader::findFeedLinks('http://www.example.com/news.html');
$firstAtomFeed = $feedLinks->atom;
```
+236
View File
@@ -0,0 +1,236 @@
# HTTP Clients and zend-feed
Several operations in zend-feed's Reader subcomponent require an HTTP client:
- importing a feed
- finding links in a feed
In order to allow developers a choice in HTTP clients, the subcomponent defines
several interfaces and classes. Elsewhere in the documentation, we reference
where an HTTP client may be used; this document details what constitutes an HTTP
client and its behavior, and some of the concrete classes available within the
component for implementing this behavior.
## ClientInterface and HeaderAwareClientInterface
First, we define two interfaces for clients,
`Zend\Feed\Reader\Http\ClientInterface` and `HeaderAwareClientInterface`:
```php
namespace Zend\Feed\Reader\Http;
interface ClientInterface
{
/**
* Make a GET request to a given URL.
*
* @param string $url
* @return ResponseInterface
*/
public function get($url);
}
interface HeaderAwareClientInterface extends ClientInterface
{
/**
* Make a GET request to a given URL.
*
* @param string $url
* @param array $headers
* @return ResponseInterface
*/
public function get($url, array $headers = []);
}
```
The first is header-agnostic, and assumes that the client will simply perform an
HTTP GET request. The second allows providing headers to the client; typically,
these are used for HTTP caching headers. `$headers` must be in the following
structure:
```php
$headers = [
'X-Header-Name' => [
'header',
'values',
],
];
```
i.e., each key is a header name, and each value is an array of values for that
header. If the header represents only a single value, it should be an array with
that value:
```php
$headers = [
'Accept' => [ 'application/rss+xml' ],
];
```
A call to `get()` should yield a *response*.
## ResponseInterface and HeaderAwareResponseInterface
Responses are modeled using `Zend\Feed\Reader\Http\ResponseInterface` and
`HeaderAwareResponseInterface`:
```php
namespace Zend\Feed\Reader\Http;
class ResponseInterface
{
/**
* Retrieve the status code.
*
* @return int
*/
public function getStatusCode();
/**
* Retrieve the response body contents.
*
* @return string
*/
public function getBody();
}
class HeaderAwareResponseInterface extends ResponseInterface
{
/**
* Retrieve a named header line.
*
* Retrieve a header by name; all values MUST be concatenated to a single
* line. If no matching header is found, return the $default value.
*
* @param string $name
* @param null|string $default
* @return string
public function getHeaderLine($name, $default = null);
}
```
Internally, `Reader` will typehint against `ClientInterface` for the bulk of
operations. In some cases, however, certain capabilities are only possible if
the response can provide headers (e.g., for caching); in such cases, it will
check the instance against `HeaderAwareResponseInterface`, and only call
`getHeaderLine()` if it matches.
## Response
zend-feed ships with a generic `ResponseInterface` implementation,
`Zend\Feed\Http\Response`. It implements `HeaderAwareResponseInterface`, and
defines the following constructor:
```php
namespace Zend\Feed\Reader\Http;
class Response implements HeaderAwareResponseInterface
{
/**
* Constructor
*
* @param int $statusCode Response status code
* @param string $body Response body
* @param array $headers Response headers, if available
*/
public function __construct($statusCode, $body, array $headers = []);
}
```
## PSR-7 Response
[PSR-7](http://www.php-fig.org/psr/psr-7/) defines a set of HTTP message
interfaces, but not a client interface. To facilitate wrapping an HTTP client
that uses PSR-7 messages, we provide `Zend\Feed\Reader\Psr7ResponseDecorator`:
```php
namespace Zend\Feed\Reader\Http;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
class Psr7ResponseDecorator implements HeaderAwareResponseInterface
{
/**
* @param PsrResponseInterface $response
*/
public function __construct(PsrResponseInterface $response);
/**
* @return PsrResponseInterface
*/
public function getDecoratedResponse();
}
```
Clients can then take the PSR-7 response they receive, pass it to the decorator,
and return the decorator.
To use the PSR-7 response, you will need to add the PSR-7 interfaces to your
application, if they are not already installed by the client of your choice:
```bash
$ composer require psr/http-message
```
## zend-http
We also provide a zend-http client decorator,
`Zend\Feed\Reader\Http\ZendHttpClientDecorator`:
```php
namespace Zend\Feed\Reader\Http;
use Zend\Http\Client as HttpClient;
class ZendHttpClientDecorator implements HeaderAwareClientInterface
{
/**
* @param HttpClient $client
*/
public function __construct(HttpClient $client);
/**
* @return HttpClient
*/
public function getDecoratedClient();
}
```
Its `get()` implementation returns a `Response` instance seeded from the
zend-http response returned, including status, body, and headers.
zend-http is the default implementation assumed by `Zend\Feed\Reader\Reader`,
but *is not installed by default*. You may install it using composer:
```bash
$ composer require zendframework/zend-http
```
## Providing a client to Reader
By default, `Zend\Feed\Reader\Reader` will lazy load a zend-http client. If you
have not installed zend-http, however, PHP will raise an error indicating the
class is not found!
As such, you have two options:
1. Install zend-http: `composer require zendframework/zend-http`.
2. Inject the `Reader` with your own HTTP client.
To accomplish the second, you will need an implementation of
`Zend\Feed\Reader\Http\ClientInterface` or `HeaderAwareClientInterface`, and an
instance of that implementation. Once you do, you can use the static method
`setHttpClient()` to inject it.
As an example, let's say you've created a PSR-7-based implementation named
`My\Http\Psr7FeedClient`. You could then do the following:
```php
use My\Http\Psr7FeedClient;
use Zend\Feed\Reader\Reader;
Reader::setHttpClient(new Psr7FeedClient());
```
Your client will then be used for all `import()` and `findFeedLinks()`
operations.
+49
View File
@@ -0,0 +1,49 @@
# Importing Feeds
`Zend\Feed` enables developers to retrieve feeds via `Zend\Feader\Reader`. If
you know the URI of a feed, use the `Zend\Feed\Reader\Reader::import()` method
to consume it:
```php
$feed = Zend\Feed\Reader\Reader::import('http://feeds.example.com/feedName');
```
> ## Importing requires an HTTP client
>
> To import a feed, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
You can also use `Zend\Feed\Reader\Reader` to fetch the contents of a feed from
a file or the contents of a PHP string variable:
```php
// importing a feed from a text file
$feedFromFile = Zend\Feed\Reader\Reader::importFile('feed.xml');
// importing a feed from a PHP string variable
$feedFromPHP = Zend\Feed\Reader\Reader::importString($feedString);
```
In each of the examples above, an object of a class that extends
`Zend\Feed\Reader\Feed\AbstractFeed` is returned upon success, depending on the
type of the feed. If an RSS feed were retrieved via one of the import methods
above, then a `Zend\Feed\Reader\Feed\Rss` object would be returned. On the other
hand, if an Atom feed were imported, then a `Zend\Feed\Reader\Feed\Atom` object
is returned. The import methods will also throw a
`Zend\Feed\Exception\Reader\RuntimeException` object upon failure, such as an
unreadable or malformed feed.
## Dumping the contents of a feed
To dump the contents of a `Zend\Feed\Reader\Feed\AbstractFeed` instance, you may
use the `saveXml()` method.
```php
assert($feed instanceof Zend\Feed\Reader\Feed\AbstractFeed);
// dump the feed to standard output
print $feed->saveXml();
```
+10
View File
@@ -0,0 +1,10 @@
<div class="container">
<div class="jumbotron">
<h1>zend-feed</h1>
<p>Consume and generate Atom and RSS feeds, and interact with Pubsubhubbub.</p>
<pre><code class="language-bash">$ composer require zendframework/zend-feed</code></pre>
</div>
</div>
+62
View File
@@ -0,0 +1,62 @@
# Introduction
`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.
In the future, this modification support could provide support for the Atom
Publishing Protocol.
`Zend\Feed` consists of `Zend\Feed\Reader` for reading RSS and Atom feeds,
`Zend\Feed\Writer` for writing RSS and Atom feeds, and `Zend\Feed\PubSubHubbub`
for working with Hub servers. Furthermore, both `Zend\Feed\Reader` and
`Zend\Feed\Writer` support extensions which allows for working with additional
data in feeds, not covered in the core API but used in conjunction with RSS and
Atom feeds.
In the example below, we demonstrate a simple use case of retrieving an RSS feed
and saving relevant portions of the feed data to a simple PHP array, which could
then be used for printing the data, storing to a database, etc.
> ## RSS optional properties
>
> Many *RSS* feeds have different channel and item properties available. The
> *RSS* specification provides for many optional properties, so be aware of this
> when writing code to work with *RSS* data. `Zend\Feed` supports all optional
> properties of the core *RSS* and *Atom* specifications.
## Reading RSS Feed Data
```php
// Fetch the latest Slashdot headlines
try {
$slashdotRss =
Zend\Feed\Reader\Reader::import('http://rss.slashdot.org/Slashdot/slashdot');
} catch (Zend\Feed\Reader\Exception\RuntimeException $e) {
// feed import failed
echo "Exception caught importing feed: {$e->getMessage()}\n";
exit;
}
// Initialize the channel/feed data array
$channel = [
'title' => $slashdotRss->getTitle(),
'link' => $slashdotRss->getLink(),
'description' => $slashdotRss->getDescription(),
'items' => [],
];
// Loop over each channel item/entry and store relevant data for each
foreach ($slashdotRss as $item) {
$channel['items'][] = [
'title' => $item->getTitle(),
'link' => $item->getLink(),
'description' => $item->getDescription(),
];
}
```
Your `$channel` array now contains the basic meta-information for the RSS
channel and all items that it contained. The process is identical for Atom
feeds since `Zend\Feed` provides a common feed API; i.e. all getters and
setters are the same regardless of feed format.
+452
View File
@@ -0,0 +1,452 @@
# Zend\\Feed\\PubSubHubbub
`Zend\Feed\PubSubHubbub` is an implementation of the [PubSubHubbub Core 0.2/0.3
Specification (Working Draft)](http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.3.html).
It offers implementations of a Pubsubhubbub Publisher and Subscriber suited to
PHP applications.
## What is PubSubHubbub?
Pubsubhubbub is an open, simple, web-scale, pubsub protocol. A common use case
to enable blogs (Publishers) to "push" updates from their RSS or Atom feeds
(Topics) to end Subscribers. These Subscribers will have subscribed to the
blog's RSS or Atom feed via a Hub, a central server which is notified of any
updates by the Publisher, and which then distributes these updates to all
Subscribers. Any feed may advertise that it supports one or more Hubs using an
Atom namespaced link element with a rel attribute of "hub" (i.e., `rel="hub"`).
Pubsubhubbub has garnered attention because it is a pubsub protocol which is
easy to implement and which operates over HTTP. Its philosophy is to replace the
traditional model where blog feeds have been polled at regular intervals to
detect and retrieve updates. Depending on the frequency of polling, this can
take a lot of time to propagate updates to interested parties from planet
aggregators to desktop readers. With a pubsub system in place, updates are not
simply polled by Subscribers, they are pushed to Subscribers, eliminating any
delay. For this reason, Pubsubhubbub forms part of what has been dubbed the
real-time web.
The protocol does not exist in isolation. Pubsub systems have been around for a
while, such as the familiar Jabber Publish-Subscribe protocol,
[XEP-0060](http://www.xmpp.org/extensions/xep-0060.html), or the less well-known
[rssCloud](http://www.rssboard.org/rsscloud-interface) (described in 2001).
However, these have not achieved widespread adoption due to either their
complexity, poor timing, or lack of suitability for web applications. rssCloud,
which was recently revived as a response to the appearance of Pubsubhubbub, has
also seen its usage increase significantly, though it lacks a formal
specification and currently does not support Atom 1.0 feeds.
Perhaps surprisingly given its relative early age, Pubsubhubbub is already in
use including in Google Reader and Feedburner, and there are plugins available
for Wordpress blogs.
## Architecture
`Zend\Feed\PubSubHubbub` implements two sides of the Pubsubhubbub 0.2/0.3
Specification: a Publisher and a Subscriber. It does not currently implement a
Hub Server.
A Publisher is responsible for notifying all supported Hubs (many can be
supported to add redundancy to the system) of any updates to its feeds, whether
they be Atom or RSS based. This is achieved by pinging the supported Hub Servers
with the URL of the updated feed. In Pubsubhubbub terminology, any updatable
resource capable of being subscribed to is referred to as a Topic. Once a ping
is received, the Hub will request the updated feed, process it for updated
items, and forward all updates to all Subscribers subscribed to that feed.
A Subscriber is any party or application which subscribes to one or more Hubs to
receive updates from a Topic hosted by a Publisher. The Subscriber never
directly communicates with the Publisher since the Hub acts as an intermediary,
accepting subscriptions and sending updates to Subscribers. The Subscriber
therefore communicates only with the Hub, either to subscribe or unsubscribe to
Topics, or when it receives updates from the Hub. This communication design
("Fat Pings") effectively removes the possibility of a "Thundering Herd" issue.
(Thundering Herds occur in a pubsub system where the Hub merely informs
Subscribers that an update is available, prompting all Subscribers to
immediately retrieve the feed from the Publisher, giving rise to a traffic
spike.) In Pubsubhubbub, the Hub distributes the actual update in a "Fat Ping"
so the Publisher is not subjected to any traffic spike.
`Zend\Feed\PubSubHubbub` implements Pubsubhubbub Publishers and Subscribers with
the classes `Zend\Feed\PubSubHubbub\Publisher` and
`Zend\Feed\PubSubHubbub\Subscriber`. In addition, the Subscriber implementation
may handle any feed updates forwarded from a Hub by using
`Zend\Feed\PubSubHubbub\Subscriber\Callback`. These classes, their use cases,
and etheir APIs are covered in subsequent sections.
## Zend\\Feed\\PubSubHubbub\\Publisher
In Pubsubhubbub, the Publisher is the party publishing a live feed with content
updates. This may be a blog, an aggregator, or even a web service with a public
feed based API. In order for these updates to be pushed to Subscribers, the
Publisher must notify all of its supported Hubs that an update has occurred
using a simple HTTP POST request containing the URI of the updated Topic (i.e.,
the updated RSS or Atom feed). The Hub will confirm receipt of the notification,
fetch the updated feed, and forward any updates to any Subscribers who have
subscribed to that Hub for updates from the relevant feed.
By design, this means the Publisher has very little to do except send these Hub
pings whenever its feeds change. As a result, the Publisher implementation is
extremely simple to use and requires very little work to setup and use when
feeds are updated.
`Zend\Feed\PubSubHubbub\Publisher` implements a full Pubsubhubbub Publisher. Its
setup for use primarily requires that it is configured with the URI endpoint for
all Hubs to be notified of updates, and the URIs of all Topics to be included in
the notifications.
The following example shows a Publisher notifying a collection of Hubs about
updates to a pair of local RSS and Atom feeds. The class retains a collection of
errors which include the Hub URLs, so that notification can be attempted again
later and/or logged if any notifications happen to fail. Each resulting error
array also includes a "response" key containing the related HTTP response
object. In the event of any errors, it is strongly recommended to attempt the
operation for failed Hub Endpoints at least once more at a future time. This may
require the use of either a scheduled task for this purpose or a job queue,
though such extra steps are optional.
```php
use Zend\Feed\PubSubHubbub\Publisher;
$publisher = Publisher;
$publisher->addHubUrls([
'http://pubsubhubbub.appspot.com/',
'http://hubbub.example.com',
]);
$publisher->addUpdatedTopicUrls([
'http://www.example.net/rss',
'http://www.example.net/atom',
]);
$publisher->notifyAll();
if (! $publisher->isSuccess()) {
// check for errors
$errors = $publisher->getErrors();
$failedHubs = [];
foreach ($errors as $error) {
$failedHubs[] = $error['hubUrl'];
}
}
// reschedule notifications for the failed Hubs in $failedHubs
```
If you prefer having more concrete control over the Publisher, the methods
`addHubUrls()` and `addUpdatedTopicUrls()` pass each array value to the singular
`addHubUrl()` and `addUpdatedTopicUrl()` public methods. There are also matching
`removeUpdatedTopicUrl()` and `removeHubUrl()` methods.
You can also skip setting Hub URIs, and notify each in turn using the
`notifyHub()` method which accepts the URI of a Hub endpoint as its only
argument.
There are no other tasks to cover. The Publisher implementation is very simple
since most of the feed processing and distribution is handled by the selected
Hubs. It is, however, important to detect errors and reschedule notifications as
soon as possible (with a reasonable maximum number of retries) to ensure
notifications reach all Subscribers. In many cases, as a final alternative, Hubs
may frequently poll your feeds to offer some additional tolerance for failures
both in terms of their own temporary downtime or Publisher errors or downtime.
## Zend\\Feed\\PubSubHubbub\\Subscriber
In Pubsubhubbub, the Subscriber is the party who wishes to receive updates to
any Topic (RSS or Atom feed). They achieve this by subscribing to one or more of
the Hubs advertised by that Topic, usually as a set of one or more Atom 1.0
links with a rel attribute of "hub" (i.e., `rel="hub"`). The Hub from that point
forward will send an Atom or RSS feed containing all updates to that
Subscriber's callback URL when it receives an update notification from the
Publisher. In this way, the Subscriber need never actually visit the original
feed (though it's still recommended at some level to ensure updates are
retrieved if ever a Hub goes offline). All subscription requests must contain
the URI of the Topic being subscribed and a callback URL which the Hub will use
to confirm the subscription and to forward updates.
The Subscriber therefore has two roles. The first is to *create* and *manage*
subscriptions, including subscribing for new Topics with a Hub, unsubscribing
(if necessary), and periodically renewing subscriptions, since they may have an
expiry set by the Hub. This is handled by `Zend\Feed\PubSubHubbub\Subscriber`.
The second role is to *accept updates* sent by a Hub to the Subscriber's
callback URL, i.e. the URI the Subscriber has assigned to handle updates. The
callback URL also handles events where the Hub contacts the Subscriber to
confirm all subscriptions and unsubscriptions. This is handled by using an
instance of `Zend\Feed\PubSubHubbub\Subscriber\Callback` when the callback URL
is accessed.
> ### Query strings in callback URLs
>
> `Zend\Feed\PubSubHubbub\Subscriber` implements the Pubsubhubbub 0.2/0.3
> specification. As this is a new specification version, not all Hubs currently
> implement it. The new specification allows the callback URL to include a query
> string which is used by this class, but not supported by all Hubs. In the
> interests of maximising compatibility, it is therefore recommended that the
> query string component of the Subscriber callback URI be presented as a path
> element, i.e. recognised as a parameter in the route associated with the
> callback URI and used by the application's router.
### Subscribing and Unsubscribing
`Zend\Feed\PubSubHubbub\Subscriber` implements a full Pubsubhubbub Subscriber
capable of subscribing to, or unsubscribing from, any Topic via any Hub
advertised by that Topic. It operates in conjunction with
`Zend\Feed\PubSubHubbub\Subscriber\Callback`, which accepts requests from a Hub
to confirm all subscription or unsubscription attempts (to prevent third-party
misuse).
Any subscription (or unsubscription) requires the relevant information before
proceeding, i.e. the URI of the Topic (Atom or RSS feed) to be subscribed to for
updates, and the URI of the endpoint for the Hub which will handle the
subscription and forwarding of the updates. The lifetime of a subscription may
be determined by the Hub, but most Hubs should support automatic subscription
refreshes by checking with the Subscriber. This is supported by
`Zend\Feed\PubSubHubbub\Subscriber\Callback` and requires no other work on your
part. It is still strongly recommended that you use the Hub-sourced subscription
time-to.live (ttl) to schedule the creation of new subscriptions (the process is
identical to that for any new subscription) to refresh it with the Hub. While it
should not be necessary per se, it covers cases where a Hub may not support
automatic subscription refreshing, and rules out Hub errors for additional
redundancy.
With the relevant information to hand, a subscription can be attempted as
demonstrated below:
```php
use Zend\Feed\PubSubHubbub\Model\Subscription;
use Zend\Feed\PubSubHubbub\Subscriber;
$storage = new Subscription;
$subscriber = new Subscriber;
$subscriber->setStorage($storage);
$subscriber->addHubUrl('http://hubbub.example.com');
$subscriber->setTopicUrl('http://www.example.net/rss.xml');
$subscriber->setCallbackUrl('http://www.mydomain.com/hubbub/callback');
$subscriber->subscribeAll();
```
In order to store subscriptions and offer access to this data for general use,
the component requires a database (a schema is provided later in this section).
By default, it is assumed the table name is "subscription", and it utilises
`Zend\Db\TableGateway\TableGateway` in the background, meaning it will use the
default adapter you have set for your application. You may also pass a specific
custom `Zend\Db\TableGateway\TableGateway` instance into the associated model
`Zend\Feed\PubSubHubbub\Model\Subscription`. This custom adapter may be as
simple in intent as changing the table name to use or as complex as you deem
necessary.
While this model is offered as a default ready-to-roll solution, you may create
your own model using any other backend or database layer (e.g. Doctrine) so long
as the resulting class implements the interface
`Zend\Feed\PubSubHubbub\Model\SubscriptionInterface`.
An example schema (MySQL) for a subscription table accessible by the provided
model may look similar to:
```sql
CREATE TABLE IF NOT EXISTS `subscription` (
`id` varchar(32) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`topic_url` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`hub_url` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`created_time` datetime DEFAULT NULL,
`lease_seconds` bigint(20) DEFAULT NULL,
`verify_token` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`secret` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`expiration_time` datetime DEFAULT NULL,
`subscription_state` varchar(12) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
```
Behind the scenes, the Subscriber above will send a request to the Hub endpoint
containing the following parameters (based on the previous example):
Parameter | Value | Explanation
--------- | ----- | -----------
`hub.callback` | `http://www.mydomain.com/hubbub/callback?xhub.subscription=5536df06b5dcb966edab3a4c4d56213c16a8184` | The URI used by a Hub to contact the Subscriber and either request confirmation of a (un)subscription request, or send updates from subscribed feeds. The appended query string contains a custom parameter (hence the xhub designation). It is a query string parameter preserved by the Hub and re-sent with all Subscriber requests. Its purpose is to allow the Subscriber to identify and look up the subscription associated with any Hub request in a backend storage medium. This is a non-standard parameter used by this component in preference to encoding a subscription key in the URI path, which is difficult to enforce generically. Nevertheless, since not all Hubs support query string parameters, we still strongly recommend adding the subscription key as a path component in the form `http://www.mydomain.com/hubbub/callback/5536df06b5dcb966edab3a4c4d56213c16a8184`. This requires defining a route capable of parsing out the final value of the key, retrieving the value, and passing it to the Subscriber callback object. The value should be passed into the method `Zend\PubSubHubbub\Subscriber\Callback::setSubscriptionKey()`. A detailed example is offered later.
`hub.lease_seconds` | `2592000` | The number of seconds for which the Subscriber would like a new subscription to remain valid (i.e. a TTL). Hubs may enforce their own maximum subscription period. All subscriptions should be renewed by re-subscribing before the subscription period ends to ensure continuity of updates. Hubs should additionally attempt to automatically refresh subscriptions before they expire by contacting Subscribers (handled automatically by the `Callback` class).
`hub.mode` | `subscribe` | Value indicating this is a subscription request. Unsubscription requests would use the "unsubscribe" value.
`hub.topic` | `http://www.example.net/rss.xml` | The URI of the Topic (i.e. Atom or RSS feed) which the Subscriber wishes to subscribe to for updates.
`hub.verify` | `sync` or `async` | Indicates to the Hub the preferred mode of verifying subscriptions or unsubscriptions. It is repeated twice in order of preference. Technically this component does not distinguish between the two modes and treats both equally.
`hub.verify_token` | `3065919804abcaa7212ae89.879827871253878386` | A verification token returned to the Subscriber by the Hub when it is confirming a subscription or unsubscription. Offers a measure of reliance that the confirmation request originates from the correct Hub to prevent misuse.
You can modify several of these parameters to indicate a different preference.
For example, you can set a different lease seconds value using
`Zend\Feed\PubSubHubbub\Subscriber::setLeaseSeconds(),` or show a preference for
the `async` verify mode by using `setPreferredVerificationMode(Zend\Feed\PubSubHubbub\PubSubHubbub::VERIFICATION_MODE_ASYNC)`.
However, the Hubs retain the capability to enforce their own preferences, and
for this reason the component is deliberately designed to work across almost any
set of options with minimum end-user configuration required. Conventions are
great when they work!
> ### Verification modes
>
> While Hubs may require the use of a specific verification mode (both are
> supported by `Zend\Feed\PubSubHubbub`), you may indicate a specific preference
> using the `setPreferredVerificationMode()` method. In `sync` (synchronous)
> mode, the Hub attempts to confirm a subscription as soon as it is received,
> and before responding to the subscription request. In `async` (asynchronous)
> mode, the Hub will return a response to the subscription request immediately,
> and its verification request may occur at a later time. Since
> `Zend\Feed\PubSubHubbub` implements the Subscriber verification role as a
> separate callback class and requires the use of a backend storage medium, it
> actually supports both transparently. In terms of end-user performance,
> asynchronous verification is very much preferred to eliminate the impact of a
> poorly performing Hub tying up end-user server resources and connections for
> too long.
Unsubscribing from a Topic follows the exact same pattern as the previous
example, with the exception that we should call `unsubscribeAll()` instead. The
parameters included are identical to a subscription request with the exception
that `hub.mode` is set to "unsubscribe".
By default, a new instance of `Zend\PubSubHubbub\Subscriber` will attempt to use
a database backed storage medium which defaults to using the default zend-db
adapter with a table name of "subscription". It is recommended to set a custom
storage solution where these defaults are not apt either by passing in a new
model supporting the required interface or by passing a new instance of
`Zend\Db\TableGateway\TableGateway` to the default model's constructor to change
the used table name.
### Handling Subscriber Callbacks
Whenever a subscription or unsubscription request is made, the Hub must verify
the request by forwarding a new verification request to the callback URL set in
the subscription or unsubscription parameters. To handle these Hub requests,
which will include all future communications containing Topic (feed) updates,
the callback URL should trigger the execution of an instance of
`Zend\Feed\PubSubHubbub\Subscriber\Callback` to handle the request.
The `Callback` class should be configured to use the same storage medium as the
`Subscriber` class. The bulk of the work is handled internal to these classes.
```php
use Zend\Feed\PubSubHubbub\Model\Subscription;
use Zend\Feed\PubSubHubbub\Subscriber\Callback;
$storage = new Subscription();
$callback = new Callback();
$callback->setStorage($storage);
$callback->handle();
$callback->sendResponse();
/*
* Check if the callback resulting in the receipt of a feed update.
* Otherwise it was either a (un)sub verification request or invalid request.
* Typically we need do nothing other than add feed update handling; the rest
* is handled internally by the class.
*/
if ($callback->hasFeedUpdate()) {
$feedString = $callback->getFeedUpdate();
/*
* Process the feed update asynchronously to avoid a Hub timeout.
*/
}
```
> #### Query and body parameters
>
> It should be noted that `Zend\Feed\PubSubHubbub\Subscriber\Callback` may
> independently parse any incoming query string and other parameters. This is
> necessary since PHP alters the structure and keys of a query string when it is
> parsed into the `$_GET` or `$_POST` superglobals; for example, all duplicate
> keys are ignored and periods are converted to underscores. Pubsubhubbub
> features both of these in the query strings it generates.
> #### Always delay feed processing
>
> It is essential that developers recognise that Hubs are only concerned with
> sending requests and receiving a response which verifies its receipt. If a
> feed update is received, it should never be processed on the spot since this
> leaves the Hub waiting for a response. Rather, any processing should be
> offloaded to another process or deferred until after a response has been
> returned to the Hub. One symptom of a failure to promptly complete Hub
> requests is that a Hub may continue to attempt delivery of the update or
> verification request leading to duplicated update attempts being processed by
> the Subscriber. This appears problematic, but in reality a Hub may apply a
> timeout of just a few seconds, and if no response is received within that time
> it may disconnect (assuming a delivery failure) and retry later. Note that
> Hubs are expected to distribute vast volumes of updates so their resources are
> stretched; please process feeds asynchronously (e.g. in a separate process or
> a job queue or even a cronjob) as much as possible.
### Setting Up And Using A Callback URL Route
As noted earlier, the `Zend\Feed\PubSubHubbub\Subscriber\Callback` class
receives the combined key associated with any subscription from the Hub via one
of two methods. The technically preferred method is to add this key to the
callback URL employed by the Hub in all future requests using a query string
parameter with the key `xhub.subscription`. However, for historical reasons
(primarily that this was not supported in Pubsubhubbub 0.1, and a late addition
to 0.2 ), it is strongly recommended to use the most compatible means of adding
this key to the callback URL by appending it to the URL's path.
Thus the URL `http://www.example.com/callback?xhub.subscription=key` would become
`http://www.example.com/callback/key`.
Since the query string method is the default in anticipation of a greater level
of future support for the full 0.2/0.3 specification, this requires some
additional work to implement.
The first step is to make the `Zend\Feed\PubSubHubbub\Subscriber\Callback` class
aware of the path contained subscription key. It's manually injected; therefore
it also requires manually defining a route for this purpose. This is achieved by
called the method `Zend\Feed\PubSubHubbub\Subscriber\Callback::setSubscriptionKey()`
with the parameter being the key value available from the router. The example
below demonstrates this using a zend-mvc controller.
```php
use Zend\Feed\PubSubHubbub\Model\Subscription;
use Zend\Feed\PubSubHubbub\Subscriber\Callback;
use Zend\Mvc\Controller\AbstractActionController;
class CallbackController extends AbstractActionController
{
public function indexAction()
{
$storage = new Subscription();
$callback = new Callback();
$callback->setStorage($storage);
/*
* Inject subscription key parsing from URL path using
* a parameter from the router.
*/
$subscriptionKey = $this->params()->fromRoute('subkey');
$callback->setSubscriptionKey($subscriptionKey);
$callback->handle();
$callback->sendResponse();
/*
* Check if the callback resulting in the receipt of a feed update.
* Otherwise it was either a (un)sub verification request or invalid
* request. Typically we need do nothing other than add feed update
* handling; the rest is handled internally by the class.
*/
if ($callback->hasFeedUpdate()) {
$feedString = $callback->getFeedUpdate();
/*
* Process the feed update asynchronously to avoid a Hub timeout.
*/
}
}
}
```
The example below illustrates adding a route mapping the path segment to a route
parameter, using zend-mvc:
```php
use Zend\Mvc\Router\Http\Segment as SegmentRoute;;
// Route defininition for enabling appending of a PuSH Subscription's lookup key
$route = SegmentRoute::factory([
'route' => '/callback/:subkey',
'constraints' => [
'subkey' => '[a-z0-9]+',
],
'defaults' => [
'controller' => 'application-index',
'action' => 'index',
]
]);
```
+825
View File
@@ -0,0 +1,825 @@
# Zend\\Feed\\Reader
`Zend\Feed\Reader` is a component used to consume RSS and Atom feeds of
any version, including RDF/RSS 1.0, RSS 2.0, Atom 0.3, and Atom 1.0. The API for
retrieving feed data is deliberately simple since `Zend\Feed\Reader` is capable
of searching any feed of any type for the information requested through the API.
If the typical elements containing this information are not present, it will
adapt and fall back on a variety of alternative elements instead. This ability
to choose from alternatives removes the need for users to create their own
abstraction layer on top of the component to make it useful or have any in-depth
knowledge of the underlying standards, current alternatives, and namespaced
extensions.
Internally, the `Zend\Feed\Reader\Reader` class works almost entirely on the
basis of making XPath queries against the feed XML's Document Object Model. This
singular approach to parsing is consistent, and the component offers a plugin
system to add to the Feed and Entry APIs by writing extensions on a similar
basis.
Performance is assisted in three ways. First of all, `Zend\Feed\Reader\Reader`
supports caching using [zend-cache](https://github.com/zendframework/zend-cache)
to maintain a copy of the original feed XML. This allows you to skip network
requests for a feed URI if the cache is valid. Second, the Feed and Entry APIs
are backed by an internal cache (non-persistent) so repeat API calls for the
same feed will avoid additional DOM or XPath use. Thirdly, importing feeds from
a URI can take advantage of HTTP Conditional `GET` requests which allow servers
to issue an empty 304 response when the requested feed has not changed since the
last time you requested it. In the final case, an zend-cache storage instance
will hold the last received feed along with the ETag and Last-Modified header
values sent in the HTTP response.
`Zend\Feed\Reader\Reader` is not capable of constructing feeds, and delegates
this responsibility to `Zend\Feed\Writer\Writer`.
## Importing Feeds
Feeds can be imported from a string, file or a URI. Importing from a URI can
additionally utilise an HTTP Conditional `GET` request. If importing fails, an
exception will be raised. The end result will be an object of type
`Zend\Feed\Reader\Feed\AbstractFeed`, the core implementations of which are
`Zend\Feed\Reader\Feed\Rss` and `Zend\Feed\Reader\Feed\Atom`. Both objects
support multiple (all existing) versions of these broad feed types.
In the following example, we import an RDF/RSS 1.0 feed and extract some basic
information that can be saved to a database or elsewhere.
```php
$feed = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rdf/');
$data = [
'title' => $feed->getTitle(),
'link' => $feed->getLink(),
'dateModified' => $feed->getDateModified(),
'description' => $feed->getDescription(),
'language' => $feed->getLanguage(),
'entries' => [],
];
foreach ($feed as $entry) {
$edata = [
'title' => $entry->getTitle(),
'description' => $entry->getDescription(),
'dateModified' => $entry->getDateModified(),
'authors' => $entry->getAuthors(),
'link' => $entry->getLink(),
'content' => $entry->getContent(),
];
$data['entries'][] = $edata;
}
```
> ## Importing requires an HTTP client
>
> To import a feed, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
The example above demonstrates `Zend\Feed\Reader\Reader`'s API, and it also
demonstrates some of its internal operation. In reality, the RDF feed selected
does not have any native date or author elements; however it does utilise the
Dublin Core 1.1 module which offers namespaced creator and date elements.
`Zend\Feed\Reader\Reader` falls back on these and similar options if no relevant
native elements exist. If it absolutely cannot find an alternative it will
return `NULL`, indicating the information could not be found in the feed. You
should note that classes implementing `Zend\Feed\Reader\Feed\AbstractFeed` also
implement the SPL `Iterator` and `Countable` interfaces.
Feeds can also be imported from strings or files.
```php
// from a URI
$feed = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rdf/');
// from a String
$feed = Zend\Feed\Reader\Reader::importString($feedXmlString);
// from a file
$feed = Zend\Feed\Reader\Reader::importFile('./feed.xml');
```
## Retrieving Underlying Feed and Entry Sources
`Zend\Feed\Reader\Reader` does its best not to stick you in a narrow confine. If
you need to work on a feed outside of `Zend\Feed\Reader\Reader`, you can extract
the base DOMDocument or DOMElement objects from any class, or even an XML
string containing these. Also provided are methods to extract the current
DOMXPath object (with all core and extension namespaces registered) and the
correct prefix used in all XPath queries for the current feed or entry. The
basic methods to use (on any object) are `saveXml()`, `getDomDocument()`,
`getElement()`, `getXpath()` and `getXpathPrefix()`. These will let you break
free of `Zend\Feed\Reader` and do whatever else you want.
- `saveXml()` returns an XML string containing only the element representing the
current object.
- `getDomDocument()` returns the DOMDocument object representing the entire feed
(even if called from an entry object).
- `getElement()` returns the DOMElement of the current object (i.e. the feed or
current entry).
- `getXpath()` returns the DOMXPath object for the current feed (even if called
from an entry object) with the namespaces of the current feed type and all
loaded extensions pre-registered.
- `getXpathPrefix()` returns the query prefix for the current object (i.e. the
feed or current entry) which includes the correct XPath query path for that
specific feed or entry.
Let's look at an example where a feed might include an RSS extension not
supported by `Zend\Feed\Reader\Reader` out of the box. Notably, you could write
and register an extension (covered later) to do this, but that's not always
warranted for a quick check. You must register any new namespaces on the
DOMXPath object before use unless they are registered by `Zend\Feed\Reader` or
an extension beforehand.
```php
$feed = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rdf/');
$xpathPrefix = $feed->getXpathPrefix();
$xpath = $feed->getXpath();
$xpath->registerNamespace('admin', 'http://webns.net/mvcb/');
$reportErrorsTo = $xpath->evaluate(
'string(' . $xpathPrefix . '/admin:errorReportsTo)'
);
```
> ### Do not register duplicate namespaces
>
> If you register an already registered namespace with a different prefix name
> to that used internally by `Zend\Feed\Reader\Reader`, it will break the
> internal operation of this component.
## Cache Support and Intelligent Requests
### Adding Cache Support to Zend\\Feed\\Reader\\Reader
`Zend\Feed\Reader\Reader` supports using a
[zend-cache](https://github.com/zendframework/zend-cache) storage instance to
cache feeds (as XML) to avoid unnecessary network requests. To add a cache,
create and configure your cache instance, and then tell
`Zend\Feed\Reader\Reader` to use it. The cache key used is
"`Zend\Feed\Reader\\`" followed by the MD5 hash of the feed's URI.
```php
$cache = Zend\Cache\StorageFactory::adapterFactory('Memory');
Zend\Feed\Reader\Reader::setCache($cache);
```
### HTTP Conditional GET Support
The big question often asked when importing a feed frequently is if it has even
changed. With a cache enabled, you can add HTTP Conditional `GET` support to
your arsenal to answer that question.
Using this method, you can request feeds from URIs and include their last known
ETag and Last-Modified response header values with the request (using the
If-None-Match and If-Modified-Since headers). If the feed on the server remains
unchanged, you should receive a 304 response which tells
`Zend\Feed\Reader\Reader` to use the cached version. If a full feed is sent in a
response with a status code of 200, this means the feed has changed and
`Zend\Feed\Reader\Reader` will parse the new version and save it to the cache.
It will also cache the new ETag and Last-Modified header values for future use.
> #### Conditional GET requires a HeaderAwareClientInterface
>
> Conditional GET support only works for `Zend\Feed\Reader\Http\HeaderAwareClientInterface`
> client implementations, as it requires the ability to send HTTP headers.
These "conditional" requests are not guaranteed to be supported by the server
you request a *URI* of, but can be attempted regardless. Most common feed
sources like blogs should however have this supported. To enable conditional
requests, you will need to provide a cache to `Zend\Feed\Reader\Reader`.
```php
$cache = Zend\Cache\StorageFactory::adapterFactory('Memory');
Zend\Feed\Reader\Reader::setCache($cache);
Zend\Feed\Reader\Reader::useHttpConditionalGet();
$feed = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rdf/');
```
In the example above, with HTTP Conditional `GET` requests enabled, the response
header values for ETag and Last-Modified will be cached along with the feed. For
the the cache's lifetime, feeds will only be updated on the cache if a non-304
response is received containing a valid RSS or Atom XML document.
If you intend on managing request headers from outside
`Zend\Feed\Reader\Reader`, you can set the relevant If-None-Matches and
If-Modified-Since request headers via the URI import method.
```php
$lastEtagReceived = '5e6cefe7df5a7e95c8b1ba1a2ccaff3d';
$lastModifiedDateReceived = 'Wed, 08 Jul 2009 13:37:22 GMT';
$feed = Zend\Feed\Reader\Reader::import(
$uri, $lastEtagReceived, $lastModifiedDateReceived
);
```
## Locating Feed URIs from Websites
These days, many websites are aware that the location of their XML feeds is not
always obvious. A small RDF, RSS, or Atom graphic helps when the user is reading
the page, but what about when a machine visits trying to identify where your
feeds are located? To assist in this, websites may point to their feeds using
`<link>` tags in the `<head>` section of their HTML. To take advantage
of this, you can use `Zend\Feed\Reader\Reader` to locate these feeds using the
static `findFeedLinks()` method.
This method calls any URI and searches for the location of RSS, RDF, and Atom
feeds assuming, the website's HTML contains the relevant links. It then returns
a value object where you can check for the existence of a RSS, RDF or Atom feed
URI.
The returned object is an `ArrayObject` subclass called
`Zend\Feed\Reader\FeedSet`, so you can cast it to an array or iterate over it to
access all the detected links. However, as a simple shortcut, you can just grab
the first RSS, RDF, or Atom link using its public properties as in the example
below. Otherwise, each element of the `ArrayObject` is a simple array with the
keys `type` and `uri` where the type is one of "rdf", "rss", or "atom".
```php
$links = Zend\Feed\Reader\Reader::findFeedLinks('http://www.planet-php.net');
if (isset($links->rdf)) {
echo $links->rdf, "\n"; // http://www.planet-php.org/rdf/
}
if (isset($links->rss)) {
echo $links->rss, "\n"; // http://www.planet-php.org/rss/
}
if (isset($links->atom)) {
echo $links->atom, "\n"; // http://www.planet-php.org/atom/
}
```
Based on these links, you can then import from whichever source you wish in the usual manner.
> ### Finding feed links requires an HTTP client
>
> To find feed links, you will need to have an [HTTP client](zend.feed.http-clients)
> available.
>
> If you are not using zend-http, you will need to inject `Reader` with the HTTP
> client. See the [section on providing a client to Reader](http-clients.md#providing-a-client-to-reader).
This quick method only gives you one link for each feed type, but websites may
indicate many links of any type. Perhaps it's a news site with a RSS feed for
each news category. You can iterate over all links using the ArrayObject's
iterator.
```php
$links = Zend\Feed\Reader::findFeedLinks('http://www.planet-php.net');
foreach ($links as $link) {
echo $link['href'], "\n";
}
```
## Attribute Collections
In an attempt to simplify return types, return types from the various feed and
entry level methods may include an object of type
`Zend\Feed\Reader\Collection\AbstractCollection`. Despite the special class name
which I'll explain below, this is just a simple subclass of SPL's `ArrayObject`.
The main purpose here is to allow the presentation of as much data as possible
from the requested elements, while still allowing access to the most relevant
data as a simple array. This also enforces a standard approach to returning such
data which previously may have wandered between arrays and objects.
The new class type acts identically to `ArrayObject` with the sole addition
being a new method `getValues()` which returns a simple flat array containing
the most relevant information.
A simple example of this is `Zend\Feed\Reader\Reader\FeedInterface::getCategories()`.
When used with any RSS or Atom feed, this method will return category data as a
container object called `Zend\Feed\Reader\Collection\Category`. The container
object will contain, per category, three fields of data: term, scheme, and label.
The "term" is the basic category name, often machine readable (i.e. plays nice
with URIs). The scheme represents a categorisation scheme (usually a URI
identifier) also known as a "domain" in RSS 2.0. The "label" is a human readable
category name which supports HTML entities. In RSS 2.0, there is no label
attribute so it is always set to the same value as the term for convenience.
To access category labels by themselves in a simple value array, you might
commit to something like:
```php
$feed = Zend\Feed\Reader\Reader::import('http://www.example.com/atom.xml');
$categories = $feed->getCategories();
$labels = [];
foreach ($categories as $cat) {
$labels[] = $cat['label']
}
```
It's a contrived example, but the point is that the labels are tied up with
other information.
However, the container class allows you to access the "most relevant" data as a
simple array using the `getValues()` method. The concept of "most relevant" is
obviously a judgement call. For categories it means the category labels (not the
terms or schemes) while for authors it would be the authors' names (not their
email addresses or URIs). The simple array is flat (just values) and passed
through `array_unique()` to remove duplication.
```php
$feed = Zend\Feed\Reader\Reader::import('http://www.example.com/atom.xml');
$categories = $feed->getCategories();
$labels = $categories->getValues();
```
The above example shows how to extract only labels and nothing else thus giving
simple access to the category labels without any additional work to extract that
data by itself.
## Retrieving Feed Information
Retrieving information from a feed (we'll cover entries and items in the next
section though they follow identical principals) uses a clearly defined API
which is exactly the same regardless of whether the feed in question is RSS,
RDF, or Atom. The same goes for sub-versions of these standards and we've tested
every single RSS and Atom version. While the underlying feed XML can differ
substantially in terms of the tags and elements they present, they nonetheless
are all trying to convey similar information and to reflect this all the
differences and wrangling over alternative tags are handled internally by
`Zend\Feed\Reader\Reader` presenting you with an identical interface for each.
Ideally, you should not have to care whether a feed is RSS or Atom so long as
you can extract the information you want.
> ### RSS feeds vary widely
>
> While determining common ground between feed types is itself complex, it
> should be noted that *RSS* in particular is a constantly disputed
> "specification". This has its roots in the original RSS 2.0 document, which
> contains ambiguities and does not detail the correct treatment of all
> elements. As a result, this component rigorously applies the RSS 2.0.11
> Specification published by the RSS Advisory Board and its accompanying RSS
> Best Practices Profile. No other interpretation of RSS
> 2.0 will be supported, though exceptions may be allowed where it does not
> directly prevent the application of the two documents mentioned above.
Of course, we don't live in an ideal world, so there may be times the API just
does not cover what you're looking for. To assist you, `Zend\Feed\Reader\Reader`
offers a plugin system which allows you to write extensions to expand the core
API and cover any additional data you are trying to extract from feeds. If
writing another extension is too much trouble, you can simply grab the
underlying DOM or XPath objects and do it by hand in your application. Of
course, we really do encourage writing an extension simply to make it more
portable and reusable, and useful extensions may be proposed to the component
for formal addition.
Below is a summary of the Core API for feeds. You should note it comprises not
only the basic RSS and Atom standards, but also accounts for a number of
included extensions bundled with `Zend\Feed\Reader\Reader`. The naming of these
extension sourced methods remain fairly generic; all Extension methods operate
at the same level as the Core API though we do allow you to retrieve any
specific extension object separately if required.
### Feed Level API Methods
Method | Description
------ | -----------
`getId()` | Returns a unique ID associated with this feed
`getTitle()` | Returns the title of the feed
`getDescription()` | Returns the text description of the feed.
`getLink()` | Returns a URI to the HTML website containing the same or similar information as this feed (i.e. if the feed is from a blog, it should provide the blog's URI where the HTML version of the entries can be read).
`getFeedLink()` | Returns the URI of this feed, which may be the same as the URI used to import the feed. There are important cases where the feed link may differ because the source URI is being updated and is intended to be removed in the future.
`getAuthors()` | Returns an object of type `Zend\Feed\Reader\Collection\Author` which is an `ArrayObject` whose elements are each simple arrays containing any combination of the keys "name", "email" and "uri". Where irrelevant to the source data, some of these keys may be omitted.
`getAuthor(integer $index = 0)` | Returns either the first author known, or with the optional $index parameter any specific index on the array of authors as described above (returning `NULL` if an invalid index).
`getDateCreated()` | Returns the date on which this feed was created. Generally only applicable to Atom, where it represents the date the resource described by an Atom 1.0 document was created. The returned date will be a `DateTime` object.
`getDateModified()` | Returns the date on which this feed was last modified. The returned date will be a `DateTime` object.
`getLastBuildDate()` | Returns the date on which this feed was last built. The returned date will be a `DateTime` object. This is only supported by RSS; Atom feeds will always return `NULL`.
`getLanguage()` | Returns the language of the feed (if defined) or simply the language noted in the XML document.
`getGenerator()` | Returns the generator of the feed, e.g. the software which generated it. This may differ between RSS and Atom since Atom defines a different notation.
`getCopyright()` | Returns any copyright notice associated with the feed.
`getHubs()` | Returns an array of all Hub Server URI endpoints which are advertised by the feed for use with the Pubsubhubbub Protocol, allowing subscriptions to the feed for real-time updates.
`getCategories()` | Returns a `Zend\Feed\Reader\Collection\Category` object containing the details of any categories associated with the overall feed. The supported fields include "term" (the machine readable category name), "scheme" (the categorisation scheme and domain for this category), and "label" (a HTML decoded human readable category name). Where any of the three fields are absent from the field, they are either set to the closest available alternative or, in the case of "scheme", set to `NULL`.
`getImage()` | Returns an array containing data relating to any feed image or logo, or `NULL` if no image found. The resulting array may contain the following keys: uri, link, title, description, height, and width. Atom logos only contain a URI so the remaining metadata is drawn from RSS feeds only.
Given the variety of feeds in the wild, some of these methods will undoubtedly
return `NULL` indicating the relevant information couldn't be located. Where
possible, `Zend\Feed\Reader\Reader` will fall back on alternative elements
during its search. For example, searching an RSS feed for a modification date is
more complicated than it looks. RSS 2.0 feeds should include a `<lastBuildDate>`
tag and/or a `<pubDate>` element. But what if it doesn't? Maybe this is an RSS
1.0 feed? Perhaps it instead has an `<atom:updated>` element with identical
information (Atom may be used to supplement RSS syntax)? Failing that, we
could simply look at the entries, pick the most recent, and use its `<pubDate>`
element. Assuming it exists, that is. Many feeds also use Dublin Core 1.0 or 1.1
`<dc:date>` elements for feeds and entries. Or we could find Atom lurking again.
The point is, `Zend\Feed\Reader\Reader` was designed to know this. When you ask
for the modification date (or anything else), it will run off and search for all
these alternatives until it either gives up and returns `NULL`, or finds an
alternative that should have the right answer.
In addition to the above methods, all feed objects implement methods for
retrieving the DOM and XPath objects for the current feeds as described
earlier. Feed objects also implement the SPL Iterator and Countable
interfaces. The extended API is summarised below.
### Extended Feed API Methods
Method | Description
------ | -----------
`getDomDocument()` | Returns the parent DOMDocument object for the entire source XML document.
`getElement()` | Returns the current feed level DOMElement object.
`saveXml()` | Returns a string containing an XML document of the entire feed element (this is not the original document, but a rebuilt version).
`getXpath()` | Returns the DOMXPath object used internally to run queries on the DOMDocument object (this includes core and extension namespaces pre-registered).
`getXpathPrefix()` | Returns the valid DOM path prefix prepended to all XPath queries matching the feed being queried.
`getEncoding()` | Returns the encoding of the source XML document (note: this cannot account for errors such as the server sending documents in a different encoding). Where not defined, the default UTF-8 encoding of Unicode is applied.
`count()` | Returns a count of the entries or items this feed contains (implements SPL `Countable` interface)
`current()` | Returns either the current entry (using the current index from `key()`).
`key()` | Returns the current entry index.
`next()` | Increments the entry index value by one.
`rewind()` | Resets the entry index to 0.
`valid()` | Checks that the current entry index is valid, i.e. it does not fall below 0 and does not exceed the number of entries existing.
`getExtensions()` | Returns an array of all extension objects loaded for the current feed (note: both feed-level and entry-level extensions exist, and only feed-level extensions are returned here). The array keys are of the form `{ExtensionName}_Feed`.
`getExtension(string $name)` | Returns an extension object for the feed registered under the provided name. This allows more fine-grained access to extensions which may otherwise be hidden within the implementation of the standard API methods.
`getType()` | Returns a static class constant (e.g. `Zend\Feed\Reader\Reader::TYPE_ATOM_03`, i.e. "Atom 0.3"), indicating exactly what kind of feed is being consumed.
## Retrieving Entry/Item Information
Retrieving information for specific entries or items (depending on whether you
speak Atom or RSS) is identical to feed level data. Accessing entries is
simply a matter of iterating over a feed object or using the SPL `Iterator`
interface feed objects implement, and calling the appropriate method on each.
### Entry API Methods
Method | Description
------ | -----------
`getId()` | Returns a unique ID for the current entry.
`getTitle()` | Returns the title of the current entry.
`getDescription()` | Returns a description of the current entry.
`getLink()` | Returns a URI to the HTML version of the current entry.
`getPermaLink()` | Returns the permanent link to the current entry. In most cases, this is the same as using `getLink()`.
`getAuthors()` | Returns an object of type `Zend\Feed\Reader\Collection\Author`, which is an `ArrayObject` whose elements are each simple arrays containing any combination of the keys "name", "email" and "uri". Where irrelevant to the source data, some of these keys may be omitted.
`getAuthor(integer $index = 0)` | Returns either the first author known, or, with the optional `$index` parameter, any specific index on the array of Authors as described above (returning `NULL` if an invalid index).
`getDateCreated()` | Returns the date on which the current entry was created. Generally only applicable to Atom where it represents the date the resource described by an Atom 1.0 document was created.
`getDateModified()` | Returns the date on which the current entry was last modified.
`getContent()` | Returns the content of the current entry (this has any entities reversed if possible, assuming the content type is HTML). The description is returned if a separate content element does not exist.
`getEnclosure()` | Returns an array containing the value of all attributes from a multi-media `<enclosure>` element including as array keys: url, length, type. In accordance with the RSS Best Practices Profile of the RSS Advisory Board, no support is offers for multiple enclosures since such support forms no part of the RSS specification.
`getCommentCount()` | Returns the number of comments made on this entry at the time the feed was last generated.
`getCommentLink()` | Returns a URI pointing to the HTML page where comments can be made on this entry.
`getCommentFeedLink([string $type = atom'|'rss'])` | Returns a URI pointing to a feed of the provided type containing all comments for this entry (type defaults to Atom/RSS depending on current feed type).
`getCategories()` | Returns a `Zend\Feed\Reader\Collection\Category` object containing the details of any categories associated with the entry. The supported fields include "term" (the machine readable category name), "scheme" (the categorisation scheme and domain for this category), and "label" (an HTML-decoded human readable category name). Where any of the three fields are absent from the field, they are either set to the closest available alternative or, in the case of "scheme", set to `NULL`.
The extended API for entries is identical to that for feeds with the exception
of the `Iterator` methods, which are not needed here.
> ### Modified vs Created dates
>
> There is often confusion over the concepts of *modified* and *created* dates.
> In Atom, these are two clearly defined concepts (so knock yourself out) but in
> RSS they are vague. RSS 2.0 defines a single `<pubDate>` element which
> typically refers to the date this entry was published, i.e. a creation date of
> sorts. This is not always the case, and it may change with updates or not. As a
> result, if you really want to check whether an entry has changed, don't rely on
> the results of `getDateModified()`. Instead, consider tracking the MD5 hash of
> three other elements concatenated, e.g. using `getTitle()`, `getDescription()`,
> and `getContent()`. If the entry was truly updated, this hash computation will
> give a different result than previously saved hashes for the same entry. This
> is obviously content oriented, and will not assist in detecting changes to
> other relevant elements. Atom feeds should not require such steps.
> Further muddying the waters, dates in feeds may follow different standards.
> Atom and Dublin Core dates should follow ISO 8601, and RSS dates should
> follow RFC 822 or RFC 2822 (which is also common). Date methods will throw an
> exception if `DateTime` cannot load the date string using one of the above
> standards, or the PHP recognised possibilities for RSS dates.
> ### Validation
>
> The values returned from these methods are not validated. This means users
> must perform validation on all retrieved data including the filtering of any
> HTML such as from `getContent()` before it is output from your application.
> Remember that most feeds come from external sources, and therefore the default
> assumption should be that they cannot be trusted.
### Extended Entry Level API Methods
Method | Description
------ | -----------
`getDomDocument()` | Returns the parent DOMDocument object for the entire feed (not just the current entry).
`getElement()` | Returns the current entry level DOMElement object.
`getXpath()` | Returns the DOMXPath object used internally to run queries on the DOMDocument object (this includes core and extension namespaces pre-registered).
`getXpathPrefix()` | Returns the valid DOM path prefix prepended to all XPath queries matching the entry being queried.
`getEncoding()` | Returns the encoding of the source XML document (note: this cannot account for errors such as the server sending documents in a different encoding). The default encoding applied in the absence of any other is the UTF-8 encoding of Unicode.
`getExtensions()` | Returns an array of all extension objects loaded for the current entry (note: both feed-level and entry-level extensions exist, and only entry-level extensions are returned here). The array keys are in the form `{ExtensionName}Entry`.
`getExtension(string $name)` | Returns an extension object for the entry registered under the provided name. This allows more fine-grained access to extensions which may otherwise be hidden within the implementation of the standard API methods.
`getType()` | Returns a static class constant (e.g. `Zend\Feed\Reader\Reader::TYPE_ATOM_03`, i.e. "Atom 0.3") indicating exactly what kind of feed is being consumed.
## Extending Feed and Entry APIs
Extending `Zend\Feed\Reader\Reader` allows you to add methods at both the feed
and entry level which cover the retrieval of information not already supported
by `Zend\Feed\Reader\Reader`. Given the number of RSS and Atom extensions that
exist, this is a good thing, since `Zend\Feed\Reader\Reader` couldn't possibly
add everything.
There are two types of extensions possible, those which retrieve information
from elements which are immediate children of the root element (e.g.
`<channel>` for RSS or `<feed>` for Atom), and those who retrieve information
from child elements of an entry (e.g. `<item>` for RSS or `<entry>` for Atom).
On the filesystem, these are grouped as classes within a namespace based on the
extension standard's name. For example, internally we have
`Zend\Feed\Reader\Extension\DublinCore\Feed` and
`Zend\Feed\Reader\Extension\DublinCore\Entry` classes which are two extensions
implementing Dublin Core 1.0 and 1.1 support.
Extensions are loaded into `Zend\Feed\Reader\Reader` using an "extension
manager". Extension managers must implement `Zend\Feed\Reader\ExtensionManagerInterface`.
Three implementations exist:
- `Zend\Feed\Reader\StandaloneExtensionManager` is a hard-coded implementation
seeded with all feed and entry implementations. You can extend it to add
extensions, though it's likely easier to copy and paste it, adding your
changes.
- `Zend\Feed\Reader\ExtensionPluginManager` is a `Zend\ServiceManager\AbstractPluginManager`
implementation, `Zend\Feed\Reader\ExtensionManager`; as such, you can extend
it to add more extensions, use a `Zend\ServiceManager\ConfigInterface` instance
to inject it with more extensions, or use its public API for adding services
(e.g., `setService()`, `setFactory()`, etc.). This implementation *does not*
implement `ExtensionManagerInterface`, and must be used with `ExtensionManager`.
- `Zend\Feed\Reader\ExtensionManager` exists for legacy purposes; prior to 2.3,
this was an `AbstractPluginManager` implementation, and the only provided
extension manager. It now implements `ExtensionManagerInterface`, and acts as
a decorator for `ExtensionPluginManager`.
By default, `Zend\Feed\Reader\Reader` composes a `StandaloneExtensionManager`. You
can inject an alternate implementation using `Reader::setExtensionManager()`:
```php
$extensions = new Zend\Feed\Reader\ExtensionPluginManager();
Zend\Feed\Reader\Reader::setExtensionManager(
new ExtensionManager($extensions)
);
```
The shipped implementations all provide the default extensions (so-called
"Core Extensions") used internally by `Zend\Feed\Reader\Reader`. These
include:
Extension | Description
--------- | -----------
DublinCore (Feed and Entry) | Implements support for Dublin Core Metadata Element Set 1.0 and 1.1.
Content (Entry only) | Implements support for Content 1.0.
Atom (Feed and Entry) | Implements support for Atom 0.3 and Atom 1.0.
Slash | Implements support for the Slash RSS 1.0 module.
WellFormedWeb | Implements support for the Well Formed Web CommentAPI 1.0.
Thread | Implements support for Atom Threading Extensions as described in RFC 4685.
Podcast | Implements support for the Podcast 1.0 DTD from Apple.
The core extensions are somewhat special since they are extremely common and
multi-faceted. For example, we have a core extension for Atom. Atom is
implemented as an extension (not just a base class) because it doubles as a
valid RSS module; you can insert Atom elements into RSS feeds. I've even seen
RDF feeds which use a lot of Atom in place of more common extensions like
Dublin Core.
The following is a list of non-Core extensions that are offered, but not registered
by default. If you want to use them, you'll need to
tell `Zend\Feed\Reader\Reader` to load them in advance of importing a feed.
Additional non-Core extensions will be included in future iterations of the
component.
Extension | Description
--------- | -----------
Syndication | Implements Syndication 1.0 support for RSS feeds.
CreativeCommons | An RSS module that adds an element at the `<channel>` or `<item>` level that specifies which Creative Commons license applies.
`Zend\Feed\Reader\Reader` requires you to explicitly register non-Core
extensions in order to expose their API to feed and entry objects. Below, we
register the optional Syndication extension, and discover that it can be
directly called from the entry API without any effort. (Note that
extension names are case sensitive and use camelCasing for multiple terms.)
```php
use Zend\Feed\Reader\Reader;
Reader::registerExtension('Syndication');
$feed = Reader::import('http://rss.slashdot.org/Slashdot/slashdot');
$updatePeriod = $feed->getUpdatePeriod();
```
In the simple example above, we checked how frequently a feed is being updated
using the `getUpdatePeriod()` method. Since it's not part of
`Zend\Feed\Reader\Reader`'s core API, it could only be a method supported by
the newly registered Syndication extension.
As you can also notice, methods provided by extensions are accessible from the
main API using method overloading. As an alternative, you can also directly
access any extension object for a similar result as seen below.
```php
use Zend\Feed\Reader\Reader;
Reader::registerExtension('Syndication');
$feed = Reader::import('http://rss.slashdot.org/Slashdot/slashdot');
$syndication = $feed->getExtension('Syndication');
$updatePeriod = $syndication->getUpdatePeriod();
```
### Writing Zend\\Feed\\Reader Extensions
Inevitably, there will be times when the `Zend\Feed\Reader` API is just
not capable of getting something you need from a feed or entry. You can use the
underlying source objects, like DOMDocument, to get these by hand; however, there
is a more reusable method available: you can write extensions supporting these new
queries.
As an example, let's take the case of a purely fictitious corporation named
Jungle Books. Jungle Books have been publishing a lot of reviews on books they
sell (from external sources and customers), which are distributed as an RSS 2.0
feed. Their marketing department realises that web applications using this feed
cannot currently figure out exactly what book is being reviewed. To make life
easier for everyone, they determine that the geek department needs to extend
RSS 2.0 to include a new element per entry supplying the ISBN-10 or ISBN-13
number of the publication the entry concerns. They define the new `<isbn>`
element quite simply with a standard name and namespace URI:
- Name: JungleBooks 1.0
- Namespace URI: http://example.com/junglebooks/rss/module/1.0/
A snippet of RSS containing this extension in practice could be something
similar to:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:jungle="http://example.com/junglebooks/rss/module/1.0/">
<channel>
<title>Jungle Books Customer Reviews</title>
<link>http://example.com/junglebooks</link>
<description>Many book reviews!</description>
<pubDate>Fri, 26 Jun 2009 19:15:10 GMT</pubDate>
<jungle:dayPopular>
http://example.com/junglebooks/book/938
</jungle:dayPopular>
<item>
<title>Review Of Flatland: A Romance of Many Dimensions</title>
<link>http://example.com/junglebooks/review/987</link>
<author>Confused Physics Student</author>
<content:encoded>
A romantic square?!
</content:encoded>
<pubDate>Thu, 25 Jun 2009 20:03:28 -0700</pubDate>
<jungle:isbn>048627263X</jungle:isbn>
</item>
</channel>
</rss>
```
Implementing this new ISBN element as a simple entry level extension would
require the following class (using your own namespace).
```php
namespace My\FeedReader\Extension\JungleBooks;
use Zend\Feed\Reader\Extension\AbstractEntry;
class Entry extends AbstractEntry
{
public function getIsbn()
{
if (isset($this->data['isbn'])) {
return $this->data['isbn'];
}
$isbn = $this->xpath->evaluate(
'string(' . $this->getXpathPrefix() . '/jungle:isbn)'
);
if (! $isbn) {
$isbn = null;
}
$this->data['isbn'] = $isbn;
return $this->data['isbn'];
}
protected function registerNamespaces()
{
$this->xpath->registerNamespace(
'jungle',
'http://example.com/junglebooks/rss/module/1.0/'
);
}
}
```
This extension creates a new method `getIsbn()`, which runs an XPath query on
the current entry to extract the ISBN number enclosed by the `<jungle:isbn>`
element. It can optionally store this to the internal non-persistent cache (no
need to keep querying the DOM if it's called again on the same entry). The
value is returned to the caller. At the end we have a protected method (it's
abstract, making it required by implementations) which registers the Jungle
Books namespace for their custom RSS module. While we call this an RSS module,
there's nothing to prevent the same element being used in Atom feeds; all
extensions which use the prefix provided by `getXpathPrefix()` are actually
neutral and work on RSS or Atom feeds with no extra code.
Since this extension is stored outside of zend-feed, you'll need to ensure your
application can autoload it. Once that's in place, you will also need to ensure
your extension manager knows about it, and then register the extension with
`Zend\Feed\Reader\Reader`.
The following example uses `Zend\Feed\Reader\ExtensionPluginManager` to manage
extensions, as it provides the ability to register new extensions without
requiring extension of the plugin manager itself. To use it, first intall
zend-servicemanager:
```bash
$ composer require zendframework/zend-servicemanager
```
From there:
```php
use My\FeedReader\Extension\JungleBooks;
use Zend\Feed\Reader\ExtensionManager;
use Zend\Feed\Reader\ExtensionPluginManager;
use Zend\Feed\Reader\Reader;
$extensions = new ExtensionPluginManager();
$extensions->setInvokableClass('JungleBooksEntry', JungleBooks\Entry::class);
Reader::setExtensionManager(new ExtensionManager($extensions));
Reader::registerExtension('JungleBooks');
$feed = Reader::import('http://example.com/junglebooks/rss');
// ISBN for whatever book the first entry in the feed was concerned with
$firstIsbn = $feed->current()->getIsbn();
```
Writing a feed extension is not much different. The example feed from earlier
included an unmentioned `<jungle:dayPopular>` element which Jungle Books have
added to their standard to include a link to the day's most popular book (in
terms of visitor traffic). Here's an extension which adds a
`getDaysPopularBookLink()` method to the feel level API.
```php
namespace My\FeedReader\Extension\JungleBooks;
use Zend\Feed\Reader\Extension\AbstractFeed;
class Feed extends AbstractFeed
{
public function getDaysPopularBookLink()
{
if (isset($this->data['dayPopular'])) {
return $this->data['dayPopular'];
}
$dayPopular = $this->xpath->evaluate(
'string(' . $this->getXpathPrefix() . '/jungle:dayPopular)'
);
if (!$dayPopular) {
$dayPopular = null;
}
$this->data['dayPopular'] = $dayPopular;
return $this->data['dayPopular'];
}
protected function registerNamespaces()
{
$this->xpath->registerNamespace(
'jungle',
'http://example.com/junglebooks/rss/module/1.0/'
);
}
}
```
Let's add to the previous example; we'll register the new class with the
extension manager, and then demonstrate using the newly exposed method:
```php
use My\FeedReader\Extension\JungleBooks;
use Zend\Feed\Reader\ExtensionManager;
use Zend\Feed\Reader\ExtensionPluginManager;
use Zend\Feed\Reader\Reader;
$extensions = new ExtensionPluginManager();
$extensions->setInvokableClass('JungleBooksEntry', JungleBooks\Entry::class);
$extensions->setInvokableClass('JungleBooksFeed', JungleBooks\Feed::class);
Reader::setExtensionManager(new ExtensionManager($extensions));
Reader::registerExtension('JungleBooks');
$feed = Reader::import('http://example.com/junglebooks/rss');
// URI to the information page of the day's most popular book with visitors
$daysPopularBookLink = $feed->getDaysPopularBookLink();
```
Going through these examples, you'll note that while we need to register the
feed and entry classes separately with the plugin manager, we don't register
them separately when registering the extension with the `Reader`. Extensions
within the same standard may or may not include both a feed and entry class, so
`Zend\Feed\Reader\Reader` only requires you to register the overall parent name,
e.g. JungleBooks, DublinCore, Slash. Internally, it can check at what level
extensions exist and load them up if found. In our case, we have a complete
extension now, spanning the classes `JungleBooks\Feed` and `JungleBooks\Entry`.
+161
View File
@@ -0,0 +1,161 @@
# Zend\\Feed\\Reader and Security
As with any data coming from a source that is beyond the developer's control,
special attention needs to be given to securing, validating and filtering that
data. Similar to data input to our application by users, data coming from RSS
and Atom feeds should also be considered unsafe and potentially dangerous, as it
allows the delivery of HTML and [xHTML](http://tools.ietf.org/html/rfc4287#section-8.1).
Because data validation and filtration is out of `Zend\Feed`'s scope, this task
is left for implementation by the developer, by using libraries such as
zend-escaper for escaping and [HTMLPurifier](http://www.htmlpurifier.org/) for
validating and filtering feed data.
Escaping and filtering of potentially insecure data is highly recommended before
outputting it anywhere in our application or before storing that data in some
storage engine (be it a simple file or a database.).
## Filtering data using HTMLPurifier
Currently, the best available library for filtering and validating (x)HTML data
in PHP is [HTMLPurifier](http://www.htmlpurifier.org/), and, as such, is the
recommended tool for this task. HTMLPurifier works by filtering out all (x)HTML
from the data, except for the tags and attributes specifically allowed in a
whitelist, and by checking and fixing nesting of tags, ensuring
standards-compliant output.
The following examples will show a basic usage of HTMLPurifier, but developers
are urged to go through and read [HTMLPurifier's documentation](http://www.htmlpurifier.org/docs).
```php
// Setting HTMLPurifier's options
$options = [
// Allow only paragraph tags
// and anchor tags wit the href attribute
[
'HTML.Allowed',
'p,a[href]'
],
// Format end output with Tidy
[
'Output.TidyFormat',
true
],
// Assume XHTML 1.0 Strict Doctype
[
'HTML.Doctype',
'XHTML 1.0 Strict'
],
// Disable cache, but see note after the example
[
'Cache.DefinitionImpl',
null
]
];
// Configuring HTMLPurifier
$config = HTMLPurifier_Config::createDefault();
foreach ($options as $option) {
$config->set($option[0], $option[1]);
}
// Creating a HTMLPurifier with it's config
$purifier = new HTMLPurifier($config);
// Fetch the RSS
try {
$rss = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rss/');
} catch (Zend\Feed\Exception\Reader\RuntimeException $e) {
// feed import failed
echo "Exception caught importing feed: {$e->getMessage()}\n";
exit;
}
// Initialize the channel data array
// See that we're cleaning the description with HTMLPurifier
$channel = [
'title' => $rss->getTitle(),
'link' => $rss->getLink(),
'description' => $purifier->purify($rss->getDescription()),
'items' => [],
];
// Loop over each channel item and store relevant data
// See that we're cleaning the descriptions with HTMLPurifier
foreach ($rss as $item) {
$channel['items'][] = [
'title' => $item->getTitle(),
'link' => $item->getLink(),
'description' => $purifier->purify($item->getDescription()),
];
}
```
> ### Tidy is required
>
> HTMLPurifier is using the PHP [Tidy extension](http://php.net/tidy) to clean
> and repair the final output. If this extension is not available, it will
> silently fail, but its availability has no impact on the library's security.
> ### Caching
>
> For the sake of this example, the HTMLPurifier's cache is disabled, but it is
> recommended to configure caching and use its standalone include file as it can
> improve the performance of HTMLPurifier substantially.
## Escaping data using zend-escaper
To help prevent XSS attacks, Zend Framework provides the [zend-escaper component](https://github.com/zendframework/zend-escaper),
which complies to the current [OWASP recommendations](https://www.owasp.org/index.php/XSS_Prevention_Cheat_Sheet),
and as such, is the recommended tool for escaping HTML tags and attributes,
Javascript, CSS and URLs before outputing any potentially insecure data to the
users.
```php
try {
$rss = Zend\Feed\Reader\Reader::import('http://www.planet-php.net/rss/');
} catch (Zend\Feed\Exception\Reader\RuntimeException $e) {
// feed import failed
echo "Exception caught importing feed: {$e->getMessage()}\n";
exit;
}
// Validate all URIs
$linkValidator = new Zend\Validator\Uri;
$link = null;
if ($linkValidator->isValid($rss->getLink())) {
$link = $rss->getLink();
}
// Escaper used for escaping data
$escaper = new Zend\Escaper\Escaper('utf-8');
// Initialize the channel data array
$channel = [
'title' => $escaper->escapeHtml($rss->getTitle()),
'link' => $escaper->escapeUrl($link),
'description' => $escaper->escapeHtml($rss->getDescription()),
'items' => [],
];
// Loop over each channel item and store relevant data
foreach ($rss as $item) {
$link = null;
if ($linkValidator->isValid($rss->getLink())) {
$link = $item->getLink();
}
$channel['items'][] = [
'title' => $escaper->escapeHtml($item->getTitle()),
'link' => $escaper->escapeUrl($link),
'description' => $escaper->escapeHtml($item->getDescription()),
];
}
```
The feed data is now safe for output to HTML templates. You can, of course, skip
escaping when simply storing the data persistently, but remember to escape it on
output later!
Of course, these are just basic examples, and cannot cover all possible
scenarios that you, as a developer, can, and most likely will, encounter. Your
responsibility is to learn what libraries and tools are at your disposal, and
when and how to use them to secure your web applications.
+280
View File
@@ -0,0 +1,280 @@
# Zend\\Feed\\Writer
`Zend\Feed\Writer` is the sibling component to `Zend\Feed\Reader` responsible
for *generating* feeds. It supports the Atom 1.0 specification (RFC 4287) and
RSS 2.0 as specified by the RSS Advisory Board (RSS 2.0.11). It does not deviate
from these standards. It does, however, offer a simple extension system which
allows for any extension and module for either of these two specifications to be
implemented if they are not provided out of the box.
In many ways, `Zend\Feed\Writer` is the inverse of `Zend\Feed\Reader`. Where
`Zend\Reader\Reader` focuses on providing an easy to use architecture fronted by
getter methods, `Zend\Feed\Writer` is fronted by similarly named setters or
mutators. This ensures the API won't pose a learning curve to anyone familiar
with `Zend\Feed\Reader`.
As a result of this design, the rest may even be obvious. Behind the scenes,
data set on any `Zend\Feed\Writer\Writer` instance is translated at render time
onto a DOMDocument object using the necessary feed elements. For each supported
feed type there is both an Atom 1.0 and RSS 2.0 renderer. Using a DOMDocument
class rather than a templating solution has numerous advantages, the most
obvious being the ability to export the DOMDocument for additional processing
and relying on PHP DOM for correct and valid rendering.
## Architecture
The architecture of `Zend\Feed\Writer` is very simple. It has two core sets of
classes: data containers and renderers.
The containers include the `Zend\Feed\Writer\Feed` and `Zend\Feed\Writer\Entry`
classes. The Entry classes can be attached to any Feed class. The sole purpose
of these containers is to collect data about the feed to generate using a simple
interface of setter methods. These methods perform some data validity testing.
For example, it will validate any passed URIs, dates, etc. These checks are not
tied to any of the feed standards definitions. The container objects also
contain methods to allow for fast rendering and export of the final feed, and
these can be reused at will.
In addition to the main data container classes, there are two additional Atom
2.0-specific classes: `Zend\Feed\Writer\Source` and `Zend\Feed\Writer\Deleted`.
The former implements Atom 2.0 source elements which carry source feed metadata
for a specific entry within an aggregate feed (i.e. the current feed is not the
entry's original source). The latter implements the [Atom Tombstones RFC](https://tools.ietf.org/html/rfc6721),
allowing feeds to carry references to entries which have been deleted.
While there are two main data container types, there are four renderers: two
matching container renderers per supported feed type. Each renderer accepts a
container, and, based on its content, attempts to generate valid feed markup. If
the renderer is unable to generate valid feed markup (perhaps due to the
container missing an obligatory data point), it will report this by throwing an
exception. While it is possible to ignore exceptions, this removes the default
safeguard of ensuring you have sufficient data set to render a wholly valid
feed.
To explain this more clearly: you may construct a set of data containers for a
feed where there is a Feed container, into which has been added some Entry
containers and a Deleted container. This forms a data hierarchy resembling a
normal feed. When rendering is performed, this hierarchy has its pieces passed
to relevant renderers, and the partial feeds (all DOMDocuments) are then pieced
together to create a complete feed. In the case of Source or Deleted (Tombstone)
containers, these are rendered only for Atom 2.0 and ignored for RSS.
Due to the system being divided between data containers and renderers,
extensions have more mandatory requirements than their equivalents in the
`Zend\Feed\Reader` subcomponent. A typical extension offering namespaced feed
and entry level elements must itself reflect the exact same architecture: i.e.
it must offer both feed and entry level data containers, and matching renderers.
There is, fortunately, no complex integration work required since all extension
classes are simply registered and automatically used by the core classes. We
cover extensions in more detail at the end of this chapter.
## Getting Started
To use `Zend\Feed\Writer\Writer`, you will provide it with data, and then
trigger the renderer. What follows is an example demonstrating generation of a
minimal Atom 1.0 feed. Each feed or entry uses a separate data container.
```php
use Zend\Feed\Writer\Feed;
/**
* Create the parent feed
*/
$feed = new Feed;
$feed->setTitle("Paddy's Blog");
$feed->setLink('http://www.example.com');
$feed->setFeedLink('http://www.example.com/atom', 'atom');
$feed->addAuthor([
'name' => 'Paddy',
'email' => 'paddy@example.com',
'uri' => 'http://www.example.com',
]);
$feed->setDateModified(time());
$feed->addHub('http://pubsubhubbub.appspot.com/');
/**
* Add one or more entries. Note that entries must
* be manually added once created.
*/
$entry = $feed->createEntry();
$entry->setTitle('All Your Base Are Belong To Us');
$entry->setLink('http://www.example.com/all-your-base-are-belong-to-us');
$entry->addAuthor([
'name' => 'Paddy',
'email' => 'paddy@example.com',
'uri' => 'http://www.example.com',
]);
$entry->setDateModified(time());
$entry->setDateCreated(time());
$entry->setDescription('Exposing the difficulty of porting games to English.');
$entry->setContent(
'I am not writing the article. The example is long enough as is ;).'
);
$feed->addEntry($entry);
/**
* Render the resulting feed to Atom 1.0 and assign to $out.
* You can substitute "atom" with "rss" to generate an RSS 2.0 feed.
*/
$out = $feed->export('atom');
```
The output rendered should be as follows:
```xml
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">Paddy's Blog</title>
<subtitle type="text">Writing about PC Games since 176 BC.</subtitle>
<updated>2009-12-14T20:28:18+00:00</updated>
<generator uri="http://framework.zend.com" version="1.10.0alpha">
Zend\Feed\Writer
</generator>
<link rel="alternate" type="text/html" href="http://www.example.com"/>
<link rel="self" type="application/atom+xml"
href="http://www.example.com/atom"/>
<id>http://www.example.com</id>
<author>
<name>Paddy</name>
<email>paddy@example.com</email>
<uri>http://www.example.com</uri>
</author>
<link rel="hub" href="http://pubsubhubbub.appspot.com/"/>
<entry>
<title type="html"><![CDATA[All Your Base Are Belong To
Us]]></title>
<summary type="html">
<![CDATA[Exposing the difficultly of porting games to
English.]]>
</summary>
<published>2009-12-14T20:28:18+00:00</published>
<updated>2009-12-14T20:28:18+00:00</updated>
<link rel="alternate" type="text/html"
href="http://www.example.com/all-your-base-are-belong-to-us"/>
<id>http://www.example.com/all-your-base-are-belong-to-us</id>
<author>
<name>Paddy</name>
<email>paddy@example.com</email>
<uri>http://www.example.com</uri>
</author>
<content type="html">
<![CDATA[I am not writing the article.
The example is long enough as is ;).]]>
</content>
</entry>
</feed>
```
This is a perfectly valid Atom 1.0 example. It should be noted that omitting an
obligatory point of data, such as a title, will trigger an exception when
rendering as Atom 1.0. This will differ for RSS 2.0, since a title may be
omitted so long as a description is present. This gives rise to exceptions that
differ between the two standards depending on the renderer in use. By design,
`Zend\Feed\Writer` will not render an invalid feed for either standard
unless the end-user deliberately elects to ignore all exceptions. This built in
safeguard was added to ensure users without in-depth knowledge of the relevant
specifications have a bit less to worry about.
## Setting Feed Data Points
Before you can render a feed, you must first setup the data necessary for the
feed being rendered. This utilises a simple setter style API, which doubles as
a method for validating the data being set. By design, the API closely matches
that for `Zend\Feed\Reader` to avoid undue confusion and uncertainty.
`Zend\Feed\Writer` offers this API via its data container classes
`Zend\Feed\Writer\Feed` and `Zend\Feed\Writer\Entry` (not to mention the Atom
2.0 specific and extension classes). These classes merely store all feed data in
a type-agnostic manner, meaning you may reuse any data container with any
renderer without requiring additional work. Both classes are also amenable to
extensions, meaning that an extension may define its own container classes which
are registered to the base container classes as extensions, and are checked when
any method call triggers the base container's `__call()` method, allowing method
overloading to the extension classes.
Here's a summary of the Core API for Feeds. You should note it comprises not
only the basic RSS and Atom standards, but also accounts for a number of
included extensions bundled with `Zend\Feed\Writer`. The naming of these
extension sourced methods remain fairly generic; all extension methods operate
at the same level as the Core API, though we do allow you to retrieve any
specific extension object separately if required.
The Feed API for data is contained in `Zend\Feed\Writer\Feed`. In addition to the API
detailed below, the class also implements the `Countable` and `Iterator` interfaces.
### Feed API Methods
Method | Description
------ | -----------
`setId()` | Set a unique identifier associated with this feed. For Atom 1.0 this is an `atom:id` element, whereas for RSS 2.0 it is added as a `guid` element. These are optional so long as a link is added; i.e. if no identifier is provided, the link is used.
`setTitle()` | Set the title of the feed.
`setDescription()` | Set the text description of the feed.
`setLink()` | Set a URI to the HTML website containing the same or similar information as this feed (i.e. if the feed is from a blog, it should provide the blog's URI where the HTML version of the entries can be read).
`setFeedLinks()` | Add a link to an XML feed, whether it is to the feed being generated, or an alternate URI pointing to the same feed but in a different format. At a minimum, it is recommended to include a link to the feed being generated so it has an identifiable final URI allowing a client to track its location changes without necessitating constant redirects. The parameter is an array of arrays, where each sub-array contains the keys "type" and "uri". The type should be one of "atom", "rss", or "rdf".
`addAuthors()` | Sets the data for authors. The parameter is an array of array,s where each sub-array may contain the keys "name", "email", and "uri". The "uri" value is only applicable for Atom feeds, since RSS contains no facility to show it. For RSS 2.0, rendering will create two elements: an author element containing the email reference with the name in brackets, and a Dublin Core creator element only containing the name.
`addAuthor()` | Sets the data for a single author following the same array format as described above for a single sub-array.
`setDateCreated()` | Sets the date on which this feed was created. Generally only applicable to Atom, where it represents the date the resource described by an Atom 1.0 document was created. The expected parameter may be a UNIX timestamp or a `DateTime` object.
`setDateModified()` | Sets the date on which this feed was last modified. The expected parameter may be a UNIX timestamp or a `DateTime` object.
`setLastBuildDate()` | Sets the date on which this feed was last build. The expected parameter may be a UNIX timestamp or a `DateTime` object. This will only be rendered for RSS 2.0 feeds, and is automatically rendered as the current date by default when not explicitly set.
`setLanguage()` | Sets the language of the feed. This will be omitted unless set.
`setGenerator()` | Allows the setting of a generator. The parameter should be an array containing the keys "name", "version", and "uri". If omitted a default generator will be added referencing `Zend\Feed\Writer`, the current zend-version version, and the Framework's URI.
`setCopyright()` | Sets a copyright notice associated with the feed.
`addHubs()` | Accepts an array of Pubsubhubbub Hub Endpoints to be rendered in the feed as Atom links so that PuSH Subscribers may subscribe to your feed. Note that you must implement a Pubsubhubbub Publisher in order for real-time updates to be enabled. A Publisher may be implemented using `Zend\Feed\Pubsubhubbub\Publisher`. The method `addHub()` allows adding a single hub at a time.
`addCategories()` | Accepts an array of categories for rendering, where each element is itself an array whose possible keys include "term", "label", and "scheme". The "term" is a typically a category name suitable for inclusion in a URI. The "label" may be a human readable category name supporting special characters (it is HTML encoded during rendering) and is a required key. The "scheme" (called the domain in RSS) is optional, but must be a valid URI. The method `addCategory()` allows adding a single category at a time.
`setImage()` | Accepts an array of image metadata for an RSS image or Atom logo. Atom 1.0 only requires a URI. RSS 2.0 requires a URI, HTML link, and an image title. RSS 2.0 optionally may send a width, height, and image description. To provide these, use an array argument with the following keys: "uri", "link", "title", "description", "height", and "width". The RSS 2.0 HTML link should point to the feed source's HTML page.
`createEntry()` | Returns a new instance of `Zend\Feed\Writer\Entry`. This is the Entry data container. New entries are not automatically assigned to the current feed, so you must explicitly call `addEntry()` to add the entry for rendering.
`addEntry()` | Adds an instance of `Zend\Feed\Writer\Entry` to the current feed container for rendering.
`createTombstone()` | Returns a new instance of `Zend\Feed\Writer\Deleted`. This is the Atom 2.0 Tombstone data container. New entries are not automatically assigned to the current feed, so you must explicitly call `addTombstone()` to add the deleted entry for rendering.
`addTombstone()` | Adds an instance of `Zend\Feed\Writer\Deleted` to the current feed container for rendering.
`removeEntry()` | Accepts a parameter indicating an array index of the entry to remove from the feed.
`export()` | Exports the entire data hierarchy to an XML feed. The method has two parameters. The first is the feed type, one of "atom" or "rss". The second is an optional boolean to set indicating whether or not Exceptions are thrown. The default is `TRUE`.
> #### Retrieval methods
>
> In addition to the setters listed above, `Feed` instances also provide
> matching getters to retrieve data from the `Feed` data container. For
> example, `setImage()` is matched with a `getImage()` method.
## Setting Entry Data Points
Below is a summary of the Core API for entries and items. You should note that
it covers not only the basic RSS and Atom standards, but also a number of
included extensions bundled with `Zend\Feed\Writer`. The naming of these
extension sourced methods remain fairly generic; all extension methods operate
at the same level as the Core API, though we do allow you to retrieve any
specific extension object separately if required.
The Entry *API* for data is contained in `Zend\Feed\Writer\Entry`.
### Entry API Methods
Method | Description
------ | -----------
`setId()` | Set a unique identifier associated with this entry. For Atom 1.0 this is an `atom:id` element, whereas for RSS 2.0 it is added as a `guid` element. These are optional so long as a link is added; i.e. if no identifier is provided, the link is used.
`setTitle()` | Set the title of the entry.
`setDescription()` | Set the text description of the entry.
`setContent()` | Set the content of the entry.
`setLink()` | Set a URI to the HTML website containing the same or similar information as this entry (i.e. if the feed is from a blog, it should provide the blog article's URI where the HTML version of the entry can be read).
`setFeedLinks()` | Add a link to an XML feed, whether it is to the feed being generated, or an alternate URI pointing to the same feed but in a different format. At a minimum, it is recommended to include a link to the feed being generated so it has an identifiable final URI allowing a client to track its location changes without necessitating constant redirects. The parameter is an array of arrays, where each sub-array contains the keys "type" and "uri". The type should be one of "atom", "rss", or "rdf". If a type is omitted, it defaults to the type used when rendering the feed.
`addAuthors()` | Sets the data for authors. The parameter is an array of array,s where each sub-array may contain the keys "name", "email", and "uri". The "uri" value is only applicable for Atom feeds, since RSS contains no facility to show it. For RSS 2.0, rendering will create two elements: an author element containing the email reference with the name in brackets, and a Dublin Core creator element only containing the name.
`addAuthor()` | Sets the data for a single author following the same format as described above for a single sub-array.
`setDateCreated()` | Sets the date on which this entry was created. Generally only applicable to Atom where it represents the date the resource described by an Atom 1.0 document was created. The expected parameter may be a UNIX timestamp or a `DateTime` object. If omitted, the date used will be the current date and time.
`setDateModified()` | Sets the date on which this entry was last modified. The expected parameter may be a UNIX timestamp or a `DateTime` object. If omitted, the date used will be the current date and time.
`setCopyright()` | Sets a copyright notice associated with the entry.
`addCategories()` | Accepts an array of categories for rendering, where each element is itself an array whose possible keys include "term", "label", and "scheme". The "term" is a typically a category name suitable for inclusion in a URI. The "label" may be a human readable category name supporting special characters (it is encoded during rendering) and is a required key. The "scheme" (called the domain in RSS) is optional but must be a valid URI.
`addCategory()` | Sets the data for a single category following the same format as described above for a single sub-array.
`setCommentCount()` | Sets the number of comments associated with this entry. Rendering differs between RSS and Atom 2.0 depending on the element or attribute needed.
`setCommentLink()` | Sets a link to an HTML page containing comments associated with this entry.
`setCommentFeedLink()` | Sets a link to an XML feed containing comments associated with this entry. The parameter is an array containing the keys "uri" and "type", where the type is one of "rdf", "rss", or "atom".
`setCommentFeedLinks()` | Same as `setCommentFeedLink()`, except it accepts an array of arrays, where each subarray contains the expected parameters of `setCommentFeedLink()`.
`setEncoding()` | Sets the encoding of entry text. This will default to UTF-8, which is the preferred encoding.
> #### Retrieval methods
>
> In addition to the setters listed above, `Entry` instances also provide
> matching getters to retrieve data from the `Entry` data container. For
> example, `setContent()` is matched with a `getContent()` method.
## Extensions
- TODO
+20
View File
@@ -0,0 +1,20 @@
docs_dir: doc/book
site_dir: doc/html
pages:
- index.md
- Introduction: intro.md
- Reader:
- "Zend\\Feed\\Reader": reader.md
- 'HTTP Clients': http-clients.md
- 'Importing Feeds': importing.md
- 'Feed Discovery': find-feeds.md
- 'Consuming RSS Feeds': consuming-rss.md
- 'Consuming Atom Feeds': consuming-atom.md
- 'Consuming Atom Entries': consuming-atom-entry.md
- Security: security.md
- Writer: writer.md
- Pubsubhubbub: pubsubhubbub.md
site_name: zend-feed
site_description: Zend\Feed
repo_url: 'https://github.com/zendframework/zend-feed'
copyright: 'Copyright (c) 2016 <a href="http://www.zend.com/">Zend Technologies USA Inc.</a>'
@@ -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
{
}
@@ -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
{
}
@@ -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
{
}
@@ -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
{
}
@@ -0,0 +1,291 @@
<?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 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
*/
protected function _detectCallbackUrl()
{
$callbackUrl = '';
if (isset($_SERVER['HTTP_X_ORIGINAL_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_ORIGINAL_URL'];
} elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_REWRITE_URL'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$callbackUrl = $_SERVER['REQUEST_URI'];
$scheme = 'http';
if ($_SERVER['HTTPS'] == 'on') {
$scheme = 'https';
}
$schemeAndHttpHost = $scheme . '://' . $this->_getHttpHost();
if (strpos($callbackUrl, $schemeAndHttpHost) === 0) {
$callbackUrl = substr($callbackUrl, strlen($schemeAndHttpHost));
}
} elseif (isset($_SERVER['ORIG_PATH_INFO'])) {
$callbackUrl= $_SERVER['ORIG_PATH_INFO'];
if (!empty($_SERVER['QUERY_STRING'])) {
$callbackUrl .= '?' . $_SERVER['QUERY_STRING'];
}
}
return $callbackUrl;
}
/**
* Get the HTTP host
*
* @return string
*/
protected function _getHttpHost()
{
if (!empty($_SERVER['HTTP_HOST'])) {
return $_SERVER['HTTP_HOST'];
}
$scheme = 'http';
if ($_SERVER['HTTPS'] == 'on') {
$scheme = 'https';
}
$name = $_SERVER['SERVER_NAME'];
$port = $_SERVER['SERVER_PORT'];
if (($scheme == 'http' && $port == 80)
|| ($scheme == 'https' && $port == 443)
) {
return $name;
}
return $name . ':' . $port;
}
/**
* Retrieve a Header value from either $_SERVER or Apache
*
* @param string $header
* @return bool|string
*/
protected function _getHeader($header)
{
$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
*/
protected function _getRawBody()
{
$body = file_get_contents('php://input');
if (strlen(trim($body)) == 0 && isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
$body = $GLOBALS['HTTP_RAW_POST_DATA'];
}
if (strlen(trim($body)) > 0) {
return $body;
}
return false;
}
}
@@ -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();
}
@@ -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
{
}
@@ -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
{
}
@@ -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
{
}
@@ -0,0 +1,211 @@
<?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
*/
protected function _normalizeHeader($name)
{
$filtered = str_replace(['-', '_'], ' ', (string) $name);
$filtered = ucwords(strtolower($filtered));
$filtered = str_replace(' ', '-', $filtered);
return $filtered;
}
}
@@ -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;
}
}
}
@@ -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 (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 (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 (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;
}
}
@@ -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);
}
@@ -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;
}
}
@@ -0,0 +1,397 @@
<?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
*/
protected function _getHttpClient()
{
$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;
}
}
@@ -0,0 +1,837 @@
<?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
*/
protected function _doRequest($mode)
{
$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
*/
protected function _getHttpClient()
{
$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
*/
protected function _getRequestParameters($hubUrl, $mode)
{
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,
'subscription_state' => ($mode == 'unsubscribe')? PubSubHubbub::SUBSCRIPTION_TODELETE : PubSubHubbub::SUBSCRIPTION_NOTVERIFIED,
];
$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
*/
protected function _generateVerifyToken()
{
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
*/
protected function _generateSubscriptionKey(array $params, $hubUrl)
{
$keyBase = $params['hub.topic'] . $hubUrl;
$key = md5($keyBase);
return $key;
}
/**
* URL Encode an array of parameters
*
* @param array $params
* @return array
*/
protected function _urlEncode(array $params)
{
$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
*/
protected function _toByteValueOrderedString(array $params)
{
$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;
}
}
@@ -0,0 +1,316 @@
<?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
*/
protected function _hasValidVerifyToken(array $httpGetData = null, $checkValue = true)
{
$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
*/
protected function _detectVerifyTokenKey(array $httpGetData = null)
{
/**
* 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
*/
protected function _parseQueryString()
{
$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;
}
}
@@ -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';
}
@@ -0,0 +1,224 @@
<?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
*/
protected function _loadExtensions()
{
$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']
);
}
}
}
@@ -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();
}
@@ -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
{
}
@@ -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();
}
@@ -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);
}
}
@@ -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);
}
}

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