default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions
@@ -0,0 +1,8 @@
{
"extensions": [
"tidy"
],
"ignore_php_platform_requirements": {
"8.1": true
}
}
@@ -0,0 +1,639 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 2.15.0 - 2021-09-20
-----
### Release Notes for [2.15.0](https://github.com/laminas/laminas-feed/milestone/10)
Feature release (minor)
### 2.15.0
- Total issues resolved: **0**
- Total pull requests resolved: **2**
- Total contributors: **2**
#### Enhancement
- [42: Provide PHP 8.1 support](https://github.com/laminas/laminas-feed/pull/42) thanks to @weierophinney
#### Duplicate,Enhancement
- [39: Remove file headers](https://github.com/laminas/laminas-feed/pull/39) thanks to @ghostwriter
## 2.14.0 - 2021-03-16
-----
### Release Notes for [2.14.0](https://github.com/laminas/laminas-feed/milestone/7)
### Added
- Adds a new `PodcastIndex` extension to each of the Reader and Writer subcomponents, allowing users to both create and consume [xmlns:podcast feeds](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md). Please [read the documentation for more details](https://docs.laminas.dev/laminas-feed/extensions/podcast-index/).
### 2.14.0
- Total issues resolved: **0**
- Total pull requests resolved: **2**
- Total contributors: **2**
#### Enhancement
- [35: Switch from Travis-CI to GitHub Actions](https://github.com/laminas/laminas-feed/pull/35) thanks to @weierophinney
- [31: Create PodcastIndex extension](https://github.com/laminas/laminas-feed/pull/31) thanks to @codedmonkey
## 2.13.1 - 2021-01-04
### Fixed
- [#33](https://github.com/laminas/laminas-feed/pull/33) fixes an issue whereby parsing a feed entry date that is in an incorrect format was incorrectly causing a `null` value to be returned for the date, rather than correctly throwing an exception.
-----
### Release Notes for [2.13.1](https://github.com/laminas/laminas-feed/milestone/6)
2.13.x bugfix release (patch)
### 2.13.1
- Total issues resolved: **1**
- Total pull requests resolved: **1**
- Total contributors: **2**
#### Bug
- [33: Fixes 32 - Method getDateModified of RSS reader doesn't iterate over different formats](https://github.com/laminas/laminas-feed/pull/33) thanks to @froschdesign
- [32: Method getDateModified of RSS reader doesn't iterate over different formats](https://github.com/laminas/laminas-feed/issues/32) thanks to @Klaasie
## 2.13.0 - 2020-11-18
### Added
- [#28](https://github.com/laminas/laminas-feed/pull/28) Adds Psalm as QA tool
- [#29](https://github.com/laminas/laminas-feed/pull/29) Adds PHP 8.0 support
### Removed
- [#29](https://github.com/laminas/laminas-feed/pull/29) Dropped support for laminas-servicemanager less than v3.3
-----
### Release Notes for [2.13.0](https://github.com/laminas/laminas-feed/milestone/2)
next feature release (minor)
### 2.13.0
- Total issues resolved: **0**
- Total pull requests resolved: **2**
- Total contributors: **1**
#### Enhancement,hacktoberfest-accepted
- [29: Update to support PHP 8.0](https://github.com/laminas/laminas-feed/pull/29) thanks to @ocean
- [28: Add Psalm integration](https://github.com/laminas/laminas-feed/pull/28) thanks to @ocean
## 2.12.3 - 2020-08-18
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#21](https://github.com/laminas/laminas-feed/pull/21) fixes the writer extension
of iTunes to support valid values for the `itunes:explicit` element.
## 2.12.2 - 2020-03-29
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Fixed `replace` version constraint in composer.json so repository can be used as replacement of `zendframework/zend-feed:^2.12.0`.
## 2.12.1 - 2020-03-23
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#17](https://github.com/laminas/laminas-feed/pull/17) fixes regular expression to extract content from atom feeds.
## 2.12.0 - 2019-03-05
### Added
- [zendframework/zend-feed#96](https://github.com/zendframework/zend-feed/pull/96) adds the methods `Laminas\Feed\Reader\Extension\Podcast\Entry::getTitle() : string`
and `Laminas\Feed\Writer\Extension\ITunes\Entry::setTitle(string $value)`; these
provide the ability to read and manipulate `<itunes:title>` tags in feeds.
### Changed
- Nothing.
### Deprecated
- [zendframework/zend-feed#101](https://github.com/zendframework/zend-feed/pull/101) deprecates the method `Laminas\Feed\Writer\Writer::lcfirst()`; use the PHP
built-in function instead.
- [zendframework/zend-feed#97](https://github.com/zendframework/zend-feed/pull/97) deprecates the classes `Laminas\Feed\Reader\AbstractEntry` (use
`Laminas\Feed\Reader\Entry\AbstractEntry` instead), `Laminas\Feed\Reader\AbstractFeed` (use `Laminas\Feed\Reader\Feed\AbstractFeed` instead), and
`Laminas\Feed\Reader\Collection` (use Laminas\Feed\Reader\Collection\Author`, `Laminas\Feed\Reader\Collection\Category`, or
`Laminas\Feed\Reader\Collection\Collection` instead, based on context).
### Removed
- Nothing.
### Fixed
- Nothing.
## 2.11.1 - 2019-03-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#99](https://github.com/zendframework/zend-feed/pull/99) provides a fix to `Laminas\Feed\Writer\Renderer\Entry\Rss` to ensure that
relative URIs provided for the feed disable the `isPermalink` flag.
- [zendframework/zend-feed#100](https://github.com/zendframework/zend-feed/pull/100) fixes parameter and return value annotations for a number of classes to
specify the correct types.
## 2.11.0 - 2019-01-29
### Added
- [zendframework/zend-feed#94](https://github.com/zendframework/zend-feed/pull/94) adds support for PHP 7.3.
- [zendframework/zend-feed#91](https://github.com/zendframework/zend-feed/pull/91) adds explicit requirements for both ext-dom and ext-libxml to the package.
### Changed
- [zendframework/zend-feed#93](https://github.com/zendframework/zend-feed/pull/93) `Writer\Feed`, `Writer\Entry` and `Writer\Deleted` all now accept
`DateTimeImmutable` instances as an arguments to methods that previously only
accepted `DateTime` or Unix Timestamps, such as `Writer\Feed::setDateModified()`.
### Deprecated
- Nothing.
### Removed
- [zendframework/zend-feed#94](https://github.com/zendframework/zend-feed/pull/94) removes support for laminas-stdlib v2 releases.
### Fixed
- Nothing.
## 2.10.3 - 2018-08-01
### Added
- Nothing.
### Changed
- This release modifies how `Laminas\Feed\Pubsubhubbub\AbstractCallback::_detectCallbackUrl()`
marshals the request URI. In prior releases, we would attempt to inspect the
`X-Rewrite-Url` and `X-Original-Url` headers, using their values, if present.
These headers are issued by the ISAPI_Rewrite module for IIS (developed by
HeliconTech). However, we have no way of guaranteeing that the module is what
issued the headers, making it an unreliable source for discovering the URI. As
such, we have removed this feature in this release.
The method is not called internally. If you are calling the method from your
own extension and need support for ISAPI_Rewrite, you will need to override
the method as follows:
```php
protected function _detectCallbackUrl()
{
$callbackUrl = null;
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_REWRITE_URL'];
}
if (isset($_SERVER['HTTP_X_ORIGINAL_URL'])) {
$callbackUrl = $_SERVER['HTTP_X_ORIGINAL_URL'];
}
return $callbackUrl ?: parent::__detectCallbackUrl();
}
```
If you use an approach such as the above, make sure you also instruct your web
server to strip any incoming headers of the same name so that you can
guarantee they are issued by the ISAPI_Rewrite module.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 2.10.2 - 2018-06-18
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#81](https://github.com/zendframework/zend-feed/pull/81) updates the `Laminas\Feed\Reader\Reader` and `Laminas\Feed\Writer\Writer` classes to
conditionally register their respective "GooglePlayPodcast" extensions only if
their extension managers are aware of it. This is done due to the fact that
existing `ExtensionManagerInterface` implementations may not register it by
default as the extension did not exist in releases prior to 2.10.0. By having
the registration conditional, we prevent an exception from being raised; users
are not impacted by its absence, as the extension features were not exposed
previously.
Both `Reader` and `Writer` emit an `E_USER_NOTICE` when the extension is not
found in the extension manager, indicating that the
`ExtensionManagerInterface` implementation should be updated to add entries
for the "GooglePlayPodcast" entry, feed, and/or renderer classes.
## 2.10.1 - 2018-06-05
### Added
- Nothing.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#79](https://github.com/zendframework/zend-feed/pull/79) fixes an issue in the `setType()` method of the iTunes feed renderer whereby it was setting
the DOM content with an uninitialized variable.
## 2.10.0 - 2018-05-24
### Added
- [zendframework/zend-feed#78](https://github.com/zendframework/zend-feed/pull/78) adds support for the Google Play Podcasts 1.0 DTD in both the Reader and
Writer subcomponents. The following new classes provide the support:
- `Laminas\Feed\Reader\Extension\GooglePlayPodcast\Entry`
- `Laminas\Feed\Reader\Extension\GooglePlayPodcast\Feed`
- `Laminas\Feed\Writer\Extension\GooglePlayPodcast\Entry`
- `Laminas\Feed\Writer\Extension\GooglePlayPodcast\Feed`
- `Laminas\Feed\Writer\Extension\GooglePlayPodcast\Renderer\Entry`
- `Laminas\Feed\Writer\Extension\GooglePlayPodcast\Renderer\Feed`
The extensions are registered by default with both `Laminas\Feed\Reader\Reader`
and `Laminas\Feed\Writer\Writer`.
- [zendframework/zend-feed#77](https://github.com/zendframework/zend-feed/pull/77) adds support for `itunes:image` for each of:
- `Laminas\Feed\Reader\Extension\Podcast\Entry`, via `getItunesImage()`; previously only the `Feed` supported it.
- `Laminas\Feed\Writer\Extension\ITunes\Entry`, via `setItunesImage()`; previously only the `Feed` supported it.
- `Laminas\Feed\Writer\Extension\ITunes\Renderer\Entry`; previously on the `Feed` supported it.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Entry::setItunesSeason()`, corresponding to the
`itunes:season` tag, and allowing setting the season number of the episode the
entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Entry::setItunesIsClosedCaptioned()`, corresponding to the
`itunes:isClosedCaptioned` tag, and allowing setting the status of closed
captioning support in the episode the entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Entry::setItunesEpisodeType()`, corresponding to the
`itunes:episodeType` tag, and allowing setting the type of episode the entry represents
(one of "full", "trailer", or "bonus", and defaulting to "full").
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Entry::setEpisode()`, corresponding to the
`itunes:episode` tag, and allowing setting the number of the episode the entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Feed::setItunesComplete()`, corresponding to the
`itunes:complete` tag. It allows setting a boolean flag, indicating whether or not the
podcast is complete (will not air new episodes).
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Writer\Extension\ITunes\Feed::setItunesType()`, corresponding to the
`itunes:type` tag, and allowing setting the podcast type (one of "serial" or "episodic").
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Entry::getEpisodeType()`, corresponding to the
`itunes:episodeType` tag, and returning the type of episode the entry represents
(one of "full", "trailer", or "bonus", and defaulting to "full").
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Entry::getSeason()`, corresponding to the
`itunes:season` tag, and returning the season number of the episode the entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Entry::isClsoedCaptioned()`, corresponding to the
`itunes:isClosedCaptioned` tag, and returning the status of closed captioning
in the episode the entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Entry::getEpisode()`, corresponding to the
`itunes:episode` tag, and returning the number of the episode the entry represents.
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Feed::isComplete()`, corresponding to the
`itunes:complete` tag. It returns a boolean, indicating whether or not the podcast is
complete (will not air new episodes).
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) adds `Laminas\Feed\Reader\Extension\Podcast\Feed::getPodcastType()`, corresponding to the
`itunes:type` tag, and providing the podcast type (one of "serial" or "episodic", defaulting
to the latter).
### Changed
- [zendframework/zend-feed#77](https://github.com/zendframework/zend-feed/pull/77) updates URI validation for `Laminas\Feed\Writer\Extension\ITunes\Feed::setItunesImage()` to
first check that we have received a string value before proceeding.
### Deprecated
- [zendframework/zend-feed#75](https://github.com/zendframework/zend-feed/pull/75) deprecates each of:
- `Laminas\Feed\Reader\Extension\Podcast\Entry::getKeywords()`
- `Laminas\Feed\Reader\Extension\Podcast\Feed::getKeywords()`
- `Laminas\Feed\Writer\Extension\ITunes\Entry::setKeywords()`
- `Laminas\Feed\Writer\Extension\ITunes\Feed::setKeywords()`
as the iTunes Podcast RSS specification no longer supports keywords.
### Removed
- Nothing.
### Fixed
- Nothing.
## 2.9.1 - 2018-05-14
### Added
- Nothing.
### Changed
- [zendframework/zend-feed#16](https://github.com/zendframework/zend-feed/pull/16) updates the `Laminas\Feed\Pubsubhubbub\AbstractCallback` to no longer use the
`$GLOBALS['HTTP_RAW_POST_DATA']` value as a fallback when `php://input` is
empty. The fallback existed because, prior to PHP 5.6, `php://input` could
only be read once. As we now require PHP 5.6, the fallback is unnecessary,
and best removed as the globals value is deprecated.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#68](https://github.com/zendframework/zend-feed/pull/68) fixes the behavior of `Laminas\Feed\Writer\AbstractFeed::setTitle()` and
`Laminas\Feed\Writer\Entry::setTitle()` to accept the string `"0"`.
- [zendframework/zend-feed#68](https://github.com/zendframework/zend-feed/pull/68) updates both `Laminas\Feed\Writer\AbstractFeed` and `Laminas\Feed\Writer\Entry`
to no longer throw an exception for entry titles which have a string value of `0`.
## 2.9.0 - 2017-12-04
### Added
- [zendframework/zend-feed#52](https://github.com/zendframework/zend-feed/pull/52) adds support for PHP
7.2
- [zendframework/zend-feed#53](https://github.com/zendframework/zend-feed/pull/53) adds a number of
additional aliases to the `Writer\ExtensionPluginManager` to ensure plugins
will be pulled as expected.
- [zendframework/zend-feed#63](https://github.com/zendframework/zend-feed/pull/63) adds the feed title
to the attributes incorporated in the `FeedSet` instance, per what was already
documented.
- [zendframework/zend-feed#55](https://github.com/zendframework/zend-feed/pull/55) makes two API
additions to the `StandaloneExtensionManager` implementations of both the reader
and writer subcomponents:
- `$manager->add($name, $class)` will add an extension class using the
provided name.
- `$manager->remove($name)` will remove an existing extension by the provided
name.
### Changed
- Nothing.
### Deprecated
- Nothing.
### Removed
- [zendframework/zend-feed#52](https://github.com/zendframework/zend-feed/pull/52) removes support for
HHVM.
### Fixed
- [zendframework/zend-feed#50](https://github.com/zendframework/zend-feed/pull/50) fixes a few issues
in the PubSubHubbub `Subscription` model where counting was being performed on
uncountable data; this ensures the subcomponent will work correctly under PHP
7.2.
## 2.8.0 - 2017-04-02
### Added
- [zendframework/zend-feed#27](https://github.com/zendframework/zend-feed/pull/27) adds a documentation
chapter demonstrating wrapping a PSR-7 client to use with `Laminas\Feed\Reader`.
- [zendframework/zend-feed#22](https://github.com/zendframework/zend-feed/pull/22) adds missing
ExtensionManagerInterface on Writer\ExtensionPluginManager.
- [zendframework/zend-feed#32](https://github.com/zendframework/zend-feed/pull/32) adds missing
ExtensionManagerInterface on Reader\ExtensionPluginManager.
### Deprecated
- Nothing.
### Removed
- [zendframework/zend-feed#38](https://github.com/zendframework/zend-feed/pull/38) dropped php 5.5
support
### Fixed
- [zendframework/zend-feed#35](https://github.com/zendframework/zend-feed/pull/35) fixed
"A non-numeric value encountered" in php 7.1
- [zendframework/zend-feed#39](https://github.com/zendframework/zend-feed/pull/39) fixed protocol
relative link absolutisation
- [zendframework/zend-feed#40](https://github.com/zendframework/zend-feed/pull/40) fixed service
manager v3 compatibility aliases in extension plugin managers
## 2.7.0 - 2016-02-11
### Added
- [zendframework/zend-feed#21](https://github.com/zendframework/zend-feed/pull/21) edits, revises, and
prepares the documentation for publication at https://docs.laminas.dev/laminas-feed/
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#20](https://github.com/zendframework/zend-feed/pull/20) makes the two
laminas-servicemanager extension manager implementations forwards compatible
with version 3, and the overall code base forwards compatible with laminas-stdlib
v3.
## 2.6.0 - 2015-11-24
### Added
- [zendframework/zend-feed#13](https://github.com/zendframework/zend-feed/pull/13) introduces
`Laminas\Feed\Writer\StandaloneExtensionManager`, an implementation of
`Laminas\Feed\Writer\ExtensionManagerInterface` that has no dependencies.
`Laminas\Feed\Writer\ExtensionManager` now composes this by default, instead of
`Laminas\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.
- [zendframework/zend-feed#14](https://github.com/zendframework/zend-feed/pull/14) introduces:
- `Laminas\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).
- `Laminas\Feed\Reader\Http\HeaderAwareResponseInterface`, which extends
`ResponseInterface` and adds the method `getHeader($name, $default = null)`.
Clients may return either a `ResponseInterface` or
`HeaderAwareResponseInterface` instance.
- `Laminas\Feed\Reader\Http\Response`, which is an implementation of
`HeaderAwareResponseInterface`. Its constructor accepts the status code,
body, and, optionally, headers.
- `Laminas\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.
- `Laminas\Feed\Reader\Http\LaminasHttpClientDecorator`, which decorates a
`Laminas\Http\Client` instance, implements `HeaderAwareClientInterface`, and
returns a `Response` instance seeded from the laminas-http response upon
calling `get()`. The class exposes a `getDecoratedClient()` method to allow
retrieval of the decorated laminas-http client instance.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [zendframework/zend-feed#5](https://github.com/zendframework/zend-feed/pull/5) fixes the enclosure
length check to allow zero and integer strings.
- [zendframework/zend-feed#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.
- [zendframework/zend-feed#14](https://github.com/zendframework/zend-feed/pull/14) makes the following
changes to fix behavior around HTTP clients used within
`Laminas\Feed\Reader\Reader`:
- `setHttpClient()` now ensures that the passed client is either a
`Laminas\Feed\Reader\Http\ClientInterface` or `Laminas\Http\Client`, raising an
`InvalidArgumentException` if neither. If a `Laminas\Http\Client` is passed, it
is passed to the constructor of `Laminas\Feed\Reader\Http\LaminasHttpClientDecorator`,
and the decorator instance is used.
- `getHttpClient()` now *always* returns a `Laminas\Feed\Reader\Http\ClientInterface`
instance. If no instance is currently registered, it lazy loads a
`LaminasHttpClientDecorator` 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`.
@@ -0,0 +1 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)
@@ -0,0 +1,26 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
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 Laminas Foundation 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.
+12
View File
@@ -0,0 +1,12 @@
# laminas-feed
[![Build Status](https://travis-ci.com/laminas/laminas-feed.svg?branch=master)](https://travis-ci.com/laminas/laminas-feed)
[![Coverage Status](https://coveralls.io/repos/github/laminas/laminas-feed/badge.svg?branch=master)](https://coveralls.io/github/laminas/laminas-feed?branch=master)
`Laminas\Feed` provides functionality for consuming RSS and Atom feeds. It provides
a natural syntax for accessing elements of feeds, feed attributes, and entry
attributes. `Laminas\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/laminas/laminas-feed/issues
- Documentation is at https://docs.laminas.dev/laminas-feed/
@@ -0,0 +1,75 @@
{
"name": "laminas/laminas-feed",
"description": "provides functionality for consuming RSS and Atom feeds",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"feed"
],
"homepage": "https://laminas.dev",
"support": {
"docs": "https://docs.laminas.dev/laminas-feed/",
"issues": "https://github.com/laminas/laminas-feed/issues",
"source": "https://github.com/laminas/laminas-feed",
"rss": "https://github.com/laminas/laminas-feed/releases.atom",
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev"
},
"config": {
"sort-packages": true
},
"extra": {
},
"require": {
"php": "^7.3 || ~8.0.0 || ~8.1.0",
"ext-dom": "*",
"ext-libxml": "*",
"laminas/laminas-escaper": "^2.9",
"laminas/laminas-stdlib": "^3.6"
},
"require-dev": {
"laminas/laminas-cache": "^2.7.2",
"laminas/laminas-coding-standard": "~2.2.1",
"laminas/laminas-db": "^2.13.3",
"laminas/laminas-http": "^2.15",
"laminas/laminas-servicemanager": "^3.7",
"laminas/laminas-validator": "^2.15",
"phpunit/phpunit": "^9.5.5",
"psalm/plugin-phpunit": "^0.13.0",
"psr/http-message": "^1.0.1",
"vimeo/psalm": "^4.1"
},
"conflict": {
"laminas/laminas-servicemanager": "<3.3",
"zendframework/zend-feed": "*"
},
"suggest": {
"laminas/laminas-cache": "Laminas\\Cache component, for optionally caching feeds between requests",
"laminas/laminas-db": "Laminas\\Db component, for use with PubSubHubbub",
"laminas/laminas-http": "Laminas\\Http for PubSubHubbub, and optionally for use with Laminas\\Feed\\Reader",
"laminas/laminas-servicemanager": "Laminas\\ServiceManager component, for easily extending ExtensionManager implementations",
"laminas/laminas-validator": "Laminas\\Validator component, for validating email addresses used in Atom feeds and entries when using the Writer subcomponent",
"psr/http-message": "PSR-7 ^1.0.1, if you wish to use Laminas\\Feed\\Reader\\Http\\Psr7ResponseDecorator"
},
"autoload": {
"psr-4": {
"Laminas\\Feed\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Feed\\": "test/"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"static-analysis": "psalm --shepherd --stats",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<ruleset
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd">
<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>
<!-- Show progress -->
<arg value="p"/>
<!-- Paths to check -->
<file>src</file>
<file>test</file>
<!-- Include all rules from Laminas Coding Standard -->
<rule ref="LaminasCodingStandard"/>
</ruleset>
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<psalm
cacheDirectory="./.psalm-cache"
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src"/>
<directory name="test"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<InternalMethod>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::method"/>
</errorLevel>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::willReturn"/>
</errorLevel>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::with"/>
</errorLevel>
</InternalMethod>
</issueHandlers>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>
@@ -0,0 +1,7 @@
<?php
namespace Laminas\Feed\Exception;
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Laminas\Feed\Exception;
interface ExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Laminas\Feed\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
namespace Laminas\Feed\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,357 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use Laminas\Http\PhpEnvironment\Response as PhpResponse;
use Laminas\Stdlib\ArrayUtils;
use Traversable;
use function array_key_exists;
use function file_get_contents;
use function function_exists;
use function gettype;
use function intval;
use function is_array;
use function is_resource;
use function sprintf;
use function str_replace;
use function stream_get_contents;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
use function trim;
abstract class AbstractCallback implements CallbackInterface
{
/**
* An instance of Laminas\Feed\Pubsubhubbub\Model\SubscriptionPersistenceInterface
* used to background save any verification tokens associated with a subscription
* or other.
*
* @var Model\SubscriptionPersistenceInterface
*/
protected $storage;
/**
* An instance of a class handling Http Responses. This is implemented in
* Laminas\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Laminas\Controller\Response\Http.
*
* @var HttpResponse|PhpResponse
*/
protected $httpResponse;
/**
* The input stream to use when retrieving the request body. Defaults to
* php://input, but can be set to another value in order to force usage
* of another input method. This should primarily be used for testing
* purposes.
*
* @var resource|string String indicates a filename or stream to open;
* resource indicates an already created stream to use.
*/
protected $inputStream = 'php://input';
/**
* The number of Subscribers for which any updates are on behalf of.
*
* @var int
*/
protected $subscriberCount = 1;
/**
* Constructor; accepts an array or Traversable object to preset
* options for the Subscriber without calling all supported setter
* methods in turn.
*
* @param null|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 $this
* @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 Laminas\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 Laminas\Feed\Pubsubhubbub\Model\SubscriptionPersistence used
* to background save any verification tokens associated with a subscription
* or other.
*
* @return $this
*/
public function setStorage(Model\SubscriptionPersistenceInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* Gets an instance of Laminas\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'
. ' Laminas\Feed\Pubsubhubbub\Model\SubscriptionPersistence'
);
}
return $this->storage;
}
/**
* An instance of a class handling Http Responses. This is implemented in
* Laminas\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Laminas\Controller\Response\Http.
*
* @param HttpResponse|PhpResponse $httpResponse
* @return $this
* @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 Laminas\Feed\Pubsubhubbub\HttpResponse or'
. ' Laminas\Http\PhpEnvironment\Response'
);
}
$this->httpResponse = $httpResponse;
return $this;
}
/**
* An instance of a class handling Http Responses. This is implemented in
* Laminas\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Laminas\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 int|string $count
* @return $this
* @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;
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* Attempt to detect the callback URL (specifically the path forward)
*
* @return string
*/
protected function _detectCallbackUrl()
{
$callbackUrl = null;
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = $_SERVER['IIS_WasUrlRewritten'] ?? null;
$unencodedUrl = $_SERVER['UNENCODED_URL'] ?? null;
if ('1' === $iisUrlRewritten && ! empty($unencodedUrl)) {
return $unencodedUrl;
}
// HTTP proxy requests setup request URI with scheme and host [and port]
// + the URL path, only use URL path.
if (isset($_SERVER['REQUEST_URI'])) {
$callbackUrl = $this->buildCallbackUrlFromRequestUri();
}
if (null !== $callbackUrl) {
return $callbackUrl;
}
if (isset($_SERVER['ORIG_PATH_INFO'])) {
return $this->buildCallbackUrlFromOrigPathInfo();
}
return '';
}
/**
* Get the HTTP host
*
* @return string
*/
protected function _getHttpHost()
{
if (! empty($_SERVER['HTTP_HOST'])) {
return $_SERVER['HTTP_HOST'];
}
$https = $_SERVER['HTTPS'] ?? null;
$scheme = $https === 'on' ? 'https' : 'http';
$name = $_SERVER['SERVER_NAME'] ?? '';
$port = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80;
if (
($scheme === 'http' && $port === 80)
|| ($scheme === 'https' && $port === 443)
) {
return $name;
}
return sprintf('%s:%d', $name, $port);
}
/**
* Retrieve a Header value from either $_SERVER or Apache
*
* @param string $header
* @return bool|string
*/
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 false|string Raw body, or false if not present
*/
protected function _getRawBody()
{
$body = is_resource($this->inputStream)
? stream_get_contents($this->inputStream)
: file_get_contents($this->inputStream);
return strlen(trim($body)) > 0 ? $body : false;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
/**
* Build the callback URL from the REQUEST_URI server parameter.
*
* @return string
*/
private function buildCallbackUrlFromRequestUri()
{
$callbackUrl = $_SERVER['REQUEST_URI'];
$https = $_SERVER['HTTPS'] ?? null;
$scheme = $https === 'on' ? 'https' : 'http';
if ($https === 'on') {
$scheme = 'https';
}
$schemeAndHttpHost = $scheme . '://' . $this->_getHttpHost();
if (strpos($callbackUrl, $schemeAndHttpHost) === 0) {
$callbackUrl = substr($callbackUrl, strlen($schemeAndHttpHost));
}
return $callbackUrl;
}
/**
* Build the callback URL from the ORIG_PATH_INFO server parameter.
*
* @return string
*/
private function buildCallbackUrlFromOrigPathInfo()
{
$callbackUrl = $_SERVER['ORIG_PATH_INFO'];
if (! empty($_SERVER['QUERY_STRING'])) {
$callbackUrl .= '?' . $_SERVER['QUERY_STRING'];
}
return $callbackUrl;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use Laminas\Http\PhpEnvironment\Response;
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 null|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 Laminas\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
* Laminas\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Laminas\Feed\Pubsubhubbub\AbstractCallback.
*
* @param HttpResponse|Response $httpResponse
*/
public function setHttpResponse($httpResponse);
/**
* An instance of a class handling Http Responses. This is implemented in
* Laminas\Feed\Pubsubhubbub\HttpResponse which shares an unenforced interface with
* (i.e. not inherited from) Laminas\Feed\Pubsubhubbub\AbstractCallback.
*
* @return HttpResponse|Response
*/
public function getHttpResponse();
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Exception;
use Laminas\Feed\Exception\ExceptionInterface as Exception;
interface ExceptionInterface extends Exception
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Exception;
use Laminas\Feed\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Exception;
use Laminas\Feed\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,217 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use function header;
use function headers_sent;
use function is_int;
use function str_replace;
use function strlen;
use function strtolower;
use function ucwords;
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 (200 === $this->statusCode) {
return;
}
if ($this->headers || (200 !== $this->statusCode)) {
$this->canSendHeaders(true);
}
$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 $this
*/
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 bool
* @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 $this
* @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 $this
*/
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
*/
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
protected function _normalizeHeader($name)
{
$filtered = str_replace(['-', '_'], ' ', (string) $name);
$filtered = ucwords(strtolower($filtered));
$filtered = str_replace(' ', '-', $filtered);
return $filtered;
}
}
@@ -0,0 +1,31 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Model;
use Laminas\Db\TableGateway\TableGateway;
use Laminas\Db\TableGateway\TableGatewayInterface;
use function array_pop;
use function explode;
use function strtolower;
class AbstractModel
{
/**
* Laminas\Db\TableGateway\TableGatewayInterface instance to host database methods
*
* @var TableGatewayInterface
*/
protected $db;
public function __construct(?TableGatewayInterface $tableGateway = null)
{
if ($tableGateway === null) {
$parts = explode('\\', static::class);
$table = strtolower(array_pop($parts));
$this->db = new TableGateway($table, null);
} else {
$this->db = $tableGateway;
}
}
}
@@ -0,0 +1,142 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Model;
use DateInterval;
use DateTime;
use Laminas\Feed\PubSubHubbub;
use function array_key_exists;
use function count;
use function is_string;
class Subscription extends AbstractModel implements SubscriptionPersistenceInterface
{
/**
* Common DateTime object to assist with unit testing
*
* @var DateTime
*/
protected $now;
/**
* Save subscription to RDMBS
*
* @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 (0 < count($result)) {
/** @psalm-suppress UndefinedInterfaceMethod */
$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)) {
/** @psalm-suppress UndefinedInterfaceMethod */
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
*
* @return $this
*/
public function setNow(DateTime $now)
{
$this->now = $now;
return $this;
}
}
@@ -0,0 +1,38 @@
<?php
namespace Laminas\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,141 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use Laminas\Escaper\Escaper;
use Laminas\Feed\Reader;
use Laminas\Http;
use function is_string;
use function str_replace;
class PubSubHubbub
{
/**
* Verification Modes
*/
public const VERIFICATION_MODE_SYNC = 'sync';
public const VERIFICATION_MODE_ASYNC = 'async';
/**
* Subscription States
*/
public const SUBSCRIPTION_VERIFIED = 'verified';
public const SUBSCRIPTION_NOTVERIFIED = 'not_verified';
public const SUBSCRIPTION_TODELETE = 'to_delete';
/** @var Escaper */
protected static $escaper;
/**
* Singleton instance if required of the HTTP client
*
* @var null|Http\Client
*/
protected static $httpClient;
/**
* 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 Laminas\Feed\Reader\Atom|Laminas\Feed\Reader\Rss
* to leverage off.
*
* @param string|Reader\Feed\AbstractFeed $source
* @return array<array-key, mixed>|null
* @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'
. ' Laminas\Feed\Reader\Feed\AbstractFeed'
);
}
return $feed->getHubs();
}
/**
* Allows the external environment to make laminas-oauth use a specific
* Client instance.
*
* @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
*
* @return void
*/
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);
return str_replace('%7E', '~', $rawencoded);
}
}
@@ -0,0 +1,404 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use Laminas\Feed\Uri;
use Laminas\Http\Client;
use Laminas\Http\Request as HttpRequest;
use Laminas\Stdlib\ArrayUtils;
use Traversable;
use function array_key_exists;
use function array_search;
use function array_unique;
use function gettype;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function urlencode;
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 Laminas\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 Laminas\Config\Config instance to preset
* options for the Publisher without calling all supported setter
* methods in turn.
*
* @param null|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 $this
* @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 $this
* @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
*
* @return $this
*/
public function addHubUrls(array $urls)
{
foreach ($urls as $url) {
$this->addHubUrl($url);
}
return $this;
}
/**
* Remove a Hub Server URL
*
* @param string $url
* @return $this
*/
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 $this
* @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
*
* @return $this
*/
public function addUpdatedTopicUrls(array $urls)
{
foreach ($urls as $url) {
$this->addUpdatedTopicUrl($url);
}
return $this;
}
/**
* Remove an updated topic URL
*
* @param string $url
* @return $this
*/
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 Laminas\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);
$client->send();
$response = $client->getResponse();
if ($response->getStatusCode() !== 204) {
$this->errors[] = [
'response' => $response,
'hubUrl' => $url,
];
}
}
}
/**
* Add an optional parameter to the update notification requests
*
* @param string|array<string, string> $name
* @param null|string $value
* @return $this
* @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
*
* @return $this
*/
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 $this
* @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 ! $this->errors;
}
/**
* Return an array of errors met from any failures, including keys:
* 'response' => the Laminas\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 Client
* @throws Exception\RuntimeException
*/
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
protected function _getHttpClient()
{
$client = PubSubHubbub::getHttpClient();
$client->setMethod(HttpRequest::METHOD_POST);
$client->setOptions([
'useragent' => 'Laminas_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,863 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
use DateInterval;
use DateTime;
use Laminas\Feed\Uri;
use Laminas\Http\Client;
use Laminas\Http\Request as HttpRequest;
use Laminas\Stdlib\ArrayUtils;
use Traversable;
use function array_key_exists;
use function array_search;
use function array_unique;
use function gettype;
use function hash;
use function implode;
use function in_array;
use function intval;
use function is_array;
use function is_string;
use function md5;
use function rand;
use function rtrim;
use function time;
use function uksort;
use function uniqid;
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;
/**
* The preferred verification mode (sync or async). By default, this
* Subscriber prefers synchronous verification, but is considered
* desirable to support asynchronous verification if possible.
*
* Laminas\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 Laminas\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 Laminas\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
* save any verification tokens associated with a subscription or other.
*
* @var Model\SubscriptionPersistenceInterface
*/
protected $storage;
/**
* 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
* Laminas\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 Laminas\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 null|array|Traversable $options
*/
public function __construct($options = null)
{
if ($options !== null) {
$this->setOptions($options);
}
}
/**
* Process any injected configuration options
*
* @param array|Traversable $options
* @return $this
* @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 $this
* @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 $this
* @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 $this
* @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.
*
* Laminas\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 $this
* @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'
. ' Laminas\Feed\Pubsubhubbub::VERIFICATION_MODE_SYNC or'
. ' Laminas\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 $this
* @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
*
* @return $this
*/
public function addHubUrls(array $urls)
{
foreach ($urls as $url) {
$this->addHubUrl($url);
}
return $this;
}
/**
* Remove a Hub Server URL
*
* @param string $url
* @return $this
*/
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
* @return $this
* @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
*
* @return $this
*/
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 $this
*/
public function usePathParameter($bool = true)
{
$this->usePathParameter = $bool;
return $this;
}
/**
* Add an optional parameter to the (un)subscribe requests
*
* @param string|array<string, string> $name
* @param null|string $value
* @return $this
* @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
*
* @return $this
*/
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 $this
* @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 Laminas\Feed\Pubsubhubbub\Model\SubscriptionPersistence used to background
* save any verification tokens associated with a subscription or other.
*
* @return $this
*/
public function setStorage(Model\SubscriptionPersistenceInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* Gets an instance of Laminas\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()
{
return ! $this->errors;
}
/**
* Return an array of errors met from any failures, including keys:
* 'response' => the Laminas\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;
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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($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 Client
*/
protected function _getHttpClient()
{
$client = PubSubHubbub::getHttpClient();
$client->setMethod(HttpRequest::METHOD_POST);
$client->setOptions([
'useragent' => 'Laminas_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');
}
// phpcs:disable Generic.Files.LineLength.TooLong
$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'],
/** @psalm-suppress PossiblyInvalidCast */
'verify_token' => hash('sha256', (string) $params['hub.verify_token']),
'secret' => null,
'expiration_time' => $expires,
'subscription_state' => $mode === 'unsubscribe' ? PubSubHubbub::SUBSCRIPTION_TODELETE : PubSubHubbub::SUBSCRIPTION_NOTVERIFIED,
];
// phpcs:enable Generic.Files.LineLength.TooLong
$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;
return md5($keyBase);
}
/**
* 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 string
*/
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);
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
/**
* This is STRICTLY for testing purposes only...
*
* @internal
*
* @var null|string
*/
protected $testStaticToken;
/**
* @internal
*/
final public function setTestStaticToken(string $token)
{
$this->testStaticToken = (string) $token;
}
}
@@ -0,0 +1,325 @@
<?php
namespace Laminas\Feed\PubSubHubbub\Subscriber;
use Laminas\Feed\PubSubHubbub;
use Laminas\Feed\PubSubHubbub\Exception;
use Laminas\Feed\Uri;
use function array_key_exists;
use function explode;
use function hash;
use function is_array;
use function rawurldecode;
use function sprintf;
use function stripos;
use function strtolower;
class Callback extends PubSubHubbub\AbstractCallback
{
/**
* Contains the content of any feeds sent as updates to the Callback URL
*
* @var string
*/
protected $feedUpdate;
/**
* 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 \Laminas\Mvc\Router\RouteMatch::getParam() method.
*
* @var null|string
*/
protected $subscriptionKey;
/**
* After verification, this is set to the verified subscription's data.
*
* @var array
*/
protected $currentSubscriptionData;
/**
* Set a subscription key to use for the current callback request manually.
* Required if usePathParameter is enabled for the Subscriber.
*
* @param string $key
* @return $this
*/
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 null|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.
*
* @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 $this
*/
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;
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\PubSubHubbub;
// phpcs:ignore WebimpressCodingStandard.NamingConventions.AbstractClass.Prefix
abstract class Version
{
public const VERSION = '2';
}
@@ -0,0 +1,226 @@
<?php
namespace Laminas\Feed\Reader;
use DOMDocument;
use DOMElement;
use DOMXPath;
use function array_key_exists;
use function call_user_func_array;
use function in_array;
use function method_exists;
/**
* @deprecated This (abstract) class is deprecated. Use Laminas\Feed\Reader\Entry\AbstractEntry instead.
*/
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath;
/**
* Registered extensions
*
* @var array
*/
protected $extensions = [];
/**
* @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
*
* @return $this
*/
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 null|Extension\AbstractEntry
*/
public function getExtension($name)
{
if (array_key_exists($name . '\Entry', $this->extensions)) {
return $this->extensions[$name . '\Entry'];
}
return null;
}
/**
* 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 Laminas\Feed\Reader\Reader
*
* @return void
*/
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
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,305 @@
<?php
namespace Laminas\Feed\Reader;
use DOMDocument;
use DOMElement;
use DOMXPath;
use function call_user_func_array;
use function count;
use function in_array;
use function method_exists;
use function strpos;
/**
* @deprecated This (abstract) class is deprecated. Use \Laminas\Feed\Reader\Feed\AbstractFeed instead.
*/
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;
/**
* 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;
/**
* Array of loaded extensions
*
* @var array
*/
protected $extensions = [];
/**
* Original Source URI (set if imported from a URI)
*
* @var string
*/
protected $originalSourceUri;
/**
* @param DOMDocument $domDocument The DOM object for the feed's XML
* @param null|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
* @return void
*/
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 null|string
*/
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 Entry\AbstractEntry
*/
public function current()
{
if (0 === strpos($this->getType(), '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();
}
/** @return array */
public function getExtensions()
{
return $this->extensions;
}
/**
* @param string $method
* @param mixed[] $args
* @return mixed
*/
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 null|Extension\AbstractFeed
*/
public function getExtension($name)
{
$extensionClass = $name . '\\Feed';
return isset($this->extensions[$extensionClass])
&& $this->extensions[$extensionClass] instanceof Extension\AbstractFeed
? $this->extensions[$extensionClass]
: null;
}
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,14 @@
<?php
namespace Laminas\Feed\Reader;
use ArrayObject;
/**
* @deprecated This class is deprecated. Use the concrete collection classes
* \Laminas\Feed\Reader\Collection\Author and \Laminas\Feed\Reader\Collection\Category
* or the generic class \Laminas\Feed\Reader\Collection\Collection instead.
*/
class Collection extends ArrayObject
{
}
@@ -0,0 +1,18 @@
<?php
namespace Laminas\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,23 @@
<?php
namespace Laminas\Feed\Reader\Collection;
use function array_unique;
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,29 @@
<?php
namespace Laminas\Feed\Reader\Collection;
use function array_unique;
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,9 @@
<?php
namespace Laminas\Feed\Reader\Collection;
use ArrayObject;
class Collection extends ArrayObject
{
}
@@ -0,0 +1,231 @@
<?php
namespace Laminas\Feed\Reader\Entry;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Exception;
use function call_user_func_array;
use function in_array;
use function method_exists;
use function sprintf;
use function version_compare;
use const PHP_VERSION;
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath;
/**
* Registered extensions
*
* @var array
*/
protected $extensions = [];
/**
* @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;
} 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
*
* @return $this
*/
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 null|Reader\Extension\AbstractEntry
*/
public function getExtension($name)
{
$extensionClass = $name . '\\Entry';
return isset($this->extensions[$extensionClass])
&& $this->extensions[$extensionClass] instanceof Reader\Extension\AbstractEntry
? $this->extensions[$extensionClass]
: null;
}
/**
* 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 Laminas\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,398 @@
<?php
namespace Laminas\Feed\Reader\Entry;
use DateTime;
use DOMElement;
use DOMXPath;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection\Author as AuthorCollection;
use Laminas\Feed\Reader\Exception\RuntimeException;
use Laminas\Feed\Reader\Extension\Atom\Entry;
use Laminas\Feed\Reader\Extension\DublinCore\Entry as DublinCoreEntry;
use Laminas\Feed\Reader\Extension\Thread\Entry as ThreadEntry;
use function array_key_exists;
use function count;
use function is_array;
use function is_string;
class Atom extends AbstractEntry implements EntryInterface
{
/**
* XPath query
*
* @var string
*/
protected $xpathQuery = '';
/**
* @param int $entryKey
* @param null|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;
}
}
/**
* @inheritDoc
* @param int $index
* @return null|array<string, string>
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_array($authors[$index])
? $authors[$index]
: null;
}
/**
* Get an array with feed authors
*
* @return AuthorCollection
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$people = $this->getAtomExtension()->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->getAtomExtension()->getContent();
$this->data['content'] = $content;
return $this->data['content'];
}
/**
* Get the entry creation date
*
* @return null|DateTime
*/
public function getDateCreated()
{
if (array_key_exists('datecreated', $this->data)) {
return $this->data['datecreated'];
}
$dateCreated = $this->getAtomExtension()->getDateCreated();
$this->data['datecreated'] = $dateCreated;
return $this->data['datecreated'];
}
/**
* Get the entry modification date
*
* @return null|DateTime
*/
public function getDateModified()
{
if (array_key_exists('datemodified', $this->data)) {
return $this->data['datemodified'];
}
$dateModified = $this->getAtomExtension()->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->getAtomExtension()->getDescription();
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Get the entry enclosure
*
* @return null|object{href: string, length: int, type: string}
*/
public function getEnclosure()
{
if (array_key_exists('enclosure', $this->data)) {
return $this->data['enclosure'];
}
$enclosure = $this->getAtomExtension()->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->getAtomExtension()->getId();
$this->data['id'] = $id;
return $this->data['id'];
}
/**
* Get a specific link
*
* @param int $index
* @return null|string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
return isset($this->data['links'][$index]) && is_string($this->data['links'][$index])
? $this->data['links'][$index]
: null;
}
/**
* Get all links
*
* @return array
*/
public function getLinks()
{
if (array_key_exists('links', $this->data)) {
return $this->data['links'];
}
$links = $this->getAtomExtension()->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->getAtomExtension()->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->getThreadExtension()->getCommentCount();
if (! $commentcount) {
$commentcount = $this->getAtomExtension()->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->getAtomExtension()->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->getAtomExtension()->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->getAtomExtension()->getCategories();
if (count($categoryCollection) === 0) {
$categoryCollection = $this->getDublinCoreExtension()->getCategories();
}
$this->data['categories'] = $categoryCollection;
return $this->data['categories'];
}
/**
* Get source feed metadata from the entry
*
* @return null|Reader\Feed\Atom\Source
*/
public function getSource()
{
if (array_key_exists('source', $this->data)) {
return $this->data['source'];
}
$source = $this->getAtomExtension()->getSource();
$this->data['source'] = $source;
return $this->data['source'];
}
/**
* Set the XPath query (incl. on all Extensions)
*
* @return void
*/
public function setXpath(DOMXPath $xpath)
{
parent::setXpath($xpath);
foreach ($this->extensions as $extension) {
$extension->setXpath($this->xpath);
}
}
private function getAtomExtension(): Entry
{
$extension = $this->getExtension('Atom');
if (! $extension instanceof Entry) {
throw new RuntimeException('Unable to retrieve Atom entry extension');
}
return $extension;
}
private function getDublinCoreExtension(): DublinCoreEntry
{
$extension = $this->getExtension('DublinCore');
if (! $extension instanceof DublinCoreEntry) {
throw new RuntimeException('Unable to retrieve DublinCore entry extension');
}
return $extension;
}
private function getThreadExtension(): ThreadEntry
{
$extension = $this->getExtension('Thread');
if (! $extension instanceof ThreadEntry) {
throw new RuntimeException('Unable to retrieve Thread entry extension');
}
return $extension;
}
}
@@ -0,0 +1,123 @@
<?php
namespace Laminas\Feed\Reader\Entry;
use DateTime;
use Laminas\Feed\Reader\Collection\Category;
interface EntryInterface
{
/**
* Get the specified author
*
* @param int $index
* @return array<string, string>|null
*/
public function getAuthor($index = 0);
/**
* Get an array with feed authors
*
* @return null|array|iterable
*/
public function getAuthors();
/**
* Get the entry content
*
* @return string
*/
public function getContent();
/**
* Get the entry creation date
*
* @return null|DateTime
*/
public function getDateCreated();
/**
* Get the entry modification date
*
* @return null|DateTime
*/
public function getDateModified();
/**
* Get the entry description
*
* @return string
*/
public function getDescription();
/**
* Get the entry enclosure
*
* @return null|object{url?: string, href?: string, length: int, type: string}
*/
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,655 @@
<?php
namespace Laminas\Feed\Reader\Entry;
use DateTime;
use DateTimeInterface;
use DOMElement;
use DOMNodeList;
use DOMXPath;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Exception;
use Laminas\Feed\Reader\Exception\RuntimeException;
use Laminas\Feed\Reader\Extension\Atom\Entry as AtomEntry;
use Laminas\Feed\Reader\Extension\Content\Entry as ContentEntry;
use Laminas\Feed\Reader\Extension\DublinCore\Entry as DublinCoreEntry;
use Laminas\Feed\Reader\Extension\Slash\Entry as SlashEntry;
use Laminas\Feed\Reader\Extension\Thread\Entry as ThreadEntry;
use Laminas\Feed\Reader\Extension\WellFormedWeb\Entry as WellFormedWebEntry;
use stdClass;
use function array_key_exists;
use function count;
use function date_create_from_format;
use function in_array;
use function is_array;
use function is_string;
use function preg_match;
use function strtotime;
use function trim;
class Rss extends AbstractEntry implements EntryInterface
{
/**
* XPath query for RDF
*
* @var string
*/
protected $xpathQueryRdf = '';
/**
* XPath query for RSS
*
* @var string
*/
protected $xpathQueryRss = '';
/**
* @param string $entryKey
* @param null|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;
}
}
/**
* @inheritDoc
* @param int $index
* @return null|array<string, string>
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_array($authors[$index])
? $authors[$index]
: null;
}
/**
* Get an array with feed authors
*
* @return null|array
*/
public function getAuthors()
{
if (array_key_exists('authors', $this->data)) {
return $this->data['authors'];
}
$authors = [];
$authorsDc = $this->getDublinCoreExtension()->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 instanceof DOMNodeList && $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->getAtomExtension()->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->getContentExtension()->getContent();
if (empty($content)) {
$content = $this->getDescription();
}
if (empty($content)) {
$content = $this->getAtomExtension()->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
*
* @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(' . $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 instanceof DateTimeInterface) {
$date = $this->getDublinCoreExtension()->getDate();
}
if (! $date instanceof DateTimeInterface) {
$date = $this->getAtomExtension()->getDateModified();
}
if (! $date instanceof DateTimeInterface) {
$date = null;
}
$this->data['datemodified'] = $date;
return $this->data['datemodified'];
}
/**
* Get the entry description
*
* @return null|string
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$rssTypes = [
Reader\Reader::TYPE_RSS_090,
Reader\Reader::TYPE_RSS_10,
];
$description = in_array($this->getType(), $rssTypes, true)
? $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:description)')
: $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/description)');
if (! $description) {
$description = $this->getDublinCoreExtension()->getDescription();
}
if (empty($description)) {
$description = $this->getAtomExtension()->getDescription();
}
$this->data['description'] = is_string($description) ? $description : null;
return $this->data['description'];
}
/**
* Get the entry enclosure
*
* @return null|object{url?: string, href?: string, length: int, type: 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 instanceof DOMNodeList && $nodeList->length > 0) {
/** @var DOMElement $node */
$node = $nodeList->item(0);
$enclosure = new stdClass();
$enclosure->url = $node->getAttribute('url');
$enclosure->length = $node->getAttribute('length');
$enclosure->type = $node->getAttribute('type');
}
}
if (! $enclosure) {
$enclosure = $this->getAtomExtension()->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->getDublinCoreExtension()->getId();
}
if (empty($id)) {
$id = $this->getAtomExtension()->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 null|string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
return isset($this->data['links'][$index]) && is_string($this->data['links'][$index])
? $this->data['links'][$index]
: null;
}
/**
* 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 instanceof DOMNodeList && $list->length) {
foreach ($list as $link) {
$links[] = $link->nodeValue;
}
} else {
$links = $this->getAtomExtension()->getLinks();
}
$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 instanceof DOMNodeList && $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->getDublinCoreExtension()->getCategories();
}
if (count($categoryCollection) === 0) {
$categoryCollection = $this->getAtomExtension()->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 = $this->getType() !== Reader\Reader::TYPE_RSS_10 && $this->getType() !== Reader\Reader::TYPE_RSS_090
? $this->xpath->evaluate('string(' . $this->xpathQueryRss . '/title)')
: $this->xpath->evaluate('string(' . $this->xpathQueryRdf . '/rss:title)');
if (! $title) {
$title = $this->getDublinCoreExtension()->getTitle();
}
if (! $title) {
$title = $this->getAtomExtension()->getTitle();
}
$this->data['title'] = is_string($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->getSlashExtension()->getCommentCount();
if (! $commentcount) {
$commentcount = $this->getThreadExtension()->getCommentCount();
}
if (! $commentcount) {
$commentcount = $this->getAtomExtension()->getCommentCount();
}
if (! $commentcount) {
$commentcount = 0;
}
$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->getAtomExtension()->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->getWellFormedWebExtension()->getCommentFeedLink();
if (! $commentfeedlink) {
$commentfeedlink = $this->getAtomExtension()->getCommentFeedLink('rss');
}
if (! $commentfeedlink) {
$commentfeedlink = $this->getAtomExtension()->getCommentFeedLink('rdf');
}
if (! $commentfeedlink) {
$commentfeedlink = null;
}
$this->data['commentfeedlink'] = $commentfeedlink;
return $this->data['commentfeedlink'];
}
/**
* Set the XPath query (incl. on all Extensions)
*
* @return void
*/
public function setXpath(DOMXPath $xpath)
{
parent::setXpath($xpath);
foreach ($this->extensions as $extension) {
$extension->setXpath($this->xpath);
}
}
private function getAtomExtension(): AtomEntry
{
$extension = $this->getExtension('Atom');
if (! $extension instanceof AtomEntry) {
throw new RuntimeException('Unable to load Atom entry extension');
}
return $extension;
}
private function getContentExtension(): ContentEntry
{
$extension = $this->getExtension('Content');
if (! $extension instanceof ContentEntry) {
throw new RuntimeException('Unable to load Content entry extension');
}
return $extension;
}
private function getDublinCoreExtension(): DublinCoreEntry
{
$extension = $this->getExtension('DublinCore');
if (! $extension instanceof DublinCoreEntry) {
throw new RuntimeException('Unable to load DublinCore entry extension');
}
return $extension;
}
private function getSlashExtension(): SlashEntry
{
$extension = $this->getExtension('Slash');
if (! $extension instanceof SlashEntry) {
throw new RuntimeException('Unable to load Slash entry extension');
}
return $extension;
}
private function getThreadExtension(): ThreadEntry
{
$extension = $this->getExtension('Thread');
if (! $extension instanceof ThreadEntry) {
throw new RuntimeException('Unable to load Thread entry extension');
}
return $extension;
}
private function getWellFormedWebExtension(): WellFormedWebEntry
{
$extension = $this->getExtension('WellFormedWeb');
if (! $extension instanceof WellFormedWebEntry) {
throw new RuntimeException('Unable to load WellFormedWeb entry extension');
}
return $extension;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Reader\Exception;
use Laminas\Feed\Exception;
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Reader\Exception;
use Laminas\Feed\Exception\ExceptionInterface as Exception;
interface ExceptionInterface extends Exception
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Reader\Exception;
use Laminas\Feed\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Reader\Exception;
use Laminas\Feed\Exception;
class InvalidHttpClientException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Reader\Exception;
use Laminas\Feed\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,225 @@
<?php
namespace Laminas\Feed\Reader\Extension;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Laminas\Feed\Reader;
abstract class AbstractEntry
{
/**
* Feed entry data
*
* @var array
*/
protected $data = [];
/**
* DOM document object
*
* @var DOMDocument
*/
protected $domDocument;
/**
* Entry instance
*
* @var DOMElement
*/
protected $entry;
/**
* Pointer to the current entry
*
* @var int
*/
protected $entryKey = 0;
/**
* XPath object
*
* @var DOMXPath
*/
protected $xpath;
/**
* XPath query
*
* @var string
*/
protected $xpathPrefix = '';
/**
* Set the entry DOMElement
*
* Has side effect of setting the DOMDocument for the entry.
*
* @return $this
*/
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 $this
*/
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()
{
return $this->getDomDocument()->encoding;
}
/**
* Set the entry type
*
* Has side effect of setting xpath prefix
*
* @param string $type
* @return $this
*/
public function setType($type)
{
if (null === $type) {
$this->data['type'] = null;
return $this;
}
$this->data['type'] = $type;
if (
$type === Reader\Reader::TYPE_RSS_10
|| $type === Reader\Reader::TYPE_RSS_090
) {
$this->setXpathPrefix('//rss:item[' . ((int) $this->entryKey + 1) . ']');
return $this;
}
if (
$type === Reader\Reader::TYPE_ATOM_10
|| $type === Reader\Reader::TYPE_ATOM_03
) {
$this->setXpathPrefix('//atom:entry[' . ((int) $this->entryKey + 1) . ']');
return $this;
}
$this->setXpathPrefix('//item[' . ((int) $this->entryKey + 1) . ']');
return $this;
}
/**
* Get the entry type
*
* @return string
*/
public function getType()
{
$type = $this->data['type'];
if ($type === null) {
$type = Reader\Reader::detectType($this->getEntryElement(), true);
$this->setType($type);
}
return $type;
}
/**
* Set the XPath query
*
* @return $this
*/
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 $this
*/
public function setXpathPrefix($prefix)
{
$this->xpathPrefix = $prefix;
return $this;
}
/**
* Register XML namespaces
*
* @return void
*/
abstract protected function registerNamespaces();
}
@@ -0,0 +1,165 @@
<?php
namespace Laminas\Feed\Reader\Extension;
use DOMDocument;
use DOMXPath;
use Laminas\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;
/**
* The base XPath query used to retrieve feed data
*
* @var DOMXPath
*/
protected $xpath;
/**
* The XPath prefix
*
* @var string
*/
protected $xpathPrefix = '';
/**
* Set the DOM document
*
* @return $this
*/
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()
{
return $this->getDomDocument()->encoding;
}
/**
* Set the feed type
*
* @param string $type
* @return $this
*/
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
*
* @return $this
*/
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 DOMXPath
*/
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,641 @@
<?php
namespace Laminas\Feed\Reader\Extension\Atom;
use DateTime;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection;
use Laminas\Feed\Reader\Extension;
use Laminas\Feed\Uri;
use stdClass;
use function array_key_exists;
use function count;
use function is_string;
use function preg_replace;
use function strlen;
use function trim;
use function version_compare;
use const PHP_VERSION;
class Entry extends Extension\AbstractEntry
{
/**
* Get the specified author
*
* @param int $index
* @return null|string
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_string($authors[$index])
? $authors[$index]
: null;
}
/**
* 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 instanceof DOMNodeList || ! $list->length) {
/**
* TODO: Limit query to feed level els only!
*/
$list = $this->getXpath()->query('//atom:author');
}
if ($list instanceof DOMNodeList && $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 string $xhtml
* @param string $prefix
* @return mixed
*/
protected function collectXhtml($xhtml, $prefix)
{
if (! empty($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 null|DateTime
*/
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 null|DateTime
*/
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 null|object{href: string, length: int, type: 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 instanceof DOMNodeList && $nodeList->length > 0) {
/** @var DOMElement $node */
$node = $nodeList->item(0);
$enclosure = new stdClass();
$enclosure->url = $node->getAttribute('href');
$enclosure->length = $node->getAttribute('length');
$enclosure->type = $node->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 null|string
*/
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 null|string
*/
public function getLink($index = 0)
{
if (! array_key_exists('links', $this->data)) {
$this->getLinks();
}
return isset($this->data['links']) && is_string($this->data['links'])
? $this->data['links']
: null;
}
/**
* 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 instanceof DOMNodeList && $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()
{
$permalink = $this->getLink(0);
return is_string($permalink) ? $permalink : '';
}
/**
* 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 (! is_string($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 instanceof DOMNodeList && $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 instanceof DOMNodeList && $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 instanceof DOMNodeList && $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 instanceof DOMNodeList && $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 null|Reader\Feed\Atom\Source
*/
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 instanceof DOMNodeList && $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 string $link
* @return null|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
*
* @return array<string,null|string>|null
* @psalm-return array{email?: null|string, name?: null|string, uri?: null|string}|null
*/
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 null|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;
}
return null;
}
}
@@ -0,0 +1,529 @@
<?php
namespace Laminas\Feed\Reader\Extension\Atom;
use DateTime;
use DOMElement;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection;
use Laminas\Feed\Reader\Extension;
use Laminas\Feed\Uri;
use function array_key_exists;
use function count;
use function is_string;
use function strlen;
class Feed extends Extension\AbstractFeed
{
/**
* Get a single author
*
* @param int $index
* @return null|string
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index]) && is_string($authors[$index])) {
return $authors[$index];
}
return null;
}
/**
* 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 null|string
*/
public function getCopyright()
{
if (array_key_exists('copyright', $this->data)) {
return $this->data['copyright'];
}
$copyright = $this->getType() === Reader\Reader::TYPE_ATOM_03
? $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:copyright)')
: $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:rights)');
$this->data['copyright'] = is_string($copyright) ? $copyright : null;
return $this->data['copyright'];
}
/**
* Get the feed creation date
*
* @return null|DateTime
*/
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 null|DateTime
*/
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 null|string
*/
public function getDescription()
{
if (array_key_exists('description', $this->data)) {
return $this->data['description'];
}
$description = $this->getType() === Reader\Reader::TYPE_ATOM_03
? $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:tagline)')
: $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:subtitle)');
$this->data['description'] = is_string($description) ? $description : null;
return $this->data['description'];
}
/**
* Get the feed generator entry
*
* @return null|string
*/
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 null|string
*/
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 null|string
*/
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 null|array
*/
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 null|string
*/
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 null|string
*/
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 null|string
*/
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 null|array
*/
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 null|string
*/
public function getTitle()
{
if (array_key_exists('title', $this->data)) {
return $this->data['title'];
}
$title = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/atom:title)');
if (empty($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
*
* @return array<string,null|string>|null
* @psalm-return array{email?: null|string, name?: null|string, uri?: null|string}|null
*/
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.
*
* @param string $link
* @return null|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;
}
/**
* Register the default namespaces for the current feed format
*
* @return void
*/
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
*
* @return null|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_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,30 @@
<?php
namespace Laminas\Feed\Reader\Extension\Content;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Extension;
class Entry extends Extension\AbstractEntry
{
/** @return string */
public function getContent()
{
if (
$this->getType() !== Reader\Reader::TYPE_RSS_10
&& $this->getType() !== Reader\Reader::TYPE_RSS_090
) {
return $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
}
return $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/content:encoded)');
}
/**
* Register RSS Content Module namespace
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
}
}
@@ -0,0 +1,81 @@
<?php
namespace Laminas\Feed\Reader\Extension\CreativeCommons;
use DOMNodeList;
use Laminas\Feed\Reader\Exception\RuntimeException;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
use function array_unique;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
class Entry extends Extension\AbstractEntry
{
/**
* Get the entry license
*
* @param int $index
* @return null|string
*/
public function getLicense($index = 0)
{
$licenses = $this->getLicenses();
if (! isset($licenses[$index])) {
return null;
}
if (! is_string($licenses[$index])) {
throw new RuntimeException(sprintf(
'Unable to retrieve license; expected string, received "%s"',
is_object($licenses[$index]) ? get_class($licenses[$index]) : gettype($licenses[$index])
));
}
return $licenses[$index];
}
/**
* 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 instanceof DOMNodeList && $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,79 @@
<?php
namespace Laminas\Feed\Reader\Extension\CreativeCommons;
use Laminas\Feed\Reader\Exception\RuntimeException;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
use function array_unique;
use function get_class;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
class Feed extends Extension\AbstractFeed
{
/**
* Get the entry license
*
* @param int $index
* @return null|string
*/
public function getLicense($index = 0)
{
$licenses = $this->getLicenses();
if (! isset($licenses[$index])) {
return null;
}
if (! is_string($licenses[$index])) {
throw new RuntimeException(sprintf(
'Unable to retrieve license; expected string, received "%s"',
is_object($licenses[$index]) ? get_class($licenses[$index]) : gettype($licenses[$index])
));
}
return $licenses[$index];
}
/**
* 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,228 @@
<?php
namespace Laminas\Feed\Reader\Extension\DublinCore;
use DateTime;
use DOMNodeList;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
use function is_array;
class Entry extends Extension\AbstractEntry
{
/**
* Get an author entry
*
* @param int $index
* @return null|array<string, string>
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_array($authors[$index])
? $authors[$index]
: null;
}
/**
* 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 instanceof DOMNodeList || ! $list->length) {
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:creator');
}
if (! $list instanceof DOMNodeList || ! $list->length) {
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc11:publisher');
if (! $list instanceof DOMNodeList || ! $list->length) {
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:publisher');
}
}
if ($list instanceof DOMNodeList && $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 instanceof DOMNodeList || ! $list->length) {
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject');
}
if ($list instanceof DOMNodeList && $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 null|DateTime
*/
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,271 @@
<?php
namespace Laminas\Feed\Reader\Extension\DublinCore;
use DateTime;
use DOMNodeList;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
use function is_string;
class Feed extends Extension\AbstractFeed
{
/**
* Get a single author
*
* @param int $index
* @return null|string
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
if (isset($authors[$index]) && is_string($authors[$index])) {
return $authors[$index];
}
return null;
}
/**
* 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 null|string
*/
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 null|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 feed ID
*
* @return null|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 feed language
*
* @return null|string
*/
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 null|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 null|DateTime
*/
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 instanceof DOMNodeList || ! $list->length) {
$list = $this->getXpath()->evaluate($this->getXpathPrefix() . '//dc10:subject');
}
if ($list instanceof DOMNodeList && $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,84 @@
<?php
namespace Laminas\Feed\Reader\Extension\GooglePlayPodcast;
use Laminas\Feed\Reader\Extension;
class Entry extends Extension\AbstractEntry
{
/**
* Get the entry block
*
* @return string
*/
public function getPlayPodcastBlock()
{
if (isset($this->data['block'])) {
return $this->data['block'];
}
$block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:block)');
if (! $block) {
$block = null;
}
$this->data['block'] = $block;
return $this->data['block'];
}
/**
* Get the entry explicit
*
* @return string
*/
public function getPlayPodcastExplicit()
{
if (isset($this->data['explicit'])) {
return $this->data['explicit'];
}
$explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:explicit)');
if (! $explicit) {
$explicit = null;
}
$this->data['explicit'] = $explicit;
return $this->data['explicit'];
}
/**
* Get the episode summary/description
*
* Uses verbiage so it does not conflict with base entry.
*
* @return string
*/
public function getPlayPodcastDescription()
{
if (isset($this->data['description'])) {
return $this->data['description'];
}
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:description)');
if (! $description) {
$description = null;
}
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Register googleplay namespace
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('googleplay', 'http://www.google.com/schemas/play-podcasts/1.0');
}
}
@@ -0,0 +1,169 @@
<?php
namespace Laminas\Feed\Reader\Extension\GooglePlayPodcast;
use DOMText;
use Laminas\Feed\Reader\Extension;
class Feed extends Extension\AbstractFeed
{
/**
* Get the entry author
*
* @return string
*/
public function getPlayPodcastAuthor()
{
if (isset($this->data['author'])) {
return $this->data['author'];
}
$author = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:author)');
if (! $author) {
$author = null;
}
$this->data['author'] = $author;
return $this->data['author'];
}
/**
* Get the entry block
*
* @return string
*/
public function getPlayPodcastBlock()
{
if (isset($this->data['block'])) {
return $this->data['block'];
}
$block = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:block)');
if (! $block) {
$block = null;
}
$this->data['block'] = $block;
return $this->data['block'];
}
/**
* Get the entry category
*
* @return null|array
*/
public function getPlayPodcastCategories()
{
if (isset($this->data['categories'])) {
return $this->data['categories'];
}
$categoryList = $this->xpath->query($this->getXpathPrefix() . '/googleplay:category');
$categories = [];
if ($categoryList->length > 0) {
foreach ($categoryList as $node) {
$children = [];
if ($node->childNodes->length > 0) {
foreach ($node->childNodes as $childNode) {
if (! $childNode instanceof DOMText) {
$children[$childNode->getAttribute('text')] = null;
}
}
}
$categories[$node->getAttribute('text')] = $children === [] ? null : $children;
}
}
if (! $categories) {
$categories = null;
}
$this->data['categories'] = $categories;
return $this->data['categories'];
}
/**
* Get the entry explicit
*
* @return string
*/
public function getPlayPodcastExplicit()
{
if (isset($this->data['explicit'])) {
return $this->data['explicit'];
}
$explicit = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:explicit)');
if (! $explicit) {
$explicit = null;
}
$this->data['explicit'] = $explicit;
return $this->data['explicit'];
}
/**
* Get the feed/podcast image
*
* @return string
*/
public function getPlayPodcastImage()
{
if (isset($this->data['image'])) {
return $this->data['image'];
}
$image = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:image/@href)');
if (! $image) {
$image = null;
}
$this->data['image'] = $image;
return $this->data['image'];
}
/**
* Get the entry description
*
* @return string
*/
public function getPlayPodcastDescription()
{
if (isset($this->data['description'])) {
return $this->data['description'];
}
$description = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/googleplay:description)');
if (! $description) {
$description = null;
}
$this->data['description'] = $description;
return $this->data['description'];
}
/**
* Register googleplay namespace
*
* @return void
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('googleplay', 'http://www.google.com/schemas/play-podcasts/1.0');
}
}
@@ -0,0 +1,313 @@
<?php
namespace Laminas\Feed\Reader\Extension\Podcast;
use Laminas\Feed\Reader\Extension;
use function trigger_error;
use const E_USER_DEPRECATED;
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
*
* @deprecated since 2.10.0; itunes:keywords is no longer part of the
* iTunes podcast RSS specification.
*
* @return string
*/
public function getKeywords()
{
trigger_error(
'itunes:keywords has been deprecated in the iTunes podcast RSS specification,'
. ' and should not be relied on.',
E_USER_DEPRECATED
);
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 title
*
* @return string
*/
public function getTitle()
{
if (isset($this->data['title'])) {
return $this->data['title'];
}
$title = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:title)');
if (! $title) {
$title = null;
}
$this->data['title'] = $title;
return $this->data['title'];
}
/**
* 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'];
}
/**
* 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 episode number
*
* @return null|int
*/
public function getEpisode()
{
if (isset($this->data['episode'])) {
return $this->data['episode'];
}
$episode = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:episode)');
if (! $episode) {
$episode = null;
}
$this->data['episode'] = null === $episode ? $episode : (int) $episode;
return $this->data['episode'];
}
/**
* Get the episode number
*
* @return string One of "full", "trailer", or "bonus"; defaults to "full".
*/
public function getEpisodeType()
{
if (isset($this->data['episodeType'])) {
return $this->data['episodeType'];
}
$type = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:episodeType)');
if (! $type) {
$type = 'full';
}
$this->data['episodeType'] = (string) $type;
return $this->data['episodeType'];
}
/**
* Is the episode closed captioned?
*
* Returns true only if itunes:isClosedCaptioned has the value 'Yes'.
*
* @return bool
*/
public function isClosedCaptioned()
{
if (isset($this->data['isClosedCaptioned'])) {
return $this->data['isClosedCaptioned'];
}
$status = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:isClosedCaptioned)');
$this->data['isClosedCaptioned'] = $status === 'Yes';
return $this->data['isClosedCaptioned'];
}
/**
* Get the season number
*
* @return null|int
*/
public function getSeason()
{
if (isset($this->data['season'])) {
return $this->data['season'];
}
$season = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:season)');
if (! $season) {
$season = null;
}
$this->data['season'] = null === $season ? $season : (int) $season;
return $this->data['season'];
}
/**
* Register iTunes namespace
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd');
}
}
@@ -0,0 +1,324 @@
<?php
namespace Laminas\Feed\Reader\Extension\Podcast;
use DOMText;
use Laminas\Feed\Reader\Extension;
use function trigger_error;
use const E_USER_DEPRECATED;
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 null|array
*/
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 = [];
if ($node->childNodes->length > 0) {
foreach ($node->childNodes as $childNode) {
if (! $childNode instanceof DOMText) {
$children[$childNode->getAttribute('text')] = null;
}
}
}
$categories[$node->getAttribute('text')] = $children === [] ? null : $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 feed/podcast 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
*
* @deprecated since 2.10.0; itunes:keywords is no longer part of the
* iTunes podcast RSS specification.
*
* @return string
*/
public function getKeywords()
{
trigger_error(
'itunes:keywords has been deprecated in the iTunes podcast RSS specification,'
. ' and should not be relied on.',
E_USER_DEPRECATED
);
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'];
}
/**
* Get the type of podcast
*
* @return string One of "episodic" or "serial". Defaults to "episodic"
* if no itunes:type tag is encountered.
*/
public function getPodcastType()
{
if (isset($this->data['podcastType'])) {
return $this->data['podcastType'];
}
$type = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:type)');
if (! $type) {
$type = 'episodic';
}
$this->data['podcastType'] = (string) $type;
return $this->data['podcastType'];
}
/**
* Is the podcast complete (no more episodes will post)?
*
* @return bool
*/
public function isComplete()
{
if (isset($this->data['complete'])) {
return $this->data['complete'];
}
$complete = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/itunes:complete)');
if (! $complete) {
$complete = false;
}
$this->data['complete'] = $complete === 'Yes';
return $this->data['complete'];
}
/**
* Register iTunes namespace
*
* @return void
*/
protected function registerNamespaces()
{
$this->xpath->registerNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd');
}
}
@@ -0,0 +1,118 @@
<?php
namespace Laminas\Feed\Reader\Extension\PodcastIndex;
use DOMElement;
use DOMNodeList;
use Laminas\Feed\Reader\Extension;
use stdClass;
use function array_key_exists;
/**
* Describes PodcastIndex data of an entry in a RSS Feed
*/
class Entry extends Extension\AbstractEntry
{
/**
* Get the entry transcript
*
* @psalm-return null|object{url: string, type: string, language: string, rel: string}
*/
public function getTranscript(): ?stdClass
{
if (array_key_exists('transcript', $this->data)) {
return $this->data['transcript'];
}
$transcript = null;
$nodeList = $this->xpath->query($this->getXpathPrefix() . '/podcast:transcript');
if ($nodeList instanceof DOMNodeList && $nodeList->length > 0) {
/** @var DOMElement $node */
$node = $nodeList->item(0);
$transcript = new stdClass();
$transcript->url = $node->getAttribute('url');
$transcript->type = $node->getAttribute('type');
$transcript->language = $node->getAttribute('language');
$transcript->rel = $node->getAttribute('rel');
}
$this->data['transcript'] = $transcript;
return $this->data['transcript'];
}
/**
* Get the entry chapters
*
* @psalm-return null|object{url: string, type: string}
*/
public function getChapters(): ?stdClass
{
if (array_key_exists('chapters', $this->data)) {
return $this->data['chapters'];
}
$chapters = null;
$nodeList = $this->xpath->query($this->getXpathPrefix() . '/podcast:chapters');
if ($nodeList instanceof DOMNodeList && $nodeList->length > 0) {
/** @var DOMElement $node */
$node = $nodeList->item(0);
$chapters = new stdClass();
$chapters->url = $node->getAttribute('url');
$chapters->type = $node->getAttribute('type');
}
$this->data['chapters'] = $chapters;
return $this->data['chapters'];
}
/**
* Get the entry soundbites
*
* @return stdClass[]
* @psalm-return array<int, object{title: string, startTime: string, duration: string}>
*/
public function getSoundbites(): array
{
if (array_key_exists('soundbites', $this->data)) {
return $this->data['soundbites'];
}
$soundbites = [];
$nodeList = $this->xpath->query($this->getXpathPrefix() . '/podcast:soundbite');
if ($nodeList->length > 0) {
foreach ($nodeList as $node) {
/** @var DOMElement $node */
$soundbite = new stdClass();
$soundbite->title = $node->nodeValue;
$soundbite->startTime = $node->getAttribute('startTime');
$soundbite->duration = $node->getAttribute('duration');
$soundbites[] = $soundbite;
}
}
$this->data['soundbites'] = $soundbites;
return $this->data['soundbites'];
}
/**
* Register PodcastIndex namespace
*/
protected function registerNamespaces(): void
{
$this->xpath->registerNamespace(
'podcast',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
);
}
}
@@ -0,0 +1,91 @@
<?php
namespace Laminas\Feed\Reader\Extension\PodcastIndex;
use Laminas\Feed\Reader\Extension;
use stdClass;
use function array_key_exists;
/**
* Describes PodcastIndex data of a RSS Feed
*/
class Feed extends Extension\AbstractFeed
{
/**
* Is the podcast locked (not available for indexing)?
*/
public function isLocked(): bool
{
if (isset($this->data['locked'])) {
return $this->data['locked'];
}
$locked = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/podcast:locked)');
if (! $locked) {
$locked = false;
}
$this->data['locked'] = $locked === 'yes';
return $this->data['locked'];
}
/**
* Get the owner of the podcast (for verification)
*/
public function getLockOwner(): ?string
{
if (isset($this->data['owner'])) {
return $this->data['owner'];
}
$owner = $this->xpath->evaluate('string(' . $this->getXpathPrefix() . '/podcast:locked/@owner)');
if (! $owner) {
$owner = null;
}
$this->data['owner'] = $owner;
return $this->data['owner'];
}
/**
* Get the entry funding link
*
* @psalm-return null|object{url: string, title: string}
*/
public function getFunding(): ?stdClass
{
if (array_key_exists('funding', $this->data)) {
return $this->data['funding'];
}
$funding = null;
$nodeList = $this->xpath->query($this->getXpathPrefix() . '/podcast:funding');
if ($nodeList->length > 0) {
$funding = new stdClass();
$funding->url = $nodeList->item(0)->getAttribute('url');
$funding->title = $nodeList->item(0)->nodeValue;
}
$this->data['funding'] = $funding;
return $this->data['funding'];
}
/**
* Register PodcastIndex namespace
*/
protected function registerNamespaces(): void
{
$this->xpath->registerNamespace(
'podcast',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Laminas\Feed\Reader\Extension\Slash;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
use function explode;
class Entry extends Extension\AbstractEntry
{
/**
* Get the entry section
*
* @return null|string
*/
public function getSection()
{
return $this->getData('section');
}
/**
* Get the entry department
*
* @return null|string
*/
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 null|mixed
*/
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,147 @@
<?php
namespace Laminas\Feed\Reader\Extension\Syndication;
use DateTime;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
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 null|DateTime
*/
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 null|mixed
*/
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,65 @@
<?php
namespace Laminas\Feed\Reader\Extension\Thread;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
class Entry extends Extension\AbstractEntry
{
/**
* Get the "in-reply-to" value
*
* @return void
*/
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 null|int
*/
public function getCommentCount()
{
return $this->getData('total');
}
/**
* Get the entry data specified by name
*
* @param string $name
* @return null|mixed
*/
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,43 @@
<?php
namespace Laminas\Feed\Reader\Extension\WellFormedWeb;
use Laminas\Feed\Reader\Extension;
use function array_key_exists;
class Entry extends Extension\AbstractEntry
{
/**
* Get the entry comment Uri
*
* @return null|string
*/
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,74 @@
<?php
namespace Laminas\Feed\Reader;
use function call_user_func_array;
use function method_exists;
use function sprintf;
/**
* Default implementation of ExtensionManagerInterface
*
* Decorator of ExtensionPluginManager.
*/
class ExtensionManager implements ExtensionManagerInterface
{
/** @var ExtensionPluginManager */
protected $pluginManager;
/**
* Seeds the extension manager with a plugin manager; if none provided,
* creates an instance.
*/
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,
self::class
));
}
return call_user_func_array([$this->pluginManager, $method], $args);
}
/**
* Get the named extension
*
* @param string $extension
* @return Extension\AbstractEntry|Extension\AbstractFeed
*/
public function get($extension)
{
return $this->pluginManager->get($extension);
}
/**
* Do we have the named extension?
*
* @param string $extension
* @return bool
*/
public function has($extension)
{
return $this->pluginManager->has($extension);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Laminas\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,243 @@
<?php
namespace Laminas\Feed\Reader;
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Zend\Feed\Reader\Extension\Atom\Entry;
use Zend\Feed\Reader\Extension\Atom\Feed;
use function get_class;
use function gettype;
use function is_object;
use function sprintf;
/**
* 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 implements ExtensionManagerInterface
{
/**
* Aliases for default set of extension classes
*
* @var array<array-key, string>
*/
protected $aliases = [
'atomentry' => Extension\Atom\Entry::class,
'atomEntry' => Extension\Atom\Entry::class,
'AtomEntry' => Extension\Atom\Entry::class,
'Atom\Entry' => Extension\Atom\Entry::class,
'atomfeed' => Extension\Atom\Feed::class,
'atomFeed' => Extension\Atom\Feed::class,
'AtomFeed' => Extension\Atom\Feed::class,
'Atom\Feed' => Extension\Atom\Feed::class,
'contententry' => Extension\Content\Entry::class,
'contentEntry' => Extension\Content\Entry::class,
'ContentEntry' => Extension\Content\Entry::class,
'Content\Entry' => Extension\Content\Entry::class,
'creativecommonsentry' => Extension\CreativeCommons\Entry::class,
'creativeCommonsEntry' => Extension\CreativeCommons\Entry::class,
'CreativeCommonsEntry' => Extension\CreativeCommons\Entry::class,
'CreativeCommons\Entry' => Extension\CreativeCommons\Entry::class,
'creativecommonsfeed' => Extension\CreativeCommons\Feed::class,
'creativeCommonsFeed' => Extension\CreativeCommons\Feed::class,
'CreativeCommonsFeed' => Extension\CreativeCommons\Feed::class,
'CreativeCommons\Feed' => Extension\CreativeCommons\Feed::class,
'dublincoreentry' => Extension\DublinCore\Entry::class,
'dublinCoreEntry' => Extension\DublinCore\Entry::class,
'DublinCoreEntry' => Extension\DublinCore\Entry::class,
'DublinCore\Entry' => Extension\DublinCore\Entry::class,
'dublincorefeed' => Extension\DublinCore\Feed::class,
'dublinCoreFeed' => Extension\DublinCore\Feed::class,
'DublinCoreFeed' => Extension\DublinCore\Feed::class,
'DublinCore\Feed' => Extension\DublinCore\Feed::class,
'googleplaypodcastentry' => Extension\GooglePlayPodcast\Entry::class,
'googlePlayPodcastEntry' => Extension\GooglePlayPodcast\Entry::class,
'GooglePlayPodcastEntry' => Extension\GooglePlayPodcast\Entry::class,
'GooglePlayPodcast\Entry' => Extension\GooglePlayPodcast\Entry::class,
'googleplaypodcastfeed' => Extension\GooglePlayPodcast\Feed::class,
'googlePlayPodcastFeed' => Extension\GooglePlayPodcast\Feed::class,
'GooglePlayPodcastFeed' => Extension\GooglePlayPodcast\Feed::class,
'GooglePlayPodcast\Feed' => Extension\GooglePlayPodcast\Feed::class,
'podcastentry' => Extension\Podcast\Entry::class,
'podcastEntry' => Extension\Podcast\Entry::class,
'PodcastEntry' => Extension\Podcast\Entry::class,
'Podcast\Entry' => Extension\Podcast\Entry::class,
'podcastfeed' => Extension\Podcast\Feed::class,
'podcastFeed' => Extension\Podcast\Feed::class,
'PodcastFeed' => Extension\Podcast\Feed::class,
'Podcast\Feed' => Extension\Podcast\Feed::class,
'podcastindexentry' => Extension\PodcastIndex\Entry::class,
'podcastIndexEntry' => Extension\PodcastIndex\Entry::class,
'PodcastIndexEntry' => Extension\PodcastIndex\Entry::class,
'PodcastIndex\Entry' => Extension\PodcastIndex\Entry::class,
'podcastindexfeed' => Extension\PodcastIndex\Feed::class,
'podcastIndexFeed' => Extension\PodcastIndex\Feed::class,
'PodcastIndexFeed' => Extension\PodcastIndex\Feed::class,
'PodcastIndex\Feed' => Extension\PodcastIndex\Feed::class,
'slashentry' => Extension\Slash\Entry::class,
'slashEntry' => Extension\Slash\Entry::class,
'SlashEntry' => Extension\Slash\Entry::class,
'Slash\Entry' => Extension\Slash\Entry::class,
'syndicationfeed' => Extension\Syndication\Feed::class,
'syndicationFeed' => Extension\Syndication\Feed::class,
'SyndicationFeed' => Extension\Syndication\Feed::class,
'Syndication\Feed' => Extension\Syndication\Feed::class,
'threadentry' => Extension\Thread\Entry::class,
'threadEntry' => Extension\Thread\Entry::class,
'ThreadEntry' => Extension\Thread\Entry::class,
'Thread\Entry' => Extension\Thread\Entry::class,
'wellformedwebentry' => Extension\WellFormedWeb\Entry::class,
'wellFormedWebEntry' => Extension\WellFormedWeb\Entry::class,
'WellFormedWebEntry' => Extension\WellFormedWeb\Entry::class,
'WellFormedWeb\Entry' => Extension\WellFormedWeb\Entry::class,
// Legacy Zend Framework aliases
Entry::class => Extension\Atom\Entry::class,
Feed::class => Extension\Atom\Feed::class,
\Zend\Feed\Reader\Extension\Content\Entry::class => Extension\Content\Entry::class,
\Zend\Feed\Reader\Extension\CreativeCommons\Entry::class => Extension\CreativeCommons\Entry::class,
\Zend\Feed\Reader\Extension\CreativeCommons\Feed::class => Extension\CreativeCommons\Feed::class,
\Zend\Feed\Reader\Extension\DublinCore\Entry::class => Extension\DublinCore\Entry::class,
\Zend\Feed\Reader\Extension\DublinCore\Feed::class => Extension\DublinCore\Feed::class,
\Zend\Feed\Reader\Extension\GooglePlayPodcast\Entry::class => Extension\GooglePlayPodcast\Entry::class,
\Zend\Feed\Reader\Extension\GooglePlayPodcast\Feed::class => Extension\GooglePlayPodcast\Feed::class,
\Zend\Feed\Reader\Extension\Podcast\Entry::class => Extension\Podcast\Entry::class,
\Zend\Feed\Reader\Extension\Podcast\Feed::class => Extension\Podcast\Feed::class,
\Zend\Feed\Reader\Extension\Slash\Entry::class => Extension\Slash\Entry::class,
\Zend\Feed\Reader\Extension\Syndication\Feed::class => Extension\Syndication\Feed::class,
\Zend\Feed\Reader\Extension\Thread\Entry::class => Extension\Thread\Entry::class,
\Zend\Feed\Reader\Extension\WellFormedWeb\Entry::class => Extension\WellFormedWeb\Entry::class,
// v2 normalized FQCNs
'zendfeedreaderextensionatomentry' => Extension\Atom\Entry::class,
'zendfeedreaderextensionatomfeed' => Extension\Atom\Feed::class,
'zendfeedreaderextensioncontententry' => Extension\Content\Entry::class,
'zendfeedreaderextensioncreativecommonsentry' => Extension\CreativeCommons\Entry::class,
'zendfeedreaderextensioncreativecommonsfeed' => Extension\CreativeCommons\Feed::class,
'zendfeedreaderextensiondublincoreentry' => Extension\DublinCore\Entry::class,
'zendfeedreaderextensiondublincorefeed' => Extension\DublinCore\Feed::class,
'zendfeedreaderextensiongoogleplaypodcastentry' => Extension\GooglePlayPodcast\Entry::class,
'zendfeedreaderextensiongoogleplaypodcastfeed' => Extension\GooglePlayPodcast\Feed::class,
'zendfeedreaderextensionpodcastentry' => Extension\Podcast\Entry::class,
'zendfeedreaderextensionpodcastfeed' => Extension\Podcast\Feed::class,
'zendfeedreaderextensionslashentry' => Extension\Slash\Entry::class,
'zendfeedreaderextensionsyndicationfeed' => Extension\Syndication\Feed::class,
'zendfeedreaderextensionthreadentry' => Extension\Thread\Entry::class,
'zendfeedreaderextensionwellformedwebentry' => Extension\WellFormedWeb\Entry::class,
];
/**
* Factories for default set of extension classes
*
* @var array<array-key, callable|string>
*/
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\GooglePlayPodcast\Entry::class => InvokableFactory::class,
Extension\GooglePlayPodcast\Feed::class => InvokableFactory::class,
Extension\Podcast\Entry::class => InvokableFactory::class,
Extension\Podcast\Feed::class => InvokableFactory::class,
Extension\PodcastIndex\Entry::class => InvokableFactory::class,
Extension\PodcastIndex\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.
'laminasfeedreaderextensionatomentry' => InvokableFactory::class,
'laminasfeedreaderextensionatomfeed' => InvokableFactory::class,
'laminasfeedreaderextensioncontententry' => InvokableFactory::class,
'laminasfeedreaderextensioncreativecommonsentry' => InvokableFactory::class,
'laminasfeedreaderextensioncreativecommonsfeed' => InvokableFactory::class,
'laminasfeedreaderextensiondublincoreentry' => InvokableFactory::class,
'laminasfeedreaderextensiondublincorefeed' => InvokableFactory::class,
'laminasfeedreaderextensiongoogleplaypodcastentry' => InvokableFactory::class,
'laminasfeedreaderextensiongoogleplaypodcastfeed' => InvokableFactory::class,
'laminasfeedreaderextensionpodcastentry' => InvokableFactory::class,
'laminasfeedreaderextensionpodcastfeed' => InvokableFactory::class,
'laminasfeedreaderextensionpodcastindexentry' => InvokableFactory::class,
'laminasfeedreaderextensionpodcastindexfeed' => InvokableFactory::class,
'laminasfeedreaderextensionslashentry' => InvokableFactory::class,
'laminasfeedreaderextensionsyndicationfeed' => InvokableFactory::class,
'laminasfeedreaderextensionthreadentry' => InvokableFactory::class,
'laminasfeedreaderextensionwellformedwebentry' => 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 $instance
* @return void
* @throws Exception\InvalidArgumentException If invalid.
*/
public function validate($instance)
{
if (
$instance instanceof Extension\AbstractEntry
|| $instance 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($instance) ? get_class($instance) : gettype($instance),
__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,319 @@
<?php
namespace Laminas\Feed\Reader\Feed;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Exception;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;
use function array_key_exists;
use function call_user_func_array;
use function count;
use function in_array;
use function method_exists;
use function sprintf;
use function strpos;
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;
/**
* 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;
/**
* Array of loaded extensions
*
* @var array
*/
protected $extensions = [];
/**
* Original Source URI (set if imported from a URI)
*
* @var string
*/
protected $originalSourceUri;
/**
* @param DOMDocument $domDocument The DOM object for the feed's XML
* @param null|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
* @return void
*/
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 null|string
*/
public function getOriginalSourceUri()
{
return $this->originalSourceUri;
}
/**
* Get the number of feed entries.
* Required by the Iterator interface.
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
return count($this->entries);
}
/**
* Return the current entry
*
* @return Reader\Entry\EntryInterface
*/
#[ReturnTypeWillChange]
public function current()
{
if (0 === strpos($this->getType(), '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
*/
#[ReturnTypeWillChange]
public function key()
{
return $this->entriesKey;
}
/**
* Move the feed pointer forward
*/
#[ReturnTypeWillChange]
public function next()
{
++$this->entriesKey;
}
/**
* Reset the pointer in the feed object
*/
#[ReturnTypeWillChange]
public function rewind()
{
$this->entriesKey = 0;
}
/**
* Check to see if the iterator is still valid
*
* @return bool
*/
#[ReturnTypeWillChange]
public function valid()
{
return 0 <= $this->entriesKey && $this->entriesKey < $this->count();
}
/** @return array */
public function getExtensions()
{
return $this->extensions;
}
/**
* @param string $method
* @param mixed[] $args
* @return mixed
*/
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 null|Reader\Extension\AbstractFeed
*/
public function getExtension($name)
{
if (array_key_exists($name . '\\Feed', $this->extensions)) {
return $this->extensions[$name . '\\Feed'];
}
return null;
}
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,401 @@
<?php
namespace Laminas\Feed\Reader\Feed;
use DateTime;
use DOMDocument;
use Laminas\Feed\Reader;
use function array_key_exists;
use function count;
use function is_array;
class Atom extends AbstractFeed
{
/**
* @param null|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 null|array<string, string>
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_array($authors[$index])
? $authors[$index]
: null;
}
/**
* 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 null|string
*/
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 null|DateTime
*/
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 null|DateTime
*/
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 void
*/
public function getLastBuildDate()
{
}
/**
* Get the feed description
*
* @return null|string
*/
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 null|string
*/
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 null|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 the feed language
*
* @return null|string
*/
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 null|string
*/
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 null|string
*/
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 null|array
*/
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 null|string
*/
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 null|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 an array of any supported PubSubHubbub endpoints
*
* @return null|array
*/
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
*
* @return void
*/
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,99 @@
<?php
namespace Laminas\Feed\Reader\Feed\Atom;
use DOMElement;
use DOMXPath;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Feed;
use function rtrim;
class Source extends Feed\Atom
{
/**
* Constructor: Create a Source object which is largely just a normal
* Laminas\Feed\Reader\AbstractFeed object only designed to retrieve feed level
* metadata from an Atom entry's source element.
*
* @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,103 @@
<?php
namespace Laminas\Feed\Reader\Feed;
use Countable;
use DateTime;
use Iterator;
use Laminas\Feed\Reader\Collection\Category;
interface FeedInterface extends Iterator, Countable
{
/**
* Get a single author
*
* @param int $index
* @return null|string
*/
public function getAuthor($index = 0);
/**
* Get an array with feed authors
*
* @return array
*/
public function getAuthors();
/**
* Get the copyright entry
*
* @return null|string
*/
public function getCopyright();
/**
* Get the feed creation date
*
* @return null|DateTime
*/
public function getDateCreated();
/**
* Get the feed modification date
*
* @return null|DateTime
*/
public function getDateModified();
/**
* Get the feed description
*
* @return null|string
*/
public function getDescription();
/**
* Get the feed generator entry
*
* @return null|string
*/
public function getGenerator();
/**
* Get the feed ID
*
* @return null|string
*/
public function getId();
/**
* Get the feed language
*
* @return null|string
*/
public function getLanguage();
/**
* Get a link to the HTML source
*
* @return null|string
*/
public function getLink();
/**
* Get a link to the XML feed
*
* @return null|string
*/
public function getFeedLink();
/**
* Get the feed title
*
* @return null|string
*/
public function getTitle();
/**
* Get all categories
*
* @return Category
*/
public function getCategories();
}
@@ -0,0 +1,724 @@
<?php
namespace Laminas\Feed\Reader\Feed;
use DateTime;
use DOMDocument;
use Laminas\Feed\Reader;
use Laminas\Feed\Reader\Collection;
use Laminas\Feed\Reader\Exception;
use function array_key_exists;
use function array_unique;
use function count;
use function is_array;
use function preg_match;
use function strtotime;
use function trim;
class Rss extends AbstractFeed
{
/**
* @param null|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 null|array<string, string>
*/
public function getAuthor($index = 0)
{
$authors = $this->getAuthors();
return isset($authors[$index]) && is_array($authors[$index])
? $authors[$index]
: null;
}
/**
* 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 null|string
*/
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 null|DateTime
*/
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,
];
foreach ($dateStandards as $standard) {
$date = DateTime::createFromFormat(
$standard,
$dateModified
);
if ($date instanceof DateTime) {
break;
}
}
if (! $date) {
throw new Exception\RuntimeException(
'Could not load date due to unrecognised'
. ' format (should follow RFC 822 or 2822).'
);
}
}
}
}
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
*
* @return DateTime
* @throws Exception\RuntimeException
*/
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 null|string
*/
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 null|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(/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 null|array
*/
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 null|string
*/
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 null|string
*/
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 null|string
*/
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 null|string
*/
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 null|string
*/
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 null|array
*/
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
*
* @return void
*/
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
*
* @return void
*/
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,185 @@
<?php
namespace Laminas\Feed\Reader;
use ArrayObject;
use DOMElement;
use DOMNodeList;
use Laminas\Feed\Uri;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;
use function array_filter;
use function array_pop;
use function explode;
use function implode;
use function ltrim;
use function sprintf;
use function strtolower;
use function trim;
class FeedSet extends ArrayObject
{
/** @var null|string */
public $rss;
/** @var null|string */
public $rdf;
/** @var null|string */
public $atom;
/**
* 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 string $uri
* @return void
*/
public function addLinks(DOMNodeList $links, $uri)
{
foreach ($links as $link) {
/** @var DOMElement $link */
if (
strtolower($link->getAttribute('rel')) !== 'alternate'
|| ! $link->getAttribute('type') || ! $link->getAttribute('href')
) {
continue;
}
if (null === $this->rss && $link->getAttribute('type') === 'application/rss+xml') {
$this->rss = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
} elseif (null === $this->atom && $link->getAttribute('type') === 'application/atom+xml') {
$this->atom = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
} elseif (null === $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),
'title' => $link->getAttribute('title'),
]);
}
}
/**
* Attempt to turn a relative URI into an absolute URI
*
* @param string $link
* @param null|string $uri OPTIONAL
* @return null|string absolutised link or null if invalid
*/
protected function absolutiseUri($link, $uri = null)
{
$linkUri = Uri::factory($link);
if ($linkUri->isAbsolute()) {
// invalid absolute link can not be recovered
return $linkUri->isValid() ? $link : null;
}
$scheme = 'http';
if ($uri !== null) {
$uri = Uri::factory($uri);
$scheme = $uri->getScheme() ?: $scheme;
}
if ($linkUri->getHost()) {
$link = $this->resolveSchemeRelativeUri($link, $scheme);
} elseif ($uri !== null) {
$link = $this->resolveRelativeUri($link, $scheme, $uri->getHost(), $uri->getPath());
}
if (! Uri::factory($link)->isValid()) {
return null;
}
return $link;
}
/**
* Resolves scheme relative link to absolute
*
* @param string $link
* @param string $scheme
* @return string
*/
private function resolveSchemeRelativeUri($link, $scheme)
{
$link = ltrim($link, '/');
return sprintf('%s://%s', $scheme, $link);
}
/**
* Resolves relative link to absolute
*
* @param string $link
* @param string $scheme
* @param string $host
* @param string $uriPath
* @return string
*/
private function resolveRelativeUri($link, $scheme, $host, $uriPath)
{
if ($link[0] !== '/') {
$link = $uriPath . '/' . $link;
}
return sprintf(
'%s://%s/%s',
$scheme,
$host,
$this->canonicalizePath($link)
);
}
/**
* Canonicalize relative path
*
* @param string $path
* @return string
*/
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
*/
#[ReturnTypeWillChange]
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,14 @@
<?php
namespace Laminas\Feed\Reader\Http;
interface ClientInterface
{
/**
* Make a GET request to a given URI
*
* @param string $uri
* @return ResponseInterface
*/
public function get($uri);
}
@@ -0,0 +1,25 @@
<?php
namespace Laminas\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
* @return HeaderAwareResponseInterface
*/
public function get($uri, array $headers = []);
}
@@ -0,0 +1,21 @@
<?php
namespace Laminas\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 null|mixed $default Default value to use if header is not present.
* @return string
*/
public function getHeaderLine($name, $default = null);
}
@@ -0,0 +1,114 @@
<?php
namespace Laminas\Feed\Reader\Http;
use Laminas\Feed\Reader\Exception;
use Laminas\Http\Client as LaminasHttpClient;
use Laminas\Http\Headers;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_numeric;
use function is_object;
use function is_string;
use function sprintf;
class LaminasHttpClientDecorator implements HeaderAwareClientInterface
{
/** @var LaminasHttpClient */
private $client;
public function __construct(LaminasHttpClient $client)
{
$this->client = $client;
}
/**
* @return LaminasHttpClient
*/
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.
*
* @return void
*/
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',
self::class,
$name
));
}
if (! is_array($values)) {
throw new Exception\InvalidArgumentException(sprintf(
'Header values provided to %s::get must be arrays of values; received %s',
self::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',
self::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.
*
* @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,56 @@
<?php
namespace Laminas\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;
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,171 @@
<?php
namespace Laminas\Feed\Reader\Http;
use Laminas\Feed\Reader\Exception;
use function get_class;
use function gettype;
use function intval;
use function is_numeric;
use function is_object;
use function is_string;
use function method_exists;
use function sprintf;
use function strtolower;
use function trim;
class Response implements HeaderAwareResponseInterface
{
/** @var string */
private $body;
/** @var array */
private $headers;
/** @var int */
private $statusCode;
/**
* @param int $statusCode
* @param object|string $body
* @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 $this->headers[$normalizedName] ?? $default;
}
/**
* Validate that we have a status code argument that will work for our context.
*
* @param int $statusCode
* @throws Exception\InvalidArgumentException For arguments not castable
* to integer HTTP status codes.
* @return void
*/
private function validateStatusCode($statusCode)
{
if (! is_numeric($statusCode) || (is_string($statusCode) && trim($statusCode) !== $statusCode)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a numeric status code; received %s',
self::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',
self::class,
$statusCode
));
}
if (intval($statusCode) !== $statusCode) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects an integer status code; received %s',
self::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.
* @return void
*/
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',
self::class,
is_object($body) ? get_class($body) : gettype($body)
));
}
/**
* Validate header values.
*
* @throws Exception\InvalidArgumentException
* @return void
*/
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',
self::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',
self::class,
is_object($value) ? get_class($value) : gettype($value),
$name
));
}
}
}
/**
* Normalize header names to lowercase.
*
* @return array
*/
private function normalizeHeaders(array $headers)
{
$normalized = [];
foreach ($headers as $name => $value) {
$normalized[strtolower($name)] = $value;
}
return $normalized;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Laminas\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,790 @@
<?php
namespace Laminas\Feed\Reader;
use DOMDocument;
use DOMXPath;
use Laminas\Cache\Storage\StorageInterface as CacheStorage;
use Laminas\Feed\Reader\Exception\InvalidHttpClientException;
use Laminas\Http as LaminasHttp;
use Laminas\Stdlib\ErrorHandler;
use function array_unique;
use function file_get_contents;
use function function_exists;
use function get_class;
use function gettype;
use function in_array;
use function ini_restore;
use function ini_set;
use function is_object;
use function is_string;
use function libxml_disable_entity_loader;
use function libxml_get_last_error;
use function libxml_use_internal_errors;
use function md5;
use function serialize;
use function sprintf;
use function strlen;
use function strpos;
use function trigger_error;
use function trim;
use function unserialize;
use const E_NOTICE;
use const E_USER_NOTICE;
use const E_WARNING;
use const LIBXML_VERSION;
use const XML_DOCUMENT_TYPE_NODE;
class Reader implements ReaderImportInterface
{
/**
* Namespace constants
*/
public const NAMESPACE_ATOM_03 = 'http://purl.org/atom/ns#';
public const NAMESPACE_ATOM_10 = 'http://www.w3.org/2005/Atom';
public const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
public const NAMESPACE_RSS_090 = 'http://my.netscape.com/rdf/simple/0.9/';
public const NAMESPACE_RSS_10 = 'http://purl.org/rss/1.0/';
/**
* Feed type constants
*/
public const TYPE_ANY = 'any';
public const TYPE_ATOM_03 = 'atom-03';
public const TYPE_ATOM_10 = 'atom-10';
public const TYPE_ATOM_10_ENTRY = 'atom-10-entry';
public const TYPE_ATOM_ANY = 'atom';
public const TYPE_RSS_090 = 'rss-090';
public const TYPE_RSS_091 = 'rss-091';
public const TYPE_RSS_091_NETSCAPE = 'rss-091n';
public const TYPE_RSS_091_USERLAND = 'rss-091u';
public const TYPE_RSS_092 = 'rss-092';
public const TYPE_RSS_093 = 'rss-093';
public const TYPE_RSS_094 = 'rss-094';
public const TYPE_RSS_10 = 'rss-10';
public const TYPE_RSS_20 = 'rss-20';
public const TYPE_RSS_ANY = 'rss';
/**
* Cache instance
*
* @var CacheStorage
*/
protected static $cache;
/**
* HTTP client object to use for retrieving feeds
*
* @var Http\ClientInterface
*/
protected static $httpClient;
/**
* Override HTTP PUT and DELETE request methods?
*
* @var bool
*/
protected static $httpMethodOverride = false;
/** @var bool */
protected static $httpConditionalGet = false;
/** @var null|ExtensionManagerInterface */
protected static $extensionManager;
/** @var array<string, string[]> */
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',
],
];
/**
* Disable the ability to load external XML entities based on libxml version
*
* If we are using libxml < 2.9, unsafe XML entity loading must be
* disabled with a flag.
*
* If we are using libxml >= 2.9, XML entity loading is disabled by default.
*
* @param bool $flag
* @return bool
*/
public static function disableEntityLoader($flag = true)
{
if (LIBXML_VERSION < 20900) {
return libxml_disable_entity_loader($flag);
}
return $flag;
}
/**
* Get the Feed cache
*
* @return CacheStorage
*/
public static function getCache()
{
return static::$cache;
}
/**
* Set the feed 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 Http\ClientInterface|LaminasHttp\Client $httpClient
* @return void
*/
public static function setHttpClient($httpClient)
{
if ($httpClient instanceof LaminasHttp\Client) {
$httpClient = new Http\LaminasHttpClientDecorator($httpClient);
}
if (! $httpClient instanceof Http\ClientInterface) {
throw new InvalidHttpClientException();
}
static::$httpClient = $httpClient;
}
/**
* Gets the HTTP client object. If none is set, a new LaminasHttp\Client will be used.
*
* @return Http\ClientInterface
*/
public static function getHttpClient()
{
if (! static::$httpClient) {
static::$httpClient = new Http\LaminasHttpClientDecorator(new LaminasHttp\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 null|string $etag OPTIONAL Last received ETag for this resource
* @param null|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 = 'Laminas_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 ((int) $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
* @return Feed\FeedInterface
* @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 ($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);
$disableEntityLoaderFlag = self::disableEntityLoader();
$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'
);
}
}
self::disableEntityLoader($disableEntityLoaderFlag);
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 (0 === strpos($type, 'rss')) {
$reader = new Feed\Rss($dom, $type);
} elseif (8 === strpos($type, 'entry')) {
$reader = new Entry\Atom($dom->documentElement, 0, self::TYPE_ATOM_10);
} elseif (0 === strpos($type, '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 Laminas\Feed\Reader can parse.'
);
}
return $reader;
}
/**
* Imports a feed from a file located at $filename.
*
* @param string $filename
* @return Feed\FeedInterface
* @throws Exception\RuntimeException
*/
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 string $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);
$disableEntityLoaderFlag = self::disableEntityLoader();
$dom = new DOMDocument();
$status = $dom->loadHTML(trim($responseHtml));
self::disableEntityLoader($disableEntityLoaderFlag);
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 string|DOMDocument|Feed\AbstractFeed $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);
$disableEntityLoaderFlag = self::disableEntityLoader();
$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'
);
}
}
self::disableEntityLoader($disableEntityLoaderFlag);
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 Laminas\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) {
return $specOnly === true ? self::TYPE_ATOM_10 : 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
*
* @return void
*/
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)
{
if (! static::hasExtension($name)) {
throw new Exception\RuntimeException(sprintf(
'Could not load extension "%s" using Plugin Loader.'
. ' Check prefix paths are configured and extension exists.',
$name
));
}
// Return early if already registered.
if (static::isRegistered($name)) {
return;
}
$manager = static::getExtensionManager();
$feedName = $name . '\Feed';
if ($manager->has($feedName)) {
static::$extensions['feed'][] = $feedName;
}
$entryName = $name . '\Entry';
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');
static::registerExtension('Podcast');
// Added in 2.10.0; check for it conditionally
static::hasExtension('GooglePlayPodcast')
? static::registerExtension('GooglePlayPodcast')
: trigger_error(
sprintf(
'Please update your %1$s\ExtensionManagerInterface implementation to add entries for'
. ' %1$s\Extension\GooglePlayPodcast\Entry and %1$s\Extension\GooglePlayPodcast\Feed.',
__NAMESPACE__
),
E_USER_NOTICE
);
// Added in development; check for it conditionally
static::hasExtension('PodcastIndex')
? static::registerExtension('PodcastIndex')
: trigger_error(
sprintf(
'Please update your %1$s\ExtensionManagerInterface implementation to add entries for'
. ' %1$s\Extension\PodcastIndex\Entry and %1$s\Extension\PodcastIndex\Feed.',
__NAMESPACE__
),
E_USER_NOTICE
);
}
/**
* Utility method to apply array_unique operation to a multidimensional
* 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;
}
/**
* Does the extension manager have the named extension?
*
* This method exists to allow us to test if an extension is present in the
* extension manager. It may be used by registerExtension() to determine if
* the extension has items present in the manager, or by
* registerCoreExtension() to determine if the core extension has entries
* in the extension manager. In the latter case, this can be useful when
* adding new extensions in a minor release, as custom extension manager
* implementations may not yet have an entry for the extension, which would
* then otherwise cause registerExtension() to fail.
*
* @param string $name
* @return bool
*/
protected static function hasExtension($name)
{
$feedName = $name . '\Feed';
$entryName = $name . '\Entry';
$manager = static::getExtensionManager();
return $manager->has($feedName) || $manager->has($entryName);
}
}
@@ -0,0 +1,51 @@
<?php
namespace Laminas\Feed\Reader;
interface ReaderImportInterface
{
/**
* Import a feed by providing a URI
*
* @param string $uri The URI to the feed
* @param null|string $etag OPTIONAL Last received ETag for this resource
* @param null|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
* @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
* @return Feed\FeedInterface
* @throws Exception\RuntimeException
*/
public static function importFile($filename);
}
@@ -0,0 +1,94 @@
<?php
namespace Laminas\Feed\Reader;
use Laminas\Feed\Reader\Exception\InvalidArgumentException;
use function array_key_exists;
use function is_a;
use function is_string;
use function sprintf;
class StandaloneExtensionManager implements ExtensionManagerInterface
{
/** @var array<string, class-string> */
private $extensions = [
'Atom\Entry' => Extension\Atom\Entry::class,
'Atom\Feed' => Extension\Atom\Feed::class,
'Content\Entry' => Extension\Content\Entry::class,
'CreativeCommons\Entry' => Extension\CreativeCommons\Entry::class,
'CreativeCommons\Feed' => Extension\CreativeCommons\Feed::class,
'DublinCore\Entry' => Extension\DublinCore\Entry::class,
'DublinCore\Feed' => Extension\DublinCore\Feed::class,
'GooglePlayPodcast\Entry' => Extension\GooglePlayPodcast\Entry::class,
'GooglePlayPodcast\Feed' => Extension\GooglePlayPodcast\Feed::class,
'Podcast\Entry' => Extension\Podcast\Entry::class,
'Podcast\Feed' => Extension\Podcast\Feed::class,
'PodcastIndex\Entry' => Extension\PodcastIndex\Entry::class,
'PodcastIndex\Feed' => Extension\PodcastIndex\Feed::class,
'Slash\Entry' => Extension\Slash\Entry::class,
'Syndication\Feed' => Extension\Syndication\Feed::class,
'Thread\Entry' => Extension\Thread\Entry::class,
'WellFormedWeb\Entry' => Extension\WellFormedWeb\Entry::class,
];
/**
* 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();
}
/**
* Add an extension.
*
* @param string $name
* @param string $class
* @return void
*/
public function add($name, $class)
{
if (
is_string($class)
&& (is_a($class, Extension\AbstractEntry::class, true)
|| is_a($class, Extension\AbstractFeed::class, true))
) {
$this->extensions[$name] = $class;
return;
}
throw new InvalidArgumentException(sprintf(
'Plugin of type %s is invalid; must implement %2$s\Extension\AbstractFeed '
. 'or %2$s\Extension\AbstractEntry',
$class,
__NAMESPACE__
));
}
/**
* Remove an extension.
*
* @param string $name
* @return void
*/
public function remove($name)
{
unset($this->extensions[$name]);
}
}
+165
View File
@@ -0,0 +1,165 @@
<?php
namespace Laminas\Feed;
use function in_array;
use function parse_url;
use function strpos;
class Uri
{
/** @var null|string */
protected $fragment;
/** @var null|string */
protected $host;
/** @var null|string */
protected $pass;
/** @var null|string */
protected $path;
/** @var null|int */
protected $port;
/** @var null|string */
protected $query;
/** @var null|string */
protected $scheme;
/** @var null|string */
protected $user;
/** @var null|bool */
protected $valid;
/**
* Valid schemes
*
* @var string[]
*/
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 = $parsed['scheme'] ?? '';
$this->host = $parsed['host'] ?? '';
$this->port = $parsed['port'] ?? null;
$this->user = $parsed['user'] ?? null;
$this->pass = $parsed['pass'] ?? null;
$this->path = $parsed['path'] ?? '';
$this->query = $parsed['query'] ?? null;
$this->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, true)) {
return false;
}
if ($this->host) {
if ($this->path && 0 !== strpos($this->path, '/')) {
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 (0 === strpos($this->path, '//')) {
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 ! empty($this->scheme);
}
}
@@ -0,0 +1,917 @@
<?php
namespace Laminas\Feed\Writer;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Laminas\Feed\Uri;
use Laminas\Validator;
use function array_key_exists;
use function date;
use function in_array;
use function is_array;
use function is_int;
use function is_numeric;
use function is_string;
use function preg_match;
use function sprintf;
use function strlen;
use function strtolower;
use function strtotime;
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;
/** @var Extension\RendererInterface[] */
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
*
* @return $this
* @throws Exception\InvalidArgumentException If any value of $author not follow the format.
*/
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
*
* @return $this
*/
public function addAuthors(array $authors)
{
foreach ($authors as $author) {
$this->addAuthor($author);
}
return $this;
}
/**
* Set the copyright entry
*
* @param string $copyright
* @return $this
* @throws Exception\InvalidArgumentException
*/
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 DateTime|DateTimeImmutable|int|null|string $date
* @return self
* @throws Exception\InvalidArgumentException
*/
public function setDateCreated($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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 DateTime|DateTimeImmutable|int|null|string $date
* @return self
* @throws Exception\InvalidArgumentException
*/
public function setDateModified($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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 DateTime|DateTimeImmutable|int|null|string $date
* @return self
* @throws Exception\InvalidArgumentException
*/
public function setLastBuildDate($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setId($id)
{
// phpcs:disable Generic.Files.LineLength.TooLong
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'
);
}
// phpcs:enable Generic.Files.LineLength.TooLong
$this->data['id'] = $id;
return $this;
}
/**
* 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.
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setTitle($title)
{
if ((empty($title) && ! is_numeric($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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
*
* @return $this
*/
public function addHubs(array $urls)
{
foreach ($urls as $url) {
$this->addHub($url);
}
return $this;
}
/**
* Add a feed category
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
*
* @return $this
*/
public function addCategories(array $categories)
{
foreach ($categories as $category) {
$this->addCategory($category);
}
return $this;
}
/**
* Get a single author
*
* @param int $index
* @return null|string
*/
public function getAuthor($index = 0)
{
if (isset($this->data['authors'][$index])) {
return $this->data['authors'][$index];
}
return null;
}
/**
* Get an array with feed authors
*
* @return null|array
*/
public function getAuthors()
{
if (! array_key_exists('authors', $this->data)) {
return null;
}
return $this->data['authors'];
}
/**
* Get the copyright entry
*
* @return null|string
*/
public function getCopyright()
{
if (! array_key_exists('copyright', $this->data)) {
return null;
}
return $this->data['copyright'];
}
/**
* Get the feed creation date
*
* @return null|string
*/
public function getDateCreated()
{
if (! array_key_exists('dateCreated', $this->data)) {
return null;
}
return $this->data['dateCreated'];
}
/**
* Get the feed modification date
*
* @return null|string
*/
public function getDateModified()
{
if (! array_key_exists('dateModified', $this->data)) {
return null;
}
return $this->data['dateModified'];
}
/**
* Get the feed last-build date
*
* @return null|string
*/
public function getLastBuildDate()
{
if (! array_key_exists('lastBuildDate', $this->data)) {
return null;
}
return $this->data['lastBuildDate'];
}
/**
* Get the feed description
*
* @return null|string
*/
public function getDescription()
{
if (! array_key_exists('description', $this->data)) {
return null;
}
return $this->data['description'];
}
/**
* Get the feed generator entry
*
* @return null|string
*/
public function getGenerator()
{
if (! array_key_exists('generator', $this->data)) {
return null;
}
return $this->data['generator'];
}
/**
* Get the feed ID
*
* @return null|string
*/
public function getId()
{
if (! array_key_exists('id', $this->data)) {
return null;
}
return $this->data['id'];
}
/**
* Get the feed image URI
*
* @return null|array
*/
public function getImage()
{
if (! array_key_exists('image', $this->data)) {
return null;
}
return $this->data['image'];
}
/**
* Get the feed language
*
* @return null|string
*/
public function getLanguage()
{
if (! array_key_exists('language', $this->data)) {
return null;
}
return $this->data['language'];
}
/**
* Get a link to the HTML source
*
* @return null|string
*/
public function getLink()
{
if (! array_key_exists('link', $this->data)) {
return null;
}
return $this->data['link'];
}
/**
* Get a link to the XML feed
*
* @return null|string
*/
public function getFeedLinks()
{
if (! array_key_exists('feedLinks', $this->data)) {
return null;
}
return $this->data['feedLinks'];
}
/**
* Get the feed title
*
* @return null|string
*/
public function getTitle()
{
if (! array_key_exists('title', $this->data)) {
return null;
}
return $this->data['title'];
}
/**
* Get the feed character encoding
*
* @return null|string
*/
public function getEncoding()
{
if (! array_key_exists('encoding', $this->data)) {
return 'UTF-8';
}
return $this->data['encoding'];
}
/**
* Get the feed's base url
*
* @return null|string
*/
public function getBaseUrl()
{
if (! array_key_exists('baseUrl', $this->data)) {
return null;
}
return $this->data['baseUrl'];
}
/**
* Get the URLs used as Pubsubhubbub hubs endpoints
*
* @return null|string
*/
public function getHubs()
{
if (! array_key_exists('hubs', $this->data)) {
return null;
}
return $this->data['hubs'];
}
/**
* Get the feed categories
*
* @return null|string
*/
public function getCategories()
{
if (! array_key_exists('categories', $this->data)) {
return null;
}
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 $this
*/
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 $this
*/
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 {
$callback = [$extension, $method];
return $callback(...$args);
} catch (Exception\BadMethodCallException $e) {
}
}
throw new Exception\BadMethodCallException(
'Method: ' . $method . ' does not exist and could not be located on a registered Extension'
);
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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;
}
/**
* Load extensions from Laminas\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());
}
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,240 @@
<?php
namespace Laminas\Feed\Writer;
use DateTime;
use DateTimeInterface;
use Laminas\Feed\Uri;
use function array_key_exists;
use function is_int;
use function is_string;
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;
/**
* Set the feed character encoding
*
* @param null|string $encoding
* @return static
* @throws Exception\InvalidArgumentException
*/
public function setEncoding($encoding)
{
if (empty($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 null|string
*/
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 $this
*/
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 $this
*/
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 string $reference
* @return $this
* @throws Exception\InvalidArgumentException
*/
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|int|DateTimeInterface $date
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setWhen($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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 null|string
*/
public function getBy()
{
if (! array_key_exists('by', $this->data)) {
return null;
}
return $this->data['by'];
}
/**
* @param string $comment
* @return $this
*/
public function setComment($comment)
{
$this->data['comment'] = $comment;
return $this;
}
/**
* @return null|string
*/
public function getComment()
{
if (! array_key_exists('comment', $this->data)) {
return null;
}
return $this->data['comment'];
}
}
@@ -0,0 +1,805 @@
<?php
namespace Laminas\Feed\Writer;
use BadMethodCallException;
use DateTime;
use DateTimeInterface;
use Laminas\Feed\Uri;
use function array_key_exists;
use function get_class;
use function gettype;
use function in_array;
use function is_int;
use function is_numeric;
use function is_object;
use function is_string;
use function sprintf;
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;
/**
* 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
*
* @return $this
* @throws Exception\InvalidArgumentException If any value of $author not follow the format.
*/
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
*
* @return $this
*/
public function addAuthors(array $authors)
{
foreach ($authors as $author) {
$this->addAuthor($author);
}
return $this;
}
/**
* Set the feed character encoding
*
* @param string $encoding
* @return $this
* @throws Exception\InvalidArgumentException
*/
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 null|string
*/
public function getEncoding()
{
if (! array_key_exists('encoding', $this->data)) {
return 'UTF-8';
}
return $this->data['encoding'];
}
/**
* Set the copyright entry
*
* @param string $copyright
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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|DateTimeInterface $date
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setDateCreated($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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|DateTimeInterface $date
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setDateModified($date = null)
{
if ($date === null) {
$date = new DateTime();
}
if (is_int($date)) {
$date = new DateTime('@' . $date);
}
if (! $date instanceof DateTimeInterface) {
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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|float|string $count
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setCommentCount($count)
{
// phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
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
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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".
*
* @return $this
*/
public function setCommentFeedLinks(array $links)
{
foreach ($links as $link) {
$this->setCommentFeedLink($link);
}
return $this;
}
/**
* Set the feed title
*
* @param string $title
* @return $this
* @throws Exception\InvalidArgumentException
*/
public function setTitle($title)
{
if ((empty($title) && ! is_numeric($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 null|string
*/
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
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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
*
* @return $this
*/
public function addCategories(array $categories)
{
foreach ($categories as $category) {
$this->addCategory($category);
}
return $this;
}
/**
* Get the entry categories
*
* @return null|string
*/
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.
*
* @return $this
* @throws Exception\InvalidArgumentException
*/
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 $this
*/
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 null|object
*/
public function getExtension($name)
{
$extensionClassName = $name . '\\Entry';
if (! isset($this->extensions[$extensionClassName])) {
return null;
}
if (! is_object($this->extensions[$extensionClassName])) {
throw new Exception\RuntimeException(sprintf(
'Extension is of invalid type; expected object, received "%s"',
gettype($this->extensions[$extensionClassName])
));
}
return $this->extensions[$extensionClassName];
}
/**
* 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 $this
*/
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 {
$callback = [$extension, $method];
return $callback(...$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 Laminas\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 Laminas\Feed\Writer\Entry object representing a new entry/item
* the feed data container's internal group of entries.
*
* @return $this
*/
public function setSource(Source $source)
{
$this->data['source'] = $source;
return $this;
}
/**
* @return null|Source
*/
public function getSource()
{
if (! isset($this->data['source'])) {
return null;
}
if (! $this->data['source'] instanceof Source) {
throw new Exception\RuntimeException(sprintf(
'Entry source is of invalid type ("%s")',
is_object($this->data['source']) ? get_class($this->data['source']) : gettype($this->data['source'])
));
}
return $this->data['source'];
}
/**
* Load extensions from Laminas\Feed\Writer\Writer
*
* @return void
*/
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
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());
}
}
}
@@ -0,0 +1,14 @@
<?php
namespace Laminas\Feed\Writer\Exception;
use Laminas\Feed\Exception;
/**
* Feed exceptions
*
* Class to represent exceptions that occur during Feed operations.
*/
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
{
}
@@ -0,0 +1,12 @@
<?php
namespace Laminas\Feed\Writer\Exception;
/**
* Feed exceptions
*
* Interface to represent exceptions that occur during Feed operations.
*/
interface ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
namespace Laminas\Feed\Writer\Exception;
use Laminas\Feed\Exception;
/**
* Feed exceptions
*
* Class to represent exceptions that occur during Feed operations.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Laminas\Feed\Writer\Exception;
use Laminas\Feed\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,141 @@
<?php
namespace Laminas\Feed\Writer\Extension;
use DOMDocument;
use DOMElement;
abstract class AbstractRenderer implements RendererInterface
{
/** @var DOMDocument */
protected $dom;
/** @var mixed */
protected $entry;
/** @var DOMElement */
protected $base;
/** @var mixed */
protected $container;
/** @var string */
protected $type;
/** @var DOMElement */
protected $rootElement;
/**
* Encoding of all text values
*
* @var string
*/
protected $encoding = 'UTF-8';
/**
* Set the data container
*
* @param mixed $container
* @return $this
*/
public function setDataContainer($container)
{
$this->container = $container;
return $this;
}
/**
* Set feed encoding
*
* @param string $enc
* @return $this
*/
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
*
* @return $this
*/
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 $this
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get feedtype
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Set root element of document
*
* @return $this
*/
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
*/
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
abstract protected function _appendNamespaces();
}
@@ -0,0 +1,103 @@
<?php
namespace Laminas\Feed\Writer\Extension\Atom\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\Feed\Writer\Extension;
use function strtolower;
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();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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
*
* @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
*
* @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;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,72 @@
<?php
namespace Laminas\Feed\Writer\Extension\Content\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\Feed\Writer\Extension;
use function strtolower;
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();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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
*
* @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;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,77 @@
<?php
namespace Laminas\Feed\Writer\Extension\DublinCore\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\Feed\Writer\Extension;
use function array_key_exists;
use function strtolower;
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();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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
*
* @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;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,77 @@
<?php
namespace Laminas\Feed\Writer\Extension\DublinCore\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\Feed\Writer\Extension;
use function array_key_exists;
use function strtolower;
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();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* 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
*
* @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;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,154 @@
<?php
namespace Laminas\Feed\Writer\Extension\GooglePlayPodcast;
use Laminas\Feed\Writer;
use Laminas\Stdlib\StringUtils;
use Laminas\Stdlib\StringWrapper\StringWrapperInterface;
use function array_key_exists;
use function ctype_alpha;
use function in_array;
use function lcfirst;
use function method_exists;
use function strlen;
use function substr;
use function ucfirst;
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 $this
*/
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 $value
* @throws Writer\Exception\InvalidArgumentException
* @return void
*/
public function setPlayPodcastBlock($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;
}
/**
* Set "explicit" flag
*
* @param bool $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastExplicit($value)
{
if (! in_array($value, ['yes', 'no', 'clean'], true)) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "explicit" may only be one of "yes", "no" or "clean"'
);
}
$this->data['explicit'] = $value;
return $this;
}
/**
* Set episode description
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastDescription($value)
{
if ($this->stringWrapper->strlen($value) > 4000) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "description" may only contain a maximum of 4000 characters'
);
}
$this->data['description'] = $value;
return $this;
}
/**
* Overloading to itunes specific setters
*
* @param string $method
* @return mixed
* @throws Writer\Exception\BadMethodCallException
*/
public function __call($method, array $params)
{
$point = lcfirst(substr($method, 14));
if (
! method_exists($this, 'setPlayPodcast' . ucfirst($point))
&& ! method_exists($this, 'addPlayPodcast' . 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];
}
}
@@ -0,0 +1,246 @@
<?php
namespace Laminas\Feed\Writer\Extension\GooglePlayPodcast;
use Laminas\Feed\Uri;
use Laminas\Feed\Writer;
use Laminas\Stdlib\StringUtils;
use Laminas\Stdlib\StringWrapper\StringWrapperInterface;
use function array_key_exists;
use function ctype_alpha;
use function in_array;
use function is_array;
use function is_string;
use function lcfirst;
use function method_exists;
use function strlen;
use function substr;
use function ucfirst;
class Feed
{
/**
* 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 $this
*/
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 $value
* @return self
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastBlock($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;
return $this;
}
/**
* Add feed authors
*
* @return $this
*/
public function addPlayPodcastAuthors(array $values)
{
foreach ($values as $value) {
$this->addPlayPodcastAuthor($value);
}
return $this;
}
/**
* Add feed author
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function addPlayPodcastAuthor($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 feed categories
*
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastCategories(array $values)
{
if (! isset($this->data['categories'])) {
$this->data['categories'] = [];
}
foreach ($values as $key => $value) {
if (! is_array($value)) {
if ($this->stringWrapper->strlen($value) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][] = $value;
} else {
if ($this->stringWrapper->strlen($key) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][$key] = [];
foreach ($value as $val) {
if ($this->stringWrapper->strlen($val) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][$key][] = $val;
}
}
}
return $this;
}
/**
* Set feed image (icon)
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastImage($value)
{
if (! is_string($value) || ! Uri::factory($value)->isValid()) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "image" may only be a valid URI/IRI'
);
}
$this->data['image'] = $value;
return $this;
}
/**
* Set "explicit" flag
*
* @param bool $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastExplicit($value)
{
if (! in_array($value, ['yes', 'no', 'clean'], true)) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "explicit" may only be one of "yes", "no" or "clean"'
);
}
$this->data['explicit'] = $value;
return $this;
}
/**
* Set podcast description
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setPlayPodcastDescription($value)
{
if ($this->stringWrapper->strlen($value) > 4000) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "description" may only contain a maximum of 4000 characters'
);
}
$this->data['description'] = $value;
return $this;
}
/**
* Overloading: proxy to internal setters
*
* @param string $method
* @return mixed
* @throws Writer\Exception\BadMethodCallException
*/
public function __call($method, array $params)
{
$point = lcfirst(substr($method, 14));
if (
! method_exists($this, 'setPlayPodcast' . ucfirst($point))
&& ! method_exists($this, 'addPlayPodcast' . 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];
}
}
@@ -0,0 +1,105 @@
<?php
namespace Laminas\Feed\Writer\Extension\GooglePlayPodcast\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\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()
{
$this->_setBlock($this->dom, $this->base);
$this->_setExplicit($this->dom, $this->base);
$this->_setDescription($this->dom, $this->base);
if ($this->called) {
$this->_appendNamespaces();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* Append namespaces to entry root
*
* @return void
*/
protected function _appendNamespaces()
{
$this->getRootElement()->setAttribute(
'xmlns:googleplay',
'http://www.google.com/schemas/play-podcasts/1.0'
);
}
/**
* Set itunes block
*
* @return void
*/
protected function _setBlock(DOMDocument $dom, DOMElement $root)
{
$block = $this->getDataContainer()->getPlayPodcastBlock();
if ($block === null) {
return;
}
$el = $dom->createElement('googleplay:block');
$text = $dom->createTextNode($block);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
/**
* Set explicit flag
*
* @return void
*/
protected function _setExplicit(DOMDocument $dom, DOMElement $root)
{
$explicit = $this->getDataContainer()->getPlayPodcastExplicit();
if ($explicit === null) {
return;
}
$el = $dom->createElement('googleplay:explicit');
$text = $dom->createTextNode($explicit);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
/**
* Set episode description
*
* @return void
*/
protected function _setDescription(DOMDocument $dom, DOMElement $root)
{
$description = $this->getDataContainer()->getPlayPodcastDescription();
if (! $description) {
return;
}
$el = $dom->createElement('googleplay:description');
$text = $dom->createTextNode($description);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,177 @@
<?php
namespace Laminas\Feed\Writer\Extension\GooglePlayPodcast\Renderer;
use DOMDocument;
use DOMElement;
use Laminas\Feed\Writer\Extension;
use function is_array;
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()
{
$this->_setAuthors($this->dom, $this->base);
$this->_setBlock($this->dom, $this->base);
$this->_setCategories($this->dom, $this->base);
$this->_setImage($this->dom, $this->base);
$this->_setExplicit($this->dom, $this->base);
$this->_setDescription($this->dom, $this->base);
if ($this->called) {
$this->_appendNamespaces();
}
}
// phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
/**
* Append feed namespaces
*
* @return void
*/
protected function _appendNamespaces()
{
$this->getRootElement()->setAttribute(
'xmlns:googleplay',
'http://www.google.com/schemas/play-podcasts/1.0'
);
}
/**
* Set feed authors
*
* @return void
*/
protected function _setAuthors(DOMDocument $dom, DOMElement $root)
{
$authors = $this->getDataContainer()->getPlayPodcastAuthors();
if (! $authors || empty($authors)) {
return;
}
foreach ($authors as $author) {
$el = $dom->createElement('googleplay:author');
$text = $dom->createTextNode($author);
$el->appendChild($text);
$root->appendChild($el);
}
$this->called = true;
}
/**
* Set feed itunes block
*
* @return void
*/
protected function _setBlock(DOMDocument $dom, DOMElement $root)
{
$block = $this->getDataContainer()->getPlayPodcastBlock();
if ($block === null) {
return;
}
$el = $dom->createElement('googleplay:block');
$text = $dom->createTextNode($block);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
/**
* Set feed categories
*
* @return void
*/
protected function _setCategories(DOMDocument $dom, DOMElement $root)
{
$cats = $this->getDataContainer()->getPlayPodcastCategories();
if (! $cats || empty($cats)) {
return;
}
foreach ($cats as $key => $cat) {
if (! is_array($cat)) {
$el = $dom->createElement('googleplay:category');
$el->setAttribute('text', $cat);
$root->appendChild($el);
} else {
$el = $dom->createElement('googleplay:category');
$el->setAttribute('text', $key);
$root->appendChild($el);
foreach ($cat as $subcat) {
$el2 = $dom->createElement('googleplay:category');
$el2->setAttribute('text', $subcat);
$el->appendChild($el2);
}
}
}
$this->called = true;
}
/**
* Set feed image (icon)
*
* @return void
*/
protected function _setImage(DOMDocument $dom, DOMElement $root)
{
$image = $this->getDataContainer()->getPlayPodcastImage();
if (! $image) {
return;
}
$el = $dom->createElement('googleplay:image');
$el->setAttribute('href', $image);
$root->appendChild($el);
$this->called = true;
}
/**
* Set explicit flag
*
* @return void
*/
protected function _setExplicit(DOMDocument $dom, DOMElement $root)
{
$explicit = $this->getDataContainer()->getPlayPodcastExplicit();
if ($explicit === null) {
return;
}
$el = $dom->createElement('googleplay:explicit');
$text = $dom->createTextNode($explicit);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
/**
* Set podcast description
*
* @return void
*/
protected function _setDescription(DOMDocument $dom, DOMElement $root)
{
$description = $this->getDataContainer()->getPlayPodcastDescription();
if (! $description) {
return;
}
$el = $dom->createElement('googleplay:description');
$text = $dom->createTextNode($description);
$el->appendChild($text);
$root->appendChild($el);
$this->called = true;
}
// phpcs:enable PSR2.Methods.MethodDeclaration.Underscore
}
@@ -0,0 +1,431 @@
<?php
namespace Laminas\Feed\Writer\Extension\ITunes;
use Laminas\Feed\Uri;
use Laminas\Feed\Writer;
use Laminas\Stdlib\StringUtils;
use Laminas\Stdlib\StringWrapper\StringWrapperInterface;
use function array_key_exists;
use function count;
use function ctype_alpha;
use function ctype_digit;
use function get_class;
use function gettype;
use function implode;
use function in_array;
use function is_bool;
use function is_float;
use function is_numeric;
use function is_object;
use function is_string;
use function lcfirst;
use function method_exists;
use function preg_match;
use function sprintf;
use function strlen;
use function substr;
use function trigger_error;
use function ucfirst;
use function var_export;
use const E_USER_DEPRECATED;
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 $this
*/
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 $value
* @throws Writer\Exception\InvalidArgumentException
* @return void
*/
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
*
* @return $this
*/
public function addItunesAuthors(array $values)
{
foreach ($values as $value) {
$this->addItunesAuthor($value);
}
return $this;
}
/**
* Add author to itunes entry
*
* @param string $value
* @return $this
* @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 $this
* @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
*
* @see https://help.apple.com/itc/podcasts_connect/#/itcb54353390
*
* @param bool $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesExplicit($value)
{
// "yes", "no" and "clean" are valid values for a previous version
if (! is_bool($value) && ! in_array($value, ['yes', 'no', 'clean'])) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "explicit" must be a boolean value'
);
}
switch ($value) {
case 'yes':
$value = true;
break;
case 'no':
case 'clean':
$value = false;
break;
}
$this->data['explicit'] = $value ? 'true' : 'false';
return $this;
}
/**
* Set keywords
*
* @deprecated since 2.10.0; itunes:keywords is no longer part of the
* iTunes podcast RSS specification.
*
* @param string[] $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesKeywords(array $value)
{
trigger_error(
'itunes:keywords has been deprecated in the iTunes podcast RSS specification,'
. ' and should not be relied on.',
E_USER_DEPRECATED
);
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 title
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesTitle($value)
{
if ($this->stringWrapper->strlen($value) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "title" may only contain a maximum of 255 characters'
);
}
$this->data['title'] = $value;
return $this;
}
/**
* Set subtitle
*
* @param string $value
* @return $this
* @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 $this
* @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;
}
/**
* Set entry image (icon)
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesImage($value)
{
if (! is_string($value) || ! Uri::factory($value)->isValid()) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "image" may only be a valid URI/IRI'
);
}
if (! in_array(substr($value, -3), ['jpg', 'png'])) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "image" may only use file extension "jpg"'
. ' or "png" which must be the last three characters of the URI'
. ' (i.e. no query string or fragment)'
);
}
$this->data['image'] = $value;
return $this;
}
/**
* Set the episode number
*
* @param int $number
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesEpisode($number)
{
if (! is_numeric($number) || is_float($number)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "number" may only be an integer; received %s',
is_object($number) ? get_class($number) : gettype($number)
));
}
$this->data['episode'] = (int) $number;
return $this;
}
/**
* Set the episode type
*
* @param string $type One of "full", "trailer", or "bonus".
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesEpisodeType($type)
{
$validTypes = ['full', 'trailer', 'bonus'];
if (! in_array($type, $validTypes, true)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "episodeType" MUST be one of the strings [%s]; received %s',
implode(', ', $validTypes),
is_object($type) ? get_class($type) : var_export($type, true)
));
}
$this->data['episodeType'] = $type;
return $this;
}
/**
* Set the status of closed captioning
*
* @param bool $status
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesIsClosedCaptioned($status)
{
if (! is_bool($status)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "isClosedCaptioned" MUST be a boolean; received %s',
is_object($status) ? get_class($status) : var_export($status, true)
));
}
if (! $status) {
return $this;
}
$this->data['isClosedCaptioned'] = true;
return $this;
}
/**
* Set the season number to which the episode belongs
*
* @param int $number
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesSeason($number)
{
if (! is_numeric($number) || is_float($number)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "season" may only be an integer; received %s',
is_object($number) ? get_class($number) : gettype($number)
));
}
$this->data['season'] = (int) $number;
return $this;
}
/**
* Overloading to itunes specific setters
*
* @param string $method
* @return mixed
* @throws Writer\Exception\BadMethodCallException
*/
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];
}
}
@@ -0,0 +1,461 @@
<?php
namespace Laminas\Feed\Writer\Extension\ITunes;
use Laminas\Feed\Uri;
use Laminas\Feed\Writer;
use Laminas\Stdlib\StringUtils;
use Laminas\Stdlib\StringWrapper\StringWrapperInterface;
use function array_key_exists;
use function count;
use function ctype_alpha;
use function ctype_digit;
use function get_class;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_object;
use function is_string;
use function lcfirst;
use function method_exists;
use function preg_match;
use function sprintf;
use function strlen;
use function substr;
use function trigger_error;
use function ucfirst;
use function var_export;
use const E_USER_DEPRECATED;
class Feed
{
/**
* 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 $this
*/
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 $value
* @return self
* @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;
return $this;
}
/**
* Add feed authors
*
* @return $this
*/
public function addItunesAuthors(array $values)
{
foreach ($values as $value) {
$this->addItunesAuthor($value);
}
return $this;
}
/**
* Add feed author
*
* @param string $value
* @return $this
* @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 feed categories
*
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesCategories(array $values)
{
if (! isset($this->data['categories'])) {
$this->data['categories'] = [];
}
foreach ($values as $key => $value) {
if (! is_array($value)) {
if ($this->stringWrapper->strlen($value) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][] = $value;
} else {
if ($this->stringWrapper->strlen($key) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][$key] = [];
foreach ($value as $val) {
if ($this->stringWrapper->strlen($val) > 255) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "category" may only contain a maximum of 255 characters each'
);
}
$this->data['categories'][$key][] = $val;
}
}
}
return $this;
}
/**
* Set feed image (icon)
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesImage($value)
{
if (! is_string($value) || ! Uri::factory($value)->isValid()) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "image" may only be a valid URI/IRI'
);
}
if (! in_array(substr($value, -3), ['jpg', 'png'])) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "image" may only'
. ' use file extension "jpg" or "png" which must be the last three'
. ' characters of the URI (i.e. no query string or fragment)'
);
}
$this->data['image'] = $value;
return $this;
}
/**
* Set feed cumulative duration
*
* @param string $value
* @return $this
* @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
*
* @see https://help.apple.com/itc/podcasts_connect/#/itcb54353390
*
* @param bool $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesExplicit($value)
{
// "yes", "no" and "clean" are valid values for a previous version
if (! is_bool($value) && ! in_array($value, ['yes', 'no', 'clean'])) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "explicit" must be a boolean value'
);
}
switch ($value) {
case 'yes':
$value = true;
break;
case 'no':
case 'clean':
$value = false;
break;
}
$this->data['explicit'] = $value ? 'true' : 'false';
return $this;
}
/**
* Set feed keywords
*
* @deprecated since 2.10.0; itunes:keywords is no longer part of the
* iTunes podcast RSS specification.
*
* @param string[] $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesKeywords(array $value)
{
trigger_error(
'itunes:keywords has been deprecated in the iTunes podcast RSS specification,'
. ' and should not be relied on.',
E_USER_DEPRECATED
);
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 new feed URL
*
* @param string $value
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesNewFeedUrl($value)
{
if (! Uri::factory($value)->isValid()) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: "newFeedUrl" may only be a valid URI/IRI'
);
}
$this->data['newFeedUrl'] = $value;
return $this;
}
/**
* Add feed owners
*
* @return $this
*/
public function addItunesOwners(array $values)
{
foreach ($values as $value) {
$this->addItunesOwner($value);
}
return $this;
}
/**
* Add feed owner
*
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function addItunesOwner(array $value)
{
if (! isset($value['name']) || ! isset($value['email'])) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "owner" must be an array containing keys "name" and "email"'
);
}
if (
$this->stringWrapper->strlen($value['name']) > 255
|| $this->stringWrapper->strlen($value['email']) > 255
) {
throw new Writer\Exception\InvalidArgumentException(
'invalid parameter: any "owner" may only contain a maximum of 255 characters'
. ' each for "name" and "email"'
);
}
if (! isset($this->data['owners'])) {
$this->data['owners'] = [];
}
$this->data['owners'][] = $value;
return $this;
}
/**
* Set feed subtitle
*
* @param string $value
* @return $this
* @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 feed summary
*
* @param string $value
* @return $this
* @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;
}
/**
* Set podcast type
*
* @param string $type
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesType($type)
{
$validTypes = ['episodic', 'serial'];
if (! in_array($type, $validTypes, true)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "type" MUST be one of [%s]; received %s',
implode(', ', $validTypes),
is_object($type) ? get_class($type) : var_export($type, true)
));
}
$this->data['type'] = $type;
return $this;
}
/**
* Set "completion" status (whether more episodes will be released)
*
* @param bool $status
* @return $this
* @throws Writer\Exception\InvalidArgumentException
*/
public function setItunesComplete($status)
{
if (! is_bool($status)) {
throw new Writer\Exception\InvalidArgumentException(sprintf(
'invalid parameter: "complete" MUST be boolean; received %s',
is_object($status) ? get_class($status) : var_export($status, true)
));
}
if (! $status) {
return $this;
}
$this->data['complete'] = 'Yes';
return $this;
}
/**
* Overloading: proxy to internal setters
*
* @param string $method
* @return mixed
* @throws Writer\Exception\BadMethodCallException
*/
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