first commit
This commit is contained in:
+94
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,13 @@
|
||||
# zend-feed
|
||||
|
||||
[](https://secure.travis-ci.org/zendframework/zend-feed)
|
||||
[](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
@@ -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.
|
||||
@@ -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;
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
```
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
+16
@@ -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
|
||||
{
|
||||
}
|
||||
+16
@@ -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
|
||||
{
|
||||
}
|
||||
+16
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Collection;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class Collection extends ArrayObject
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Entry;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
abstract class AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Feed entry data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* DOM document object
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $domDocument = null;
|
||||
|
||||
/**
|
||||
* Entry instance
|
||||
*
|
||||
* @var DOMElement
|
||||
*/
|
||||
protected $entry = null;
|
||||
|
||||
/**
|
||||
* Pointer to the current entry
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $entryKey = 0;
|
||||
|
||||
/**
|
||||
* XPath object
|
||||
*
|
||||
* @var DOMXPath
|
||||
*/
|
||||
protected $xpath = null;
|
||||
|
||||
/**
|
||||
* Registered extensions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extensions = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DOMElement $entry
|
||||
* @param int $entryKey
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(DOMElement $entry, $entryKey, $type = null)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$this->entryKey = $entryKey;
|
||||
$this->domDocument = $entry->ownerDocument;
|
||||
if ($type !== null) {
|
||||
$this->data['type'] = $type;
|
||||
} elseif ($this->domDocument !== null) {
|
||||
$this->data['type'] = Reader\Reader::detectType($this->domDocument);
|
||||
} else {
|
||||
$this->data['type'] = Reader\Reader::TYPE_ANY;
|
||||
}
|
||||
$this->loadExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOM
|
||||
*
|
||||
* @return DOMDocument
|
||||
*/
|
||||
public function getDomDocument()
|
||||
{
|
||||
return $this->domDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry element
|
||||
*
|
||||
* @return DOMElement
|
||||
*/
|
||||
public function getElement()
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Entry's encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
$assumed = $this->getDomDocument()->encoding;
|
||||
if (empty($assumed)) {
|
||||
$assumed = 'UTF-8';
|
||||
}
|
||||
return $assumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry as xml
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function saveXml()
|
||||
{
|
||||
$dom = new DOMDocument('1.0', $this->getEncoding());
|
||||
$deep = version_compare(PHP_VERSION, '7', 'ge') ? 1 : true;
|
||||
$entry = $dom->importNode($this->getElement(), $deep);
|
||||
$dom->appendChild($entry);
|
||||
return $dom->saveXml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->data['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the XPath query object
|
||||
*
|
||||
* @return DOMXPath
|
||||
*/
|
||||
public function getXpath()
|
||||
{
|
||||
if (!$this->xpath) {
|
||||
$this->setXpath(new DOMXPath($this->getDomDocument()));
|
||||
}
|
||||
return $this->xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath query
|
||||
*
|
||||
* @param DOMXPath $xpath
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setXpath(DOMXPath $xpath)
|
||||
{
|
||||
$this->xpath = $xpath;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered extensions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExtensions()
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Extension object with the matching name (postfixed with _Entry)
|
||||
*
|
||||
* @param string $name
|
||||
* @return Reader\Extension\AbstractEntry
|
||||
*/
|
||||
public function getExtension($name)
|
||||
{
|
||||
if (array_key_exists($name . '\\Entry', $this->extensions)) {
|
||||
return $this->extensions[$name . '\\Entry'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method overloading: call given method on first extension implementing it
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
* @throws Exception\RuntimeException if no extensions implements the method
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
foreach ($this->extensions as $extension) {
|
||||
if (method_exists($extension, $method)) {
|
||||
return call_user_func_array([$extension, $method], $args);
|
||||
}
|
||||
}
|
||||
throw new Exception\RuntimeException(sprintf(
|
||||
'Method: %s does not exist and could not be located on a registered Extension',
|
||||
$method
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load extensions from Zend\Feed\Reader\Reader
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function loadExtensions()
|
||||
{
|
||||
$all = Reader\Reader::getExtensions();
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
$feed = $all['entry'];
|
||||
foreach ($feed as $extension) {
|
||||
if (in_array($extension, $all['core'])) {
|
||||
continue;
|
||||
}
|
||||
$plugin = $manager->get($extension);
|
||||
$plugin->setEntryElement($this->getElement());
|
||||
$plugin->setEntryKey($this->entryKey);
|
||||
$plugin->setType($this->data['type']);
|
||||
$this->extensions[$extension] = $plugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Entry;
|
||||
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
|
||||
class Atom extends AbstractEntry implements EntryInterface
|
||||
{
|
||||
/**
|
||||
* XPath query
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $xpathQuery = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DOMElement $entry
|
||||
* @param int $entryKey
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(DOMElement $entry, $entryKey, $type = null)
|
||||
{
|
||||
parent::__construct($entry, $entryKey, $type);
|
||||
|
||||
// Everyone by now should know XPath indices start from 1 not 0
|
||||
$this->xpathQuery = '//atom:entry[' . ($this->entryKey + 1) . ']';
|
||||
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
$extensions = ['Atom\Entry', 'Thread\Entry', 'DublinCore\Entry'];
|
||||
|
||||
foreach ($extensions as $name) {
|
||||
$extension = $manager->get($name);
|
||||
$extension->setEntryElement($entry);
|
||||
$extension->setEntryKey($entryKey);
|
||||
$extension->setType($type);
|
||||
$this->extensions[$name] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$people = $this->getExtension('Atom')->getAuthors();
|
||||
|
||||
$this->data['authors'] = $people;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if (array_key_exists('content', $this->data)) {
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
$content = $this->getExtension('Atom')->getContent();
|
||||
|
||||
$this->data['content'] = $content;
|
||||
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry creation date
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (array_key_exists('datecreated', $this->data)) {
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
$dateCreated = $this->getExtension('Atom')->getDateCreated();
|
||||
|
||||
$this->data['datecreated'] = $dateCreated;
|
||||
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry modification date
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$dateModified = $this->getExtension('Atom')->getDateModified();
|
||||
|
||||
$this->data['datemodified'] = $dateModified;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = $this->getExtension('Atom')->getDescription();
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry enclosure
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEnclosure()
|
||||
{
|
||||
if (array_key_exists('enclosure', $this->data)) {
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
$enclosure = $this->getExtension('Atom')->getEnclosure();
|
||||
|
||||
$this->data['enclosure'] = $enclosure;
|
||||
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->getExtension('Atom')->getId();
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific link
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getLink($index = 0)
|
||||
{
|
||||
if (!array_key_exists('links', $this->data)) {
|
||||
$this->getLinks();
|
||||
}
|
||||
|
||||
if (isset($this->data['links'][$index])) {
|
||||
return $this->data['links'][$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
if (array_key_exists('links', $this->data)) {
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
$links = $this->getExtension('Atom')->getLinks();
|
||||
|
||||
$this->data['links'] = $links;
|
||||
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permalink to the entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPermalink()
|
||||
{
|
||||
return $this->getLink(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->getExtension('Atom')->getTitle();
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of comments/replies for current entry
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
if (array_key_exists('commentcount', $this->data)) {
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
$commentcount = $this->getExtension('Thread')->getCommentCount();
|
||||
|
||||
if (!$commentcount) {
|
||||
$commentcount = $this->getExtension('Atom')->getCommentCount();
|
||||
}
|
||||
|
||||
$this->data['commentcount'] = $commentcount;
|
||||
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to the HTML page where comments can be made on this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentLink()
|
||||
{
|
||||
if (array_key_exists('commentlink', $this->data)) {
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
$commentlink = $this->getExtension('Atom')->getCommentLink();
|
||||
|
||||
$this->data['commentlink'] = $commentlink;
|
||||
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to a feed of all comments for this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentFeedLink()
|
||||
{
|
||||
if (array_key_exists('commentfeedlink', $this->data)) {
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink();
|
||||
|
||||
$this->data['commentfeedlink'] = $commentfeedlink;
|
||||
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category data as a Reader\Reader_Collection_Category object
|
||||
*
|
||||
* @return Reader\Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
$categoryCollection = $this->getExtension('Atom')->getCategories();
|
||||
|
||||
if (count($categoryCollection) == 0) {
|
||||
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source feed metadata from the entry
|
||||
*
|
||||
* @return Reader\Feed\Atom\Source|null
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if (array_key_exists('source', $this->data)) {
|
||||
return $this->data['source'];
|
||||
}
|
||||
|
||||
$source = $this->getExtension('Atom')->getSource();
|
||||
|
||||
$this->data['source'] = $source;
|
||||
|
||||
return $this->data['source'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath query (incl. on all Extensions)
|
||||
*
|
||||
* @param DOMXPath $xpath
|
||||
* @return void
|
||||
*/
|
||||
public function setXpath(DOMXPath $xpath)
|
||||
{
|
||||
parent::setXpath($xpath);
|
||||
foreach ($this->extensions as $extension) {
|
||||
$extension->setXpath($this->xpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Entry;
|
||||
|
||||
use Zend\Feed\Reader\Collection\Category;
|
||||
|
||||
interface EntryInterface
|
||||
{
|
||||
/**
|
||||
* Get the specified author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0);
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors();
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent();
|
||||
|
||||
/**
|
||||
* Get the entry creation date
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateCreated();
|
||||
|
||||
/**
|
||||
* Get the entry modification date
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateModified();
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription();
|
||||
|
||||
/**
|
||||
* Get the entry enclosure
|
||||
*
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function getEnclosure();
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId();
|
||||
|
||||
/**
|
||||
* Get a specific link
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getLink($index = 0);
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks();
|
||||
|
||||
/**
|
||||
* Get a permalink to the entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPermalink();
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle();
|
||||
|
||||
/**
|
||||
* Get the number of comments/replies for current entry
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCommentCount();
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to the HTML page where comments can be made on this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentLink();
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to a feed of all comments for this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentFeedLink();
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Category
|
||||
*/
|
||||
public function getCategories();
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Entry;
|
||||
|
||||
use DateTime;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
class Rss extends AbstractEntry implements EntryInterface
|
||||
{
|
||||
/**
|
||||
* XPath query for RDF
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $xpathQueryRdf = '';
|
||||
|
||||
/**
|
||||
* XPath query for RSS
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $xpathQueryRss = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DOMElement $entry
|
||||
* @param string $entryKey
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(DOMElement $entry, $entryKey, $type = null)
|
||||
{
|
||||
parent::__construct($entry, $entryKey, $type);
|
||||
$this->xpathQueryRss = '//item[' . ($this->entryKey+1) . ']';
|
||||
$this->xpathQueryRdf = '//rss:item[' . ($this->entryKey+1) . ']';
|
||||
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
$extensions = [
|
||||
'DublinCore\Entry',
|
||||
'Content\Entry',
|
||||
'Atom\Entry',
|
||||
'WellFormedWeb\Entry',
|
||||
'Slash\Entry',
|
||||
'Thread\Entry',
|
||||
];
|
||||
foreach ($extensions as $name) {
|
||||
$extension = $manager->get($name);
|
||||
$extension->setEntryElement($entry);
|
||||
$extension->setEntryKey($entryKey);
|
||||
$extension->setType($type);
|
||||
$this->extensions[$name] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an author entry
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
$authorsDc = $this->getExtension('DublinCore')->getAuthors();
|
||||
if (!empty($authorsDc)) {
|
||||
foreach ($authorsDc as $author) {
|
||||
$authors[] = [
|
||||
'name' => $author['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query($this->xpathQueryRss . '//author');
|
||||
} else {
|
||||
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:author');
|
||||
}
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$string = trim($author->nodeValue);
|
||||
$data = [];
|
||||
// Pretty rough parsing - but it's a catchall
|
||||
if (preg_match("/^.*@[^ ]*/", $string, $matches)) {
|
||||
$data['email'] = trim($matches[0]);
|
||||
if (preg_match("/\((.*)\)$/", $string, $matches)) {
|
||||
$data['name'] = $matches[1];
|
||||
}
|
||||
$authors[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = $this->getExtension('Atom')->getAuthors();
|
||||
} else {
|
||||
$authors = new Reader\Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = null;
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if (array_key_exists('content', $this->data)) {
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
$content = $this->getExtension('Content')->getContent();
|
||||
|
||||
if (!$content) {
|
||||
$content = $this->getDescription();
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
$content = $this->getExtension('Atom')->getContent();
|
||||
}
|
||||
|
||||
$this->data['content'] = $content;
|
||||
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry's date of creation
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
return $this->getDateModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry's date of modification
|
||||
*
|
||||
* @throws Exception\RuntimeException
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$dateModified = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/pubDate)');
|
||||
if ($dateModified) {
|
||||
$dateModifiedParsed = strtotime($dateModified);
|
||||
if ($dateModifiedParsed) {
|
||||
$date = new DateTime('@' . $dateModifiedParsed);
|
||||
} else {
|
||||
$dateStandards = [DateTime::RSS, DateTime::RFC822,
|
||||
DateTime::RFC2822, null];
|
||||
foreach ($dateStandards as $standard) {
|
||||
try {
|
||||
$date = date_create_from_format($standard, $dateModified);
|
||||
break;
|
||||
} catch (\Exception $e) {
|
||||
if ($standard === null) {
|
||||
throw new Exception\RuntimeException(
|
||||
'Could not load date due to unrecognised'
|
||||
.' format (should follow RFC 822 or 2822):'
|
||||
. $e->getMessage(),
|
||||
0, $e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getExtension('DublinCore')->getDate();
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getExtension('Atom')->getDateModified();
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = null;
|
||||
}
|
||||
|
||||
$this->data['datemodified'] = $date;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$description = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/description)');
|
||||
} else {
|
||||
$description = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:description)');
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = $this->getExtension('DublinCore')->getDescription();
|
||||
}
|
||||
|
||||
if (empty($description)) {
|
||||
$description = $this->getExtension('Atom')->getDescription();
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry enclosure
|
||||
* @return string
|
||||
*/
|
||||
public function getEnclosure()
|
||||
{
|
||||
if (array_key_exists('enclosure', $this->data)) {
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
$enclosure = null;
|
||||
|
||||
if ($this->getType() == Reader\Reader::TYPE_RSS_20) {
|
||||
$nodeList = $this->xpath->query($this->xpathQueryRss . '/enclosure');
|
||||
|
||||
if ($nodeList->length > 0) {
|
||||
$enclosure = new \stdClass();
|
||||
$enclosure->url = $nodeList->item(0)->getAttribute('url');
|
||||
$enclosure->length = $nodeList->item(0)->getAttribute('length');
|
||||
$enclosure->type = $nodeList->item(0)->getAttribute('type');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$enclosure) {
|
||||
$enclosure = $this->getExtension('Atom')->getEnclosure();
|
||||
}
|
||||
|
||||
$this->data['enclosure'] = $enclosure;
|
||||
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$id = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/guid)');
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
$id = $this->getExtension('DublinCore')->getId();
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
$id = $this->getExtension('Atom')->getId();
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
if ($this->getPermalink()) {
|
||||
$id = $this->getPermalink();
|
||||
} elseif ($this->getTitle()) {
|
||||
$id = $this->getTitle();
|
||||
} else {
|
||||
$id = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific link
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getLink($index = 0)
|
||||
{
|
||||
if (!array_key_exists('links', $this->data)) {
|
||||
$this->getLinks();
|
||||
}
|
||||
|
||||
if (isset($this->data['links'][$index])) {
|
||||
return $this->data['links'][$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
if (array_key_exists('links', $this->data)) {
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query($this->xpathQueryRss . '//link');
|
||||
} else {
|
||||
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:link');
|
||||
}
|
||||
|
||||
if (!$list->length) {
|
||||
$links = $this->getExtension('Atom')->getLinks();
|
||||
} else {
|
||||
foreach ($list as $link) {
|
||||
$links[] = $link->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['links'] = $links;
|
||||
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Reader\Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query($this->xpathQueryRss . '//category');
|
||||
} else {
|
||||
$list = $this->xpath->query($this->xpathQueryRdf . '//rss:category');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Reader\Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->nodeValue,
|
||||
'scheme' => $category->getAttribute('domain'),
|
||||
'label' => $category->nodeValue,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
|
||||
}
|
||||
|
||||
if (count($categoryCollection) == 0) {
|
||||
$categoryCollection = $this->getExtension('Atom')->getCategories();
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permalink to the entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPermalink()
|
||||
{
|
||||
return $this->getLink(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$title = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/title)');
|
||||
} else {
|
||||
$title = $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:title)');
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = $this->getExtension('DublinCore')->getTitle();
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = $this->getExtension('Atom')->getTitle();
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of comments/replies for current entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
if (array_key_exists('commentcount', $this->data)) {
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
$commentcount = $this->getExtension('Slash')->getCommentCount();
|
||||
|
||||
if (!$commentcount) {
|
||||
$commentcount = $this->getExtension('Thread')->getCommentCount();
|
||||
}
|
||||
|
||||
if (!$commentcount) {
|
||||
$commentcount = $this->getExtension('Atom')->getCommentCount();
|
||||
}
|
||||
|
||||
if (!$commentcount) {
|
||||
$commentcount = null;
|
||||
}
|
||||
|
||||
$this->data['commentcount'] = $commentcount;
|
||||
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to the HTML page where comments can be made on this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentLink()
|
||||
{
|
||||
if (array_key_exists('commentlink', $this->data)) {
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
$commentlink = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$commentlink = $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/comments)');
|
||||
}
|
||||
|
||||
if (!$commentlink) {
|
||||
$commentlink = $this->getExtension('Atom')->getCommentLink();
|
||||
}
|
||||
|
||||
if (!$commentlink) {
|
||||
$commentlink = null;
|
||||
}
|
||||
|
||||
$this->data['commentlink'] = $commentlink;
|
||||
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to a feed of all comments for this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentFeedLink()
|
||||
{
|
||||
if (array_key_exists('commentfeedlink', $this->data)) {
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
$commentfeedlink = $this->getExtension('WellFormedWeb')->getCommentFeedLink();
|
||||
|
||||
if (!$commentfeedlink) {
|
||||
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rss');
|
||||
}
|
||||
|
||||
if (!$commentfeedlink) {
|
||||
$commentfeedlink = $this->getExtension('Atom')->getCommentFeedLink('rdf');
|
||||
}
|
||||
|
||||
if (!$commentfeedlink) {
|
||||
$commentfeedlink = null;
|
||||
}
|
||||
|
||||
$this->data['commentfeedlink'] = $commentfeedlink;
|
||||
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath query (incl. on all Extensions)
|
||||
*
|
||||
* @param DOMXPath $xpath
|
||||
* @return void
|
||||
*/
|
||||
public function setXpath(DOMXPath $xpath)
|
||||
{
|
||||
parent::setXpath($xpath);
|
||||
foreach ($this->extensions as $extension) {
|
||||
$extension->setXpath($this->xpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Exception;
|
||||
|
||||
use Zend\Feed\Exception\ExceptionInterface as Exception;
|
||||
|
||||
interface ExceptionInterface extends Exception
|
||||
{
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
class InvalidHttpClientException extends Exception\InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
|
||||
abstract class AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Feed entry data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* DOM document object
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $domDocument = null;
|
||||
|
||||
/**
|
||||
* Entry instance
|
||||
*
|
||||
* @var DOMElement
|
||||
*/
|
||||
protected $entry = null;
|
||||
|
||||
/**
|
||||
* Pointer to the current entry
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $entryKey = 0;
|
||||
|
||||
/**
|
||||
* XPath object
|
||||
*
|
||||
* @var DOMXPath
|
||||
*/
|
||||
protected $xpath = null;
|
||||
|
||||
/**
|
||||
* XPath query
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $xpathPrefix = '';
|
||||
|
||||
/**
|
||||
* Set the entry DOMElement
|
||||
*
|
||||
* Has side effect of setting the DOMDocument for the entry.
|
||||
*
|
||||
* @param DOMElement $entry
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setEntryElement(DOMElement $entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$this->domDocument = $entry->ownerDocument;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry DOMElement
|
||||
*
|
||||
* @return DOMElement
|
||||
*/
|
||||
public function getEntryElement()
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entry key
|
||||
*
|
||||
* @param string $entryKey
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setEntryKey($entryKey)
|
||||
{
|
||||
$this->entryKey = $entryKey;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOM
|
||||
*
|
||||
* @return DOMDocument
|
||||
*/
|
||||
public function getDomDocument()
|
||||
{
|
||||
return $this->domDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Entry's encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
$assumed = $this->getDomDocument()->encoding;
|
||||
return $assumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entry type
|
||||
*
|
||||
* Has side effect of setting xpath prefix
|
||||
*
|
||||
* @param string $type
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
if (null === $type) {
|
||||
$this->data['type'] = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->data['type'] = $type;
|
||||
if ($type === Reader\Reader::TYPE_RSS_10
|
||||
|| $type === Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$this->setXpathPrefix('//rss:item[' . ($this->entryKey + 1) . ']');
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($type === Reader\Reader::TYPE_ATOM_10
|
||||
|| $type === Reader\Reader::TYPE_ATOM_03
|
||||
) {
|
||||
$this->setXpathPrefix('//atom:entry[' . ($this->entryKey + 1) . ']');
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->setXpathPrefix('//item[' . ($this->entryKey + 1) . ']');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
$type = $this->data['type'];
|
||||
if ($type === null) {
|
||||
$type = Reader\Reader::detectType($this->getEntryElement(), true);
|
||||
$this->setType($type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath query
|
||||
*
|
||||
* @param DOMXPath $xpath
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setXpath(DOMXPath $xpath)
|
||||
{
|
||||
$this->xpath = $xpath;
|
||||
$this->registerNamespaces();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the XPath query object
|
||||
*
|
||||
* @return DOMXPath
|
||||
*/
|
||||
public function getXpath()
|
||||
{
|
||||
if (!$this->xpath) {
|
||||
$this->setXpath(new DOMXPath($this->getDomDocument()));
|
||||
}
|
||||
return $this->xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the entry to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the XPath prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getXpathPrefix()
|
||||
{
|
||||
return $this->xpathPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath prefix
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setXpathPrefix($prefix)
|
||||
{
|
||||
$this->xpathPrefix = $prefix;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register XML namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function registerNamespaces();
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
|
||||
abstract class AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Parsed feed data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Parsed feed data in the shape of a DOMDocument
|
||||
*
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $domDocument = null;
|
||||
|
||||
/**
|
||||
* The base XPath query used to retrieve feed data
|
||||
*
|
||||
* @var DOMXPath
|
||||
*/
|
||||
protected $xpath = null;
|
||||
|
||||
/**
|
||||
* The XPath prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $xpathPrefix = '';
|
||||
|
||||
/**
|
||||
* Set the DOM document
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setDomDocument(DOMDocument $dom)
|
||||
{
|
||||
$this->domDocument = $dom;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOM
|
||||
*
|
||||
* @return DOMDocument
|
||||
*/
|
||||
public function getDomDocument()
|
||||
{
|
||||
return $this->domDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Feed's encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
$assumed = $this->getDomDocument()->encoding;
|
||||
return $assumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed type
|
||||
*
|
||||
* @param string $type
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->data['type'] = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed type
|
||||
*
|
||||
* If null, it will attempt to autodetect the type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
$type = $this->data['type'];
|
||||
if (null === $type) {
|
||||
$type = Reader\Reader::detectType($this->getDomDocument());
|
||||
$this->setType($type);
|
||||
}
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the feed as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray() // untested
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath query
|
||||
*
|
||||
* @param DOMXPath $xpath
|
||||
* @return AbstractEntry
|
||||
*/
|
||||
public function setXpath(DOMXPath $xpath = null)
|
||||
{
|
||||
if (null === $xpath) {
|
||||
$this->xpath = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->xpath = $xpath;
|
||||
$this->registerNamespaces();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DOMXPath object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getXpath()
|
||||
{
|
||||
if (null === $this->xpath) {
|
||||
$this->setXpath(new DOMXPath($this->getDomDocument()));
|
||||
}
|
||||
|
||||
return $this->xpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the XPath prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getXpathPrefix()
|
||||
{
|
||||
return $this->xpathPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XPath prefix
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return void
|
||||
*/
|
||||
public function setXpathPrefix($prefix)
|
||||
{
|
||||
$this->xpathPrefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*/
|
||||
abstract protected function registerNamespaces();
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Atom;
|
||||
|
||||
use DateTime;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use stdClass;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Collection;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
use Zend\Feed\Uri;
|
||||
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the specified author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return Collection\Author
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:author');
|
||||
|
||||
if (!$list->length) {
|
||||
/**
|
||||
* TODO: Limit query to feed level els only!
|
||||
*/
|
||||
$list = $this->getXpath()->query('//atom:author');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$author = $this->getAuthorFromElement($author);
|
||||
if (!empty($author)) {
|
||||
$authors[] = $author;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = new Collection\Author();
|
||||
} else {
|
||||
$authors = new Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if (array_key_exists('content', $this->data)) {
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
$content = null;
|
||||
|
||||
$el = $this->getXpath()->query($this->getXpathPrefix() . '/atom:content');
|
||||
if ($el->length > 0) {
|
||||
$el = $el->item(0);
|
||||
$type = $el->getAttribute('type');
|
||||
switch ($type) {
|
||||
case '':
|
||||
case 'text':
|
||||
case 'text/plain':
|
||||
case 'html':
|
||||
case 'text/html':
|
||||
$content = $el->nodeValue;
|
||||
break;
|
||||
case 'xhtml':
|
||||
$this->getXpath()->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
|
||||
$xhtml = $this->getXpath()->query(
|
||||
$this->getXpathPrefix() . '/atom:content/xhtml:div'
|
||||
)->item(0);
|
||||
$d = new DOMDocument('1.0', $this->getEncoding());
|
||||
$deep = version_compare(PHP_VERSION, '7', 'ge') ? 1 : true;
|
||||
$xhtmls = $d->importNode($xhtml, $deep);
|
||||
$d->appendChild($xhtmls);
|
||||
$content = $this->collectXhtml(
|
||||
$d->saveXML(),
|
||||
$d->lookupPrefix('http://www.w3.org/1999/xhtml')
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$content) {
|
||||
$content = $this->getDescription();
|
||||
}
|
||||
|
||||
$this->data['content'] = trim($content);
|
||||
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out XHTML to remove the namespacing
|
||||
*
|
||||
* @param $xhtml
|
||||
* @param $prefix
|
||||
* @return mixed
|
||||
*/
|
||||
protected function collectXhtml($xhtml, $prefix)
|
||||
{
|
||||
if (!empty($prefix)) {
|
||||
$prefix = $prefix . ':';
|
||||
}
|
||||
$matches = [
|
||||
"/<\?xml[^<]*>[^<]*<" . $prefix . "div[^<]*/",
|
||||
"/<\/" . $prefix . "div>\s*$/"
|
||||
];
|
||||
$xhtml = preg_replace($matches, '', $xhtml);
|
||||
if (!empty($prefix)) {
|
||||
$xhtml = preg_replace("/(<[\/]?)" . $prefix . "([a-zA-Z]+)/", '$1$2', $xhtml);
|
||||
}
|
||||
return $xhtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry creation date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (array_key_exists('datecreated', $this->data)) {
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)');
|
||||
} else {
|
||||
$dateCreated = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)');
|
||||
}
|
||||
|
||||
if ($dateCreated) {
|
||||
$date = new DateTime($dateCreated);
|
||||
}
|
||||
|
||||
$this->data['datecreated'] = $date;
|
||||
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry modification date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getAtomType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)');
|
||||
} else {
|
||||
$dateModified = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)');
|
||||
}
|
||||
|
||||
if ($dateModified) {
|
||||
$date = new DateTime($dateModified);
|
||||
}
|
||||
|
||||
$this->data['datemodified'] = $date;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:summary)');
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry enclosure
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEnclosure()
|
||||
{
|
||||
if (array_key_exists('enclosure', $this->data)) {
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
$enclosure = null;
|
||||
|
||||
$nodeList = $this->getXpath()->query($this->getXpathPrefix() . '/atom:link[@rel="enclosure"]');
|
||||
|
||||
if ($nodeList->length > 0) {
|
||||
$enclosure = new stdClass();
|
||||
$enclosure->url = $nodeList->item(0)->getAttribute('href');
|
||||
$enclosure->length = $nodeList->item(0)->getAttribute('length');
|
||||
$enclosure->type = $nodeList->item(0)->getAttribute('type');
|
||||
}
|
||||
|
||||
$this->data['enclosure'] = $enclosure;
|
||||
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)');
|
||||
|
||||
if (!$id) {
|
||||
if ($this->getPermalink()) {
|
||||
$id = $this->getPermalink();
|
||||
} elseif ($this->getTitle()) {
|
||||
$id = $this->getTitle();
|
||||
} else {
|
||||
$id = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URI of the feed (if set).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
if (array_key_exists('baseUrl', $this->data)) {
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
$baseUrl = $this->getXpath()->evaluate(
|
||||
'string('
|
||||
. $this->getXpathPrefix()
|
||||
. '/@xml:base[1]'
|
||||
. ')'
|
||||
);
|
||||
|
||||
if (!$baseUrl) {
|
||||
$baseUrl = $this->getXpath()->evaluate('string(//@xml:base[1])');
|
||||
}
|
||||
|
||||
if (!$baseUrl) {
|
||||
$baseUrl = null;
|
||||
}
|
||||
|
||||
$this->data['baseUrl'] = $baseUrl;
|
||||
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific link
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getLink($index = 0)
|
||||
{
|
||||
if (!array_key_exists('links', $this->data)) {
|
||||
$this->getLinks();
|
||||
}
|
||||
|
||||
if (isset($this->data['links'][$index])) {
|
||||
return $this->data['links'][$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
if (array_key_exists('links', $this->data)) {
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
$list = $this->getXpath()->query(
|
||||
$this->getXpathPrefix() . '//atom:link[@rel="alternate"]/@href' . '|' .
|
||||
$this->getXpathPrefix() . '//atom:link[not(@rel)]/@href'
|
||||
);
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $link) {
|
||||
$links[] = $this->absolutiseUri($link->value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['links'] = $links;
|
||||
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a permalink to the entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPermalink()
|
||||
{
|
||||
return $this->getLink(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)');
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of comments/replies for current entry
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
if (array_key_exists('commentcount', $this->data)) {
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
$count = null;
|
||||
|
||||
$this->getXpath()->registerNamespace('thread10', 'http://purl.org/syndication/thread/1.0');
|
||||
$list = $this->getXpath()->query(
|
||||
$this->getXpathPrefix() . '//atom:link[@rel="replies"]/@thread10:count'
|
||||
);
|
||||
|
||||
if ($list->length) {
|
||||
$count = $list->item(0)->value;
|
||||
}
|
||||
|
||||
$this->data['commentcount'] = $count;
|
||||
|
||||
return $this->data['commentcount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to the HTML page where comments can be made on this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentLink()
|
||||
{
|
||||
if (array_key_exists('commentlink', $this->data)) {
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
$link = null;
|
||||
|
||||
$list = $this->getXpath()->query(
|
||||
$this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="text/html"]/@href'
|
||||
);
|
||||
|
||||
if ($list->length) {
|
||||
$link = $list->item(0)->value;
|
||||
$link = $this->absolutiseUri($link);
|
||||
}
|
||||
|
||||
$this->data['commentlink'] = $link;
|
||||
|
||||
return $this->data['commentlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to a feed of all comments for this entry
|
||||
*
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentFeedLink($type = 'atom')
|
||||
{
|
||||
if (array_key_exists('commentfeedlink', $this->data)) {
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
$link = null;
|
||||
|
||||
$list = $this->getXpath()->query(
|
||||
$this->getXpathPrefix() . '//atom:link[@rel="replies" and @type="application/' . $type.'+xml"]/@href'
|
||||
);
|
||||
|
||||
if ($list->length) {
|
||||
$link = $list->item(0)->value;
|
||||
$link = $this->absolutiseUri($link);
|
||||
}
|
||||
|
||||
$this->data['commentfeedlink'] = $link;
|
||||
|
||||
return $this->data['commentfeedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
if ($this->getAtomType() == Reader\Reader::TYPE_ATOM_10) {
|
||||
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom:category');
|
||||
} else {
|
||||
/**
|
||||
* Since Atom 0.3 did not support categories, it would have used the
|
||||
* Dublin Core extension. However there is a small possibility Atom 0.3
|
||||
* may have been retrofitted to use Atom 1.0 instead.
|
||||
*/
|
||||
$this->getXpath()->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10);
|
||||
$list = $this->getXpath()->query($this->getXpathPrefix() . '//atom10:category');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->getAttribute('term'),
|
||||
'scheme' => $category->getAttribute('scheme'),
|
||||
'label' => $category->getAttribute('label')
|
||||
];
|
||||
}
|
||||
} else {
|
||||
return new Collection\Category;
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source feed metadata from the entry
|
||||
*
|
||||
* @return Reader\Feed\Atom\Source|null
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if (array_key_exists('source', $this->data)) {
|
||||
return $this->data['source'];
|
||||
}
|
||||
|
||||
$source = null;
|
||||
// TODO: Investigate why _getAtomType() fails here. Is it even needed?
|
||||
if ($this->getType() == Reader\Reader::TYPE_ATOM_10) {
|
||||
$list = $this->getXpath()->query($this->getXpathPrefix() . '/atom:source[1]');
|
||||
if ($list->length) {
|
||||
$element = $list->item(0);
|
||||
$source = new Reader\Feed\Atom\Source($element, $this->getXpathPrefix());
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['source'] = $source;
|
||||
return $this->data['source'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to absolutise the URI, i.e. if a relative URI apply the
|
||||
* xml:base value as a prefix to turn into an absolute URI.
|
||||
*
|
||||
* @param $link
|
||||
* @return string
|
||||
*/
|
||||
protected function absolutiseUri($link)
|
||||
{
|
||||
if (!Uri::factory($link)->isAbsolute()) {
|
||||
if ($this->getBaseUrl() !== null) {
|
||||
$link = $this->getBaseUrl() . $link;
|
||||
if (!Uri::factory($link)->isValid()) {
|
||||
$link = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an author entry
|
||||
*
|
||||
* @param DOMElement $element
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorFromElement(DOMElement $element)
|
||||
{
|
||||
$author = [];
|
||||
|
||||
$emailNode = $element->getElementsByTagName('email');
|
||||
$nameNode = $element->getElementsByTagName('name');
|
||||
$uriNode = $element->getElementsByTagName('uri');
|
||||
|
||||
if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) {
|
||||
$author['email'] = $emailNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) {
|
||||
$author['name'] = $nameNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) {
|
||||
$author['uri'] = $uriNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if (empty($author)) {
|
||||
return;
|
||||
}
|
||||
return $author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
switch ($this->getAtomType()) {
|
||||
case Reader\Reader::TYPE_ATOM_03:
|
||||
$this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03);
|
||||
break;
|
||||
default:
|
||||
$this->getXpath()->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the presence of any Atom namespaces in use
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAtomType()
|
||||
{
|
||||
$dom = $this->getDomDocument();
|
||||
$prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03);
|
||||
$prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10);
|
||||
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03)
|
||||
|| !empty($prefixAtom03)) {
|
||||
return Reader\Reader::TYPE_ATOM_03;
|
||||
}
|
||||
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10)
|
||||
|| !empty($prefixAtom10)) {
|
||||
return Reader\Reader::TYPE_ATOM_10;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Atom;
|
||||
|
||||
use DateTime;
|
||||
use DOMElement;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Collection;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
use Zend\Feed\Uri;
|
||||
|
||||
class Feed extends Extension\AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return Collection\Author
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$list = $this->xpath->query('//atom:author');
|
||||
|
||||
$authors = [];
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$author = $this->getAuthorFromElement($author);
|
||||
if (!empty($author)) {
|
||||
$authors[] = $author;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = new Collection\Author();
|
||||
} else {
|
||||
$authors = new Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (array_key_exists('copyright', $this->data)) {
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
$copyright = null;
|
||||
|
||||
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:copyright)');
|
||||
} else {
|
||||
$copyright = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:rights)');
|
||||
}
|
||||
|
||||
if (!$copyright) {
|
||||
$copyright = null;
|
||||
}
|
||||
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed creation date
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (array_key_exists('datecreated', $this->data)) {
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:created)');
|
||||
} else {
|
||||
$dateCreated = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:published)');
|
||||
}
|
||||
|
||||
if ($dateCreated) {
|
||||
$date = new DateTime($dateCreated);
|
||||
}
|
||||
|
||||
$this->data['datecreated'] = $date;
|
||||
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed modification date
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:modified)');
|
||||
} else {
|
||||
$dateModified = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:updated)');
|
||||
}
|
||||
|
||||
if ($dateModified) {
|
||||
$date = new DateTime($dateModified);
|
||||
}
|
||||
|
||||
$this->data['datemodified'] = $date;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = null;
|
||||
|
||||
if ($this->getType() === Reader\Reader::TYPE_ATOM_03) {
|
||||
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:tagline)');
|
||||
} else {
|
||||
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:subtitle)');
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed generator entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerator()
|
||||
{
|
||||
if (array_key_exists('generator', $this->data)) {
|
||||
return $this->data['generator'];
|
||||
}
|
||||
// TODO: Add uri support
|
||||
$generator = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:generator)');
|
||||
|
||||
if (!$generator) {
|
||||
$generator = null;
|
||||
}
|
||||
|
||||
$this->data['generator'] = $generator;
|
||||
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:id)');
|
||||
|
||||
if (!$id) {
|
||||
if ($this->getLink()) {
|
||||
$id = $this->getLink();
|
||||
} elseif ($this->getTitle()) {
|
||||
$id = $this->getTitle();
|
||||
} else {
|
||||
$id = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
if (array_key_exists('language', $this->data)) {
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
$language = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:lang)');
|
||||
|
||||
if (!$language) {
|
||||
$language = $this->xpath->evaluate('string(//@xml:lang[1])');
|
||||
}
|
||||
|
||||
if (!$language) {
|
||||
$language = null;
|
||||
}
|
||||
|
||||
$this->data['language'] = $language;
|
||||
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed image
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
if (array_key_exists('image', $this->data)) {
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
$imageUrl = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:logo)');
|
||||
|
||||
if (!$imageUrl) {
|
||||
$image = null;
|
||||
} else {
|
||||
$image = ['uri' => $imageUrl];
|
||||
}
|
||||
|
||||
$this->data['image'] = $image;
|
||||
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URI of the feed (if set).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
if (array_key_exists('baseUrl', $this->data)) {
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
$baseUrl = $this->xpath->evaluate('string(//@xml:base[1])');
|
||||
|
||||
if (!$baseUrl) {
|
||||
$baseUrl = null;
|
||||
}
|
||||
$this->data['baseUrl'] = $baseUrl;
|
||||
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the source website
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
if (array_key_exists('link', $this->data)) {
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
$link = null;
|
||||
|
||||
$list = $this->xpath->query(
|
||||
$this->getXpathPrefix() . '/atom:link[@rel="alternate"]/@href' . '|' .
|
||||
$this->getXpathPrefix() . '/atom:link[not(@rel)]/@href'
|
||||
);
|
||||
|
||||
if ($list->length) {
|
||||
$link = $list->item(0)->nodeValue;
|
||||
$link = $this->absolutiseUri($link);
|
||||
}
|
||||
|
||||
$this->data['link'] = $link;
|
||||
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the feed's XML Url
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFeedLink()
|
||||
{
|
||||
if (array_key_exists('feedlink', $this->data)) {
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
$link = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:link[@rel="self"]/@href)');
|
||||
|
||||
$link = $this->absolutiseUri($link);
|
||||
|
||||
$this->data['feedlink'] = $link;
|
||||
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of any supported Pusubhubbub endpoints
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getHubs()
|
||||
{
|
||||
if (array_key_exists('hubs', $this->data)) {
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
$hubs = [];
|
||||
|
||||
$list = $this->xpath->query($this->getXpathPrefix()
|
||||
. '//atom:link[@rel="hub"]/@href');
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $uri) {
|
||||
$hubs[] = $this->absolutiseUri($uri->nodeValue);
|
||||
}
|
||||
} else {
|
||||
$hubs = null;
|
||||
}
|
||||
|
||||
$this->data['hubs'] = $hubs;
|
||||
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)');
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
if ($this->getType() == Reader\Reader::TYPE_ATOM_10) {
|
||||
$list = $this->xpath->query($this->getXpathPrefix() . '/atom:category');
|
||||
} else {
|
||||
/**
|
||||
* Since Atom 0.3 did not support categories, it would have used the
|
||||
* Dublin Core extension. However there is a small possibility Atom 0.3
|
||||
* may have been retrofittied to use Atom 1.0 instead.
|
||||
*/
|
||||
$this->xpath->registerNamespace('atom10', Reader\Reader::NAMESPACE_ATOM_10);
|
||||
$list = $this->xpath->query($this->getXpathPrefix() . '/atom10:category');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->getAttribute('term'),
|
||||
'scheme' => $category->getAttribute('scheme'),
|
||||
'label' => $category->getAttribute('label')
|
||||
];
|
||||
}
|
||||
} else {
|
||||
return new Collection\Category;
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an author entry in RSS format
|
||||
*
|
||||
* @param DOMElement $element
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorFromElement(DOMElement $element)
|
||||
{
|
||||
$author = [];
|
||||
|
||||
$emailNode = $element->getElementsByTagName('email');
|
||||
$nameNode = $element->getElementsByTagName('name');
|
||||
$uriNode = $element->getElementsByTagName('uri');
|
||||
|
||||
if ($emailNode->length && strlen($emailNode->item(0)->nodeValue) > 0) {
|
||||
$author['email'] = $emailNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if ($nameNode->length && strlen($nameNode->item(0)->nodeValue) > 0) {
|
||||
$author['name'] = $nameNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if ($uriNode->length && strlen($uriNode->item(0)->nodeValue) > 0) {
|
||||
$author['uri'] = $uriNode->item(0)->nodeValue;
|
||||
}
|
||||
|
||||
if (empty($author)) {
|
||||
return;
|
||||
}
|
||||
return $author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to absolutise the URI, i.e. if a relative URI apply the
|
||||
* xml:base value as a prefix to turn into an absolute URI.
|
||||
*/
|
||||
protected function absolutiseUri($link)
|
||||
{
|
||||
if (!Uri::factory($link)->isAbsolute()) {
|
||||
if ($this->getBaseUrl() !== null) {
|
||||
$link = $this->getBaseUrl() . $link;
|
||||
if (!Uri::factory($link)->isValid()) {
|
||||
$link = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
if ($this->getType() == Reader\Reader::TYPE_ATOM_10
|
||||
|| $this->getType() == Reader\Reader::TYPE_ATOM_03
|
||||
) {
|
||||
return; // pre-registered at Feed level
|
||||
}
|
||||
$atomDetected = $this->getAtomType();
|
||||
switch ($atomDetected) {
|
||||
case Reader\Reader::TYPE_ATOM_03:
|
||||
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03);
|
||||
break;
|
||||
default:
|
||||
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the presence of any Atom namespaces in use
|
||||
*/
|
||||
protected function getAtomType()
|
||||
{
|
||||
$dom = $this->getDomDocument();
|
||||
$prefixAtom03 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_03);
|
||||
$prefixAtom10 = $dom->lookupPrefix(Reader\Reader::NAMESPACE_ATOM_10);
|
||||
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_10)
|
||||
|| !empty($prefixAtom10)
|
||||
) {
|
||||
return Reader\Reader::TYPE_ATOM_10;
|
||||
}
|
||||
if ($dom->isDefaultNamespace(Reader\Reader::NAMESPACE_ATOM_03)
|
||||
|| !empty($prefixAtom03)
|
||||
) {
|
||||
return Reader\Reader::TYPE_ATOM_03;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Content;
|
||||
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
public function getContent()
|
||||
{
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
|
||||
} else {
|
||||
$content = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register RSS Content Module namespace
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\CreativeCommons;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the entry license
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLicense($index = 0)
|
||||
{
|
||||
$licenses = $this->getLicenses();
|
||||
|
||||
if (isset($licenses[$index])) {
|
||||
return $licenses[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry licenses
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLicenses()
|
||||
{
|
||||
$name = 'licenses';
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$licenses = [];
|
||||
$list = $this->xpath->evaluate($this->getXpathPrefix() . '//cc:license');
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $license) {
|
||||
$licenses[] = $license->nodeValue;
|
||||
}
|
||||
|
||||
$licenses = array_unique($licenses);
|
||||
} else {
|
||||
$cc = new Feed();
|
||||
$licenses = $cc->getLicenses();
|
||||
}
|
||||
|
||||
$this->data[$name] = $licenses;
|
||||
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Creative Commons namespaces
|
||||
*
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('cc', 'http://backend.userland.com/creativeCommonsRssModule');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\CreativeCommons;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Feed extends Extension\AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Get the entry license
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLicense($index = 0)
|
||||
{
|
||||
$licenses = $this->getLicenses();
|
||||
|
||||
if (isset($licenses[$index])) {
|
||||
return $licenses[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry licenses
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLicenses()
|
||||
{
|
||||
$name = 'licenses';
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$licenses = [];
|
||||
$list = $this->xpath->evaluate('channel/cc:license');
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $license) {
|
||||
$licenses[] = $license->nodeValue;
|
||||
}
|
||||
|
||||
$licenses = array_unique($licenses);
|
||||
}
|
||||
|
||||
$this->data[$name] = $licenses;
|
||||
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Creative Commons namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('cc', 'http://backend.userland.com/creativeCommonsRssModule');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\DublinCore;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Collection;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get an author entry
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:creator');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:creator');
|
||||
}
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:publisher');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:publisher');
|
||||
}
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$authors[] = [
|
||||
'name' => $author->nodeValue
|
||||
];
|
||||
}
|
||||
$authors = new Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
} else {
|
||||
$authors = null;
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories (subjects under DC)
|
||||
*
|
||||
* @return Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:subject');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->nodeValue,
|
||||
'scheme' => null,
|
||||
'label' => $category->nodeValue,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$categoryCollection = new Collection\Category;
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:description)');
|
||||
|
||||
if (!$description) {
|
||||
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:description)');
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:identifier)');
|
||||
|
||||
if (!$id) {
|
||||
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:identifier)');
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:title)');
|
||||
|
||||
if (!$title) {
|
||||
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:title)');
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getDate()
|
||||
{
|
||||
if (array_key_exists('date', $this->data)) {
|
||||
return $this->data['date'];
|
||||
}
|
||||
|
||||
$d = null;
|
||||
$date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:date)');
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:date)');
|
||||
}
|
||||
|
||||
if ($date) {
|
||||
$d = new DateTime($date);
|
||||
}
|
||||
|
||||
$this->data['date'] = $d;
|
||||
|
||||
return $this->data['date'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register DC namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->getXpath()->registerNamespace('dc10', 'http://purl.org/dc/elements/1.0/');
|
||||
$this->getXpath()->registerNamespace('dc11', 'http://purl.org/dc/elements/1.1/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\DublinCore;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Collection;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Feed extends Extension\AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
$list = $this->getXpath()->query('//dc11:creator');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->query('//dc10:creator');
|
||||
}
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->query('//dc11:publisher');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->query('//dc10:publisher');
|
||||
}
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$authors[] = [
|
||||
'name' => $author->nodeValue
|
||||
];
|
||||
}
|
||||
$authors = new Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
} else {
|
||||
$authors = null;
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (array_key_exists('copyright', $this->data)) {
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
$copyright = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:rights)');
|
||||
|
||||
if (!$copyright) {
|
||||
$copyright = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:rights)');
|
||||
}
|
||||
|
||||
if (!$copyright) {
|
||||
$copyright = null;
|
||||
}
|
||||
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:description)');
|
||||
|
||||
if (!$description) {
|
||||
$description = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:description)');
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:identifier)');
|
||||
|
||||
if (!$id) {
|
||||
$id = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:identifier)');
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
if (array_key_exists('language', $this->data)) {
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
$language = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:language)');
|
||||
|
||||
if (!$language) {
|
||||
$language = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:language)');
|
||||
}
|
||||
|
||||
if (!$language) {
|
||||
$language = null;
|
||||
}
|
||||
|
||||
$this->data['language'] = $language;
|
||||
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:title)');
|
||||
|
||||
if (!$title) {
|
||||
$title = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:title)');
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getDate()
|
||||
{
|
||||
if (array_key_exists('date', $this->data)) {
|
||||
return $this->data['date'];
|
||||
}
|
||||
|
||||
$d = null;
|
||||
$date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc11:date)');
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getXpath()->evaluate('string(' . $this->getXpathPrefix() . '/dc10:date)');
|
||||
}
|
||||
|
||||
if ($date) {
|
||||
$d = new DateTime($date);
|
||||
}
|
||||
|
||||
$this->data['date'] = $d;
|
||||
|
||||
return $this->data['date'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories (subjects under DC)
|
||||
*
|
||||
* @return Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:subject');
|
||||
|
||||
if (!$list->length) {
|
||||
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->nodeValue,
|
||||
'scheme' => null,
|
||||
'label' => $category->nodeValue,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$categoryCollection = new Collection\Category;
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->getXpath()->registerNamespace('dc10', 'http://purl.org/dc/elements/1.0/');
|
||||
$this->getXpath()->registerNamespace('dc11', 'http://purl.org/dc/elements/1.1/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Podcast;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the entry author
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCastAuthor()
|
||||
{
|
||||
if (isset($this->data['author'])) {
|
||||
return $this->data['author'];
|
||||
}
|
||||
|
||||
$author = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:author)');
|
||||
|
||||
if (!$author) {
|
||||
$author = null;
|
||||
}
|
||||
|
||||
$this->data['author'] = $author;
|
||||
|
||||
return $this->data['author'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry block
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBlock()
|
||||
{
|
||||
if (isset($this->data['block'])) {
|
||||
return $this->data['block'];
|
||||
}
|
||||
|
||||
$block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:block)');
|
||||
|
||||
if (!$block) {
|
||||
$block = null;
|
||||
}
|
||||
|
||||
$this->data['block'] = $block;
|
||||
|
||||
return $this->data['block'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry duration
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDuration()
|
||||
{
|
||||
if (isset($this->data['duration'])) {
|
||||
return $this->data['duration'];
|
||||
}
|
||||
|
||||
$duration = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:duration)');
|
||||
|
||||
if (!$duration) {
|
||||
$duration = null;
|
||||
}
|
||||
|
||||
$this->data['duration'] = $duration;
|
||||
|
||||
return $this->data['duration'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry explicit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExplicit()
|
||||
{
|
||||
if (isset($this->data['explicit'])) {
|
||||
return $this->data['explicit'];
|
||||
}
|
||||
|
||||
$explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:explicit)');
|
||||
|
||||
if (!$explicit) {
|
||||
$explicit = null;
|
||||
}
|
||||
|
||||
$this->data['explicit'] = $explicit;
|
||||
|
||||
return $this->data['explicit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry keywords
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeywords()
|
||||
{
|
||||
if (isset($this->data['keywords'])) {
|
||||
return $this->data['keywords'];
|
||||
}
|
||||
|
||||
$keywords = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:keywords)');
|
||||
|
||||
if (!$keywords) {
|
||||
$keywords = null;
|
||||
}
|
||||
|
||||
$this->data['keywords'] = $keywords;
|
||||
|
||||
return $this->data['keywords'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry subtitle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSubtitle()
|
||||
{
|
||||
if (isset($this->data['subtitle'])) {
|
||||
return $this->data['subtitle'];
|
||||
}
|
||||
|
||||
$subtitle = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:subtitle)');
|
||||
|
||||
if (!$subtitle) {
|
||||
$subtitle = null;
|
||||
}
|
||||
|
||||
$this->data['subtitle'] = $subtitle;
|
||||
|
||||
return $this->data['subtitle'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry summary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSummary()
|
||||
{
|
||||
if (isset($this->data['summary'])) {
|
||||
return $this->data['summary'];
|
||||
}
|
||||
|
||||
$summary = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:summary)');
|
||||
|
||||
if (!$summary) {
|
||||
$summary = null;
|
||||
}
|
||||
|
||||
$this->data['summary'] = $summary;
|
||||
|
||||
return $this->data['summary'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register iTunes namespace
|
||||
*
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Podcast;
|
||||
|
||||
use DOMText;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Feed extends Extension\AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Get the entry author
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCastAuthor()
|
||||
{
|
||||
if (isset($this->data['author'])) {
|
||||
return $this->data['author'];
|
||||
}
|
||||
|
||||
$author = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:author)');
|
||||
|
||||
if (!$author) {
|
||||
$author = null;
|
||||
}
|
||||
|
||||
$this->data['author'] = $author;
|
||||
|
||||
return $this->data['author'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry block
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBlock()
|
||||
{
|
||||
if (isset($this->data['block'])) {
|
||||
return $this->data['block'];
|
||||
}
|
||||
|
||||
$block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:block)');
|
||||
|
||||
if (!$block) {
|
||||
$block = null;
|
||||
}
|
||||
|
||||
$this->data['block'] = $block;
|
||||
|
||||
return $this->data['block'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry category
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getItunesCategories()
|
||||
{
|
||||
if (isset($this->data['categories'])) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
$categoryList = $this->xpath->query($this->getXpathPrefix() . '/itunes:category');
|
||||
|
||||
$categories = [];
|
||||
|
||||
if ($categoryList->length > 0) {
|
||||
foreach ($categoryList as $node) {
|
||||
$children = null;
|
||||
|
||||
if ($node->childNodes->length > 0) {
|
||||
$children = [];
|
||||
|
||||
foreach ($node->childNodes as $childNode) {
|
||||
if (!($childNode instanceof DOMText)) {
|
||||
$children[$childNode->getAttribute('text')] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$categories[$node->getAttribute('text')] = $children;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$categories) {
|
||||
$categories = null;
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categories;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry explicit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExplicit()
|
||||
{
|
||||
if (isset($this->data['explicit'])) {
|
||||
return $this->data['explicit'];
|
||||
}
|
||||
|
||||
$explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:explicit)');
|
||||
|
||||
if (!$explicit) {
|
||||
$explicit = null;
|
||||
}
|
||||
|
||||
$this->data['explicit'] = $explicit;
|
||||
|
||||
return $this->data['explicit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getItunesImage()
|
||||
{
|
||||
if (isset($this->data['image'])) {
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
$image = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:image/@href)');
|
||||
|
||||
if (!$image) {
|
||||
$image = null;
|
||||
}
|
||||
|
||||
$this->data['image'] = $image;
|
||||
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry keywords
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeywords()
|
||||
{
|
||||
if (isset($this->data['keywords'])) {
|
||||
return $this->data['keywords'];
|
||||
}
|
||||
|
||||
$keywords = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:keywords)');
|
||||
|
||||
if (!$keywords) {
|
||||
$keywords = null;
|
||||
}
|
||||
|
||||
$this->data['keywords'] = $keywords;
|
||||
|
||||
return $this->data['keywords'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry's new feed url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNewFeedUrl()
|
||||
{
|
||||
if (isset($this->data['new-feed-url'])) {
|
||||
return $this->data['new-feed-url'];
|
||||
}
|
||||
|
||||
$newFeedUrl = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:new-feed-url)');
|
||||
|
||||
if (!$newFeedUrl) {
|
||||
$newFeedUrl = null;
|
||||
}
|
||||
|
||||
$this->data['new-feed-url'] = $newFeedUrl;
|
||||
|
||||
return $this->data['new-feed-url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry owner
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOwner()
|
||||
{
|
||||
if (isset($this->data['owner'])) {
|
||||
return $this->data['owner'];
|
||||
}
|
||||
|
||||
$owner = null;
|
||||
|
||||
$email = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:owner/itunes:email)');
|
||||
$name = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:owner/itunes:name)');
|
||||
|
||||
if (!empty($email)) {
|
||||
$owner = $email . (empty($name) ? '' : ' (' . $name . ')');
|
||||
} elseif (!empty($name)) {
|
||||
$owner = $name;
|
||||
}
|
||||
|
||||
if (!$owner) {
|
||||
$owner = null;
|
||||
}
|
||||
|
||||
$this->data['owner'] = $owner;
|
||||
|
||||
return $this->data['owner'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry subtitle
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSubtitle()
|
||||
{
|
||||
if (isset($this->data['subtitle'])) {
|
||||
return $this->data['subtitle'];
|
||||
}
|
||||
|
||||
$subtitle = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:subtitle)');
|
||||
|
||||
if (!$subtitle) {
|
||||
$subtitle = null;
|
||||
}
|
||||
|
||||
$this->data['subtitle'] = $subtitle;
|
||||
|
||||
return $this->data['subtitle'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry summary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSummary()
|
||||
{
|
||||
if (isset($this->data['summary'])) {
|
||||
return $this->data['summary'];
|
||||
}
|
||||
|
||||
$summary = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:summary)');
|
||||
|
||||
if (!$summary) {
|
||||
$summary = null;
|
||||
}
|
||||
|
||||
$this->data['summary'] = $summary;
|
||||
|
||||
return $this->data['summary'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register iTunes namespace
|
||||
*
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Slash;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the entry section
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSection()
|
||||
{
|
||||
return $this->getData('section');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry department
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDepartment()
|
||||
{
|
||||
return $this->getData('department');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry hit_parade
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHitParade()
|
||||
{
|
||||
$name = 'hit_parade';
|
||||
|
||||
if (isset($this->data[$name])) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$stringParade = $this->getData($name);
|
||||
$hitParade = [];
|
||||
|
||||
if (!empty($stringParade)) {
|
||||
$stringParade = explode(',', $stringParade);
|
||||
|
||||
foreach ($stringParade as $hit) {
|
||||
$hitParade[] = $hit + 0; //cast to integer
|
||||
}
|
||||
}
|
||||
|
||||
$this->data[$name] = $hitParade;
|
||||
return $hitParade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry comments
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
$name = 'comments';
|
||||
|
||||
if (isset($this->data[$name])) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$comments = $this->getData($name, 'string');
|
||||
|
||||
if (!$comments) {
|
||||
$this->data[$name] = null;
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
return $comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry data specified by name
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getData($name, $type = 'string')
|
||||
{
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$data = $this->xpath->evaluate($type . '(' . $this->getXpathPrefix() . '/slash10:' . $name . ')');
|
||||
|
||||
if (!$data) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
$this->data[$name] = $data;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Slash namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('slash10', 'http://purl.org/rss/1.0/modules/slash/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Syndication;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
class Feed extends Extension\AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Get update period
|
||||
*
|
||||
* @return string
|
||||
* @throws Reader\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function getUpdatePeriod()
|
||||
{
|
||||
$name = 'updatePeriod';
|
||||
$period = $this->getData($name);
|
||||
|
||||
if ($period === null) {
|
||||
$this->data[$name] = 'daily';
|
||||
return 'daily'; //Default specified by spec
|
||||
}
|
||||
|
||||
switch ($period) {
|
||||
case 'hourly':
|
||||
case 'daily':
|
||||
case 'weekly':
|
||||
case 'yearly':
|
||||
return $period;
|
||||
default:
|
||||
throw new Reader\Exception\InvalidArgumentException("Feed specified invalid update period: '$period'."
|
||||
. " Must be one of hourly, daily, weekly or yearly"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update frequency
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUpdateFrequency()
|
||||
{
|
||||
$name = 'updateFrequency';
|
||||
$freq = $this->getData($name, 'number');
|
||||
|
||||
if (!$freq || $freq < 1) {
|
||||
$this->data[$name] = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $freq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update frequency as ticks
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUpdateFrequencyAsTicks()
|
||||
{
|
||||
$name = 'updateFrequency';
|
||||
$freq = $this->getData($name, 'number');
|
||||
|
||||
if (!$freq || $freq < 1) {
|
||||
$this->data[$name] = 1;
|
||||
$freq = 1;
|
||||
}
|
||||
|
||||
$period = $this->getUpdatePeriod();
|
||||
$ticks = 1;
|
||||
|
||||
switch ($period) {
|
||||
case 'yearly':
|
||||
$ticks *= 52; //TODO: fix generalisation, how?
|
||||
// no break
|
||||
case 'weekly':
|
||||
$ticks *= 7;
|
||||
// no break
|
||||
case 'daily':
|
||||
$ticks *= 24;
|
||||
// no break
|
||||
case 'hourly':
|
||||
$ticks *= 3600;
|
||||
break;
|
||||
default: //Never arrive here, exception thrown in getPeriod()
|
||||
break;
|
||||
}
|
||||
|
||||
return $ticks / $freq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update base
|
||||
*
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function getUpdateBase()
|
||||
{
|
||||
$updateBase = $this->getData('updateBase');
|
||||
$date = null;
|
||||
if ($updateBase) {
|
||||
$date = DateTime::createFromFormat(DateTime::W3C, $updateBase);
|
||||
}
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry data specified by name
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @return mixed|null
|
||||
*/
|
||||
private function getData($name, $type = 'string')
|
||||
{
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$data = $this->xpath->evaluate($type . '(' . $this->getXpathPrefix() . '/syn10:' . $name . ')');
|
||||
|
||||
if (!$data) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
$this->data[$name] = $data;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Syndication namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('syn10', 'http://purl.org/rss/1.0/modules/syndication/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\Thread;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the "in-reply-to" value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInReplyTo()
|
||||
{
|
||||
// TODO: to be implemented
|
||||
}
|
||||
|
||||
// TODO: Implement "replies" and "updated" constructs from standard
|
||||
|
||||
/**
|
||||
* Get the total number of threaded responses (i.e comments)
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
return $this->getData('total');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry data specified by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getData($name)
|
||||
{
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$data = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/thread10:' . $name . ')');
|
||||
|
||||
if (!$data) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
$this->data[$name] = $data;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Atom Thread Extension 1.0 namespace
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('thread10', 'http://purl.org/syndication/thread/1.0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Extension\WellFormedWeb;
|
||||
|
||||
use Zend\Feed\Reader\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractEntry
|
||||
{
|
||||
/**
|
||||
* Get the entry comment Uri
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCommentFeedLink()
|
||||
{
|
||||
$name = 'commentRss';
|
||||
if (array_key_exists($name, $this->data)) {
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
$data = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/wfw:' . $name . ')');
|
||||
|
||||
if (!$data) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
$this->data[$name] = $data;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Slash namespaces
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
$this->xpath->registerNamespace('wfw', 'http://wellformedweb.org/CommentAPI/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Default implementation of ExtensionManagerInterface
|
||||
*
|
||||
* Decorator of ExtensionPluginManager.
|
||||
*/
|
||||
class ExtensionManager implements ExtensionManagerInterface
|
||||
{
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Seeds the extension manager with a plugin manager; if none provided,
|
||||
* creates an instance.
|
||||
*
|
||||
* @param null|ExtensionPluginManager $pluginManager
|
||||
*/
|
||||
public function __construct(ExtensionPluginManager $pluginManager = null)
|
||||
{
|
||||
if (null === $pluginManager) {
|
||||
$pluginManager = new ExtensionPluginManager();
|
||||
}
|
||||
$this->pluginManager = $pluginManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method overloading
|
||||
*
|
||||
* Proxy to composed ExtensionPluginManager instance.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
* @throws Exception\BadMethodCallException
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if (!method_exists($this->pluginManager, $method)) {
|
||||
throw new Exception\BadMethodCallException(sprintf(
|
||||
'Method by name of %s does not exist in %s',
|
||||
$method,
|
||||
__CLASS__
|
||||
));
|
||||
}
|
||||
return call_user_func_array([$this->pluginManager, $method], $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the named extension
|
||||
*
|
||||
* @param string $name
|
||||
* @return Extension\AbstractEntry|Extension\AbstractFeed
|
||||
*/
|
||||
public function get($name)
|
||||
{
|
||||
return $this->pluginManager->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do we have the named extension?
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return $this->pluginManager->has($name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
|
||||
interface ExtensionManagerInterface
|
||||
{
|
||||
/**
|
||||
* Do we have the extension?
|
||||
*
|
||||
* @param string $extension
|
||||
* @return bool
|
||||
*/
|
||||
public function has($extension);
|
||||
|
||||
/**
|
||||
* Retrieve the extension
|
||||
*
|
||||
* @param string $extension
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($extension);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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 Zend\ServiceManager\AbstractPluginManager;
|
||||
use Zend\ServiceManager\Exception\InvalidServiceException;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
/**
|
||||
* Plugin manager implementation for feed reader extensions based on the
|
||||
* AbstractPluginManager.
|
||||
*
|
||||
* Validation checks that we have an Extension\AbstractEntry or
|
||||
* Extension\AbstractFeed.
|
||||
*/
|
||||
class ExtensionPluginManager extends AbstractPluginManager
|
||||
{
|
||||
/**
|
||||
* Aliases for default set of extension classes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $aliases = [
|
||||
'atomentry' => Extension\Atom\Entry::class,
|
||||
'atomEntry' => Extension\Atom\Entry::class,
|
||||
'AtomEntry' => Extension\Atom\Entry::class,
|
||||
'atomfeed' => Extension\Atom\Feed::class,
|
||||
'atomFeed' => Extension\Atom\Feed::class,
|
||||
'AtomFeed' => Extension\Atom\Feed::class,
|
||||
'contententry' => Extension\Content\Entry::class,
|
||||
'contentEntry' => Extension\Content\Entry::class,
|
||||
'ContentEntry' => Extension\Content\Entry::class,
|
||||
'creativecommonsentry' => Extension\CreativeCommons\Entry::class,
|
||||
'creativeCommonsEntry' => Extension\CreativeCommons\Entry::class,
|
||||
'CreativeCommonsEntry' => Extension\CreativeCommons\Entry::class,
|
||||
'creativecommonsfeed' => Extension\CreativeCommons\Feed::class,
|
||||
'creativeCommonsFeed' => Extension\CreativeCommons\Feed::class,
|
||||
'CreativeCommonsFeed' => Extension\CreativeCommons\Feed::class,
|
||||
'dublincoreentry' => Extension\DublinCore\Entry::class,
|
||||
'dublinCoreEntry' => Extension\DublinCore\Entry::class,
|
||||
'DublinCoreEntry' => Extension\DublinCore\Entry::class,
|
||||
'dublincorefeed' => Extension\DublinCore\Feed::class,
|
||||
'dublinCoreFeed' => Extension\DublinCore\Feed::class,
|
||||
'DublinCoreFeed' => Extension\DublinCore\Feed::class,
|
||||
'podcastentry' => Extension\Podcast\Entry::class,
|
||||
'podcastEntry' => Extension\Podcast\Entry::class,
|
||||
'PodcastEntry' => Extension\Podcast\Entry::class,
|
||||
'podcastfeed' => Extension\Podcast\Feed::class,
|
||||
'podcastFeed' => Extension\Podcast\Feed::class,
|
||||
'PodcastFeed' => Extension\Podcast\Feed::class,
|
||||
'slashentry' => Extension\Slash\Entry::class,
|
||||
'slashEntry' => Extension\Slash\Entry::class,
|
||||
'SlashEntry' => Extension\Slash\Entry::class,
|
||||
'syndicationfeed' => Extension\Syndication\Feed::class,
|
||||
'syndicationFeed' => Extension\Syndication\Feed::class,
|
||||
'SyndicationFeed' => Extension\Syndication\Feed::class,
|
||||
'threadentry' => Extension\Thread\Entry::class,
|
||||
'threadEntry' => Extension\Thread\Entry::class,
|
||||
'ThreadEntry' => Extension\Thread\Entry::class,
|
||||
'wellformedwebentry' => Extension\WellFormedWeb\Entry::class,
|
||||
'wellFormedWebEntry' => Extension\WellFormedWeb\Entry::class,
|
||||
'WellFormedWebEntry' => Extension\WellFormedWeb\Entry::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Factories for default set of extension classes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $factories = [
|
||||
Extension\Atom\Entry::class => InvokableFactory::class,
|
||||
Extension\Atom\Feed::class => InvokableFactory::class,
|
||||
Extension\Content\Entry::class => InvokableFactory::class,
|
||||
Extension\CreativeCommons\Entry::class => InvokableFactory::class,
|
||||
Extension\CreativeCommons\Feed::class => InvokableFactory::class,
|
||||
Extension\DublinCore\Entry::class => InvokableFactory::class,
|
||||
Extension\DublinCore\Feed::class => InvokableFactory::class,
|
||||
Extension\Podcast\Entry::class => InvokableFactory::class,
|
||||
Extension\Podcast\Feed::class => InvokableFactory::class,
|
||||
Extension\Slash\Entry::class => InvokableFactory::class,
|
||||
Extension\Syndication\Feed::class => InvokableFactory::class,
|
||||
Extension\Thread\Entry::class => InvokableFactory::class,
|
||||
Extension\WellFormedWeb\Entry::class => InvokableFactory::class,
|
||||
// Legacy (v2) due to alias resolution; canonical form of resolved
|
||||
// alias is used to look up the factory, while the non-normalized
|
||||
// resolved alias is used as the requested name passed to the factory.
|
||||
'zendfeedreaderextensionatomentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionatomfeed' => InvokableFactory::class,
|
||||
'zendfeedreaderextensioncontententry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensioncreativecommonsentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensioncreativecommonsfeed' => InvokableFactory::class,
|
||||
'zendfeedreaderextensiondublincoreentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensiondublincorefeed' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionpodcastentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionpodcastfeed' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionslashentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionsyndicationfeed' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionthreadentry' => InvokableFactory::class,
|
||||
'zendfeedreaderextensionwellformedwebentry' => InvokableFactory::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Do not share instances (v2)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $shareByDefault = false;
|
||||
|
||||
/**
|
||||
* Do not share instances (v3)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $sharedByDefault = false;
|
||||
|
||||
/**
|
||||
* Validate the plugin
|
||||
*
|
||||
* Checks that the extension loaded is of a valid type.
|
||||
*
|
||||
* @param mixed $plugin
|
||||
* @return void
|
||||
* @throws Exception\InvalidArgumentException if invalid
|
||||
*/
|
||||
public function validate($plugin)
|
||||
{
|
||||
if ($plugin instanceof Extension\AbstractEntry
|
||||
|| $plugin instanceof Extension\AbstractFeed
|
||||
) {
|
||||
// we're okay
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidServiceException(sprintf(
|
||||
'Plugin of type %s is invalid; must implement %s\Extension\AbstractFeed '
|
||||
. 'or %s\Extension\AbstractEntry',
|
||||
(is_object($plugin) ? get_class($plugin) : gettype($plugin)),
|
||||
__NAMESPACE__,
|
||||
__NAMESPACE__
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the plugin (v2)
|
||||
*
|
||||
* @param mixed $plugin
|
||||
* @return void
|
||||
* @throws Exception\InvalidArgumentException if invalid
|
||||
*/
|
||||
public function validatePlugin($plugin)
|
||||
{
|
||||
try {
|
||||
$this->validate($plugin);
|
||||
} catch (InvalidServiceException $e) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Plugin of type %s is invalid; must implement %s\Extension\AbstractFeed '
|
||||
. 'or %s\Extension\AbstractEntry',
|
||||
(is_object($plugin) ? get_class($plugin) : gettype($plugin)),
|
||||
__NAMESPACE__,
|
||||
__NAMESPACE__
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?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\Feed;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
/**
|
||||
*/
|
||||
abstract class AbstractFeed implements 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\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\Entry\EntryInterface
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
if (substr($this->getType(), 0, 3) == 'rss') {
|
||||
$reader = new Reader\Entry\Rss($this->entries[$this->key()], $this->key(), $this->getType());
|
||||
} else {
|
||||
$reader = new Reader\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\Reader::getExtensions();
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
$feed = $all['feed'];
|
||||
foreach ($feed as $extension) {
|
||||
if (in_array($extension, $all['core'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$manager->has($extension)) {
|
||||
throw new Exception\RuntimeException(sprintf('Unable to load extension "%s"; cannot find class', $extension));
|
||||
}
|
||||
$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,408 @@
|
||||
<?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\Feed;
|
||||
|
||||
use DOMDocument;
|
||||
use Zend\Feed\Reader;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Atom extends AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(DOMDocument $dom, $type = null)
|
||||
{
|
||||
parent::__construct($dom, $type);
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
|
||||
$atomFeed = $manager->get('Atom\Feed');
|
||||
$atomFeed->setDomDocument($dom);
|
||||
$atomFeed->setType($this->data['type']);
|
||||
$atomFeed->setXpath($this->xpath);
|
||||
$this->extensions['Atom\\Feed'] = $atomFeed;
|
||||
|
||||
$atomFeed = $manager->get('DublinCore\Feed');
|
||||
$atomFeed->setDomDocument($dom);
|
||||
$atomFeed->setType($this->data['type']);
|
||||
$atomFeed->setXpath($this->xpath);
|
||||
$this->extensions['DublinCore\\Feed'] = $atomFeed;
|
||||
|
||||
foreach ($this->extensions as $extension) {
|
||||
$extension->setXpathPrefix('/atom:feed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = $this->getExtension('Atom')->getAuthors();
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (array_key_exists('copyright', $this->data)) {
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
$copyright = $this->getExtension('Atom')->getCopyright();
|
||||
|
||||
if (!$copyright) {
|
||||
$copyright = null;
|
||||
}
|
||||
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed creation date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (array_key_exists('datecreated', $this->data)) {
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
$dateCreated = $this->getExtension('Atom')->getDateCreated();
|
||||
|
||||
if (!$dateCreated) {
|
||||
$dateCreated = null;
|
||||
}
|
||||
|
||||
$this->data['datecreated'] = $dateCreated;
|
||||
|
||||
return $this->data['datecreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed modification date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$dateModified = $this->getExtension('Atom')->getDateModified();
|
||||
|
||||
if (!$dateModified) {
|
||||
$dateModified = null;
|
||||
}
|
||||
|
||||
$this->data['datemodified'] = $dateModified;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed lastBuild date. This is not implemented in Atom.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLastBuildDate()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
$description = $this->getExtension('Atom')->getDescription();
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed generator entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerator()
|
||||
{
|
||||
if (array_key_exists('generator', $this->data)) {
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
$generator = $this->getExtension('Atom')->getGenerator();
|
||||
|
||||
$this->data['generator'] = $generator;
|
||||
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = $this->getExtension('Atom')->getId();
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
if (array_key_exists('language', $this->data)) {
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
$language = $this->getExtension('Atom')->getLanguage();
|
||||
|
||||
if (!$language) {
|
||||
$language = $this->xpath->evaluate('string(//@xml:lang[1])');
|
||||
}
|
||||
|
||||
if (!$language) {
|
||||
$language = null;
|
||||
}
|
||||
|
||||
$this->data['language'] = $language;
|
||||
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the source website
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
if (array_key_exists('baseUrl', $this->data)) {
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
$baseUrl = $this->getExtension('Atom')->getBaseUrl();
|
||||
|
||||
$this->data['baseUrl'] = $baseUrl;
|
||||
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the source website
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
if (array_key_exists('link', $this->data)) {
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
$link = $this->getExtension('Atom')->getLink();
|
||||
|
||||
$this->data['link'] = $link;
|
||||
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feed image data
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
if (array_key_exists('image', $this->data)) {
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
$link = $this->getExtension('Atom')->getImage();
|
||||
|
||||
$this->data['image'] = $link;
|
||||
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the feed's XML Url
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFeedLink()
|
||||
{
|
||||
if (array_key_exists('feedlink', $this->data)) {
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
$link = $this->getExtension('Atom')->getFeedLink();
|
||||
|
||||
if ($link === null || empty($link)) {
|
||||
$link = $this->getOriginalSourceUri();
|
||||
}
|
||||
|
||||
$this->data['feedlink'] = $link;
|
||||
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
$title = $this->getExtension('Atom')->getTitle();
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of any supported Pusubhubbub endpoints
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getHubs()
|
||||
{
|
||||
if (array_key_exists('hubs', $this->data)) {
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
$hubs = $this->getExtension('Atom')->getHubs();
|
||||
|
||||
$this->data['hubs'] = $hubs;
|
||||
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Reader\Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
$categoryCollection = $this->getExtension('Atom')->getCategories();
|
||||
|
||||
if (count($categoryCollection) == 0) {
|
||||
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all entries to the internal entries array
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function indexEntries()
|
||||
{
|
||||
if ($this->getType() == Reader\Reader::TYPE_ATOM_10 ||
|
||||
$this->getType() == Reader\Reader::TYPE_ATOM_03) {
|
||||
$entries = $this->xpath->evaluate('//atom:entry');
|
||||
|
||||
foreach ($entries as $index => $entry) {
|
||||
$this->entries[$index] = $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
switch ($this->data['type']) {
|
||||
case Reader\Reader::TYPE_ATOM_03:
|
||||
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_03);
|
||||
break;
|
||||
case Reader\Reader::TYPE_ATOM_10:
|
||||
default:
|
||||
$this->xpath->registerNamespace('atom', Reader\Reader::NAMESPACE_ATOM_10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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\Feed\Atom;
|
||||
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Feed;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Source extends Feed\Atom
|
||||
{
|
||||
/**
|
||||
* Constructor: Create a Source object which is largely just a normal
|
||||
* Zend\Feed\Reader\AbstractFeed object only designed to retrieve feed level
|
||||
* metadata from an Atom entry's source element.
|
||||
*
|
||||
* @param DOMElement $source
|
||||
* @param string $xpathPrefix Passed from parent Entry object
|
||||
* @param string $type Nearly always Atom 1.0
|
||||
*/
|
||||
public function __construct(DOMElement $source, $xpathPrefix, $type = Reader\Reader::TYPE_ATOM_10)
|
||||
{
|
||||
$this->domDocument = $source->ownerDocument;
|
||||
$this->xpath = new DOMXPath($this->domDocument);
|
||||
$this->data['type'] = $type;
|
||||
$this->registerNamespaces();
|
||||
$this->loadExtensions();
|
||||
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
$extensions = ['Atom\Feed', 'DublinCore\Feed'];
|
||||
|
||||
foreach ($extensions as $name) {
|
||||
$extension = $manager->get($name);
|
||||
$extension->setDomDocument($this->domDocument);
|
||||
$extension->setType($this->data['type']);
|
||||
$extension->setXpath($this->xpath);
|
||||
$this->extensions[$name] = $extension;
|
||||
}
|
||||
|
||||
foreach ($this->extensions as $extension) {
|
||||
$extension->setXpathPrefix(rtrim($xpathPrefix, '/') . '/atom:source');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since this is not an Entry carrier but a vehicle for Feed metadata, any
|
||||
* applicable Entry methods are stubbed out and do nothing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function indexEntries()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\Feed;
|
||||
|
||||
use Countable;
|
||||
use Iterator;
|
||||
|
||||
/**
|
||||
*/
|
||||
interface FeedInterface extends Iterator, Countable
|
||||
{
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0);
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors();
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright();
|
||||
|
||||
/**
|
||||
* Get the feed creation date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateCreated();
|
||||
|
||||
/**
|
||||
* Get the feed modification date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateModified();
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription();
|
||||
|
||||
/**
|
||||
* Get the feed generator entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerator();
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId();
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage();
|
||||
|
||||
/**
|
||||
* Get a link to the HTML source
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink();
|
||||
|
||||
/**
|
||||
* Get a link to the XML feed
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFeedLink();
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle();
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return \Zend\Feed\Reader\Collection\Category
|
||||
*/
|
||||
public function getCategories();
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
<?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\Feed;
|
||||
|
||||
use DateTime;
|
||||
use DOMDocument;
|
||||
use Zend\Feed\Reader;
|
||||
use Zend\Feed\Reader\Collection;
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Rss extends AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(DOMDocument $dom, $type = null)
|
||||
{
|
||||
parent::__construct($dom, $type);
|
||||
|
||||
$manager = Reader\Reader::getExtensionManager();
|
||||
|
||||
$feed = $manager->get('DublinCore\Feed');
|
||||
$feed->setDomDocument($dom);
|
||||
$feed->setType($this->data['type']);
|
||||
$feed->setXpath($this->xpath);
|
||||
$this->extensions['DublinCore\Feed'] = $feed;
|
||||
|
||||
$feed = $manager->get('Atom\Feed');
|
||||
$feed->setDomDocument($dom);
|
||||
$feed->setType($this->data['type']);
|
||||
$feed->setXpath($this->xpath);
|
||||
$this->extensions['Atom\Feed'] = $feed;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
|
||||
) {
|
||||
$xpathPrefix = '/rss/channel';
|
||||
} else {
|
||||
$xpathPrefix = '/rdf:RDF/rss:channel';
|
||||
}
|
||||
foreach ($this->extensions as $extension) {
|
||||
$extension->setXpathPrefix($xpathPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
$authors = $this->getAuthors();
|
||||
|
||||
if (isset($authors[$index])) {
|
||||
return $authors[$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (array_key_exists('authors', $this->data)) {
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
$authorsDc = $this->getExtension('DublinCore')->getAuthors();
|
||||
if (!empty($authorsDc)) {
|
||||
foreach ($authorsDc as $author) {
|
||||
$authors[] = [
|
||||
'name' => $author['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Technically RSS doesn't specific author element use at the feed level
|
||||
* but it's supported on a "just in case" basis.
|
||||
*/
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10
|
||||
&& $this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query('//author');
|
||||
} else {
|
||||
$list = $this->xpath->query('//rss:author');
|
||||
}
|
||||
if ($list->length) {
|
||||
foreach ($list as $author) {
|
||||
$string = trim($author->nodeValue);
|
||||
$data = [];
|
||||
// Pretty rough parsing - but it's a catchall
|
||||
if (preg_match("/^.*@[^ ]*/", $string, $matches)) {
|
||||
$data['email'] = trim($matches[0]);
|
||||
if (preg_match("/\((.*)\)$/", $string, $matches)) {
|
||||
$data['name'] = $matches[1];
|
||||
}
|
||||
$authors[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = $this->getExtension('Atom')->getAuthors();
|
||||
} else {
|
||||
$authors = new Reader\Collection\Author(
|
||||
Reader\Reader::arrayUnique($authors)
|
||||
);
|
||||
}
|
||||
|
||||
if (count($authors) == 0) {
|
||||
$authors = null;
|
||||
}
|
||||
|
||||
$this->data['authors'] = $authors;
|
||||
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (array_key_exists('copyright', $this->data)) {
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
$copyright = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$copyright = $this->xpath->evaluate('string(/rss/channel/copyright)');
|
||||
}
|
||||
|
||||
if (!$copyright && $this->getExtension('DublinCore') !== null) {
|
||||
$copyright = $this->getExtension('DublinCore')->getCopyright();
|
||||
}
|
||||
|
||||
if (empty($copyright)) {
|
||||
$copyright = $this->getExtension('Atom')->getCopyright();
|
||||
}
|
||||
|
||||
if (!$copyright) {
|
||||
$copyright = null;
|
||||
}
|
||||
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed creation date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
return $this->getDateModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed modification date
|
||||
*
|
||||
* @return DateTime
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (array_key_exists('datemodified', $this->data)) {
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$dateModified = $this->xpath->evaluate('string(/rss/channel/pubDate)');
|
||||
if (!$dateModified) {
|
||||
$dateModified = $this->xpath->evaluate('string(/rss/channel/lastBuildDate)');
|
||||
}
|
||||
if ($dateModified) {
|
||||
$dateModifiedParsed = strtotime($dateModified);
|
||||
if ($dateModifiedParsed) {
|
||||
$date = new DateTime('@' . $dateModifiedParsed);
|
||||
} else {
|
||||
$dateStandards = [DateTime::RSS, DateTime::RFC822,
|
||||
DateTime::RFC2822, null];
|
||||
foreach ($dateStandards as $standard) {
|
||||
try {
|
||||
$date = DateTime::createFromFormat($standard, $dateModified);
|
||||
break;
|
||||
} catch (\Exception $e) {
|
||||
if ($standard === null) {
|
||||
throw new Exception\RuntimeException(
|
||||
'Could not load date due to unrecognised'
|
||||
.' format (should follow RFC 822 or 2822):'
|
||||
. $e->getMessage(),
|
||||
0, $e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getExtension('DublinCore')->getDate();
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = $this->getExtension('Atom')->getDateModified();
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = null;
|
||||
}
|
||||
|
||||
$this->data['datemodified'] = $date;
|
||||
|
||||
return $this->data['datemodified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed lastBuild date
|
||||
*
|
||||
* @throws Exception\RuntimeException
|
||||
* @return DateTime
|
||||
*/
|
||||
public function getLastBuildDate()
|
||||
{
|
||||
if (array_key_exists('lastBuildDate', $this->data)) {
|
||||
return $this->data['lastBuildDate'];
|
||||
}
|
||||
|
||||
$date = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$lastBuildDate = $this->xpath->evaluate('string(/rss/channel/lastBuildDate)');
|
||||
if ($lastBuildDate) {
|
||||
$lastBuildDateParsed = strtotime($lastBuildDate);
|
||||
if ($lastBuildDateParsed) {
|
||||
$date = new DateTime('@' . $lastBuildDateParsed);
|
||||
} else {
|
||||
$dateStandards = [DateTime::RSS, DateTime::RFC822,
|
||||
DateTime::RFC2822, null];
|
||||
foreach ($dateStandards as $standard) {
|
||||
try {
|
||||
$date = DateTime::createFromFormat($standard, $lastBuildDateParsed);
|
||||
break;
|
||||
} catch (\Exception $e) {
|
||||
if ($standard === null) {
|
||||
throw new Exception\RuntimeException(
|
||||
'Could not load date due to unrecognised'
|
||||
.' format (should follow RFC 822 or 2822):'
|
||||
. $e->getMessage(),
|
||||
0, $e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$date = null;
|
||||
}
|
||||
|
||||
$this->data['lastBuildDate'] = $date;
|
||||
|
||||
return $this->data['lastBuildDate'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (array_key_exists('description', $this->data)) {
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$description = $this->xpath->evaluate('string(/rss/channel/description)');
|
||||
} else {
|
||||
$description = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:description)');
|
||||
}
|
||||
|
||||
if (!$description && $this->getExtension('DublinCore') !== null) {
|
||||
$description = $this->getExtension('DublinCore')->getDescription();
|
||||
}
|
||||
|
||||
if (empty($description)) {
|
||||
$description = $this->getExtension('Atom')->getDescription();
|
||||
}
|
||||
|
||||
if (!$description) {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (array_key_exists('id', $this->data)) {
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
$id = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$id = $this->xpath->evaluate('string(/rss/channel/guid)');
|
||||
}
|
||||
|
||||
if (!$id && $this->getExtension('DublinCore') !== null) {
|
||||
$id = $this->getExtension('DublinCore')->getId();
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
$id = $this->getExtension('Atom')->getId();
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
if ($this->getLink()) {
|
||||
$id = $this->getLink();
|
||||
} elseif ($this->getTitle()) {
|
||||
$id = $this->getTitle();
|
||||
} else {
|
||||
$id = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed image data
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
if (array_key_exists('image', $this->data)) {
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query('/rss/channel/image');
|
||||
$prefix = '/rss/channel/image[1]';
|
||||
} else {
|
||||
$list = $this->xpath->query('/rdf:RDF/rss:channel/rss:image');
|
||||
$prefix = '/rdf:RDF/rss:channel/rss:image[1]';
|
||||
}
|
||||
if ($list->length > 0) {
|
||||
$image = [];
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/url)');
|
||||
if ($value) {
|
||||
$image['uri'] = $value;
|
||||
}
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/link)');
|
||||
if ($value) {
|
||||
$image['link'] = $value;
|
||||
}
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/title)');
|
||||
if ($value) {
|
||||
$image['title'] = $value;
|
||||
}
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/height)');
|
||||
if ($value) {
|
||||
$image['height'] = $value;
|
||||
}
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/width)');
|
||||
if ($value) {
|
||||
$image['width'] = $value;
|
||||
}
|
||||
$value = $this->xpath->evaluate('string(' . $prefix . '/description)');
|
||||
if ($value) {
|
||||
$image['description'] = $value;
|
||||
}
|
||||
} else {
|
||||
$image = null;
|
||||
}
|
||||
|
||||
$this->data['image'] = $image;
|
||||
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
if (array_key_exists('language', $this->data)) {
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
$language = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$language = $this->xpath->evaluate('string(/rss/channel/language)');
|
||||
}
|
||||
|
||||
if (!$language && $this->getExtension('DublinCore') !== null) {
|
||||
$language = $this->getExtension('DublinCore')->getLanguage();
|
||||
}
|
||||
|
||||
if (empty($language)) {
|
||||
$language = $this->getExtension('Atom')->getLanguage();
|
||||
}
|
||||
|
||||
if (!$language) {
|
||||
$language = $this->xpath->evaluate('string(//@xml:lang[1])');
|
||||
}
|
||||
|
||||
if (!$language) {
|
||||
$language = null;
|
||||
}
|
||||
|
||||
$this->data['language'] = $language;
|
||||
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the feed
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
if (array_key_exists('link', $this->data)) {
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$link = $this->xpath->evaluate('string(/rss/channel/link)');
|
||||
} else {
|
||||
$link = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:link)');
|
||||
}
|
||||
|
||||
if (empty($link)) {
|
||||
$link = $this->getExtension('Atom')->getLink();
|
||||
}
|
||||
|
||||
if (!$link) {
|
||||
$link = null;
|
||||
}
|
||||
|
||||
$this->data['link'] = $link;
|
||||
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the feed XML
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFeedLink()
|
||||
{
|
||||
if (array_key_exists('feedlink', $this->data)) {
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
$link = $this->getExtension('Atom')->getFeedLink();
|
||||
|
||||
if ($link === null || empty($link)) {
|
||||
$link = $this->getOriginalSourceUri();
|
||||
}
|
||||
|
||||
$this->data['feedlink'] = $link;
|
||||
|
||||
return $this->data['feedlink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed generator entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerator()
|
||||
{
|
||||
if (array_key_exists('generator', $this->data)) {
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
$generator = null;
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$generator = $this->xpath->evaluate('string(/rss/channel/generator)');
|
||||
}
|
||||
|
||||
if (!$generator) {
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$generator = $this->xpath->evaluate('string(/rss/channel/atom:generator)');
|
||||
} else {
|
||||
$generator = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/atom:generator)');
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($generator)) {
|
||||
$generator = $this->getExtension('Atom')->getGenerator();
|
||||
}
|
||||
|
||||
if (!$generator) {
|
||||
$generator = null;
|
||||
}
|
||||
|
||||
$this->data['generator'] = $generator;
|
||||
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (array_key_exists('title', $this->data)) {
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$title = $this->xpath->evaluate('string(/rss/channel/title)');
|
||||
} else {
|
||||
$title = $this->xpath->evaluate('string(/rdf:RDF/rss:channel/rss:title)');
|
||||
}
|
||||
|
||||
if (!$title && $this->getExtension('DublinCore') !== null) {
|
||||
$title = $this->getExtension('DublinCore')->getTitle();
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = $this->getExtension('Atom')->getTitle();
|
||||
}
|
||||
|
||||
if (!$title) {
|
||||
$title = null;
|
||||
}
|
||||
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of any supported Pusubhubbub endpoints
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getHubs()
|
||||
{
|
||||
if (array_key_exists('hubs', $this->data)) {
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
$hubs = $this->getExtension('Atom')->getHubs();
|
||||
|
||||
if (empty($hubs)) {
|
||||
$hubs = null;
|
||||
} else {
|
||||
$hubs = array_unique($hubs);
|
||||
}
|
||||
|
||||
$this->data['hubs'] = $hubs;
|
||||
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*
|
||||
* @return Reader\Collection\Category
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (array_key_exists('categories', $this->data)) {
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 &&
|
||||
$this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$list = $this->xpath->query('/rss/channel//category');
|
||||
} else {
|
||||
$list = $this->xpath->query('/rdf:RDF/rss:channel//rss:category');
|
||||
}
|
||||
|
||||
if ($list->length) {
|
||||
$categoryCollection = new Collection\Category;
|
||||
foreach ($list as $category) {
|
||||
$categoryCollection[] = [
|
||||
'term' => $category->nodeValue,
|
||||
'scheme' => $category->getAttribute('domain'),
|
||||
'label' => $category->nodeValue,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$categoryCollection = $this->getExtension('DublinCore')->getCategories();
|
||||
}
|
||||
|
||||
if (count($categoryCollection) == 0) {
|
||||
$categoryCollection = $this->getExtension('Atom')->getCategories();
|
||||
}
|
||||
|
||||
$this->data['categories'] = $categoryCollection;
|
||||
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all entries to the internal entries array
|
||||
*
|
||||
*/
|
||||
protected function indexEntries()
|
||||
{
|
||||
if ($this->getType() !== Reader\Reader::TYPE_RSS_10 && $this->getType() !== Reader\Reader::TYPE_RSS_090) {
|
||||
$entries = $this->xpath->evaluate('//item');
|
||||
} else {
|
||||
$entries = $this->xpath->evaluate('//rss:item');
|
||||
}
|
||||
|
||||
foreach ($entries as $index => $entry) {
|
||||
$this->entries[$index] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default namespaces for the current feed format
|
||||
*
|
||||
*/
|
||||
protected function registerNamespaces()
|
||||
{
|
||||
switch ($this->data['type']) {
|
||||
case Reader\Reader::TYPE_RSS_10:
|
||||
$this->xpath->registerNamespace('rdf', Reader\Reader::NAMESPACE_RDF);
|
||||
$this->xpath->registerNamespace('rss', Reader\Reader::NAMESPACE_RSS_10);
|
||||
break;
|
||||
|
||||
case Reader\Reader::TYPE_RSS_090:
|
||||
$this->xpath->registerNamespace('rdf', Reader\Reader::NAMESPACE_RDF);
|
||||
$this->xpath->registerNamespace('rss', Reader\Reader::NAMESPACE_RSS_090);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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;
|
||||
use DOMNodeList;
|
||||
use Zend\Feed\Uri;
|
||||
|
||||
class FeedSet extends ArrayObject
|
||||
{
|
||||
public $rss = null;
|
||||
|
||||
public $rdf = null;
|
||||
|
||||
public $atom = null;
|
||||
|
||||
/**
|
||||
* Import a DOMNodeList from any document containing a set of links
|
||||
* for alternate versions of a document, which will normally refer to
|
||||
* RSS/RDF/Atom feeds for the current document.
|
||||
*
|
||||
* All such links are stored internally, however the first instance of
|
||||
* each RSS, RDF or Atom type has its URI stored as a public property
|
||||
* as a shortcut where the use case is simply to get a quick feed ref.
|
||||
*
|
||||
* Note that feeds are not loaded at this point, but will be lazy
|
||||
* loaded automatically when each links 'feed' array key is accessed.
|
||||
*
|
||||
* @param DOMNodeList $links
|
||||
* @param string $uri
|
||||
* @return void
|
||||
*/
|
||||
public function addLinks(DOMNodeList $links, $uri)
|
||||
{
|
||||
foreach ($links as $link) {
|
||||
if (strtolower($link->getAttribute('rel')) !== 'alternate'
|
||||
|| !$link->getAttribute('type') || !$link->getAttribute('href')) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($this->rss) && $link->getAttribute('type') == 'application/rss+xml') {
|
||||
$this->rss = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
|
||||
} elseif (!isset($this->atom) && $link->getAttribute('type') == 'application/atom+xml') {
|
||||
$this->atom = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
|
||||
} elseif (!isset($this->rdf) && $link->getAttribute('type') == 'application/rdf+xml') {
|
||||
$this->rdf = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
|
||||
}
|
||||
$this[] = new static([
|
||||
'rel' => 'alternate',
|
||||
'type' => $link->getAttribute('type'),
|
||||
'href' => $this->absolutiseUri(trim($link->getAttribute('href')), $uri),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to turn a relative URI into an absolute URI
|
||||
*/
|
||||
protected function absolutiseUri($link, $uri = null)
|
||||
{
|
||||
$linkUri = Uri::factory($link);
|
||||
if (!$linkUri->isAbsolute() or !$linkUri->isValid()) {
|
||||
if ($uri !== null) {
|
||||
$uri = Uri::factory($uri);
|
||||
|
||||
if ($link[0] !== '/') {
|
||||
$link = $uri->getPath() . '/' . $link;
|
||||
}
|
||||
|
||||
$link = sprintf(
|
||||
'%s://%s/%s',
|
||||
($uri->getScheme() ?: 'http'),
|
||||
$uri->getHost(),
|
||||
$this->canonicalizePath($link)
|
||||
);
|
||||
|
||||
if (!Uri::factory($link)->isValid()) {
|
||||
$link = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize relative path
|
||||
*/
|
||||
protected function canonicalizePath($path)
|
||||
{
|
||||
$parts = array_filter(explode('/', $path));
|
||||
$absolutes = [];
|
||||
foreach ($parts as $part) {
|
||||
if ('.' == $part) {
|
||||
continue;
|
||||
}
|
||||
if ('..' == $part) {
|
||||
array_pop($absolutes);
|
||||
} else {
|
||||
$absolutes[] = $part;
|
||||
}
|
||||
}
|
||||
return implode('/', $absolutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supports lazy loading of feeds using Reader::import() but
|
||||
* delegates any other operations to the parent class.
|
||||
*
|
||||
* @param string $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
if ($offset == 'feed' && !$this->offsetExists('feed')) {
|
||||
if (!$this->offsetExists('href')) {
|
||||
return;
|
||||
}
|
||||
$feed = Reader::import($this->offsetGet('href'));
|
||||
$this->offsetSet('feed', $feed);
|
||||
return $feed;
|
||||
}
|
||||
return parent::offsetGet($offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Http;
|
||||
|
||||
interface ClientInterface
|
||||
{
|
||||
/**
|
||||
* Make a GET request to a given URI
|
||||
*
|
||||
* @param string $uri
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function get($uri);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?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\Http;
|
||||
|
||||
interface HeaderAwareClientInterface extends ClientInterface
|
||||
{
|
||||
/**
|
||||
* Allow specifying headers to use when fetching a feed.
|
||||
*
|
||||
* Headers MUST be in the format:
|
||||
*
|
||||
* <code>
|
||||
* [
|
||||
* 'header-name' => [
|
||||
* 'header',
|
||||
* 'values'
|
||||
* ]
|
||||
* ]
|
||||
* </code>
|
||||
*
|
||||
* @param string $uri
|
||||
* @param array $headers
|
||||
* @return HeaderAwareResponseInterface
|
||||
*/
|
||||
public function get($uri, array $headers = []);
|
||||
}
|
||||
+28
@@ -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\Http;
|
||||
|
||||
interface HeaderAwareResponseInterface extends ResponseInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve a header (as a single line) from the response.
|
||||
*
|
||||
* Header name lookups MUST be case insensitive.
|
||||
*
|
||||
* Since the only header values the feed reader consumes are singular
|
||||
* in nature, this method is expected to return a string, and not
|
||||
* an array of values.
|
||||
*
|
||||
* @param string $name Header name to retrieve.
|
||||
* @param mixed $default Default value to use if header is not present.
|
||||
* @return string
|
||||
*/
|
||||
public function getHeaderLine($name, $default = null);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Http;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
|
||||
|
||||
/**
|
||||
* ResponseInterface wrapper for a PSR-7 response.
|
||||
*/
|
||||
class Psr7ResponseDecorator implements HeaderAwareResponseInterface
|
||||
{
|
||||
/**
|
||||
* @var Psr7ResponseInterface
|
||||
*/
|
||||
private $decoratedResponse;
|
||||
|
||||
/**
|
||||
* @param Psr7ResponseInterface $response
|
||||
*/
|
||||
public function __construct(Psr7ResponseInterface $response)
|
||||
{
|
||||
$this->decoratedResponse = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the original PSR-7 response being decorated.
|
||||
*
|
||||
* @return Psr7ResponseInterface
|
||||
*/
|
||||
public function getDecoratedResponse()
|
||||
{
|
||||
return $this->decoratedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return (string) $this->decoratedResponse->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->decoratedResponse->getStatusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getHeaderLine($name, $default = null)
|
||||
{
|
||||
if (! $this->decoratedResponse->hasHeader($name)) {
|
||||
return $default;
|
||||
}
|
||||
return $this->decoratedResponse->getHeaderLine($name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Zend Framework (http://framework.zend.com/)
|
||||
*
|
||||
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
||||
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
*/
|
||||
|
||||
namespace Zend\Feed\Reader\Http;
|
||||
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
class Response implements HeaderAwareResponseInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $headers;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $statusCode;
|
||||
|
||||
/**
|
||||
* @param int $statusCode
|
||||
* @param string|object $body
|
||||
* @param array $headers
|
||||
* @throws Exception\InvalidArgumentException
|
||||
*/
|
||||
public function __construct($statusCode, $body = '', array $headers = [])
|
||||
{
|
||||
$this->validateStatusCode($statusCode);
|
||||
$this->validateBody($body);
|
||||
$this->validateHeaders($headers);
|
||||
|
||||
$this->statusCode = (int) $statusCode;
|
||||
$this->body = (string) $body;
|
||||
$this->headers = $this->normalizeHeaders($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getHeaderLine($name, $default = null)
|
||||
{
|
||||
$normalizedName = strtolower($name);
|
||||
return isset($this->headers[$normalizedName])
|
||||
? $this->headers[$normalizedName]
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that we have a status code argument that will work for our context.
|
||||
*
|
||||
* @param mixed $body
|
||||
* @throws Exception\InvalidArgumentException for arguments not castable
|
||||
* to integer HTTP status codes.
|
||||
*/
|
||||
private function validateStatusCode($statusCode)
|
||||
{
|
||||
if (! is_numeric($statusCode)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'%s expects a numeric status code; received %s',
|
||||
__CLASS__,
|
||||
(is_object($statusCode) ? get_class($statusCode) : gettype($statusCode))
|
||||
));
|
||||
}
|
||||
|
||||
if (100 > $statusCode || 599 < $statusCode) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'%s expects an integer status code between 100 and 599 inclusive; received %s',
|
||||
__CLASS__,
|
||||
$statusCode
|
||||
));
|
||||
}
|
||||
|
||||
if (intval($statusCode) != $statusCode) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'%s expects an integer status code; received %s',
|
||||
__CLASS__,
|
||||
$statusCode
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that we have a body argument that will work for our context.
|
||||
*
|
||||
* @param mixed $body
|
||||
* @throws Exception\InvalidArgumentException for arguments not castable
|
||||
* to strings.
|
||||
*/
|
||||
private function validateBody($body)
|
||||
{
|
||||
if (is_string($body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_object($body) && method_exists($body, '__toString')) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'%s expects a string body, or an object that can cast to string; received %s',
|
||||
__CLASS__,
|
||||
(is_object($body) ? get_class($body) : gettype($body))
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate header values.
|
||||
*
|
||||
* @param array $headers
|
||||
* @throws Exception\InvalidArgumentException
|
||||
*/
|
||||
private function validateHeaders(array $headers)
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
if (! is_string($name) || is_numeric($name) || empty($name)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Header names provided to %s must be non-empty, non-numeric strings; received %s',
|
||||
__CLASS__,
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
if (! is_string($value) && ! is_numeric($value)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Individual header values provided to %s must be a string or numeric; received %s for header %s',
|
||||
__CLASS__,
|
||||
(is_object($value) ? get_class($value) : gettype($value)),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize header names to lowercase.
|
||||
*
|
||||
* @param array $headers
|
||||
* @return array
|
||||
*/
|
||||
private function normalizeHeaders(array $headers)
|
||||
{
|
||||
$normalized = [];
|
||||
foreach ($headers as $name => $value) {
|
||||
$normalized[strtolower($name)] = $value;
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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\Http;
|
||||
|
||||
interface ResponseInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve the response body
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Retrieve the HTTP response status code
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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\Http;
|
||||
|
||||
use Zend\Http\Client as ZendHttpClient;
|
||||
use Zend\Http\Headers;
|
||||
use Zend\Feed\Reader\Exception;
|
||||
|
||||
class ZendHttpClientDecorator implements HeaderAwareClientInterface
|
||||
{
|
||||
/**
|
||||
* @var ZendHttpClient
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @param ZendHttpClient $client
|
||||
*/
|
||||
public function __construct(ZendHttpClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZendHttpClient
|
||||
*/
|
||||
public function getDecoratedClient()
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get($uri, array $headers = [])
|
||||
{
|
||||
$this->client->resetParameters();
|
||||
$this->client->setMethod('GET');
|
||||
$this->client->setHeaders(new Headers());
|
||||
$this->client->setUri($uri);
|
||||
if (! empty($headers)) {
|
||||
$this->injectHeaders($headers);
|
||||
}
|
||||
$response = $this->client->send();
|
||||
|
||||
return new Response(
|
||||
$response->getStatusCode(),
|
||||
$response->getBody(),
|
||||
$this->prepareResponseHeaders($response->getHeaders())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject header values into the client.
|
||||
*
|
||||
* @param array $headerValues
|
||||
*/
|
||||
private function injectHeaders(array $headerValues)
|
||||
{
|
||||
$headers = $this->client->getRequest()->getHeaders();
|
||||
foreach ($headerValues as $name => $values) {
|
||||
if (! is_string($name) || is_numeric($name) || empty($name)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Header names provided to %s::get must be non-empty, non-numeric strings; received %s',
|
||||
__CLASS__,
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
if (! is_array($values)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Header values provided to %s::get must be arrays of values; received %s',
|
||||
__CLASS__,
|
||||
(is_object($values) ? get_class($values) : gettype($values))
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (! is_string($value) && ! is_numeric($value)) {
|
||||
throw new Exception\InvalidArgumentException(sprintf(
|
||||
'Individual header values provided to %s::get must be strings or numbers; '
|
||||
. 'received %s for header %s',
|
||||
__CLASS__,
|
||||
(is_object($value) ? get_class($value) : gettype($value)),
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
$headers->addHeaderLine($name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize headers to use with HeaderAwareResponseInterface.
|
||||
*
|
||||
* Ensures multi-value headers are represented as a single string, via
|
||||
* comma concatenation.
|
||||
*
|
||||
* @param Headers $headers
|
||||
* @return array
|
||||
*/
|
||||
private function prepareResponseHeaders(Headers $headers)
|
||||
{
|
||||
$normalized = [];
|
||||
foreach ($headers->toArray() as $name => $value) {
|
||||
$normalized[$name] = is_array($value) ? implode(', ', $value) : $value;
|
||||
}
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
<?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 DOMXPath;
|
||||
use Zend\Cache\Storage\StorageInterface as CacheStorage;
|
||||
use Zend\Http as ZendHttp;
|
||||
use Zend\Stdlib\ErrorHandler;
|
||||
use Zend\Feed\Reader\Exception\InvalidHttpClientException;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Reader implements ReaderImportInterface
|
||||
{
|
||||
/**
|
||||
* Namespace constants
|
||||
*/
|
||||
const NAMESPACE_ATOM_03 = 'http://purl.org/atom/ns#';
|
||||
const NAMESPACE_ATOM_10 = 'http://www.w3.org/2005/Atom';
|
||||
const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||
const NAMESPACE_RSS_090 = 'http://my.netscape.com/rdf/simple/0.9/';
|
||||
const NAMESPACE_RSS_10 = 'http://purl.org/rss/1.0/';
|
||||
|
||||
/**
|
||||
* Feed type constants
|
||||
*/
|
||||
const TYPE_ANY = 'any';
|
||||
const TYPE_ATOM_03 = 'atom-03';
|
||||
const TYPE_ATOM_10 = 'atom-10';
|
||||
const TYPE_ATOM_10_ENTRY = 'atom-10-entry';
|
||||
const TYPE_ATOM_ANY = 'atom';
|
||||
const TYPE_RSS_090 = 'rss-090';
|
||||
const TYPE_RSS_091 = 'rss-091';
|
||||
const TYPE_RSS_091_NETSCAPE = 'rss-091n';
|
||||
const TYPE_RSS_091_USERLAND = 'rss-091u';
|
||||
const TYPE_RSS_092 = 'rss-092';
|
||||
const TYPE_RSS_093 = 'rss-093';
|
||||
const TYPE_RSS_094 = 'rss-094';
|
||||
const TYPE_RSS_10 = 'rss-10';
|
||||
const TYPE_RSS_20 = 'rss-20';
|
||||
const TYPE_RSS_ANY = 'rss';
|
||||
|
||||
/**
|
||||
* Cache instance
|
||||
*
|
||||
* @var CacheStorage
|
||||
*/
|
||||
protected static $cache = null;
|
||||
|
||||
/**
|
||||
* HTTP client object to use for retrieving feeds
|
||||
*
|
||||
* @var Http\ClientInterface
|
||||
*/
|
||||
protected static $httpClient = null;
|
||||
|
||||
/**
|
||||
* Override HTTP PUT and DELETE request methods?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $httpMethodOverride = false;
|
||||
|
||||
protected static $httpConditionalGet = false;
|
||||
|
||||
protected static $extensionManager = null;
|
||||
|
||||
protected static $extensions = [
|
||||
'feed' => [
|
||||
'DublinCore\Feed',
|
||||
'Atom\Feed'
|
||||
],
|
||||
'entry' => [
|
||||
'Content\Entry',
|
||||
'DublinCore\Entry',
|
||||
'Atom\Entry'
|
||||
],
|
||||
'core' => [
|
||||
'DublinCore\Feed',
|
||||
'Atom\Feed',
|
||||
'Content\Entry',
|
||||
'DublinCore\Entry',
|
||||
'Atom\Entry'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the Feed cache
|
||||
*
|
||||
* @return CacheStorage
|
||||
*/
|
||||
public static function getCache()
|
||||
{
|
||||
return static::$cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed cache
|
||||
*
|
||||
* @param CacheStorage $cache
|
||||
* @return void
|
||||
*/
|
||||
public static function setCache(CacheStorage $cache)
|
||||
{
|
||||
static::$cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP client instance
|
||||
*
|
||||
* Sets the HTTP client object to use for retrieving the feeds.
|
||||
*
|
||||
* @param ZendHttp\Client | Http\ClientInterface $httpClient
|
||||
* @return void
|
||||
*/
|
||||
public static function setHttpClient($httpClient)
|
||||
{
|
||||
if ($httpClient instanceof ZendHttp\Client) {
|
||||
$httpClient = new Http\ZendHttpClientDecorator($httpClient);
|
||||
}
|
||||
|
||||
if (! $httpClient instanceof Http\ClientInterface) {
|
||||
throw new InvalidHttpClientException();
|
||||
}
|
||||
static::$httpClient = $httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTTP client object. If none is set, a new ZendHttp\Client will be used.
|
||||
*
|
||||
* @return Http\ClientInterface
|
||||
*/
|
||||
public static function getHttpClient()
|
||||
{
|
||||
if (! static::$httpClient) {
|
||||
static::$httpClient = new Http\ZendHttpClientDecorator(new ZendHttp\Client());
|
||||
}
|
||||
|
||||
return static::$httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle using POST instead of PUT and DELETE HTTP methods
|
||||
*
|
||||
* Some feed implementations do not accept PUT and DELETE HTTP
|
||||
* methods, or they can't be used because of proxies or other
|
||||
* measures. This allows turning on using POST where PUT and
|
||||
* DELETE would normally be used; in addition, an
|
||||
* X-Method-Override header will be sent with a value of PUT or
|
||||
* DELETE as appropriate.
|
||||
*
|
||||
* @param bool $override Whether to override PUT and DELETE.
|
||||
* @return void
|
||||
*/
|
||||
public static function setHttpMethodOverride($override = true)
|
||||
{
|
||||
static::$httpMethodOverride = $override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP override state
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function getHttpMethodOverride()
|
||||
{
|
||||
return static::$httpMethodOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag indicating whether or not to use HTTP conditional GET
|
||||
*
|
||||
* @param bool $bool
|
||||
* @return void
|
||||
*/
|
||||
public static function useHttpConditionalGet($bool = true)
|
||||
{
|
||||
static::$httpConditionalGet = $bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a feed by providing a URI
|
||||
*
|
||||
* @param string $uri The URI to the feed
|
||||
* @param string $etag OPTIONAL Last received ETag for this resource
|
||||
* @param string $lastModified OPTIONAL Last-Modified value for this resource
|
||||
* @return Feed\FeedInterface
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function import($uri, $etag = null, $lastModified = null)
|
||||
{
|
||||
$cache = self::getCache();
|
||||
$client = self::getHttpClient();
|
||||
$cacheId = 'Zend_Feed_Reader_' . md5($uri);
|
||||
|
||||
if (static::$httpConditionalGet && $cache) {
|
||||
$headers = [];
|
||||
$data = $cache->getItem($cacheId);
|
||||
if ($data && $client instanceof Http\HeaderAwareClientInterface) {
|
||||
// Only check for ETag and last modified values in the cache
|
||||
// if we have a client capable of emitting headers in the first place.
|
||||
if ($etag === null) {
|
||||
$etag = $cache->getItem($cacheId . '_etag');
|
||||
}
|
||||
if ($lastModified === null) {
|
||||
$lastModified = $cache->getItem($cacheId . '_lastmodified');
|
||||
}
|
||||
if ($etag) {
|
||||
$headers['If-None-Match'] = [$etag];
|
||||
}
|
||||
if ($lastModified) {
|
||||
$headers['If-Modified-Since'] = [$lastModified];
|
||||
}
|
||||
}
|
||||
$response = $client->get($uri, $headers);
|
||||
if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 304) {
|
||||
throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode());
|
||||
}
|
||||
if ($response->getStatusCode() == 304) {
|
||||
$responseXml = $data;
|
||||
} else {
|
||||
$responseXml = $response->getBody();
|
||||
$cache->setItem($cacheId, $responseXml);
|
||||
|
||||
if ($response instanceof Http\HeaderAwareResponseInterface) {
|
||||
if ($response->getHeaderLine('ETag', false)) {
|
||||
$cache->setItem($cacheId . '_etag', $response->getHeaderLine('ETag'));
|
||||
}
|
||||
if ($response->getHeaderLine('Last-Modified', false)) {
|
||||
$cache->setItem($cacheId . '_lastmodified', $response->getHeaderLine('Last-Modified'));
|
||||
}
|
||||
}
|
||||
}
|
||||
return static::importString($responseXml);
|
||||
} elseif ($cache) {
|
||||
$data = $cache->getItem($cacheId);
|
||||
if ($data) {
|
||||
return static::importString($data);
|
||||
}
|
||||
$response = $client->get($uri);
|
||||
if ((int) $response->getStatusCode() !== 200) {
|
||||
throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode());
|
||||
}
|
||||
$responseXml = $response->getBody();
|
||||
$cache->setItem($cacheId, $responseXml);
|
||||
return static::importString($responseXml);
|
||||
} else {
|
||||
$response = $client->get($uri);
|
||||
if ((int) $response->getStatusCode() !== 200) {
|
||||
throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode());
|
||||
}
|
||||
$reader = static::importString($response->getBody());
|
||||
$reader->setOriginalSourceUri($uri);
|
||||
return $reader;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a feed from a remote URI
|
||||
*
|
||||
* Performs similarly to import(), except it uses the HTTP client passed to
|
||||
* the method, and does not take into account cached data.
|
||||
*
|
||||
* Primary purpose is to make it possible to use the Reader with alternate
|
||||
* HTTP client implementations.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param Http\ClientInterface $client
|
||||
* @return self
|
||||
* @throws Exception\RuntimeException if response is not an Http\ResponseInterface
|
||||
*/
|
||||
public static function importRemoteFeed($uri, Http\ClientInterface $client)
|
||||
{
|
||||
$response = $client->get($uri);
|
||||
if (! $response instanceof Http\ResponseInterface) {
|
||||
throw new Exception\RuntimeException(sprintf(
|
||||
'Did not receive a %s\Http\ResponseInterface from the provided HTTP client; received "%s"',
|
||||
__NAMESPACE__,
|
||||
(is_object($response) ? get_class($response) : gettype($response))
|
||||
));
|
||||
}
|
||||
|
||||
if ((int) $response->getStatusCode() !== 200) {
|
||||
throw new Exception\RuntimeException('Feed failed to load, got response code ' . $response->getStatusCode());
|
||||
}
|
||||
$reader = static::importString($response->getBody());
|
||||
$reader->setOriginalSourceUri($uri);
|
||||
return $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a feed from a string
|
||||
*
|
||||
* @param string $string
|
||||
* @return Feed\FeedInterface
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function importString($string)
|
||||
{
|
||||
$trimmed = trim($string);
|
||||
if (!is_string($string) || empty($trimmed)) {
|
||||
throw new Exception\InvalidArgumentException('Only non empty strings are allowed as input');
|
||||
}
|
||||
|
||||
$libxmlErrflag = libxml_use_internal_errors(true);
|
||||
$oldValue = libxml_disable_entity_loader(true);
|
||||
$dom = new DOMDocument;
|
||||
$status = $dom->loadXML(trim($string));
|
||||
foreach ($dom->childNodes as $child) {
|
||||
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid XML: Detected use of illegal DOCTYPE'
|
||||
);
|
||||
}
|
||||
}
|
||||
libxml_disable_entity_loader($oldValue);
|
||||
libxml_use_internal_errors($libxmlErrflag);
|
||||
|
||||
if (!$status) {
|
||||
// Build error message
|
||||
$error = libxml_get_last_error();
|
||||
if ($error && $error->message) {
|
||||
$error->message = trim($error->message);
|
||||
$errormsg = "DOMDocument cannot parse XML: {$error->message}";
|
||||
} else {
|
||||
$errormsg = "DOMDocument cannot parse XML: Please check the XML document's validity";
|
||||
}
|
||||
throw new Exception\RuntimeException($errormsg);
|
||||
}
|
||||
|
||||
$type = static::detectType($dom);
|
||||
|
||||
static::registerCoreExtensions();
|
||||
|
||||
if (substr($type, 0, 3) == 'rss') {
|
||||
$reader = new Feed\Rss($dom, $type);
|
||||
} elseif (substr($type, 8, 5) == 'entry') {
|
||||
$reader = new Entry\Atom($dom->documentElement, 0, self::TYPE_ATOM_10);
|
||||
} elseif (substr($type, 0, 4) == 'atom') {
|
||||
$reader = new Feed\Atom($dom, $type);
|
||||
} else {
|
||||
throw new Exception\RuntimeException('The URI used does not point to a '
|
||||
. 'valid Atom, RSS or RDF feed that Zend\Feed\Reader can parse.');
|
||||
}
|
||||
return $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a feed from a file located at $filename.
|
||||
*
|
||||
* @param string $filename
|
||||
* @throws Exception\RuntimeException
|
||||
* @return Feed\FeedInterface
|
||||
*/
|
||||
public static function importFile($filename)
|
||||
{
|
||||
ErrorHandler::start();
|
||||
$feed = file_get_contents($filename);
|
||||
$err = ErrorHandler::stop();
|
||||
if ($feed === false) {
|
||||
throw new Exception\RuntimeException("File '{$filename}' could not be loaded", 0, $err);
|
||||
}
|
||||
return static::importString($feed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find feed links
|
||||
*
|
||||
* @param $uri
|
||||
* @return FeedSet
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function findFeedLinks($uri)
|
||||
{
|
||||
$client = static::getHttpClient();
|
||||
$response = $client->get($uri);
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception\RuntimeException("Failed to access $uri, got response code " . $response->getStatusCode());
|
||||
}
|
||||
$responseHtml = $response->getBody();
|
||||
$libxmlErrflag = libxml_use_internal_errors(true);
|
||||
$oldValue = libxml_disable_entity_loader(true);
|
||||
$dom = new DOMDocument;
|
||||
$status = $dom->loadHTML(trim($responseHtml));
|
||||
libxml_disable_entity_loader($oldValue);
|
||||
libxml_use_internal_errors($libxmlErrflag);
|
||||
if (!$status) {
|
||||
// Build error message
|
||||
$error = libxml_get_last_error();
|
||||
if ($error && $error->message) {
|
||||
$error->message = trim($error->message);
|
||||
$errormsg = "DOMDocument cannot parse HTML: {$error->message}";
|
||||
} else {
|
||||
$errormsg = "DOMDocument cannot parse HTML: Please check the XML document's validity";
|
||||
}
|
||||
throw new Exception\RuntimeException($errormsg);
|
||||
}
|
||||
$feedSet = new FeedSet;
|
||||
$links = $dom->getElementsByTagName('link');
|
||||
$feedSet->addLinks($links, $uri);
|
||||
return $feedSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the feed type of the provided feed
|
||||
*
|
||||
* @param Feed\AbstractFeed|DOMDocument|string $feed
|
||||
* @param bool $specOnly
|
||||
* @return string
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function detectType($feed, $specOnly = false)
|
||||
{
|
||||
if ($feed instanceof Feed\AbstractFeed) {
|
||||
$dom = $feed->getDomDocument();
|
||||
} elseif ($feed instanceof DOMDocument) {
|
||||
$dom = $feed;
|
||||
} elseif (is_string($feed) && !empty($feed)) {
|
||||
ErrorHandler::start(E_NOTICE|E_WARNING);
|
||||
ini_set('track_errors', 1);
|
||||
$oldValue = libxml_disable_entity_loader(true);
|
||||
$dom = new DOMDocument;
|
||||
$status = $dom->loadXML($feed);
|
||||
foreach ($dom->childNodes as $child) {
|
||||
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid XML: Detected use of illegal DOCTYPE'
|
||||
);
|
||||
}
|
||||
}
|
||||
libxml_disable_entity_loader($oldValue);
|
||||
ini_restore('track_errors');
|
||||
ErrorHandler::stop();
|
||||
if (!$status) {
|
||||
if (!isset($phpErrormsg)) {
|
||||
if (function_exists('xdebug_is_enabled')) {
|
||||
$phpErrormsg = '(error message not available, when XDebug is running)';
|
||||
} else {
|
||||
$phpErrormsg = '(error message not available)';
|
||||
}
|
||||
}
|
||||
throw new Exception\RuntimeException("DOMDocument cannot parse XML: $phpErrormsg");
|
||||
}
|
||||
} else {
|
||||
throw new Exception\InvalidArgumentException('Invalid object/scalar provided: must'
|
||||
. ' be of type Zend\Feed\Reader\Feed, DomDocument or string');
|
||||
}
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
if ($xpath->query('/rss')->length) {
|
||||
$type = self::TYPE_RSS_ANY;
|
||||
$version = $xpath->evaluate('string(/rss/@version)');
|
||||
|
||||
if (strlen($version) > 0) {
|
||||
switch ($version) {
|
||||
case '2.0':
|
||||
$type = self::TYPE_RSS_20;
|
||||
break;
|
||||
|
||||
case '0.94':
|
||||
$type = self::TYPE_RSS_094;
|
||||
break;
|
||||
|
||||
case '0.93':
|
||||
$type = self::TYPE_RSS_093;
|
||||
break;
|
||||
|
||||
case '0.92':
|
||||
$type = self::TYPE_RSS_092;
|
||||
break;
|
||||
|
||||
case '0.91':
|
||||
$type = self::TYPE_RSS_091;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
$xpath->registerNamespace('rdf', self::NAMESPACE_RDF);
|
||||
|
||||
if ($xpath->query('/rdf:RDF')->length) {
|
||||
$xpath->registerNamespace('rss', self::NAMESPACE_RSS_10);
|
||||
|
||||
if ($xpath->query('/rdf:RDF/rss:channel')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:image')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:item')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:textinput')->length
|
||||
) {
|
||||
return self::TYPE_RSS_10;
|
||||
}
|
||||
|
||||
$xpath->registerNamespace('rss', self::NAMESPACE_RSS_090);
|
||||
|
||||
if ($xpath->query('/rdf:RDF/rss:channel')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:image')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:item')->length
|
||||
|| $xpath->query('/rdf:RDF/rss:textinput')->length
|
||||
) {
|
||||
return self::TYPE_RSS_090;
|
||||
}
|
||||
}
|
||||
|
||||
$xpath->registerNamespace('atom', self::NAMESPACE_ATOM_10);
|
||||
|
||||
if ($xpath->query('//atom:feed')->length) {
|
||||
return self::TYPE_ATOM_10;
|
||||
}
|
||||
|
||||
if ($xpath->query('//atom:entry')->length) {
|
||||
if ($specOnly == true) {
|
||||
return self::TYPE_ATOM_10;
|
||||
} else {
|
||||
return self::TYPE_ATOM_10_ENTRY;
|
||||
}
|
||||
}
|
||||
|
||||
$xpath->registerNamespace('atom', self::NAMESPACE_ATOM_03);
|
||||
|
||||
if ($xpath->query('//atom:feed')->length) {
|
||||
return self::TYPE_ATOM_03;
|
||||
}
|
||||
|
||||
return self::TYPE_ANY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set plugin manager for use with Extensions
|
||||
*
|
||||
* @param ExtensionManagerInterface $extensionManager
|
||||
*/
|
||||
public static function setExtensionManager(ExtensionManagerInterface $extensionManager)
|
||||
{
|
||||
static::$extensionManager = $extensionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin manager for use with Extensions
|
||||
*
|
||||
* @return ExtensionManagerInterface
|
||||
*/
|
||||
public static function getExtensionManager()
|
||||
{
|
||||
if (!isset(static::$extensionManager)) {
|
||||
static::setExtensionManager(new StandaloneExtensionManager());
|
||||
}
|
||||
return static::$extensionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an Extension by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return void
|
||||
* @throws Exception\RuntimeException if unable to resolve Extension class
|
||||
*/
|
||||
public static function registerExtension($name)
|
||||
{
|
||||
$feedName = $name . '\Feed';
|
||||
$entryName = $name . '\Entry';
|
||||
$manager = static::getExtensionManager();
|
||||
if (static::isRegistered($name)) {
|
||||
if ($manager->has($feedName) || $manager->has($entryName)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$manager->has($feedName) && !$manager->has($entryName)) {
|
||||
throw new Exception\RuntimeException('Could not load extension: ' . $name
|
||||
. ' using Plugin Loader. Check prefix paths are configured and extension exists.');
|
||||
}
|
||||
if ($manager->has($feedName)) {
|
||||
static::$extensions['feed'][] = $feedName;
|
||||
}
|
||||
if ($manager->has($entryName)) {
|
||||
static::$extensions['entry'][] = $entryName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a given named Extension registered?
|
||||
*
|
||||
* @param string $extensionName
|
||||
* @return bool
|
||||
*/
|
||||
public static function isRegistered($extensionName)
|
||||
{
|
||||
$feedName = $extensionName . '\Feed';
|
||||
$entryName = $extensionName . '\Entry';
|
||||
if (in_array($feedName, static::$extensions['feed'])
|
||||
|| in_array($entryName, static::$extensions['entry'])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of extensions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getExtensions()
|
||||
{
|
||||
return static::$extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset class state to defaults
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function reset()
|
||||
{
|
||||
static::$cache = null;
|
||||
static::$httpClient = null;
|
||||
static::$httpMethodOverride = false;
|
||||
static::$httpConditionalGet = false;
|
||||
static::$extensionManager = null;
|
||||
static::$extensions = [
|
||||
'feed' => [
|
||||
'DublinCore\Feed',
|
||||
'Atom\Feed'
|
||||
],
|
||||
'entry' => [
|
||||
'Content\Entry',
|
||||
'DublinCore\Entry',
|
||||
'Atom\Entry'
|
||||
],
|
||||
'core' => [
|
||||
'DublinCore\Feed',
|
||||
'Atom\Feed',
|
||||
'Content\Entry',
|
||||
'DublinCore\Entry',
|
||||
'Atom\Entry'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register core (default) extensions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function registerCoreExtensions()
|
||||
{
|
||||
static::registerExtension('DublinCore');
|
||||
static::registerExtension('Content');
|
||||
static::registerExtension('Atom');
|
||||
static::registerExtension('Slash');
|
||||
static::registerExtension('WellFormedWeb');
|
||||
static::registerExtension('Thread');
|
||||
static::registerExtension('Podcast');
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to apply array_unique operation to a multidimensional
|
||||
* array.
|
||||
*
|
||||
* @param array
|
||||
* @return array
|
||||
*/
|
||||
public static function arrayUnique(array $array)
|
||||
{
|
||||
foreach ($array as &$value) {
|
||||
$value = serialize($value);
|
||||
}
|
||||
$array = array_unique($array);
|
||||
foreach ($array as &$value) {
|
||||
$value = unserialize($value);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
|
||||
interface ReaderImportInterface
|
||||
{
|
||||
/**
|
||||
* Import a feed by providing a URI
|
||||
*
|
||||
* @param string $uri The URI to the feed
|
||||
* @param string $etag OPTIONAL Last received ETag for this resource
|
||||
* @param string $lastModified OPTIONAL Last-Modified value for this resource
|
||||
* @return Feed\FeedInterface
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function import($uri, $etag = null, $lastModified = null);
|
||||
|
||||
/**
|
||||
* Import a feed from a remote URI
|
||||
*
|
||||
* Performs similarly to import(), except it uses the HTTP client passed to
|
||||
* the method, and does not take into account cached data.
|
||||
*
|
||||
* Primary purpose is to make it possible to use the Reader with alternate
|
||||
* HTTP client implementations.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param Http\ClientInterface $client
|
||||
* @return self
|
||||
* @throws Exception\RuntimeException if response is not an Http\ResponseInterface
|
||||
*/
|
||||
public static function importRemoteFeed($uri, Http\ClientInterface $client);
|
||||
|
||||
|
||||
/**
|
||||
* Import a feed from a string
|
||||
*
|
||||
* @param string $string
|
||||
* @return Feed\FeedInterface
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public static function importString($string);
|
||||
|
||||
|
||||
/**
|
||||
* Imports a feed from a file located at $filename.
|
||||
*
|
||||
* @param string $filename
|
||||
* @throws Exception\RuntimeException
|
||||
* @return Feed\FeedInterface
|
||||
*/
|
||||
public static function importFile($filename);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
class StandaloneExtensionManager implements ExtensionManagerInterface
|
||||
{
|
||||
private $extensions = [
|
||||
'Atom\Entry' => 'Zend\Feed\Reader\Extension\Atom\Entry',
|
||||
'Atom\Feed' => 'Zend\Feed\Reader\Extension\Atom\Feed',
|
||||
'Content\Entry' => 'Zend\Feed\Reader\Extension\Content\Entry',
|
||||
'CreativeCommons\Entry' => 'Zend\Feed\Reader\Extension\CreativeCommons\Entry',
|
||||
'CreativeCommons\Feed' => 'Zend\Feed\Reader\Extension\CreativeCommons\Feed',
|
||||
'DublinCore\Entry' => 'Zend\Feed\Reader\Extension\DublinCore\Entry',
|
||||
'DublinCore\Feed' => 'Zend\Feed\Reader\Extension\DublinCore\Feed',
|
||||
'Podcast\Entry' => 'Zend\Feed\Reader\Extension\Podcast\Entry',
|
||||
'Podcast\Feed' => 'Zend\Feed\Reader\Extension\Podcast\Feed',
|
||||
'Slash\Entry' => 'Zend\Feed\Reader\Extension\Slash\Entry',
|
||||
'Syndication\Feed' => 'Zend\Feed\Reader\Extension\Syndication\Feed',
|
||||
'Thread\Entry' => 'Zend\Feed\Reader\Extension\Thread\Entry',
|
||||
'WellFormedWeb\Entry' => 'Zend\Feed\Reader\Extension\WellFormedWeb\Entry',
|
||||
];
|
||||
|
||||
/**
|
||||
* Do we have the extension?
|
||||
*
|
||||
* @param string $extension
|
||||
* @return bool
|
||||
*/
|
||||
public function has($extension)
|
||||
{
|
||||
return array_key_exists($extension, $this->extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the extension
|
||||
*
|
||||
* @param string $extension
|
||||
* @return Extension\AbstractEntry|Extension\AbstractFeed
|
||||
*/
|
||||
public function get($extension)
|
||||
{
|
||||
$class = $this->extensions[$extension];
|
||||
return new $class();
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
<?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;
|
||||
|
||||
class Uri
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fragment;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $pass;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $port;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $scheme;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $valid;
|
||||
|
||||
/**
|
||||
* Valid schemes
|
||||
*/
|
||||
protected $validSchemes = [
|
||||
'http',
|
||||
'https',
|
||||
'file',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
*/
|
||||
public function __construct($uri)
|
||||
{
|
||||
$parsed = parse_url($uri);
|
||||
if (false === $parsed) {
|
||||
$this->valid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scheme = isset($parsed['scheme']) ? $parsed['scheme'] : null;
|
||||
$this->host = isset($parsed['host']) ? $parsed['host'] : null;
|
||||
$this->port = isset($parsed['port']) ? $parsed['port'] : null;
|
||||
$this->user = isset($parsed['user']) ? $parsed['user'] : null;
|
||||
$this->pass = isset($parsed['pass']) ? $parsed['pass'] : null;
|
||||
$this->path = isset($parsed['path']) ? $parsed['path'] : null;
|
||||
$this->query = isset($parsed['query']) ? $parsed['query'] : null;
|
||||
$this->fragment = isset($parsed['fragment']) ? $parsed['fragment'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
*
|
||||
* Useful for chained validations
|
||||
*
|
||||
* @param string $uri
|
||||
* @return self
|
||||
*/
|
||||
public static function factory($uri)
|
||||
{
|
||||
return new static($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the host
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URI path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the scheme
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getScheme()
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the URI valid?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
if (false === $this->valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->scheme && !in_array($this->scheme, $this->validSchemes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->host) {
|
||||
if ($this->path && substr($this->path, 0, 1) != '/') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// no host, but user and/or port... what?
|
||||
if ($this->user || $this->port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->path) {
|
||||
// Check path-only (no host) URI
|
||||
if (substr($this->path, 0, 2) == '//') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! ($this->query || $this->fragment)) {
|
||||
// No host, path, query or fragment - this is not a valid URI
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the URI absolute?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAbsolute()
|
||||
{
|
||||
return ($this->scheme !== null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
<?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\Writer;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Uri;
|
||||
use Zend\Validator;
|
||||
|
||||
class AbstractFeed
|
||||
{
|
||||
/**
|
||||
* Contains all Feed level date to append in feed output
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Holds the value "atom" or "rss" depending on the feed type set when
|
||||
* when last exported.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* @var $extensions
|
||||
*/
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* Constructor: Primarily triggers the registration of core extensions and
|
||||
* loads those appropriate to this data container.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Writer::registerCoreExtensions();
|
||||
$this->_loadExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single author
|
||||
*
|
||||
* The following option keys are supported:
|
||||
* 'name' => (string) The name
|
||||
* 'email' => (string) An optional email
|
||||
* 'uri' => (string) An optional and valid URI
|
||||
*
|
||||
* @param array $author
|
||||
* @throws Exception\InvalidArgumentException If any value of $author not follow the format.
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addAuthor(array $author)
|
||||
{
|
||||
// Check array values
|
||||
if (!array_key_exists('name', $author)
|
||||
|| empty($author['name'])
|
||||
|| !is_string($author['name'])
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: author array must include a "name" key with a non-empty string value');
|
||||
}
|
||||
|
||||
if (isset($author['email'])) {
|
||||
if (empty($author['email']) || !is_string($author['email'])) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: "email" array value must be a non-empty string');
|
||||
}
|
||||
}
|
||||
if (isset($author['uri'])) {
|
||||
if (empty($author['uri']) || !is_string($author['uri']) ||
|
||||
!Uri::factory($author['uri'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['authors'][] = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array with feed authors
|
||||
*
|
||||
* @see addAuthor
|
||||
* @param array $authors
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addAuthors(array $authors)
|
||||
{
|
||||
foreach ($authors as $author) {
|
||||
$this->addAuthor($author);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the copyright entry
|
||||
*
|
||||
* @param string $copyright
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setCopyright($copyright)
|
||||
{
|
||||
if (empty($copyright) || !is_string($copyright)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed creation date
|
||||
*
|
||||
* @param null|int|DateTime
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setDateCreated($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp'
|
||||
. ' passed as parameter');
|
||||
}
|
||||
$this->data['dateCreated'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed modification date
|
||||
*
|
||||
* @param null|int|DateTime
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setDateModified($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp'
|
||||
. ' passed as parameter');
|
||||
}
|
||||
$this->data['dateModified'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed last-build date. Ignored for Atom 1.0.
|
||||
*
|
||||
* @param null|int|DateTime
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setLastBuildDate($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp'
|
||||
. ' passed as parameter');
|
||||
}
|
||||
$this->data['lastBuildDate'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed description
|
||||
*
|
||||
* @param string $description
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
if (empty($description) || !is_string($description)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed generator entry
|
||||
*
|
||||
* @param array|string $name
|
||||
* @param null|string $version
|
||||
* @param null|string $uri
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setGenerator($name, $version = null, $uri = null)
|
||||
{
|
||||
if (is_array($name)) {
|
||||
$data = $name;
|
||||
if (empty($data['name']) || !is_string($data['name'])) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "name" must be a non-empty string');
|
||||
}
|
||||
$generator = ['name' => $data['name']];
|
||||
if (isset($data['version'])) {
|
||||
if (empty($data['version']) || !is_string($data['version'])) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "version" must be a non-empty string');
|
||||
}
|
||||
$generator['version'] = $data['version'];
|
||||
}
|
||||
if (isset($data['uri'])) {
|
||||
if (empty($data['uri']) || !is_string($data['uri']) || !Uri::factory($data['uri'])->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "uri" must be a non-empty string and a valid URI/IRI');
|
||||
}
|
||||
$generator['uri'] = $data['uri'];
|
||||
}
|
||||
} else {
|
||||
if (empty($name) || !is_string($name)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "name" must be a non-empty string');
|
||||
}
|
||||
$generator = ['name' => $name];
|
||||
if (isset($version)) {
|
||||
if (empty($version) || !is_string($version)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "version" must be a non-empty string');
|
||||
}
|
||||
$generator['version'] = $version;
|
||||
}
|
||||
if (isset($uri)) {
|
||||
if (empty($uri) || !is_string($uri) || !Uri::factory($uri)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "uri" must be a non-empty string and a valid URI/IRI');
|
||||
}
|
||||
$generator['uri'] = $uri;
|
||||
}
|
||||
}
|
||||
$this->data['generator'] = $generator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed ID - URI or URN (via PCRE pattern) supported
|
||||
*
|
||||
* @param string $id
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
if ((empty($id) || !is_string($id) || !Uri::factory($id)->isValid())
|
||||
&& !preg_match("#^urn:[a-zA-Z0-9][a-zA-Z0-9\-]{1,31}:([a-zA-Z0-9\(\)\+\,\.\:\=\@\;\$\_\!\*\-]|%[0-9a-fA-F]{2})*#", $id)
|
||||
&& !$this->_validateTagUri($id)
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a URI using the tag scheme (RFC 4151)
|
||||
*
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
protected function _validateTagUri($id)
|
||||
{
|
||||
if (preg_match('/^tag:(?P<name>.*),(?P<date>\d{4}-?\d{0,2}-?\d{0,2}):(?P<specific>.*)(.*:)*$/', $id, $matches)) {
|
||||
$dvalid = false;
|
||||
$date = $matches['date'];
|
||||
$d6 = strtotime($date);
|
||||
if ((strlen($date) == 4) && $date <= date('Y')) {
|
||||
$dvalid = true;
|
||||
} elseif ((strlen($date) == 7) && ($d6 < strtotime("now"))) {
|
||||
$dvalid = true;
|
||||
} elseif ((strlen($date) == 10) && ($d6 < strtotime("now"))) {
|
||||
$dvalid = true;
|
||||
}
|
||||
$validator = new Validator\EmailAddress;
|
||||
if ($validator->isValid($matches['name'])) {
|
||||
$nvalid = true;
|
||||
} else {
|
||||
$nvalid = $validator->isValid('info@' . $matches['name']);
|
||||
}
|
||||
return $dvalid && $nvalid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a feed image (URI at minimum). Parameter is a single array with the
|
||||
* required key 'uri'. When rendering as RSS, the required keys are 'uri',
|
||||
* 'title' and 'link'. RSS also specifies three optional parameters 'width',
|
||||
* 'height' and 'description'. Only 'uri' is required and used for Atom rendering.
|
||||
*
|
||||
* @param array $data
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setImage(array $data)
|
||||
{
|
||||
if (empty($data['uri']) || !is_string($data['uri'])
|
||||
|| !Uri::factory($data['uri'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter \'uri\''
|
||||
. ' must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['image'] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed language
|
||||
*
|
||||
* @param string $language
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setLanguage($language)
|
||||
{
|
||||
if (empty($language) || !is_string($language)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['language'] = $language;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a link to the HTML source
|
||||
*
|
||||
* @param string $link
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setLink($link)
|
||||
{
|
||||
if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['link'] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a link to an XML feed for any feed type/version
|
||||
*
|
||||
* @param string $link
|
||||
* @param string $type
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setFeedLink($link, $type)
|
||||
{
|
||||
if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "link"" must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
if (!in_array(strtolower($type), ['rss', 'rdf', 'atom'])) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "type"; You must declare the type of feed the link points to, i.e. RSS, RDF or Atom');
|
||||
}
|
||||
$this->data['feedLinks'][strtolower($type)] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed title
|
||||
*
|
||||
* @param string $title
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
if (empty($title) || !is_string($title)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed character encoding
|
||||
*
|
||||
* @param string $encoding
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
if (empty($encoding) || !is_string($encoding)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['encoding'] = $encoding;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed's base URL
|
||||
*
|
||||
* @param string $url
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setBaseUrl($url)
|
||||
{
|
||||
if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "url" array value'
|
||||
. ' must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['baseUrl'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Pubsubhubbub hub endpoint URL
|
||||
*
|
||||
* @param string $url
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addHub($url)
|
||||
{
|
||||
if (empty($url) || !is_string($url) || !Uri::factory($url)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "url" array value'
|
||||
. ' must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
if (!isset($this->data['hubs'])) {
|
||||
$this->data['hubs'] = [];
|
||||
}
|
||||
$this->data['hubs'][] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Pubsubhubbub hub endpoint URLs
|
||||
*
|
||||
* @param array $urls
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addHubs(array $urls)
|
||||
{
|
||||
foreach ($urls as $url) {
|
||||
$this->addHub($url);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a feed category
|
||||
*
|
||||
* @param array $category
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addCategory(array $category)
|
||||
{
|
||||
if (!isset($category['term'])) {
|
||||
throw new Exception\InvalidArgumentException('Each category must be an array and '
|
||||
. 'contain at least a "term" element containing the machine '
|
||||
. ' readable category name');
|
||||
}
|
||||
if (isset($category['scheme'])) {
|
||||
if (empty($category['scheme'])
|
||||
|| !is_string($category['scheme'])
|
||||
|| !Uri::factory($category['scheme'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('The Atom scheme or RSS domain of'
|
||||
. ' a category must be a valid URI');
|
||||
}
|
||||
}
|
||||
if (!isset($this->data['categories'])) {
|
||||
$this->data['categories'] = [];
|
||||
}
|
||||
$this->data['categories'][] = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of feed categories
|
||||
*
|
||||
* @param array $categories
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function addCategories(array $categories)
|
||||
{
|
||||
foreach ($categories as $category) {
|
||||
$this->addCategory($category);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single author
|
||||
*
|
||||
* @param int $index
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAuthor($index = 0)
|
||||
{
|
||||
if (isset($this->data['authors'][$index])) {
|
||||
return $this->data['authors'][$index];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (!array_key_exists('authors', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copyright entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (!array_key_exists('copyright', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed creation date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (!array_key_exists('dateCreated', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['dateCreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed modification date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (!array_key_exists('dateModified', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['dateModified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed last-build date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLastBuildDate()
|
||||
{
|
||||
if (!array_key_exists('lastBuildDate', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['lastBuildDate'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (!array_key_exists('description', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed generator entry
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerator()
|
||||
{
|
||||
if (!array_key_exists('generator', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['generator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed ID
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (!array_key_exists('id', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed image URI
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getImage()
|
||||
{
|
||||
if (!array_key_exists('image', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed language
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
if (!array_key_exists('language', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['language'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the HTML source
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
if (!array_key_exists('link', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the XML feed
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFeedLinks()
|
||||
{
|
||||
if (!array_key_exists('feedLinks', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['feedLinks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed title
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (!array_key_exists('title', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed character encoding
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
if (!array_key_exists('encoding', $this->data)) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
return $this->data['encoding'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed's base url
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBaseUrl()
|
||||
{
|
||||
if (!array_key_exists('baseUrl', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['baseUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URLs used as Pubsubhubbub hubs endpoints
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getHubs()
|
||||
{
|
||||
if (!array_key_exists('hubs', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['hubs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed categories
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (!array_key_exists('categories', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the instance and deletes all data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current feed type being exported to "rss" or "atom". This allows
|
||||
* other objects to gracefully choose whether to execute or not, depending
|
||||
* on their appropriateness for the current type, e.g. renderers.
|
||||
*
|
||||
* @param string $type
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current or last feed type exported.
|
||||
*
|
||||
* @return string Value will be "rss" or "atom"
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a specific data point
|
||||
*
|
||||
* @param string $name
|
||||
* @return AbstractFeed
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
if (isset($this->data[$name])) {
|
||||
unset($this->data[$name]);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return call_user_func_array([$extension, $method], $args);
|
||||
} catch (Exception\BadMethodCallException $e) {
|
||||
}
|
||||
}
|
||||
throw new Exception\BadMethodCallException(
|
||||
'Method: ' . $method . ' does not exist and could not be located on a registered Extension'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load extensions from Zend\Feed\Writer\Writer
|
||||
*
|
||||
* @throws Exception\RuntimeException
|
||||
* @return void
|
||||
*/
|
||||
protected function _loadExtensions()
|
||||
{
|
||||
$all = Writer::getExtensions();
|
||||
$manager = Writer::getExtensionManager();
|
||||
$exts = $all['feed'];
|
||||
foreach ($exts as $ext) {
|
||||
if (!$manager->has($ext)) {
|
||||
throw new Exception\RuntimeException(sprintf('Unable to load extension "%s"; could not resolve to class', $ext));
|
||||
}
|
||||
$this->extensions[$ext] = $manager->get($ext);
|
||||
$this->extensions[$ext]->setEncoding($this->getEncoding());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?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\Writer;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Uri;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Deleted
|
||||
{
|
||||
/**
|
||||
* Internal array containing all data associated with this entry or item.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Holds the value "atom" or "rss" depending on the feed type set when
|
||||
* when last exported.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* Set the feed character encoding
|
||||
*
|
||||
* @param $encoding
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return string|null
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
if (empty($encoding) || !is_string($encoding)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['encoding'] = $encoding;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed character encoding
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
if (!array_key_exists('encoding', $this->data)) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
return $this->data['encoding'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a specific data point
|
||||
*
|
||||
* @param string $name
|
||||
* @return Deleted
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
if (isset($this->data[$name])) {
|
||||
unset($this->data[$name]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current feed type being exported to "rss" or "atom". This allows
|
||||
* other objects to gracefully choose whether to execute or not, depending
|
||||
* on their appropriateness for the current type, e.g. renderers.
|
||||
*
|
||||
* @param string $type
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current or last feed type exported.
|
||||
*
|
||||
* @return string Value will be "rss" or "atom"
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set reference
|
||||
*
|
||||
* @param $reference
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setReference($reference)
|
||||
{
|
||||
if (empty($reference) || !is_string($reference)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: reference must be a non-empty string');
|
||||
}
|
||||
$this->data['reference'] = $reference;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getReference()
|
||||
{
|
||||
if (!array_key_exists('reference', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['reference'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set when
|
||||
*
|
||||
* @param null|string|DateTime $date
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setWhen($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp'
|
||||
. ' passed as parameter');
|
||||
}
|
||||
$this->data['when'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTime
|
||||
*/
|
||||
public function getWhen()
|
||||
{
|
||||
if (!array_key_exists('when', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['when'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set by
|
||||
*
|
||||
* @param array $by
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setBy(array $by)
|
||||
{
|
||||
$author = [];
|
||||
if (!array_key_exists('name', $by)
|
||||
|| empty($by['name'])
|
||||
|| !is_string($by['name'])
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: author array must include a'
|
||||
. ' "name" key with a non-empty string value');
|
||||
}
|
||||
$author['name'] = $by['name'];
|
||||
if (isset($by['email'])) {
|
||||
if (empty($by['email']) || !is_string($by['email'])) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "email" array'
|
||||
. ' value must be a non-empty string');
|
||||
}
|
||||
$author['email'] = $by['email'];
|
||||
}
|
||||
if (isset($by['uri'])) {
|
||||
if (empty($by['uri'])
|
||||
|| !is_string($by['uri'])
|
||||
|| !Uri::factory($by['uri'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "uri" array value must'
|
||||
. ' be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$author['uri'] = $by['uri'];
|
||||
}
|
||||
$this->data['by'] = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBy()
|
||||
{
|
||||
if (!array_key_exists('by', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['by'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comment
|
||||
* @return Deleted
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
$this->data['comment'] = $comment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
if (!array_key_exists('comment', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['comment'];
|
||||
}
|
||||
}
|
||||
+765
@@ -0,0 +1,765 @@
|
||||
<?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\Writer;
|
||||
|
||||
use DateTime;
|
||||
use Zend\Feed\Uri;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry
|
||||
{
|
||||
/**
|
||||
* Internal array containing all data associated with this entry or item.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Registered extensions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extensions = [];
|
||||
|
||||
/**
|
||||
* Holds the value "atom" or "rss" depending on the feed type set when
|
||||
* when last exported.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* Constructor: Primarily triggers the registration of core extensions and
|
||||
* loads those appropriate to this data container.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
Writer::registerCoreExtensions();
|
||||
$this->_loadExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single author
|
||||
*
|
||||
* The following option keys are supported:
|
||||
* 'name' => (string) The name
|
||||
* 'email' => (string) An optional email
|
||||
* 'uri' => (string) An optional and valid URI
|
||||
*
|
||||
* @param array $author
|
||||
* @throws Exception\InvalidArgumentException If any value of $author not follow the format.
|
||||
* @return Entry
|
||||
*/
|
||||
public function addAuthor(array $author)
|
||||
{
|
||||
// Check array values
|
||||
if (!array_key_exists('name', $author)
|
||||
|| empty($author['name'])
|
||||
|| !is_string($author['name'])
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: author array must include a "name" key with a non-empty string value');
|
||||
}
|
||||
|
||||
if (isset($author['email'])) {
|
||||
if (empty($author['email']) || !is_string($author['email'])) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: "email" array value must be a non-empty string');
|
||||
}
|
||||
}
|
||||
if (isset($author['uri'])) {
|
||||
if (empty($author['uri']) || !is_string($author['uri']) ||
|
||||
!Uri::factory($author['uri'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException(
|
||||
'Invalid parameter: "uri" array value must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
}
|
||||
|
||||
$this->data['authors'][] = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array with feed authors
|
||||
*
|
||||
* @see addAuthor
|
||||
* @param array $authors
|
||||
* @return Entry
|
||||
*/
|
||||
public function addAuthors(array $authors)
|
||||
{
|
||||
foreach ($authors as $author) {
|
||||
$this->addAuthor($author);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed character encoding
|
||||
*
|
||||
* @param string $encoding
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setEncoding($encoding)
|
||||
{
|
||||
if (empty($encoding) || !is_string($encoding)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['encoding'] = $encoding;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feed character encoding
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
if (!array_key_exists('encoding', $this->data)) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
return $this->data['encoding'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the copyright entry
|
||||
*
|
||||
* @param string $copyright
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setCopyright($copyright)
|
||||
{
|
||||
if (empty($copyright) || !is_string($copyright)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['copyright'] = $copyright;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entry's content
|
||||
*
|
||||
* @param string $content
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setContent($content)
|
||||
{
|
||||
if (empty($content) || !is_string($content)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['content'] = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed creation date
|
||||
*
|
||||
* @param null|int|DateTime $date
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setDateCreated($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp passed as parameter');
|
||||
}
|
||||
$this->data['dateCreated'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed modification date
|
||||
*
|
||||
* @param null|int|DateTime $date
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setDateModified($date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTime();
|
||||
} elseif (is_int($date)) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (!$date instanceof DateTime) {
|
||||
throw new Exception\InvalidArgumentException('Invalid DateTime object or UNIX Timestamp passed as parameter');
|
||||
}
|
||||
$this->data['dateModified'] = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed description
|
||||
*
|
||||
* @param string $description
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
if (empty($description) || !is_string($description)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['description'] = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed ID
|
||||
*
|
||||
* @param string $id
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
if (empty($id) || !is_string($id)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['id'] = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a link to the HTML source of this entry
|
||||
*
|
||||
* @param string $link
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setLink($link)
|
||||
{
|
||||
if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['link'] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of comments associated with this entry
|
||||
*
|
||||
* @param int $count
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setCommentCount($count)
|
||||
{
|
||||
if (!is_numeric($count) || (int) $count != $count || (int) $count < 0) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "count" must be a positive integer number or zero');
|
||||
}
|
||||
$this->data['commentCount'] = (int) $count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a link to a HTML page containing comments associated with this entry
|
||||
*
|
||||
* @param string $link
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setCommentLink($link)
|
||||
{
|
||||
if (empty($link) || !is_string($link) || !Uri::factory($link)->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "link" must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
$this->data['commentLink'] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a link to an XML feed for any comments associated with this entry
|
||||
*
|
||||
* @param array $link
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setCommentFeedLink(array $link)
|
||||
{
|
||||
if (!isset($link['uri']) || !is_string($link['uri']) || !Uri::factory($link['uri'])->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "link" must be a non-empty string and valid URI/IRI');
|
||||
}
|
||||
if (!isset($link['type']) || !in_array($link['type'], ['atom', 'rss', 'rdf'])) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: "type" must be one'
|
||||
. ' of "atom", "rss" or "rdf"');
|
||||
}
|
||||
if (!isset($this->data['commentFeedLinks'])) {
|
||||
$this->data['commentFeedLinks'] = [];
|
||||
}
|
||||
$this->data['commentFeedLinks'][] = $link;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a links to an XML feed for any comments associated with this entry.
|
||||
* Each link is an array with keys "uri" and "type", where type is one of:
|
||||
* "atom", "rss" or "rdf".
|
||||
*
|
||||
* @param array $links
|
||||
* @return Entry
|
||||
*/
|
||||
public function setCommentFeedLinks(array $links)
|
||||
{
|
||||
foreach ($links as $link) {
|
||||
$this->setCommentFeedLink($link);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the feed title
|
||||
*
|
||||
* @param string $title
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
if (empty($title) || !is_string($title)) {
|
||||
throw new Exception\InvalidArgumentException('Invalid parameter: parameter must be a non-empty string');
|
||||
}
|
||||
$this->data['title'] = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array with feed authors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthors()
|
||||
{
|
||||
if (!array_key_exists('authors', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['authors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if (!array_key_exists('content', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['content'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry copyright information
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCopyright()
|
||||
{
|
||||
if (!array_key_exists('copyright', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['copyright'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry creation date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
if (!array_key_exists('dateCreated', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['dateCreated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry modification date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDateModified()
|
||||
{
|
||||
if (!array_key_exists('dateModified', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['dateModified'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
if (!array_key_exists('description', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['description'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (!array_key_exists('id', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a link to the HTML source
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
if (!array_key_exists('link', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['link'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all links
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
if (!array_key_exists('links', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['links'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
if (!array_key_exists('title', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of comments/replies for current entry
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCommentCount()
|
||||
{
|
||||
if (!array_key_exists('commentCount', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['commentCount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI pointing to the HTML page where comments can be made on this entry
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentLink()
|
||||
{
|
||||
if (!array_key_exists('commentLink', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['commentLink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of URIs pointing to a feed of all comments for this entry
|
||||
* where the array keys indicate the feed type (atom, rss or rdf).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCommentFeedLinks()
|
||||
{
|
||||
if (!array_key_exists('commentFeedLinks', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['commentFeedLinks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry category
|
||||
*
|
||||
* @param array $category
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function addCategory(array $category)
|
||||
{
|
||||
if (!isset($category['term'])) {
|
||||
throw new Exception\InvalidArgumentException('Each category must be an array and '
|
||||
. 'contain at least a "term" element containing the machine '
|
||||
. ' readable category name');
|
||||
}
|
||||
if (isset($category['scheme'])) {
|
||||
if (empty($category['scheme'])
|
||||
|| !is_string($category['scheme'])
|
||||
|| !Uri::factory($category['scheme'])->isValid()
|
||||
) {
|
||||
throw new Exception\InvalidArgumentException('The Atom scheme or RSS domain of'
|
||||
. ' a category must be a valid URI');
|
||||
}
|
||||
}
|
||||
if (!isset($this->data['categories'])) {
|
||||
$this->data['categories'] = [];
|
||||
}
|
||||
$this->data['categories'][] = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of entry categories
|
||||
*
|
||||
* @param array $categories
|
||||
* @return Entry
|
||||
*/
|
||||
public function addCategories(array $categories)
|
||||
{
|
||||
foreach ($categories as $category) {
|
||||
$this->addCategory($category);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry categories
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCategories()
|
||||
{
|
||||
if (!array_key_exists('categories', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['categories'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an enclosure to the entry. The array parameter may contain the
|
||||
* keys 'uri', 'type' and 'length'. Only 'uri' is required for Atom, though the
|
||||
* others must also be provided or RSS rendering (where they are required)
|
||||
* will throw an Exception.
|
||||
*
|
||||
* @param array $enclosure
|
||||
* @throws Exception\InvalidArgumentException
|
||||
* @return Entry
|
||||
*/
|
||||
public function setEnclosure(array $enclosure)
|
||||
{
|
||||
if (!isset($enclosure['uri'])) {
|
||||
throw new Exception\InvalidArgumentException('Enclosure "uri" is not set');
|
||||
}
|
||||
if (!Uri::factory($enclosure['uri'])->isValid()) {
|
||||
throw new Exception\InvalidArgumentException('Enclosure "uri" is not a valid URI/IRI');
|
||||
}
|
||||
$this->data['enclosure'] = $enclosure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an array of all enclosures to be added to entry.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEnclosure()
|
||||
{
|
||||
if (!array_key_exists('enclosure', $this->data)) {
|
||||
return;
|
||||
}
|
||||
return $this->data['enclosure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a specific data point
|
||||
*
|
||||
* @param string $name
|
||||
* @return Entry
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
if (isset($this->data[$name])) {
|
||||
unset($this->data[$name]);
|
||||
}
|
||||
|
||||
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 object
|
||||
*/
|
||||
public function getExtension($name)
|
||||
{
|
||||
if (array_key_exists($name . '\\Entry', $this->extensions)) {
|
||||
return $this->extensions[$name . '\\Entry'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current feed type being exported to "rss" or "atom". This allows
|
||||
* other objects to gracefully choose whether to execute or not, depending
|
||||
* on their appropriateness for the current type, e.g. renderers.
|
||||
*
|
||||
* @param string $type
|
||||
* @return Entry
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current or last feed type exported.
|
||||
*
|
||||
* @return string Value will be "rss" or "atom"
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return call_user_func_array([$extension, $method], $args);
|
||||
} catch (\BadMethodCallException $e) {
|
||||
}
|
||||
}
|
||||
throw new Exception\BadMethodCallException('Method: ' . $method
|
||||
. ' does not exist and could not be located on a registered Extension');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Zend\Feed\Writer\Source data container for use. This is NOT
|
||||
* added to the current feed automatically, but is necessary to create a
|
||||
* container with some initial values preset based on the current feed data.
|
||||
*
|
||||
* @return Source
|
||||
*/
|
||||
public function createSource()
|
||||
{
|
||||
$source = new Source;
|
||||
if ($this->getEncoding()) {
|
||||
$source->setEncoding($this->getEncoding());
|
||||
}
|
||||
$source->setType($this->getType());
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a Zend\Feed\Writer\Entry object representing a new entry/item
|
||||
* the feed data container's internal group of entries.
|
||||
*
|
||||
* @param Source $source
|
||||
* @return Entry
|
||||
*/
|
||||
public function setSource(Source $source)
|
||||
{
|
||||
$this->data['source'] = $source;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if (isset($this->data['source'])) {
|
||||
return $this->data['source'];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load extensions from Zend\Feed\Writer\Writer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _loadExtensions()
|
||||
{
|
||||
$all = Writer::getExtensions();
|
||||
$manager = Writer::getExtensionManager();
|
||||
$exts = $all['entry'];
|
||||
foreach ($exts as $ext) {
|
||||
$this->extensions[$ext] = $manager->get($ext);
|
||||
$this->extensions[$ext]->setEncoding($this->getEncoding());
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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\Writer\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
/**
|
||||
* Feed exceptions
|
||||
*
|
||||
* Class to represent exceptions that occur during Feed operations.
|
||||
*/
|
||||
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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\Writer\Exception;
|
||||
|
||||
/**
|
||||
* Feed exceptions
|
||||
*
|
||||
* Interface to represent exceptions that occur during Feed operations.
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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\Writer\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
/**
|
||||
* Feed exceptions
|
||||
*
|
||||
* Class to represent exceptions that occur during Feed operations.
|
||||
*/
|
||||
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\Writer\Exception;
|
||||
|
||||
use Zend\Feed\Exception;
|
||||
|
||||
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?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\Writer\Extension;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
*/
|
||||
abstract class AbstractRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @var DOMDocument
|
||||
*/
|
||||
protected $dom = null;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $entry = null;
|
||||
|
||||
/**
|
||||
* @var DOMElement
|
||||
*/
|
||||
protected $base = null;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $container = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* @var DOMElement
|
||||
*/
|
||||
protected $rootElement = null;
|
||||
|
||||
/**
|
||||
* Encoding of all text values
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $encoding = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Set the data container
|
||||
*
|
||||
* @param mixed $container
|
||||
* @return AbstractRenderer
|
||||
*/
|
||||
public function setDataContainer($container)
|
||||
{
|
||||
$this->container = $container;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed encoding
|
||||
*
|
||||
* @param string $enc
|
||||
* @return AbstractRenderer
|
||||
*/
|
||||
public function setEncoding($enc)
|
||||
{
|
||||
$this->encoding = $enc;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feed encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set DOMDocument and DOMElement on which to operate
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $base
|
||||
* @return AbstractRenderer
|
||||
*/
|
||||
public function setDomDocument(DOMDocument $dom, DOMElement $base)
|
||||
{
|
||||
$this->dom = $dom;
|
||||
$this->base = $base;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data container being rendered
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDataContainer()
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed type
|
||||
*
|
||||
* @param string $type
|
||||
* @return AbstractRenderer
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedtype
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set root element of document
|
||||
*
|
||||
* @param DOMElement $root
|
||||
* @return AbstractRenderer
|
||||
*/
|
||||
public function setRootElement(DOMElement $root)
|
||||
{
|
||||
$this->rootElement = $root;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get root element
|
||||
*
|
||||
* @return DOMElement
|
||||
*/
|
||||
public function getRootElement()
|
||||
{
|
||||
return $this->rootElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append namespaces to feed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function _appendNamespaces();
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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\Writer\Extension\Atom\Renderer;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Zend\Feed\Writer\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Feed extends Extension\AbstractRenderer
|
||||
{
|
||||
/**
|
||||
* Set to TRUE if a rendering method actually renders something. This
|
||||
* is used to prevent premature appending of a XML namespace declaration
|
||||
* until an element which requires it is actually appended.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $called = false;
|
||||
|
||||
/**
|
||||
* Render feed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
/**
|
||||
* RSS 2.0 only. Used mainly to include Atom links and
|
||||
* Pubsubhubbub Hub endpoint URIs under the Atom namespace
|
||||
*/
|
||||
if (strtolower($this->getType()) == 'atom') {
|
||||
return;
|
||||
}
|
||||
$this->_setFeedLinks($this->dom, $this->base);
|
||||
$this->_setHubs($this->dom, $this->base);
|
||||
if ($this->called) {
|
||||
$this->_appendNamespaces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append namespaces to root element of feed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _appendNamespaces()
|
||||
{
|
||||
$this->getRootElement()->setAttribute('xmlns:atom',
|
||||
'http://www.w3.org/2005/Atom');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed link elements
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $root
|
||||
* @return void
|
||||
*/
|
||||
protected function _setFeedLinks(DOMDocument $dom, DOMElement $root)
|
||||
{
|
||||
$flinks = $this->getDataContainer()->getFeedLinks();
|
||||
if (!$flinks || empty($flinks)) {
|
||||
return;
|
||||
}
|
||||
foreach ($flinks as $type => $href) {
|
||||
if (strtolower($type) == $this->getType()) { // issue 2605
|
||||
$mime = 'application/' . strtolower($type) . '+xml';
|
||||
$flink = $dom->createElement('atom:link');
|
||||
$root->appendChild($flink);
|
||||
$flink->setAttribute('rel', 'self');
|
||||
$flink->setAttribute('type', $mime);
|
||||
$flink->setAttribute('href', $href);
|
||||
}
|
||||
}
|
||||
$this->called = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set PuSH hubs
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $root
|
||||
* @return void
|
||||
*/
|
||||
protected function _setHubs(DOMDocument $dom, DOMElement $root)
|
||||
{
|
||||
$hubs = $this->getDataContainer()->getHubs();
|
||||
if (!$hubs || empty($hubs)) {
|
||||
return;
|
||||
}
|
||||
foreach ($hubs as $hubUrl) {
|
||||
$hub = $dom->createElement('atom:link');
|
||||
$hub->setAttribute('rel', 'hub');
|
||||
$hub->setAttribute('href', $hubUrl);
|
||||
$root->appendChild($hub);
|
||||
}
|
||||
$this->called = true;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?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\Writer\Extension\Content\Renderer;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Zend\Feed\Writer\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractRenderer
|
||||
{
|
||||
/**
|
||||
* Set to TRUE if a rendering method actually renders something. This
|
||||
* is used to prevent premature appending of a XML namespace declaration
|
||||
* until an element which requires it is actually appended.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $called = false;
|
||||
|
||||
/**
|
||||
* Render entry
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (strtolower($this->getType()) == 'atom') {
|
||||
return;
|
||||
}
|
||||
$this->_setContent($this->dom, $this->base);
|
||||
if ($this->called) {
|
||||
$this->_appendNamespaces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append namespaces to root element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _appendNamespaces()
|
||||
{
|
||||
$this->getRootElement()->setAttribute('xmlns:content',
|
||||
'http://purl.org/rss/1.0/modules/content/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry content
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $root
|
||||
* @return void
|
||||
*/
|
||||
protected function _setContent(DOMDocument $dom, DOMElement $root)
|
||||
{
|
||||
$content = $this->getDataContainer()->getContent();
|
||||
if (!$content) {
|
||||
return;
|
||||
}
|
||||
$element = $dom->createElement('content:encoded');
|
||||
$root->appendChild($element);
|
||||
$cdata = $dom->createCDATASection($content);
|
||||
$element->appendChild($cdata);
|
||||
$this->called = true;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?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\Writer\Extension\DublinCore\Renderer;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Zend\Feed\Writer\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry extends Extension\AbstractRenderer
|
||||
{
|
||||
/**
|
||||
* Set to TRUE if a rendering method actually renders something. This
|
||||
* is used to prevent premature appending of a XML namespace declaration
|
||||
* until an element which requires it is actually appended.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $called = false;
|
||||
|
||||
/**
|
||||
* Render entry
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (strtolower($this->getType()) == 'atom') {
|
||||
return;
|
||||
}
|
||||
$this->_setAuthors($this->dom, $this->base);
|
||||
if ($this->called) {
|
||||
$this->_appendNamespaces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append namespaces to entry
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _appendNamespaces()
|
||||
{
|
||||
$this->getRootElement()->setAttribute('xmlns:dc',
|
||||
'http://purl.org/dc/elements/1.1/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry author elements
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $root
|
||||
* @return void
|
||||
*/
|
||||
protected function _setAuthors(DOMDocument $dom, DOMElement $root)
|
||||
{
|
||||
$authors = $this->getDataContainer()->getAuthors();
|
||||
if (!$authors || empty($authors)) {
|
||||
return;
|
||||
}
|
||||
foreach ($authors as $data) {
|
||||
$author = $this->dom->createElement('dc:creator');
|
||||
if (array_key_exists('name', $data)) {
|
||||
$text = $dom->createTextNode($data['name']);
|
||||
$author->appendChild($text);
|
||||
$root->appendChild($author);
|
||||
}
|
||||
}
|
||||
$this->called = true;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?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\Writer\Extension\DublinCore\Renderer;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Zend\Feed\Writer\Extension;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Feed extends Extension\AbstractRenderer
|
||||
{
|
||||
/**
|
||||
* Set to TRUE if a rendering method actually renders something. This
|
||||
* is used to prevent premature appending of a XML namespace declaration
|
||||
* until an element which requires it is actually appended.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $called = false;
|
||||
|
||||
/**
|
||||
* Render feed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
if (strtolower($this->getType()) == 'atom') {
|
||||
return;
|
||||
}
|
||||
$this->_setAuthors($this->dom, $this->base);
|
||||
if ($this->called) {
|
||||
$this->_appendNamespaces();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append namespaces to feed element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _appendNamespaces()
|
||||
{
|
||||
$this->getRootElement()->setAttribute('xmlns:dc',
|
||||
'http://purl.org/dc/elements/1.1/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed authors
|
||||
*
|
||||
* @param DOMDocument $dom
|
||||
* @param DOMElement $root
|
||||
* @return void
|
||||
*/
|
||||
protected function _setAuthors(DOMDocument $dom, DOMElement $root)
|
||||
{
|
||||
$authors = $this->getDataContainer()->getAuthors();
|
||||
if (!$authors || empty($authors)) {
|
||||
return;
|
||||
}
|
||||
foreach ($authors as $data) {
|
||||
$author = $this->dom->createElement('dc:creator');
|
||||
if (array_key_exists('name', $data)) {
|
||||
$text = $dom->createTextNode($data['name']);
|
||||
$author->appendChild($text);
|
||||
$root->appendChild($author);
|
||||
}
|
||||
}
|
||||
$this->called = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?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\Writer\Extension\ITunes;
|
||||
|
||||
use Zend\Feed\Writer;
|
||||
use Zend\Feed\Writer\Extension;
|
||||
use Zend\Stdlib\StringUtils;
|
||||
use Zend\Stdlib\StringWrapper\StringWrapperInterface;
|
||||
|
||||
/**
|
||||
*/
|
||||
class Entry
|
||||
{
|
||||
/**
|
||||
* Array of Feed data for rendering by Extension's renderers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Encoding of all text values
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $encoding = 'UTF-8';
|
||||
|
||||
/**
|
||||
* The used string wrapper supporting encoding
|
||||
*
|
||||
* @var StringWrapperInterface
|
||||
*/
|
||||
protected $stringWrapper;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->stringWrapper = StringUtils::getWrapper($this->encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set feed encoding
|
||||
*
|
||||
* @param string $enc
|
||||
* @return Entry
|
||||
*/
|
||||
public function setEncoding($enc)
|
||||
{
|
||||
$this->stringWrapper = StringUtils::getWrapper($enc);
|
||||
$this->encoding = $enc;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feed encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEncoding()
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a block value of "yes" or "no". You may also set an empty string.
|
||||
*
|
||||
* @param string
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesBlock($value)
|
||||
{
|
||||
if (!ctype_alpha($value) && strlen($value) > 0) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only'
|
||||
. ' contain alphabetic characters');
|
||||
}
|
||||
|
||||
if ($this->stringWrapper->strlen($value) > 255) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "block" may only'
|
||||
. ' contain a maximum of 255 characters');
|
||||
}
|
||||
$this->data['block'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add authors to itunes entry
|
||||
*
|
||||
* @param array $values
|
||||
* @return Entry
|
||||
*/
|
||||
public function addItunesAuthors(array $values)
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
$this->addItunesAuthor($value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add author to itunes entry
|
||||
*
|
||||
* @param string $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function addItunesAuthor($value)
|
||||
{
|
||||
if ($this->stringWrapper->strlen($value) > 255) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: any "author" may only'
|
||||
. ' contain a maximum of 255 characters each');
|
||||
}
|
||||
if (!isset($this->data['authors'])) {
|
||||
$this->data['authors'] = [];
|
||||
}
|
||||
$this->data['authors'][] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set duration
|
||||
*
|
||||
* @param int $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesDuration($value)
|
||||
{
|
||||
$value = (string) $value;
|
||||
if (!ctype_digit($value)
|
||||
&& !preg_match("/^\d+:[0-5]{1}[0-9]{1}$/", $value)
|
||||
&& !preg_match("/^\d+:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/", $value)
|
||||
) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "duration" may only'
|
||||
. ' be of a specified [[HH:]MM:]SS format');
|
||||
}
|
||||
$this->data['duration'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "explicit" flag
|
||||
*
|
||||
* @param bool $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesExplicit($value)
|
||||
{
|
||||
if (!in_array($value, ['yes', 'no', 'clean'])) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "explicit" may only'
|
||||
. ' be one of "yes", "no" or "clean"');
|
||||
}
|
||||
$this->data['explicit'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set keywords
|
||||
*
|
||||
* @param array $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesKeywords(array $value)
|
||||
{
|
||||
if (count($value) > 12) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only'
|
||||
. ' contain a maximum of 12 terms');
|
||||
}
|
||||
|
||||
$concat = implode(',', $value);
|
||||
if ($this->stringWrapper->strlen($concat) > 255) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "keywords" may only'
|
||||
. ' have a concatenated length of 255 chars where terms are delimited'
|
||||
. ' by a comma');
|
||||
}
|
||||
$this->data['keywords'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set subtitle
|
||||
*
|
||||
* @param string $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesSubtitle($value)
|
||||
{
|
||||
if ($this->stringWrapper->strlen($value) > 255) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "subtitle" may only'
|
||||
. ' contain a maximum of 255 characters');
|
||||
}
|
||||
$this->data['subtitle'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set summary
|
||||
*
|
||||
* @param string $value
|
||||
* @return Entry
|
||||
* @throws Writer\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function setItunesSummary($value)
|
||||
{
|
||||
if ($this->stringWrapper->strlen($value) > 4000) {
|
||||
throw new Writer\Exception\InvalidArgumentException('invalid parameter: "summary" may only'
|
||||
. ' contain a maximum of 4000 characters');
|
||||
}
|
||||
$this->data['summary'] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloading to itunes specific setters
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $params
|
||||
* @throws Writer\Exception\BadMethodCallException
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, array $params)
|
||||
{
|
||||
$point = lcfirst(substr($method, 9));
|
||||
if (!method_exists($this, 'setItunes' . ucfirst($point))
|
||||
&& !method_exists($this, 'addItunes' . ucfirst($point))
|
||||
) {
|
||||
throw new Writer\Exception\BadMethodCallException(
|
||||
'invalid method: ' . $method
|
||||
);
|
||||
}
|
||||
if (!array_key_exists($point, $this->data)
|
||||
|| empty($this->data[$point])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return $this->data[$point];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user