default services conflit ?
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,12 @@
|
||||
# laminas-feed
|
||||
|
||||
[](https://travis-ci.com/laminas/laminas-feed)
|
||||
[](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
@@ -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
|
||||
{
|
||||
}
|
||||
+9
@@ -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;
|
||||
}
|
||||
}
|
||||
+38
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
+105
@@ -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
|
||||
}
|
||||
+177
@@ -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
Reference in New Issue
Block a user