mirror of
https://github.com/Chouchen/ShikiryuRSS.git
synced 2024-10-31 14:38:51 +01:00
commit
e2febcfacf
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/vendor/
|
||||||
|
/.idea
|
||||||
|
/.phpunit.result.cache
|
||||||
|
error.log
|
79
README.md
79
README.md
@ -1,9 +1,29 @@
|
|||||||
All documentation @ http://labs.shikiryu.com/SRSS/#_how
|
# SћíкïяўЦЯSS
|
||||||
|
|
||||||
|
> PHP library reading and creating RSS
|
||||||
|
|
||||||
**disclaimer:**
|
**disclaimer:**
|
||||||
This class is functionnal. Anyway, use it only if you don't have other choices.
|
This class is functional. Anyway, use it only if you don't have other choices.
|
||||||
For example, [Zend](http://framework.zend.com/manual/current/en/modules/zend.feed.introduction.html) and Symfony got their own RSS factory, don't add another one in.
|
For example, [Zend](http://framework.zend.com/manual/current/en/modules/zend.feed.introduction.html) and Symfony got their own RSS factory, don't add another one in.
|
||||||
|
|
||||||
|
|
||||||
|
## :books: Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#package-installation)
|
||||||
|
- [Usage](#rocket-usage)
|
||||||
|
- [Features](#sparkles-features)
|
||||||
|
- [Support](#hammer_and_wrench-support)
|
||||||
|
- [Contributing](#memo-contributing)
|
||||||
|
- [License](#scroll-license)
|
||||||
|
|
||||||
|
## :package: Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
composer install shikiryu/shikiryurss
|
||||||
|
```
|
||||||
|
|
||||||
|
## :rocket: Usage
|
||||||
|
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
How to make it read RSS?
|
How to make it read RSS?
|
||||||
@ -12,7 +32,7 @@ First, we need to load the RSS :
|
|||||||
|
|
||||||
$rss = SRSS::read('http://exemple.com/rss.xml');
|
$rss = SRSS::read('http://exemple.com/rss.xml');
|
||||||
|
|
||||||
Easy, right? Then you can extract general informations :
|
Easy, right? Then you can extract general information :
|
||||||
|
|
||||||
echo $rss->title; // will display blog title
|
echo $rss->title; // will display blog title
|
||||||
|
|
||||||
@ -22,8 +42,7 @@ Then, you can take care of articles. You can select a precise article :
|
|||||||
|
|
||||||
Or looping them :
|
Or looping them :
|
||||||
|
|
||||||
foreach($rss as $article)
|
foreach($rss as $article) {
|
||||||
{
|
|
||||||
echo '<a href="'.$article->link.'">'. SRSSTools::formatDate('d/m/y', $item->pubDate).' '.$item->title.'';
|
echo '<a href="'.$article->link.'">'. SRSSTools::formatDate('d/m/y', $item->pubDate).' '.$item->title.'';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +54,10 @@ You can also save it into your server with :
|
|||||||
|
|
||||||
$rss->save('/www/rss/rss.xml'); // example
|
$rss->save('/www/rss/rss.xml'); // example
|
||||||
|
|
||||||
|
Or finally, you can display it with :
|
||||||
|
|
||||||
|
$rss->show();
|
||||||
|
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
How to make it create RSS?
|
How to make it create RSS?
|
||||||
@ -43,7 +66,7 @@ First, we need to initialize the RSS :
|
|||||||
|
|
||||||
$rss = SRSS::create();
|
$rss = SRSS::create();
|
||||||
|
|
||||||
Easy, right? Then you can add general informations :
|
Easy, right? Then you can add general information :
|
||||||
|
|
||||||
$rss->title = 'My Awesome Blog';
|
$rss->title = 'My Awesome Blog';
|
||||||
$rss->link = 'http://shikiryu.com/devblog/';
|
$rss->link = 'http://shikiryu.com/devblog/';
|
||||||
@ -53,7 +76,7 @@ Those 3 are mandatory to validate your RSS, other options can be added.
|
|||||||
Then, you can add articles. Let's imagine $content contains an array from your database.
|
Then, you can add articles. Let's imagine $content contains an array from your database.
|
||||||
|
|
||||||
foreach($content as $item){
|
foreach($content as $item){
|
||||||
$rssitem= new SRSSItem; // we create an item
|
$rssitem = new Item(); // we create an item
|
||||||
$rssitem->title = $item["title"]; // adding title (option)
|
$rssitem->title = $item["title"]; // adding title (option)
|
||||||
$rssitem->link = $item['link']; // adding link (option)
|
$rssitem->link = $item['link']; // adding link (option)
|
||||||
$rssitem->pubDate = $item["date"]; // date automatically transformed into RSS format (option)
|
$rssitem->pubDate = $item["date"]; // date automatically transformed into RSS format (option)
|
||||||
@ -72,5 +95,43 @@ The other one does the opposite and add the next item in top of your RSS
|
|||||||
|
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
Contact :
|
## :sparkles: Features
|
||||||
http://shikiryu.com/contact
|
|
||||||
|
<dl>
|
||||||
|
<dt>Read every RSS 2.0</dt>
|
||||||
|
<dd>
|
||||||
|
Based on RSS 2.0 specifications.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Write and validate RSS 2.0 file</dt>
|
||||||
|
<dd>
|
||||||
|
Based on RSS 2.0 specifications.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
## :hammer_and_wrench: Support
|
||||||
|
|
||||||
|
Please [open an issue](https://github.com/Chouchen/ShikiryuRSS/issues) for support.
|
||||||
|
|
||||||
|
|
||||||
|
## :memo: Contributing
|
||||||
|
|
||||||
|
Please contribute using [Github Flow](https://guides.github.com/introduction/flow/). Create a branch, add commits, and [open a pull request](https://github.com/Chouchen/ShikiryuRSS/pulls).
|
||||||
|
|
||||||
|
## :scroll: License
|
||||||
|
|
||||||
|
[Creative Commons Attribution NonCommercial (CC-BY-NC)](<https://tldrlegal.com/license/creative-commons-attribution-noncommercial-(cc-nc)>) © [Chouchen](https://github.com/Chouchen/)
|
||||||
|
|
||||||
|
All documentation @ http://labs.shikiryu.com/SRSS/#_how.
|
||||||
|
|
||||||
|
Contact : https://shikiryu.com/contact
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
27
composer.json
Normal file
27
composer.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "shikiryu/shikiryurss",
|
||||||
|
"description": "A RSS reader and writer",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"license": "proprietary",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Shikiryu",
|
||||||
|
"email": "projets@shiki.fr",
|
||||||
|
"homepage": "https://shikiryu.com",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Shikiryu\\SRSS\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-libxml": "*"
|
||||||
|
}
|
||||||
|
}
|
1752
composer.lock
generated
Normal file
1752
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
phpunit.xml
Normal file
23
phpunit.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit bootstrap = "vendor/autoload.php"
|
||||||
|
backupGlobals = "false"
|
||||||
|
backupStaticAttributes = "false"
|
||||||
|
colors = "true"
|
||||||
|
convertErrorsToExceptions = "true"
|
||||||
|
convertNoticesToExceptions = "true"
|
||||||
|
convertWarningsToExceptions = "true"
|
||||||
|
processIsolation = "false"
|
||||||
|
stopOnFailure = "false">
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Project Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
</php>
|
||||||
|
|
||||||
|
</phpunit>
|
98
src/Builder/ChannelBuilder.php
Normal file
98
src/Builder/ChannelBuilder.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Builder;
|
||||||
|
|
||||||
|
use DOMElement;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel;
|
||||||
|
|
||||||
|
class ChannelBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
private \DOMDocument $document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DOMDocument $document
|
||||||
|
*/
|
||||||
|
public function __construct(\DOMDocument $document)
|
||||||
|
{
|
||||||
|
$this->document = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Shikiryu\SRSS\Entity\Channel $channel
|
||||||
|
*
|
||||||
|
* @return \DOMElement|false
|
||||||
|
* @throws \DOMException
|
||||||
|
*/
|
||||||
|
public function build(Channel $channel)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('channel');
|
||||||
|
foreach (array_filter($channel->toArray(), static fn($el) => !empty($el)) as $name => $value) {
|
||||||
|
if ($name === 'category') {
|
||||||
|
/** @var \Shikiryu\SRSS\Entity\Channel\Category $category */
|
||||||
|
foreach ($value as $category) {
|
||||||
|
$node->appendChild($this->buildCategory($category));
|
||||||
|
}
|
||||||
|
} elseif ($name === 'cloud') {
|
||||||
|
$node->appendChild($this->buildCloud($value));
|
||||||
|
} elseif ($name === 'image') {
|
||||||
|
$node->appendChild($this->buildImage($value));
|
||||||
|
} else {
|
||||||
|
$new_node = $this->document->createElement($name, $value);
|
||||||
|
$node->appendChild($new_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Shikiryu\SRSS\Entity\Channel\Category $category
|
||||||
|
*
|
||||||
|
* @return bool|\DOMElement
|
||||||
|
* @throws \DOMException
|
||||||
|
*/
|
||||||
|
private function buildCategory(Channel\Category $category): bool|DOMElement
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('category', $category->value);
|
||||||
|
$node->setAttribute('domain', $category->domain);
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Shikiryu\SRSS\Entity\Channel\Cloud $cloud
|
||||||
|
*
|
||||||
|
* @return \DOMElement|false
|
||||||
|
* @throws \DOMException
|
||||||
|
*/
|
||||||
|
private function buildCloud(Channel\Cloud $cloud)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('cloud');
|
||||||
|
foreach (get_object_vars($cloud) as $name => $value) {
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$node->setAttribute($name, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Shikiryu\SRSS\Entity\Channel\Image $image
|
||||||
|
*
|
||||||
|
* @return \DOMElement|false
|
||||||
|
* @throws \DOMException
|
||||||
|
*/
|
||||||
|
private function buildImage(Channel\Image $image)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('image');
|
||||||
|
foreach (get_object_vars($image) as $name => $value) {
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$node->appendChild($this->document->createElement($name, $value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
}
|
105
src/Builder/ItemBuilder.php
Normal file
105
src/Builder/ItemBuilder.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Builder;
|
||||||
|
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\Entity\Media\Content;
|
||||||
|
|
||||||
|
class ItemBuilder
|
||||||
|
{
|
||||||
|
private \DOMDocument $document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DOMDocument $document
|
||||||
|
*/
|
||||||
|
public function __construct(\DOMDocument $document)
|
||||||
|
{
|
||||||
|
$this->document = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Shikiryu\SRSS\Entity\Item $item
|
||||||
|
*
|
||||||
|
* @return \DOMElement|false
|
||||||
|
* @throws \DOMException
|
||||||
|
*/
|
||||||
|
public function build(Item $item): bool|\DOMElement
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('item');
|
||||||
|
|
||||||
|
foreach (array_filter($item->toArray()) as $name => $value) {
|
||||||
|
if ($name === 'category') {
|
||||||
|
/** @var \Shikiryu\SRSS\Entity\Item\Category $category */
|
||||||
|
foreach ($value as $category) {
|
||||||
|
$node->appendChild($this->buildCategory($category));
|
||||||
|
}
|
||||||
|
} elseif ($name === 'medias') {
|
||||||
|
$group = null;
|
||||||
|
if (count($value) > 1) {
|
||||||
|
$group = $node->appendChild($this->document->createElement('media:group'));
|
||||||
|
}
|
||||||
|
foreach ($value as $media) {
|
||||||
|
if (null === $group) {
|
||||||
|
$node->appendChild($this->buildMedia($media));
|
||||||
|
} else {
|
||||||
|
$group->appendChild($this->buildMedia($media));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($group !== null) {
|
||||||
|
$node->appendChild($group);
|
||||||
|
}
|
||||||
|
} elseif ($name === 'enclosure') {
|
||||||
|
$node->appendChild($this->buildEnclosure($value));
|
||||||
|
} elseif ($name === 'source') {
|
||||||
|
$node->appendChild($this->buildSource($value));
|
||||||
|
} else {
|
||||||
|
$new_node = $this->document->createElement($name, $value);
|
||||||
|
$node->appendChild($new_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCategory(Item\Category $category)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('category', $category->value);
|
||||||
|
$node->setAttribute('domain', $category->domain);
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEnclosure(Item\Enclosure $enclosure)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('enclosure');
|
||||||
|
foreach (get_object_vars($enclosure) as $name => $value) {
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$node->setAttribute($name, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSource(Item\Source $source)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('source', $source->value);
|
||||||
|
$node->setAttribute('url', $source->url);
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMedia(Content $media)
|
||||||
|
{
|
||||||
|
$node = $this->document->createElement('media:content');
|
||||||
|
foreach (get_object_vars($media) as $name => $value) {
|
||||||
|
if (!is_array($value)) {
|
||||||
|
$node->setAttribute($name, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
65
src/Builder/SRSSBuilder.php
Normal file
65
src/Builder/SRSSBuilder.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Builder;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use Shikiryu\SRSS\Exception\DOMBuilderException;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class SRSSBuilder extends DomDocument
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws \Shikiryu\SRSS\Exception\DOMBuilderException
|
||||||
|
*/
|
||||||
|
private function buildRSS(SRSS $srss): SRSSBuilder
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$root = $this->createElement('rss');
|
||||||
|
|
||||||
|
$root->setAttribute('version', '2.0');
|
||||||
|
|
||||||
|
$srss->channel->generator = 'Shikiryu RSS';
|
||||||
|
|
||||||
|
$channel_builder = new ChannelBuilder($this);
|
||||||
|
$channel = $channel_builder->build($srss->channel);
|
||||||
|
|
||||||
|
$item_builder = new ItemBuilder($this);
|
||||||
|
foreach ($srss->items as $item) {
|
||||||
|
$channel->appendChild($item_builder->build($item));
|
||||||
|
}
|
||||||
|
|
||||||
|
$root->appendChild($channel);
|
||||||
|
$this->appendChild($root);
|
||||||
|
$this->encoding = 'UTF-8';
|
||||||
|
$this->formatOutput = true;
|
||||||
|
$this->preserveWhiteSpace = false;
|
||||||
|
// $docs = 'http://www.scriptol.fr/rss/RSS-2.0.html';
|
||||||
|
|
||||||
|
} catch (\DOMException $e) {
|
||||||
|
throw new DOMBuilderException($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Shikiryu\SRSS\Exception\DOMBuilderException
|
||||||
|
*/
|
||||||
|
public function build(SRSS $srss, string $filepath): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->buildRSS($srss)
|
||||||
|
->save($filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return false|string
|
||||||
|
* @throws \Shikiryu\SRSS\Exception\DOMBuilderException
|
||||||
|
*/
|
||||||
|
public function show(SRSS $srss): bool|string
|
||||||
|
{
|
||||||
|
return $this
|
||||||
|
->buildRSS($srss)
|
||||||
|
->saveXml();
|
||||||
|
}
|
||||||
|
}
|
112
src/Entity/Channel.php
Normal file
112
src/Entity/Channel.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Category;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Cloud;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://cyber.harvard.edu/rss/rss.html#requiredChannelElements
|
||||||
|
*/
|
||||||
|
class Channel extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public string $title = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public string $link = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
*/
|
||||||
|
public string $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lang
|
||||||
|
*/
|
||||||
|
public ?string $language = null;
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $copyright = null;
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $managingEditor = null;
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $webMaster = null;
|
||||||
|
/**
|
||||||
|
* @date
|
||||||
|
*/
|
||||||
|
public ?string $pubDate = null;
|
||||||
|
/**
|
||||||
|
* @date
|
||||||
|
*/
|
||||||
|
public ?string $lastBuildDate = null;
|
||||||
|
/**
|
||||||
|
* @var Category[]
|
||||||
|
*/
|
||||||
|
public ?array $category = null;
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $generator = null;
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $docs = null;
|
||||||
|
/**
|
||||||
|
* @var Cloud|null
|
||||||
|
*/
|
||||||
|
public ?Cloud $cloud = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?string $ttl = null;
|
||||||
|
public ?Image $image = null;
|
||||||
|
public ?string $rating = null;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
* The purpose of the <textInput> element is something of a mystery. You can use it to specify a search engine box. Or to allow a reader to provide feedback. Most aggregators ignore it.
|
||||||
|
*/
|
||||||
|
public ?string $textInput = null;
|
||||||
|
/**
|
||||||
|
* @hour
|
||||||
|
*/
|
||||||
|
public ?string $skipHours = null;
|
||||||
|
/**
|
||||||
|
* @day
|
||||||
|
*/
|
||||||
|
public ?string $skipDays = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$vars = get_object_vars($this);
|
||||||
|
unset($vars['validated']);
|
||||||
|
return $vars;
|
||||||
|
}
|
||||||
|
}
|
34
src/Entity/Channel/Category.php
Normal file
34
src/Entity/Channel/Category.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Channel;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Category extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $domain = null;
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $value = null;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
46
src/Entity/Channel/Cloud.php
Normal file
46
src/Entity/Channel/Cloud.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Channel;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Cloud extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $domain = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $port = null;
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $path = null;
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $registerProcedure = null;
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public ?string $protocol = null;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
53
src/Entity/Channel/Image.php
Normal file
53
src/Entity/Channel/Image.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Channel;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Image extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $url = null;
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $title = null;
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $link = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
* @max 144
|
||||||
|
*/
|
||||||
|
public int $width = 88; // Maximum value for width is 144, default value is 88.
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
* @max 400
|
||||||
|
*/
|
||||||
|
public int $height = 31; //Maximum value for height is 400, default value is 31.
|
||||||
|
|
||||||
|
public string $description;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
79
src/Entity/Item.php
Normal file
79
src/Entity/Item.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\Item\Category;
|
||||||
|
use Shikiryu\SRSS\Entity\Item\Enclosure;
|
||||||
|
use Shikiryu\SRSS\Entity\Item\Source;
|
||||||
|
use Shikiryu\SRSS\Entity\Media\Content;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://cyber.harvard.edu/rss/rss.html#hrelementsOfLtitemgt
|
||||||
|
*/
|
||||||
|
class Item extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @requiredOr description
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $title = null;
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $link = null;
|
||||||
|
/**
|
||||||
|
* @requiredOr title
|
||||||
|
*/
|
||||||
|
public ?string $description = null;
|
||||||
|
/**
|
||||||
|
* @email
|
||||||
|
*/
|
||||||
|
public ?string $author = null;
|
||||||
|
/**
|
||||||
|
* @var Category[]
|
||||||
|
*/
|
||||||
|
public ?array $category = null;
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $comments = null;
|
||||||
|
/**
|
||||||
|
* @var Enclosure|null
|
||||||
|
*/
|
||||||
|
public ?Enclosure $enclosure = null;
|
||||||
|
public ?string $guid = null;
|
||||||
|
/**
|
||||||
|
* @date
|
||||||
|
*/
|
||||||
|
public ?string $pubDate = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Source|null
|
||||||
|
*/
|
||||||
|
public ?Source $source = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Content[]
|
||||||
|
* @contentMedia
|
||||||
|
*/
|
||||||
|
public array $medias = [];
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$vars = get_object_vars($this);
|
||||||
|
unset($vars['validated']);
|
||||||
|
return $vars;
|
||||||
|
}
|
||||||
|
}
|
34
src/Entity/Item/Category.php
Normal file
34
src/Entity/Item/Category.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Item;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Category extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public string $domain;
|
||||||
|
/**
|
||||||
|
* @string
|
||||||
|
*/
|
||||||
|
public string $value;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
40
src/Entity/Item/Enclosure.php
Normal file
40
src/Entity/Item/Enclosure.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Item;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Enclosure extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $url = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $length = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mediaType
|
||||||
|
*/
|
||||||
|
public ?string $type = null;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
35
src/Entity/Item/Source.php
Normal file
35
src/Entity/Item/Source.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Item;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Source extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $url = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $value = null;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
82
src/Entity/Media/Content.php
Normal file
82
src/Entity/Media/Content.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity\Media;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Entity\SRSSElement;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
class Content extends HasValidator implements SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $url = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $filesize = null;
|
||||||
|
/**
|
||||||
|
* @mediaType
|
||||||
|
*/
|
||||||
|
public ?string $type = null;
|
||||||
|
/**
|
||||||
|
* @mediaMedium
|
||||||
|
*/
|
||||||
|
public ?string $medium = null;
|
||||||
|
/**
|
||||||
|
* @bool
|
||||||
|
*/
|
||||||
|
public ?bool $isDefault = null;
|
||||||
|
/**
|
||||||
|
* @mediaExpression
|
||||||
|
*/
|
||||||
|
public ?string $expression = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $bitrate = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $framerate = null;
|
||||||
|
/**
|
||||||
|
* @float
|
||||||
|
*/
|
||||||
|
public ?float $samplerate = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $channels = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $duration = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $height = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?int $width = null;
|
||||||
|
/**
|
||||||
|
* @lang
|
||||||
|
*/
|
||||||
|
public ?string $lang = null;
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (new Validator())->isObjectValid($this);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
16
src/Entity/SRSSElement.php
Normal file
16
src/Entity/SRSSElement.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Entity;
|
||||||
|
|
||||||
|
interface SRSSElement
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isValid(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(): array;
|
||||||
|
}
|
12
src/Exception/ChannelNotFoundInRSSException.php
Normal file
12
src/Exception/ChannelNotFoundInRSSException.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Exception;
|
||||||
|
|
||||||
|
class ChannelNotFoundInRSSException extends SRSSException
|
||||||
|
{
|
||||||
|
public function __construct($file)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('Invalid file `%s`: <channel> not found', $file));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/Exception/DOMBuilderException.php
Normal file
12
src/Exception/DOMBuilderException.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Exception;
|
||||||
|
|
||||||
|
class DOMBuilderException extends SRSSException
|
||||||
|
{
|
||||||
|
public function __construct(\DOMException $e)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('Error while building DOM (%s)', $e->getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/Exception/PropertyNotFoundException.php
Normal file
15
src/Exception/PropertyNotFoundException.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Exception;
|
||||||
|
|
||||||
|
class PropertyNotFoundException extends SRSSException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $name
|
||||||
|
*/
|
||||||
|
public function __construct($class, $name)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('Property `%s` not found in `%s`', $name, $class));
|
||||||
|
}
|
||||||
|
}
|
21
src/Exception/SRSSException.php
Normal file
21
src/Exception/SRSSException.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class SRSSException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct($msg)
|
||||||
|
{
|
||||||
|
parent :: __construct($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getError(): string
|
||||||
|
{
|
||||||
|
return 'Une exception a été générée : <strong>Message : ' . $this->getMessage() . '</strong> à la ligne : ' . $this->getLine();
|
||||||
|
}
|
||||||
|
}
|
12
src/Exception/UnreadableRSSException.php
Normal file
12
src/Exception/UnreadableRSSException.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Exception;
|
||||||
|
|
||||||
|
class UnreadableRSSException extends SRSSException
|
||||||
|
{
|
||||||
|
public function __construct($file)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('File `%s` is unreadable.', $file));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
72
src/Parser/ItemParser.php
Normal file
72
src/Parser/ItemParser.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Parser;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMNode;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string|null $description
|
||||||
|
*/
|
||||||
|
class ItemParser extends DomDocument
|
||||||
|
{
|
||||||
|
|
||||||
|
protected DOMNode $node; // item node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Item $item
|
||||||
|
* @param $nodes
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function _loadChildAttributes(Item $item, $nodes): void
|
||||||
|
{
|
||||||
|
foreach ($nodes->childNodes as $child) {
|
||||||
|
if ($child->nodeType === XML_ELEMENT_NODE && $child->nodeName !== 'item') {
|
||||||
|
if ($child->nodeName === 'media:group') {
|
||||||
|
self::_loadChildAttributes($item, $child);
|
||||||
|
} elseif ($child->nodeName === 'media:content') {
|
||||||
|
$item->medias[] = MediaContentParser::read($child);
|
||||||
|
} elseif ($child->nodeName === 'category') {
|
||||||
|
$category = new Item\Category();
|
||||||
|
foreach($child->attributes as $attribute) {
|
||||||
|
$category->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
$category->value = $child->nodeValue;
|
||||||
|
$item->category[] = $category;
|
||||||
|
} elseif ($child->nodeName === 'enclosure') {
|
||||||
|
$enclosure = new Item\Enclosure();
|
||||||
|
foreach($child->attributes as $attribute) {
|
||||||
|
$enclosure->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
$item->enclosure = $enclosure;
|
||||||
|
} elseif ($child->nodeName === 'source') {
|
||||||
|
$source = new Item\Source();
|
||||||
|
foreach($child->attributes as $attribute) {
|
||||||
|
$source->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
$source->value = $child->nodeValue;
|
||||||
|
$item->source = $source;
|
||||||
|
} else {
|
||||||
|
$item->{$child->nodeName} = trim($child->nodeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DOMNode|null $node
|
||||||
|
*
|
||||||
|
* @return Item
|
||||||
|
*/
|
||||||
|
public static function read(?DOMNode $node = null): Item
|
||||||
|
{
|
||||||
|
$item = new Item();
|
||||||
|
if ($node instanceof DOMNode) {
|
||||||
|
self::_loadChildAttributes($item, $node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
}
|
62
src/Parser/MediaContentParser.php
Normal file
62
src/Parser/MediaContentParser.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Parser;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use DOMException;
|
||||||
|
use DOMNode;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\Entity\Media\Content;
|
||||||
|
use Shikiryu\SRSS\Exception\SRSSException;
|
||||||
|
use Shikiryu\SRSS\SRSSTools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string|null $description
|
||||||
|
*/
|
||||||
|
class MediaContentParser extends DomDocument
|
||||||
|
{
|
||||||
|
protected DOMNode $node; // item node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DomNode $node
|
||||||
|
*/
|
||||||
|
public function __construct($node = null)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Content $item
|
||||||
|
* @param $nodes
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function _loadChildAttributes(Content $item, $nodes): void
|
||||||
|
{
|
||||||
|
foreach ($nodes->attributes as $attribute) {
|
||||||
|
|
||||||
|
if (property_exists(Content::class, $attribute->name)) {
|
||||||
|
$item->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DOMNode|null $node
|
||||||
|
*
|
||||||
|
* @return Content
|
||||||
|
*/
|
||||||
|
public static function read(?DOMNode $node = null): Content
|
||||||
|
{
|
||||||
|
$content = new Content();
|
||||||
|
if ($node instanceof DOMNode) {
|
||||||
|
self::_loadChildAttributes($content, $node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
135
src/Parser/SRSSParser.php
Normal file
135
src/Parser/SRSSParser.php
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Parser;
|
||||||
|
|
||||||
|
use DOMDocument;
|
||||||
|
use DOMNode;
|
||||||
|
use DOMNodeList;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Exception\ChannelNotFoundInRSSException;
|
||||||
|
use Shikiryu\SRSS\Exception\SRSSException;
|
||||||
|
use Shikiryu\SRSS\Exception\UnreadableRSSException;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class SRSSParser extends DomDocument
|
||||||
|
{
|
||||||
|
private SRSS $doc;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
parent::__construct();
|
||||||
|
$this->doc = new SRSS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destructor
|
||||||
|
* manage of libxml errors
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
foreach (libxml_get_errors() as $error) {
|
||||||
|
error_log($error->message, 3, 'error.log');
|
||||||
|
}
|
||||||
|
libxml_clear_errors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $link
|
||||||
|
*
|
||||||
|
* @return SRSS
|
||||||
|
* @throws ChannelNotFoundInRSSException
|
||||||
|
* @throws SRSSException
|
||||||
|
* @throws UnreadableRSSException
|
||||||
|
*/
|
||||||
|
public function parse(string $link): SRSS
|
||||||
|
{
|
||||||
|
if (@$this->load($link)) { // We don't want the warning in case of bad XML. Let's manage it with an exception.
|
||||||
|
$channel = $this->getElementsByTagName('channel');
|
||||||
|
if($channel->length === 1){ // Good URL and good RSS
|
||||||
|
$this->_parseChannel(); // loading channel properties
|
||||||
|
$this->_parseItems(); // loading all items
|
||||||
|
|
||||||
|
return $this->doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ChannelNotFoundInRSSException($link);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnreadableRSSException($link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SRSSException
|
||||||
|
*/
|
||||||
|
private function _parseItems(): void
|
||||||
|
{
|
||||||
|
$channel = $this->_getChannel();
|
||||||
|
/** @var DOMNodeList $items */
|
||||||
|
$items = $channel->getElementsByTagName('item');
|
||||||
|
$length = $items->length;
|
||||||
|
$this->doc->items = [];
|
||||||
|
if ($length > 0) {
|
||||||
|
for($i = 0; $i < $length; $i++) {
|
||||||
|
$this->doc->items[$i] = ItemParser::read($items->item($i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* putting all RSS attributes into the object
|
||||||
|
* @throws SRSSException
|
||||||
|
*/
|
||||||
|
private function _parseChannel(): void
|
||||||
|
{
|
||||||
|
$node_channel = $this->_getChannel();
|
||||||
|
$this->doc->channel = new Channel();
|
||||||
|
|
||||||
|
foreach($node_channel->childNodes as $child) {
|
||||||
|
if ($child->nodeType === XML_ELEMENT_NODE && $child->nodeName !== 'item') {
|
||||||
|
if($child->nodeName === 'image') {
|
||||||
|
$image = new Image();
|
||||||
|
foreach($child->childNodes as $children) {
|
||||||
|
if($children->nodeType === XML_ELEMENT_NODE) {
|
||||||
|
$image->{$children->nodeName} = $children->nodeValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->doc->channel->image = $image;
|
||||||
|
|
||||||
|
} elseif($child->nodeName === 'cloud') {
|
||||||
|
$cloud = new Channel\Cloud();
|
||||||
|
foreach($child->attributes as $attribute) {
|
||||||
|
$cloud->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
$this->doc->channel->cloud = $cloud;
|
||||||
|
} elseif($child->nodeName === 'category') {
|
||||||
|
$category = new Channel\Category();
|
||||||
|
foreach($child->attributes as $attribute) {
|
||||||
|
$category->{$attribute->name} = $attribute->value;
|
||||||
|
}
|
||||||
|
$category->value = $child->nodeValue;
|
||||||
|
$this->doc->channel->category[] = $category;
|
||||||
|
} else {
|
||||||
|
$this->doc->channel->{$child->nodeName} = $child->nodeValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of current RSS channel
|
||||||
|
* @return DOMNode
|
||||||
|
* @throws SRSSException
|
||||||
|
*/
|
||||||
|
private function _getChannel(): DOMNode
|
||||||
|
{
|
||||||
|
$channel = $this->getElementsByTagName('channel');
|
||||||
|
if($channel->length !== 1) {
|
||||||
|
throw new ChannelNotFoundInRSSException('channel node not created, or too many channel nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channel->item(0);
|
||||||
|
}
|
||||||
|
}
|
291
src/SRSS.php
Normal file
291
src/SRSS.php
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS;
|
||||||
|
|
||||||
|
use Iterator;
|
||||||
|
use ReflectionException;
|
||||||
|
use Shikiryu\SRSS\Builder\SRSSBuilder;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Category;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Cloud;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\Exception\ChannelNotFoundInRSSException;
|
||||||
|
use Shikiryu\SRSS\Exception\PropertyNotFoundException;
|
||||||
|
use Shikiryu\SRSS\Exception\SRSSException;
|
||||||
|
use Shikiryu\SRSS\Exception\UnreadableRSSException;
|
||||||
|
use Shikiryu\SRSS\Parser\SRSSParser;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property null|string $title
|
||||||
|
* @property null|string $link
|
||||||
|
* @property null|string $description
|
||||||
|
* @property null|string $language
|
||||||
|
* @property null|string $copyright
|
||||||
|
* @property null|string $managingEditor
|
||||||
|
* @property null|string $webMaster
|
||||||
|
* @property null|string $pubDate
|
||||||
|
* @property null|string $lastBuildDate
|
||||||
|
* @property null|Category[] $category
|
||||||
|
* @property null|string $generator
|
||||||
|
* @property null|string $docs
|
||||||
|
* @property null|Cloud $cloud
|
||||||
|
* @property null|string $ttl
|
||||||
|
* @property null|Image $image
|
||||||
|
* @property null|string $rating
|
||||||
|
* @property null|string $textInput
|
||||||
|
* @property null|string $skipHours
|
||||||
|
* @property null|string $skipDays
|
||||||
|
*/
|
||||||
|
class SRSS implements Iterator
|
||||||
|
{
|
||||||
|
public Channel $channel;
|
||||||
|
|
||||||
|
/** @var Item[] */
|
||||||
|
public array $items; // array of SRSSItems
|
||||||
|
|
||||||
|
private int $position; // Iterator position
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->items = [];
|
||||||
|
$this->position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $link url of the rss
|
||||||
|
*
|
||||||
|
* @return SRSS
|
||||||
|
* @throws ChannelNotFoundInRSSException
|
||||||
|
* @throws UnreadableRSSException
|
||||||
|
* @throws SRSSException
|
||||||
|
*/
|
||||||
|
public static function read(string $link): SRSS
|
||||||
|
{
|
||||||
|
return (new SRSSParser())->parse($link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SRSS
|
||||||
|
*/
|
||||||
|
public static function create(): SRSS
|
||||||
|
{
|
||||||
|
$doc = new SRSS;
|
||||||
|
|
||||||
|
$doc->channel = new Channel();
|
||||||
|
$doc->items = [];
|
||||||
|
|
||||||
|
return $doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if current RSS is a valid one (based on specifications)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$valid = true;
|
||||||
|
foreach ($this->getItems() as $item) {
|
||||||
|
if ($item->isValid() === false) {
|
||||||
|
$valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($valid && $this->channel->isValid());
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $name
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function __isset($name)
|
||||||
|
{
|
||||||
|
return isset($this->channel->{$name});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setter of others attributes
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* @param $val
|
||||||
|
*
|
||||||
|
* @throws SRSSException
|
||||||
|
*/
|
||||||
|
public function __set($name, $val)
|
||||||
|
{
|
||||||
|
if (!property_exists(Channel::class, $name)) {
|
||||||
|
throw new PropertyNotFoundException(Channel::class, $name);
|
||||||
|
}
|
||||||
|
if ((new Validator())->isValidValueForObjectProperty($this->channel, $name, $val)) {
|
||||||
|
if (SRSSTools::getPropertyType(Channel::class, $name) === 'array') {
|
||||||
|
$this->channel->{$name}[] = $val;
|
||||||
|
} else {
|
||||||
|
$this->channel->{$name} = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of others attributes
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
*
|
||||||
|
* @return null|string
|
||||||
|
*/
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
return $this->channel->{$name} ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rewind from Iterator
|
||||||
|
*/
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* current from Iterator
|
||||||
|
*/
|
||||||
|
public function current(): Item
|
||||||
|
{
|
||||||
|
return $this->items[$this->position];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key from Iterator
|
||||||
|
*/
|
||||||
|
public function key(): int
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* next from Iterator
|
||||||
|
*/
|
||||||
|
public function next(): void
|
||||||
|
{
|
||||||
|
++$this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* valid from Iterator
|
||||||
|
*/
|
||||||
|
public function valid(): bool
|
||||||
|
{
|
||||||
|
return isset($this->items[$this->position]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of 1st item
|
||||||
|
* @return Item|null
|
||||||
|
*/
|
||||||
|
public function getFirst(): ?Item
|
||||||
|
{
|
||||||
|
return $this->getItem(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of last item
|
||||||
|
* @return Item
|
||||||
|
*/
|
||||||
|
public function getLast(): Item
|
||||||
|
{
|
||||||
|
$items = $this->getItems();
|
||||||
|
|
||||||
|
return $items[array_key_last($items)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of an item
|
||||||
|
*
|
||||||
|
* @param $i int
|
||||||
|
*
|
||||||
|
* @return Item|null
|
||||||
|
*/
|
||||||
|
public function getItem(int $i): ?Item
|
||||||
|
{
|
||||||
|
$i--;
|
||||||
|
|
||||||
|
return $this->items[$i] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getter of all items
|
||||||
|
* @return Item[]
|
||||||
|
*/
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform current object into an array
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$doc = $this->channel->toArray();
|
||||||
|
|
||||||
|
foreach ($this->getItems() as $item) {
|
||||||
|
$doc['items'][] = $item->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Item $rssItem
|
||||||
|
*
|
||||||
|
* @return array|Item[]
|
||||||
|
*/
|
||||||
|
public function addItem(Item $rssItem): array
|
||||||
|
{
|
||||||
|
$this->items[] = $rssItem;
|
||||||
|
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Item $firstItem
|
||||||
|
*
|
||||||
|
* @return array|Item[]
|
||||||
|
*/
|
||||||
|
public function addItemBefore(Item $firstItem): array
|
||||||
|
{
|
||||||
|
array_unshift($this->items, $firstItem);
|
||||||
|
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws \Shikiryu\SRSS\Exception\DOMBuilderException
|
||||||
|
*/
|
||||||
|
public function save(string $path): void
|
||||||
|
{
|
||||||
|
(new SRSSBuilder('1.0', 'UTF-8'))->build($this, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return false|string
|
||||||
|
* @throws \Shikiryu\SRSS\Exception\DOMBuilderException
|
||||||
|
*/
|
||||||
|
public function show(): bool|string
|
||||||
|
{
|
||||||
|
return (new SRSSBuilder('1.0', 'UTF-8'))->show($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
211
src/SRSSTools.php
Normal file
211
src/SRSSTools.php
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
class SRSSTools
|
||||||
|
{
|
||||||
|
public static function getPropertyType($object, $property): ?string
|
||||||
|
{
|
||||||
|
$rp = new ReflectionProperty($object, $property);
|
||||||
|
return $rp->getType()?->getName();
|
||||||
|
}
|
||||||
|
public static function check($check, $flag)
|
||||||
|
{
|
||||||
|
return match ($flag) {
|
||||||
|
'nohtml' => self::noHTML($check),
|
||||||
|
'link' => self::checkLink($check),
|
||||||
|
'html' => self::HTML4XML($check),
|
||||||
|
'date' => self::getRSSDate($check),
|
||||||
|
'email' => self::checkEmail($check),
|
||||||
|
'int' => self::checkInt($check),
|
||||||
|
'hour' => self::checkHour($check),
|
||||||
|
'day' => self::checkDay($check),
|
||||||
|
'media_type' => self::checkMediaType($check),
|
||||||
|
'media_medium' => self::checkMediaMedium($check),
|
||||||
|
'bool' => self::checkBool($check),
|
||||||
|
'medium_expression' => self::checkMediumExpression($check)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format the RSS to the wanted format
|
||||||
|
*
|
||||||
|
* @param $format string wanted format
|
||||||
|
* @param $date string RSS date
|
||||||
|
*
|
||||||
|
* @return string date
|
||||||
|
*/
|
||||||
|
public static function formatDate(string $format, string $date): string
|
||||||
|
{
|
||||||
|
return date($format, strtotime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format a date for RSS format
|
||||||
|
*
|
||||||
|
* @param string $date date to format
|
||||||
|
* @param string $format
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getRSSDate(string $date, string $format = ''): string
|
||||||
|
{
|
||||||
|
$date_position = 'dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU';
|
||||||
|
if($format !== '' && preg_match('~^(['.$date_position.']{1})([-/])(['.$date_position.']{1})([-/])(['.$date_position.']{1})$~', $format)){
|
||||||
|
$datetime = DateTime::createFromFormat($format, $date);
|
||||||
|
if ($datetime === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datetime->format(DATE_RSS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtotime($date) !==false ) {
|
||||||
|
return date("D, d M Y H:i:s T", strtotime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count(explode(' ', $date)) === 2) {
|
||||||
|
return DateTime::createFromFormat('Y-m-d H:i:s', $date)->format(DateTimeInterface::RSS);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$j, $m, $a] = explode('/', $date);
|
||||||
|
|
||||||
|
return date("D, d M Y H:i:s T", strtotime($a.'-'.$m.'-'.$j));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if it's an url
|
||||||
|
*
|
||||||
|
* @param $check string to check
|
||||||
|
*
|
||||||
|
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
||||||
|
*/
|
||||||
|
public static function checkLink(string $check): bool|string
|
||||||
|
{
|
||||||
|
return filter_var($check, FILTER_VALIDATE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make a string XML-compatible
|
||||||
|
*
|
||||||
|
* @param $check string to format
|
||||||
|
*
|
||||||
|
* @return string formatted string
|
||||||
|
*/
|
||||||
|
public static function HTML4XML(string $check): string
|
||||||
|
{
|
||||||
|
return sprintf('<![CDATA[ %s ]]>', htmlspecialchars($check));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete html tags
|
||||||
|
*
|
||||||
|
* @param $check string to format
|
||||||
|
*
|
||||||
|
* @return string formatted string
|
||||||
|
*/
|
||||||
|
public static function noHTML(string $check): string
|
||||||
|
{
|
||||||
|
return strip_tags($check);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if it's a day (in RSS terms)
|
||||||
|
*
|
||||||
|
* @param $check string to check
|
||||||
|
*
|
||||||
|
* @return string the day, or empty string
|
||||||
|
*/
|
||||||
|
public static function checkDay(string $check): string
|
||||||
|
{
|
||||||
|
$possibleDay = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||||
|
return in_array(strtolower($check), $possibleDay) ? $check : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if it's an email
|
||||||
|
*
|
||||||
|
* @param $check string to check
|
||||||
|
*
|
||||||
|
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
||||||
|
*/
|
||||||
|
public static function checkEmail(string $check): bool|string
|
||||||
|
{
|
||||||
|
return filter_var($check, FILTER_VALIDATE_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if it's an hour (in RSS terms)
|
||||||
|
*
|
||||||
|
* @param $check string to check
|
||||||
|
*
|
||||||
|
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
||||||
|
*/
|
||||||
|
public static function checkHour(string $check): bool|string
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'options' => [
|
||||||
|
'default' => 0,
|
||||||
|
'min_range' => 0,
|
||||||
|
'max_range' => 23
|
||||||
|
]
|
||||||
|
];
|
||||||
|
return filter_var($check, FILTER_VALIDATE_INT, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if it's an int
|
||||||
|
*
|
||||||
|
* @param $check int to check
|
||||||
|
*
|
||||||
|
* @return int|boolean the filtered data, or FALSE if the filter fails.
|
||||||
|
*/
|
||||||
|
public static function checkInt(int $check): bool|int
|
||||||
|
{
|
||||||
|
return filter_var($check, FILTER_VALIDATE_INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $check
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function checkMediaType($check): mixed
|
||||||
|
{
|
||||||
|
return $check;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $check
|
||||||
|
*
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public static function checkMediaMedium($check): ?string
|
||||||
|
{
|
||||||
|
return in_array($check, ['image', 'audio', 'video', 'document', 'executable']) ? $check : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $check
|
||||||
|
*
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public static function checkBool($check): ?string
|
||||||
|
{
|
||||||
|
return in_array($check, ['true', 'false']) ? $check : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $check
|
||||||
|
*
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public static function checkMediumExpression($check): ?string
|
||||||
|
{
|
||||||
|
return in_array($check, ['sample', 'full', 'nonstop']) ? $check : null;
|
||||||
|
}
|
||||||
|
}
|
11
src/Validator/HasValidator.php
Normal file
11
src/Validator/HasValidator.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Validator;
|
||||||
|
|
||||||
|
abstract class HasValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var bool[]
|
||||||
|
*/
|
||||||
|
public array $validated = [];
|
||||||
|
}
|
280
src/Validator/Validator.php
Normal file
280
src/Validator/Validator.php
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Validator;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use Shikiryu\SRSS\Entity\Media\Content;
|
||||||
|
|
||||||
|
class Validator
|
||||||
|
{
|
||||||
|
protected ?object $object = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $object
|
||||||
|
* @param $property
|
||||||
|
* @return ReflectionProperty|null
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
private function getReflectedProperty($object, $property): ?ReflectionProperty
|
||||||
|
{
|
||||||
|
$properties = array_filter(
|
||||||
|
$this->_getClassProperties(get_class($object)),
|
||||||
|
static fn($p) => $p->getName() === $property
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($properties) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current($properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $object
|
||||||
|
* @param $property
|
||||||
|
* @param $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isValidValueForObjectProperty($object, $property, $value): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$property = $this->getReflectedProperty($object, $property);
|
||||||
|
} catch (ReflectionException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$propertyAnnotations = $this->_getPropertyAnnotations($property);
|
||||||
|
|
||||||
|
if (empty($value) && !in_array('required', $propertyAnnotations, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($propertyAnnotations as $propertyAnnotation) {
|
||||||
|
$annotation = explode(' ', $propertyAnnotation);
|
||||||
|
|
||||||
|
$object->validated[$property->name] = $this->_validateProperty($annotation, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count(array_filter($object->validated, static fn($v) => ($v !== null && $v === false))) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $object
|
||||||
|
* @param $property
|
||||||
|
* @return bool
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
private function objectHasProperty($object, $property): bool
|
||||||
|
{
|
||||||
|
return $this->getReflectedProperty($object, $property) instanceof ReflectionProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function isPropertyValid($object, $property): bool
|
||||||
|
{
|
||||||
|
return $this->objectHasProperty($object, $property) &&
|
||||||
|
$this->isValidValueForObjectProperty($object, $property, $object->{$property});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function isObjectValid($object): bool
|
||||||
|
{
|
||||||
|
if (!$object->validated) {
|
||||||
|
$object = $this->validateObject($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !in_array(false, $object->validated, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function validateObject($object)
|
||||||
|
{
|
||||||
|
$this->object = $object;
|
||||||
|
$properties = $this->_getClassProperties(get_class($object));
|
||||||
|
$properties = array_map(fn($property) => array_merge(
|
||||||
|
['name' => $property->name],
|
||||||
|
['rules' => $this->_getPropertyAnnotations($property)]
|
||||||
|
), $properties);
|
||||||
|
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$propertyValue = $object->{$property['name']};
|
||||||
|
|
||||||
|
if (empty($propertyValue) && !in_array('required', $property['rules'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($property['rules'] as $propertyAnnotation) {
|
||||||
|
$annotation = explode(' ', $propertyAnnotation);
|
||||||
|
|
||||||
|
$object->validated[$property['name']] = $this->_validateProperty($annotation, $propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateProperty(array $annotation, $property): bool
|
||||||
|
{
|
||||||
|
if ($annotation[0] === 'var') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($annotation) === 1) {
|
||||||
|
return $this->{sprintf('_validate%s', ucfirst($annotation[0]))}($property);
|
||||||
|
}
|
||||||
|
|
||||||
|
$args_annotation = array_splice($annotation, 1);
|
||||||
|
|
||||||
|
return $this->{sprintf('_validate%s', ucfirst($annotation[0]))}($property, ...$args_annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ReflectionProperty[]
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
private function _getClassProperties($class): array
|
||||||
|
{
|
||||||
|
return (new ReflectionClass($class))->getProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _getPropertyAnnotations(ReflectionProperty $property): array
|
||||||
|
{
|
||||||
|
preg_match_all('#@(.*?)\n#s', $property->getDocComment(), $annotations);
|
||||||
|
|
||||||
|
return array_map(static fn($annotation) => trim($annotation), $annotations[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateString($value): bool
|
||||||
|
{
|
||||||
|
return is_string($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateInt($value): bool
|
||||||
|
{
|
||||||
|
return is_numeric($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateRequired($value): bool
|
||||||
|
{
|
||||||
|
return !empty(trim($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateRequiredOr($value, $other_values): bool
|
||||||
|
{
|
||||||
|
if (!empty($value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($other_values as $other_value) {
|
||||||
|
if (!empty($this->object->$other_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return bool
|
||||||
|
* https://cyber.harvard.edu/rss/languages.html
|
||||||
|
*/
|
||||||
|
private function _validateLang($value): bool
|
||||||
|
{
|
||||||
|
return in_array(strtolower($value), [
|
||||||
|
'af','sq','eu','be','bg','ca','zh-cn','zh-tw','hr','cs','da','nl','nl-be','nl-nl','en','en-au','en-bz',
|
||||||
|
'en-ca','en-ie','en-jm','en-nz','en-ph','en-za','en-tt','en-gb','en-us','en-zw','et','fo','fi','fr','fr-be',
|
||||||
|
'fr-ca','fr-fr','fr-lu','fr-mc','fr-ch','gl','gd','de','de-at','de-de','de-li','de-lu','de-ch','el','haw',
|
||||||
|
'hu','is','in','ga','it','it-it','it-ch','ja','ko','mk','no','pl','pt','pt-br','pt-pt','ro','ro-mo','ro-ro',
|
||||||
|
'ru','ru-mo','ru-ru','sr','sk','sl','es','es-ar','es-bo','es-cl','es-co','es-cr','es-do','es-ec','es-sv',
|
||||||
|
'es-gt','es-hn','es-mx','es-ni','es-pa','es-py','es-pe','es-pr','es-es','es-uy','es-ve','sv','sv-fi','sv-se',
|
||||||
|
'tr','uk',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function _validateNoHtml($value): bool
|
||||||
|
{
|
||||||
|
return strip_tags($value) === $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateUrl($value): bool
|
||||||
|
{
|
||||||
|
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateDate($value): bool
|
||||||
|
{
|
||||||
|
return DateTime::createFromFormat(DateTimeInterface::RSS, $value) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateHour($value): bool
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'options' => [
|
||||||
|
'default' => 0,
|
||||||
|
'min_range' => 0,
|
||||||
|
'max_range' => 23
|
||||||
|
]
|
||||||
|
];
|
||||||
|
return filter_var($value, FILTER_VALIDATE_INT, $options) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateDay($value): bool
|
||||||
|
{
|
||||||
|
return in_array(
|
||||||
|
strtolower($value),
|
||||||
|
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateContentMedia($value): bool
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
foreach ($value as $content) {
|
||||||
|
if (!$content->isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Content) {
|
||||||
|
return $value->isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateMediaType($value): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private function _validateMediaMedium($value): bool
|
||||||
|
{
|
||||||
|
return in_array($value, ['image', 'audio', 'video', 'document', 'executable']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateMediaExpression($value): bool
|
||||||
|
{
|
||||||
|
return in_array($value, ['sample', 'full', 'nonstop']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateEmail($value): bool
|
||||||
|
{
|
||||||
|
return filter_var($value, FILTER_VALIDATE_EMAIL);
|
||||||
|
}
|
||||||
|
}
|
817
srss.php
817
srss.php
@ -1,817 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @property string generator
|
|
||||||
* @property null|string image
|
|
||||||
* @property null|string cloud
|
|
||||||
* @property null|string title
|
|
||||||
* @property null|string link
|
|
||||||
* @property null|string description
|
|
||||||
*/
|
|
||||||
class SRSS extends DomDocument implements Iterator
|
|
||||||
{
|
|
||||||
protected $xpath; // xpath engine
|
|
||||||
protected $items; // array of SRSSItems
|
|
||||||
protected $attr; // array of RSS attributes
|
|
||||||
private $position; // Iterator position
|
|
||||||
|
|
||||||
// lists of possible attributes for RSS
|
|
||||||
protected $possibleAttr = array(
|
|
||||||
'title' => 'nohtml',
|
|
||||||
'link' => 'link',
|
|
||||||
'description' => 'html',
|
|
||||||
'language' => '',
|
|
||||||
//'language' => 'lang',
|
|
||||||
'copyright' => 'nohtml',
|
|
||||||
'pubDate' => 'date',
|
|
||||||
'lastBuildDate' => 'date',
|
|
||||||
'category' => 'nohtml',
|
|
||||||
'docs' => 'link',
|
|
||||||
'cloud' => '',
|
|
||||||
'generator' => 'nohtml',
|
|
||||||
'managingEditor' => 'email',
|
|
||||||
'webMaster' => 'email',
|
|
||||||
'ttl' => 'int',
|
|
||||||
'image' => '',
|
|
||||||
'rating' => 'nohtml',
|
|
||||||
//'textInput' => '',
|
|
||||||
'skipHours' => 'hour',
|
|
||||||
'skipDays' => 'day',
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
parent::__construct();
|
|
||||||
$this->xpath = new DOMXpath($this);
|
|
||||||
$this->attr = array();
|
|
||||||
$this->items = array();
|
|
||||||
$this->position = 0;
|
|
||||||
$this->formatOutput = true;
|
|
||||||
$this->preserveWhiteSpace = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destructor
|
|
||||||
* manage of libxml errors
|
|
||||||
*/
|
|
||||||
public function __destruct()
|
|
||||||
{
|
|
||||||
foreach (libxml_get_errors() as $error) {
|
|
||||||
error_log($error->message, 3, 'error.log');
|
|
||||||
}
|
|
||||||
libxml_clear_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $link string url of the rss
|
|
||||||
* @throws SRSSException
|
|
||||||
* @return SRSS
|
|
||||||
*/
|
|
||||||
public static function read($link)
|
|
||||||
{
|
|
||||||
$doc = new SRSS;
|
|
||||||
if(@$doc->load($link)) // We don't want the warning in case of bad XML. Let's manage it with an exception.
|
|
||||||
{
|
|
||||||
$channel = $doc->getElementsByTagName('channel');
|
|
||||||
if($channel->length == 1){ // Good URL and good RSS
|
|
||||||
$doc->_loadAttributes(); // loading channel properties
|
|
||||||
$doc->getItems(); // loading all items
|
|
||||||
return $doc;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new SRSSException('invalid file '.$link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new SRSSException('Can not open file '.$link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return SRSS
|
|
||||||
*/
|
|
||||||
public static function create()
|
|
||||||
{
|
|
||||||
$doc = new SRSS;
|
|
||||||
$root = $doc->createElement('rss');
|
|
||||||
$root->setAttribute('version', '2.0');
|
|
||||||
$channel = $doc->createElement('channel');
|
|
||||||
$root->appendChild($channel);
|
|
||||||
$doc->appendChild($root);
|
|
||||||
$doc->encoding = "UTF-8";
|
|
||||||
$doc->generator = 'Shikiryu RSS';
|
|
||||||
// $docs = 'http://www.scriptol.fr/rss/RSS-2.0.html';
|
|
||||||
return $doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of "image"'s channel attributes
|
|
||||||
* @return string|array
|
|
||||||
*/
|
|
||||||
public function image()
|
|
||||||
{
|
|
||||||
$args = func_get_args();
|
|
||||||
if(func_num_args() == 0) $args[0] = 'url';
|
|
||||||
$img = $this->xpath->query('//channel/image');
|
|
||||||
if($img->length != 1) return null; // <image> is not in channel
|
|
||||||
$img = $img->item(0);
|
|
||||||
$r = array();
|
|
||||||
foreach($img->childNodes as $child)
|
|
||||||
{
|
|
||||||
if($child->nodeType == XML_ELEMENT_NODE && in_array($child->nodeName, $args))
|
|
||||||
{
|
|
||||||
$r[$child->nodeName] = $child->nodeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (func_num_args() > 1) ? $r : $r[$args[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setter of "image"'s channel attributes
|
|
||||||
* @param $url string picture's url
|
|
||||||
* @param $title string picture's title
|
|
||||||
* @param $link string link on the picture
|
|
||||||
* @param $width int width
|
|
||||||
* @param $height int height
|
|
||||||
* @param $description string description
|
|
||||||
*/
|
|
||||||
public function setImage($url, $title, $link, $width = 0, $height = 0, $description = '')
|
|
||||||
{
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
$array = array();
|
|
||||||
$url = SRSSTools::checkLink($url);
|
|
||||||
$array['url'] = $url;
|
|
||||||
$title = SRSSTools::noHTML($title);
|
|
||||||
$array['title'] = $title;
|
|
||||||
$link = SRSSTools::checkLink($link);
|
|
||||||
$array['link'] = $link;
|
|
||||||
if($width != 0)
|
|
||||||
{
|
|
||||||
$width = SRSSTools::checkInt($width);
|
|
||||||
$array['width'] = $width;
|
|
||||||
}
|
|
||||||
if($height != 0)
|
|
||||||
{
|
|
||||||
$height = SRSSTools::checkInt($height);
|
|
||||||
$array['height'] = $height;
|
|
||||||
}
|
|
||||||
if($description != 0)
|
|
||||||
{
|
|
||||||
$description = SRSSTools::noHTML($description);
|
|
||||||
$array['description'] = $description;
|
|
||||||
}
|
|
||||||
if($this->image == null)
|
|
||||||
{
|
|
||||||
$node = $this->createElement('image');
|
|
||||||
$urlNode = $this->createElement('url', $url);
|
|
||||||
$titleNode = $this->createElement('title', $title);
|
|
||||||
$linkNode = $this->createElement('link', $link);
|
|
||||||
$node->appendChild($urlNode);
|
|
||||||
$node->appendChild($titleNode);
|
|
||||||
$node->appendChild($linkNode);
|
|
||||||
if($width != 0)
|
|
||||||
{
|
|
||||||
$widthNode = $this->createElement('width', $width);
|
|
||||||
$node->appendChild($widthNode);
|
|
||||||
}
|
|
||||||
if($height != 0)
|
|
||||||
{
|
|
||||||
$heightNode = $this->createElement('height', $height);
|
|
||||||
$node->appendChild($heightNode);
|
|
||||||
}
|
|
||||||
if($description != '')
|
|
||||||
{
|
|
||||||
$descNode = $this->createElement('description', $description);
|
|
||||||
$node->appendChild($descNode);
|
|
||||||
}
|
|
||||||
$channel->appendChild($node);
|
|
||||||
}
|
|
||||||
$this->attr['image'] = $array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setter of "cloud"'s channel attributes
|
|
||||||
* @param $domain string domain
|
|
||||||
* @param $port int port
|
|
||||||
* @param $path string path
|
|
||||||
* @param $registerProcedure string register procedure
|
|
||||||
* @param $protocol string protocol
|
|
||||||
*/
|
|
||||||
public function setCloud($domain, $port, $path, $registerProcedure, $protocol)
|
|
||||||
{
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
$array = array();
|
|
||||||
$domain = SRSSTools::noHTML($domain);
|
|
||||||
$array['domain'] = $domain;
|
|
||||||
$port = SRSSTools::checkInt($port);
|
|
||||||
$array['port'] = $port;
|
|
||||||
$path = SRSSTools::noHTML($path);
|
|
||||||
$array['path'] = $path;
|
|
||||||
$registerProcedure = SRSSTools::noHTML($registerProcedure);
|
|
||||||
$array['registerProcedure'] = $registerProcedure;
|
|
||||||
$protocol = SRSSTools::noHTML($protocol);
|
|
||||||
$array['protocol'] = $protocol;
|
|
||||||
if($this->cloud == null)
|
|
||||||
{
|
|
||||||
$node = $this->createElement('cloud');
|
|
||||||
$node->setAttribute('domain', $domain);
|
|
||||||
$node->setAttribute('port', $port);
|
|
||||||
$node->setAttribute('path', $path);
|
|
||||||
$node->setAttribute('registerProcedure', $registerProcedure);
|
|
||||||
$node->setAttribute('protocol', $protocol);
|
|
||||||
$channel->appendChild($node);
|
|
||||||
}
|
|
||||||
$this->attr['cloud'] = $array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if current RSS is a valid one (based on specifications)
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValid()
|
|
||||||
{
|
|
||||||
$valid = true;
|
|
||||||
$items = $this->getItems();
|
|
||||||
$invalidItems = array();
|
|
||||||
$i = 1;
|
|
||||||
foreach($items as $item){
|
|
||||||
if($item->isValid() === false){
|
|
||||||
$invalidItems[] = $i;
|
|
||||||
$valid = false;
|
|
||||||
}
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
return ($valid && $this->title != null && $this->link != null && $this->description != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of current RSS channel
|
|
||||||
* @return DOMElement
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
private function _getChannel()
|
|
||||||
{
|
|
||||||
$channel = $this->getElementsByTagName('channel');
|
|
||||||
if($channel->length != 1) throw new SRSSException('channel node not created, or too many channel nodes');
|
|
||||||
return $channel->item(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setter of others attributes
|
|
||||||
* @param $name
|
|
||||||
* @param $val
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
public function __set($name, $val)
|
|
||||||
{
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
if(array_key_exists($name, $this->possibleAttr)){
|
|
||||||
$flag = $this->possibleAttr[$name];
|
|
||||||
$val = SRSSTools::check($val, $flag);
|
|
||||||
if(!empty($val)){
|
|
||||||
if($this->$name == null){
|
|
||||||
$node = $this->createElement($name, $val);
|
|
||||||
$channel->appendChild($node);
|
|
||||||
}
|
|
||||||
$this->attr[$name] = $val;
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
throw new SRSSException($name.' is not a possible item');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of others attributes
|
|
||||||
* @param $name
|
|
||||||
* @return null|string
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
public function __get($name)
|
|
||||||
{
|
|
||||||
if(isset($this->attr[$name]))
|
|
||||||
return $this->attr[$name];
|
|
||||||
// $channel = $this->_getChannel();
|
|
||||||
if(array_key_exists($name, $this->possibleAttr)){
|
|
||||||
$tmp = $this->xpath->query('//channel/'.$name);
|
|
||||||
if($tmp->length != 1) return null;
|
|
||||||
return $tmp->item(0)->nodeValue;
|
|
||||||
}else{
|
|
||||||
throw new SRSSException($name.' is not a possible value.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a SRSS Item as an item into current RSS as first item
|
|
||||||
* @param SRSSItem $item
|
|
||||||
*/
|
|
||||||
public function addItemBefore(SRSSItem $item)
|
|
||||||
{
|
|
||||||
$node = $this->importNode($item->getItem(), true);
|
|
||||||
$items = $this->getElementsByTagName('item');
|
|
||||||
if($items->length != 0){
|
|
||||||
$firstNode = $items->item(0);
|
|
||||||
if($firstNode != null)
|
|
||||||
$firstNode->parentNode->insertBefore($node, $firstNode);
|
|
||||||
else
|
|
||||||
$this->addItem($item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->addItem($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a SRSS Item as an item into current RSS
|
|
||||||
* @param SRSSItem $item
|
|
||||||
*/
|
|
||||||
public function addItem(SRSSItem $item)
|
|
||||||
{
|
|
||||||
$node = $this->importNode($item->getItem(), true);
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
$channel->appendChild($node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* rewind from Iterator
|
|
||||||
*/
|
|
||||||
public function rewind() {
|
|
||||||
$this->position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* current from Iterator
|
|
||||||
*/
|
|
||||||
public function current() {
|
|
||||||
return $this->items[$this->position];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* key from Iterator
|
|
||||||
*/
|
|
||||||
public function key() {
|
|
||||||
return $this->position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* next from Iterator
|
|
||||||
*/
|
|
||||||
public function next() {
|
|
||||||
++$this->position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* valid from Iterator
|
|
||||||
*/
|
|
||||||
public function valid() {
|
|
||||||
return isset($this->items[$this->position]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of 1st item
|
|
||||||
* @return SRSSItem
|
|
||||||
*/
|
|
||||||
public function getFirst()
|
|
||||||
{
|
|
||||||
return $this->getItem(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of last item
|
|
||||||
* @return SRSSItem
|
|
||||||
*/
|
|
||||||
public function getLast()
|
|
||||||
{
|
|
||||||
$items = $this->getItems();
|
|
||||||
return $items[count($items)-1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of an item
|
|
||||||
* @param $i int
|
|
||||||
* @return SRSSItem
|
|
||||||
*/
|
|
||||||
public function getItem($i)
|
|
||||||
{
|
|
||||||
$i--;
|
|
||||||
return isset($this->items[$i]) ? $this->items[$i] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of all items
|
|
||||||
* @return SRSSItem[]
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
public function getItems()
|
|
||||||
{
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
$item = $channel->getElementsByTagName('item');
|
|
||||||
$length = $item->length;
|
|
||||||
$this->items = array();
|
|
||||||
if($length > 0){
|
|
||||||
for($i = 0; $i < $length; $i++)
|
|
||||||
{
|
|
||||||
$this->items[$i] = new SRSSItem($item->item($i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $this->items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* display XML
|
|
||||||
* see DomDocument's docs
|
|
||||||
*/
|
|
||||||
public function show()
|
|
||||||
{
|
|
||||||
return $this->saveXml();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* putting all RSS attributes into the object
|
|
||||||
*/
|
|
||||||
private function _loadAttributes()
|
|
||||||
{
|
|
||||||
$channel = $this->_getChannel();
|
|
||||||
foreach($channel->childNodes as $child)
|
|
||||||
{
|
|
||||||
if($child->nodeType == XML_ELEMENT_NODE && $child->nodeName != 'item')
|
|
||||||
{
|
|
||||||
if($child->nodeName == 'image'){
|
|
||||||
foreach($child->childNodes as $children)
|
|
||||||
{
|
|
||||||
if($children->nodeType == XML_ELEMENT_NODE)
|
|
||||||
$this->attr['image'][$children->nodeName] = $children->nodeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
$this->attr[$child->nodeName] = $child->nodeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform current object into an array
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function toArray()
|
|
||||||
{
|
|
||||||
$doc = array();
|
|
||||||
foreach($this->attr as $attrName => $attrVal)
|
|
||||||
{
|
|
||||||
$doc[$attrName] = $attrVal;
|
|
||||||
}
|
|
||||||
foreach($this->getItems() as $item)
|
|
||||||
{
|
|
||||||
$doc['items'][] = $item->toArray();
|
|
||||||
}
|
|
||||||
return $doc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property null|string enclosure
|
|
||||||
* @property null|string description
|
|
||||||
*/
|
|
||||||
class SRSSItem extends DomDocument
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var DOMElement
|
|
||||||
*/
|
|
||||||
protected $node; // item node
|
|
||||||
protected $attr; // item's properties
|
|
||||||
|
|
||||||
// possible properties' names
|
|
||||||
protected $possibilities = array(
|
|
||||||
'title' => 'nohtml',
|
|
||||||
'link' => 'link',
|
|
||||||
'description' => 'html',
|
|
||||||
'author' => 'email',
|
|
||||||
'category' => 'nohtml',
|
|
||||||
'comments' => 'link',
|
|
||||||
'enclosure' => '',
|
|
||||||
'guid' => 'nohtml',
|
|
||||||
'pubDate' => 'date',
|
|
||||||
'source' => 'link',
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
* @param DomNode $node
|
|
||||||
*/
|
|
||||||
public function __construct($node = null)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
if($node instanceof DOMElement) $this->node = $this->importNode($node, true);
|
|
||||||
else $this->node = $this->importNode(new DomElement('item'));
|
|
||||||
$this->_loadAttributes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* putting all item attributes into the object
|
|
||||||
*/
|
|
||||||
private function _loadAttributes()
|
|
||||||
{
|
|
||||||
foreach($this->node->childNodes as $child)
|
|
||||||
{
|
|
||||||
if($child->nodeType == XML_ELEMENT_NODE && $child->nodeName != 'item')
|
|
||||||
{
|
|
||||||
$this->{$child->nodeName} = $child->nodeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter of item DomElement
|
|
||||||
*/
|
|
||||||
public function getItem()
|
|
||||||
{
|
|
||||||
$this->appendChild($this->node);
|
|
||||||
return $this->getElementsByTagName('item')->item(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setter for enclosure's properties
|
|
||||||
* @param $url string url
|
|
||||||
* @param $length int length
|
|
||||||
* @param $type string type
|
|
||||||
*/
|
|
||||||
public function setEnclosure($url, $length, $type)
|
|
||||||
{
|
|
||||||
$array = array();
|
|
||||||
$url = SRSSTools::checkLink($url);
|
|
||||||
$array['url'] = $url;
|
|
||||||
$length = SRSSTools::checkInt($length);
|
|
||||||
$array['length'] = $length;
|
|
||||||
$type = SRSSTools::noHTML($type);
|
|
||||||
$array['type'] = $type;
|
|
||||||
if($this->enclosure == null)
|
|
||||||
{
|
|
||||||
$node = $this->createElement('enclosure');
|
|
||||||
$node->setAttribute('url', $url);
|
|
||||||
$node->setAttribute('length', $length);
|
|
||||||
$node->setAttribute('type', $type);
|
|
||||||
$this->node->appendChild($node);
|
|
||||||
}
|
|
||||||
$this->attr['enclosure'] = $array;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if current item is valid (following specifications)
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isValid()
|
|
||||||
{
|
|
||||||
return $this->description != null ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main setter for properties
|
|
||||||
* @param $name
|
|
||||||
* @param $val
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
public function __set($name, $val)
|
|
||||||
{
|
|
||||||
if(array_key_exists($name, $this->possibilities))
|
|
||||||
{
|
|
||||||
$flag = $this->possibilities[$name];
|
|
||||||
if($flag != '')
|
|
||||||
$val = SRSSTools::check($val, $flag);
|
|
||||||
if(!empty($val)){
|
|
||||||
if($this->$name == null){
|
|
||||||
$this->node->appendChild(new DomElement($name, $val));
|
|
||||||
}
|
|
||||||
$this->attr[$name] = $val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw New SRSSException($name.' is not a possible item : '.print_r($this->possibilities));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main getter for properties
|
|
||||||
* @param $name
|
|
||||||
* @return null|string
|
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
|
||||||
public function __get($name)
|
|
||||||
{
|
|
||||||
if(isset($this->attr[$name]))
|
|
||||||
return $this->attr[$name];
|
|
||||||
if(array_key_exists($name, $this->possibilities))
|
|
||||||
{
|
|
||||||
$tmp = $this->node->getElementsByTagName($name);
|
|
||||||
if($tmp->length != 1) return null;
|
|
||||||
return $tmp->item(0)->nodeValue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw New SRSSException($name.' is not a possible item : '.print_r($this->possibilities));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* display item's XML
|
|
||||||
* see DomDocument's docs
|
|
||||||
*/
|
|
||||||
public function show()
|
|
||||||
{
|
|
||||||
return $this->saveXml();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* transform current item's object into an array
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function toArray()
|
|
||||||
{
|
|
||||||
$infos = array();
|
|
||||||
foreach($this->attr as $attrName => $attrVal)
|
|
||||||
{
|
|
||||||
$infos[$attrName] = $attrVal;
|
|
||||||
}
|
|
||||||
return $infos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SRSSException extends Exception
|
|
||||||
{
|
|
||||||
public function __construct($msg)
|
|
||||||
{
|
|
||||||
parent :: __construct($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getError()
|
|
||||||
{
|
|
||||||
$return = 'Une exception a été générée : <strong>Message : ' . $this->getMessage() . '</strong> à la ligne : ' . $this->getLine();
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SRSSTools
|
|
||||||
{
|
|
||||||
public static function check($check, $flag)
|
|
||||||
{
|
|
||||||
switch($flag){
|
|
||||||
case 'nohtml':
|
|
||||||
return self::noHTML($check);
|
|
||||||
break;
|
|
||||||
case 'link':
|
|
||||||
return self::checkLink($check);
|
|
||||||
break;
|
|
||||||
case 'html':
|
|
||||||
return self::HTML4XML($check);
|
|
||||||
break;
|
|
||||||
/*case 'lang':
|
|
||||||
return self::noHTML($check);
|
|
||||||
break;*/
|
|
||||||
case 'date':
|
|
||||||
return self::getRSSDate($check);
|
|
||||||
break;
|
|
||||||
case 'email':
|
|
||||||
return self::checkEmail($check);
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
return self::checkInt($check);
|
|
||||||
break;
|
|
||||||
case 'hour':
|
|
||||||
return self::checkHour($check);
|
|
||||||
break;
|
|
||||||
case 'day':
|
|
||||||
return self::checkDay($check);
|
|
||||||
break;
|
|
||||||
case '':
|
|
||||||
return $check;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new SRSSEXception('flag '.$flag.' does not exist.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* format the RSS to the wanted format
|
|
||||||
* @param $format string wanted format
|
|
||||||
* @param $date string RSS date
|
|
||||||
* @return string date
|
|
||||||
*/
|
|
||||||
public static function formatDate($format, $date)
|
|
||||||
{
|
|
||||||
return date($format, strtotime($date));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* format a date for RSS format
|
|
||||||
* @param string $date date to format
|
|
||||||
* @param string $format
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function getRSSDate($date, $format='')
|
|
||||||
{
|
|
||||||
$datepos = 'dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU';
|
|
||||||
if($format != '' && preg_match('~^(['.$datepos.']{1})(-|/)(['.$datepos.']{1})(-|/)(['.$datepos.']{1})$~', $format, $match)){
|
|
||||||
$sep = $match[2];
|
|
||||||
$format = '%'.$match[1].$sep.'%'.$match[3].$sep.'%'.$match[5];
|
|
||||||
if($dateArray = strptime($date, $format)){
|
|
||||||
$mois = intval($dateArray['tm_mon']) + 1;
|
|
||||||
$annee = strlen($dateArray['tm_year']) > 2 ? '20'.substr($dateArray['tm_year'], -2) : '19'.$dateArray['tm_year'];
|
|
||||||
$date = $annee.'-'.$mois.'-'.$dateArray['tm_mday'];
|
|
||||||
return date("D, d M Y H:i:s T", strtotime($date));
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
else if(strtotime($date) !==false ){
|
|
||||||
return date("D, d M Y H:i:s T", strtotime($date));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
list($j, $m, $a) = explode('/', $date);
|
|
||||||
return date("D, d M Y H:i:s T", strtotime($a.'-'.$m.'-'.$j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if it's an url
|
|
||||||
* @param $check string to check
|
|
||||||
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
|
||||||
*/
|
|
||||||
public static function checkLink($check)
|
|
||||||
{
|
|
||||||
return filter_var($check, FILTER_VALIDATE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* make a string XML-compatible
|
|
||||||
* @param $check string to format
|
|
||||||
* @return string formatted string
|
|
||||||
* TODO CDATA ?
|
|
||||||
*/
|
|
||||||
public static function HTML4XML($check)
|
|
||||||
{
|
|
||||||
return htmlspecialchars($check);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete html tags
|
|
||||||
* @param $check string to format
|
|
||||||
* @return string formatted string
|
|
||||||
*/
|
|
||||||
public static function noHTML($check)
|
|
||||||
{
|
|
||||||
return strip_tags($check);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if it's a day (in RSS terms)
|
|
||||||
* @param $check string to check
|
|
||||||
* @return string the day, or empty string
|
|
||||||
*/
|
|
||||||
public static function checkDay($check)
|
|
||||||
{
|
|
||||||
$possibleDay = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');
|
|
||||||
return in_array(strtolower($check), $possibleDay) ? $check : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if it's an email
|
|
||||||
* @param $check string to check
|
|
||||||
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
|
||||||
*/
|
|
||||||
public static function checkEmail($check)
|
|
||||||
{
|
|
||||||
return filter_var($check, FILTER_VALIDATE_EMAIL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if it's an hour (in RSS terms)
|
|
||||||
* @param $check string to check
|
|
||||||
* @return string|boolean the filtered data, or FALSE if the filter fails.
|
|
||||||
*/
|
|
||||||
public static function checkHour($check)
|
|
||||||
{
|
|
||||||
$options = array(
|
|
||||||
'options' => array(
|
|
||||||
'default' => 0,
|
|
||||||
'min_range' => 0,
|
|
||||||
'max_range' => 23
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return filter_var($check, FILTER_VALIDATE_INT, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if it's an int
|
|
||||||
* @param $check int to check
|
|
||||||
* @return int|boolean the filtered data, or FALSE if the filter fails.
|
|
||||||
*/
|
|
||||||
public static function checkInt($check)
|
|
||||||
{
|
|
||||||
return filter_var($check, FILTER_VALIDATE_INT);
|
|
||||||
}
|
|
||||||
}
|
|
56
tests/BasicBuilderTest.php
Normal file
56
tests/BasicBuilderTest.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\Builder\SRSSBuilder;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
use Shikiryu\SRSS\SRSSTools;
|
||||||
|
|
||||||
|
class BasicBuilderTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $saved = __DIR__.'/resources/tmp/build/testCreateBasicRSS.rss';
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
if (file_exists($this->saved)) {
|
||||||
|
unlink($this->saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function testCreateBasicRSS(): void
|
||||||
|
{
|
||||||
|
$title = 'My Blog';
|
||||||
|
$description = 'is the best';
|
||||||
|
$link = 'http://shikiryu.com/devblog/';
|
||||||
|
$srss = SRSS::create();
|
||||||
|
$srss->title = $title;
|
||||||
|
$srss->description = $description;
|
||||||
|
$srss->link = $link;
|
||||||
|
$items = [
|
||||||
|
['title' => 'title 1', 'link' => 'http://shikiryu.com/devblog/article-1', 'pubDate' => SRSSTools::getRSSDate('2012-03-05 12:02:01'), 'description' => 'description 1'],
|
||||||
|
['title' => 'title 2', 'link' => 'http://shikiryu.com/devblog/article-2', 'pubDate' => SRSSTools::getRSSDate('2022-03-05 22:02:02'), 'description' => 'description 2'],
|
||||||
|
['title' => 'title 3', 'link' => 'http://shikiryu.com/devblog/article-3', 'pubDate' => SRSSTools::getRSSDate('2032-03-05 32:02:03'), 'description' => 'description 3'],
|
||||||
|
['title' => 'title 4', 'link' => 'http://shikiryu.com/devblog/article-4', 'pubDate' => SRSSTools::getRSSDate('2042-03-05 42:02:04'), 'description' => 'description 4'],
|
||||||
|
];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$rssItem = new Item();
|
||||||
|
$rssItem->title = $item['title'];
|
||||||
|
$rssItem->link = $item['link'];
|
||||||
|
$rssItem->pubDate = $item['pubDate'];
|
||||||
|
$rssItem->description = $item['description'];
|
||||||
|
$srss->addItem($rssItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertTrue($srss->isValid());
|
||||||
|
|
||||||
|
self::assertEquals($title, $srss->title);
|
||||||
|
self::assertEquals($description, $srss->description);
|
||||||
|
self::assertEquals($link, $srss->link);
|
||||||
|
|
||||||
|
$builder = new SRSSBuilder();
|
||||||
|
$builder->build($srss, $this->saved);
|
||||||
|
|
||||||
|
self::assertFileExists($this->saved);
|
||||||
|
|
||||||
|
self::assertIsString($srss->show());
|
||||||
|
}
|
||||||
|
}
|
99
tests/BasicReaderTest.php
Normal file
99
tests/BasicReaderTest.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Category;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Cloud;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class BasicReaderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testReadBasicRSS(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/basic.xml');
|
||||||
|
self::assertEquals('test Home Page', $rss->title);
|
||||||
|
$first_item = $rss->getFirst();
|
||||||
|
self::assertNotNull($first_item);
|
||||||
|
self::assertEquals('RSS Tutorial', $first_item->title);
|
||||||
|
|
||||||
|
self::assertTrue($rss->isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSpecificationExampleRSS(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/harvard.xml');
|
||||||
|
self::assertEquals('Liftoff News', $rss->title);
|
||||||
|
self::assertEquals('http://liftoff.msfc.nasa.gov/', $rss->link);
|
||||||
|
self::assertEquals('Liftoff to Space Exploration.', $rss->description);
|
||||||
|
self::assertEquals('en-us', $rss->language);
|
||||||
|
self::assertEquals('Tue, 10 Jun 2003 04:00:00 GMT', $rss->pubDate);
|
||||||
|
self::assertEquals('Tue, 10 Jun 2003 09:41:01 GMT', $rss->lastBuildDate);
|
||||||
|
self::assertEquals('http://blogs.law.harvard.edu/tech/rss', $rss->docs);
|
||||||
|
self::assertEquals('Weblog Editor 2.0', $rss->generator);
|
||||||
|
self::assertEquals('editor@example.com', $rss->managingEditor);
|
||||||
|
self::assertEquals('webmaster@example.com', $rss->webMaster);
|
||||||
|
self::assertCount(4, $rss->items);
|
||||||
|
self::assertEquals('Star City', $rss->getFirst()->title);
|
||||||
|
self::assertEquals('http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp', $rss->getLast()->link);
|
||||||
|
self::assertEquals('Fri, 30 May 2003 11:06:42 GMT', $rss->getItem(2)->pubDate);
|
||||||
|
|
||||||
|
self::assertTrue($rss->isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChannelImage(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/media/cnn.xml');
|
||||||
|
$image = $rss->image;
|
||||||
|
self::assertInstanceOf(Image::class, $image);
|
||||||
|
self::assertEquals('http://i2.cdn.turner.com/cnn/2015/images/09/24/cnn.digital.png', $image->url, var_export($image, true));
|
||||||
|
self::assertEquals('CNN.com - RSS Channel - Entertainment', $image->title, var_export($image, true));
|
||||||
|
self::assertEquals('https://www.cnn.com/entertainment/index.html', $image->link, var_export($image, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChannelCategory(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/cloud.xml');
|
||||||
|
$categories = $rss->category;
|
||||||
|
self::assertCount(1, $categories);
|
||||||
|
$category = $categories[0];
|
||||||
|
self::assertInstanceOf(Category::class, $category);
|
||||||
|
self::assertEquals('http://www.weblogs.com/rssUpdates/changes.xml', $category->domain, var_export($category, true));
|
||||||
|
self::assertEquals('rssUpdates', $category->value, var_export($category, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCloud(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/cloud.xml');
|
||||||
|
$cloud = $rss->cloud;
|
||||||
|
self::assertInstanceOf(Cloud::class, $cloud);
|
||||||
|
self::assertEquals('radio.xmlstoragesystem.com', $cloud->domain, var_export($cloud, true));
|
||||||
|
self::assertEquals('80', $cloud->port, var_export($cloud, true));
|
||||||
|
self::assertEquals('/RPC2', $cloud->path, var_export($cloud, true));
|
||||||
|
self::assertEquals('xmlStorageSystem.rssPleaseNotify', $cloud->registerProcedure, var_export($cloud, true));
|
||||||
|
self::assertEquals('xml-rpc', $cloud->protocol, var_export($cloud, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSource(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/basic.xml');
|
||||||
|
$firstItem = $rss->getFirst();
|
||||||
|
self::assertInstanceOf(Item::class, $firstItem);
|
||||||
|
$source = $firstItem->source;
|
||||||
|
self::assertInstanceOf(Item\Source::class, $source);
|
||||||
|
self::assertEquals('http://www.tomalak.org/links2.xml', $source->url);
|
||||||
|
self::assertEquals('Tomalak\'s Realm', $source->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnclosure(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/basic.xml');
|
||||||
|
$item = $rss->getItem(2);
|
||||||
|
self::assertInstanceOf(Item::class, $item);
|
||||||
|
$enclosure = $item->enclosure;
|
||||||
|
self::assertInstanceOf(Item\Enclosure::class, $enclosure);
|
||||||
|
self::assertEquals('http://www.scripting.com/mp3s/touchOfGrey.mp3', $enclosure->url);
|
||||||
|
self::assertEquals('5588242', $enclosure->length);
|
||||||
|
self::assertEquals('audio/mpeg', $enclosure->type);
|
||||||
|
}
|
||||||
|
}
|
101
tests/CompleteBuilderTest.php
Normal file
101
tests/CompleteBuilderTest.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Category;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Cloud;
|
||||||
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Entity\Item\Enclosure;
|
||||||
|
use Shikiryu\SRSS\Entity\Item\Source;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class CompleteBuilderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testBuildACompleteRSS()
|
||||||
|
{
|
||||||
|
$file = __DIR__ . '/resources/tmp/build/complete.rss';
|
||||||
|
$title = 'Test';
|
||||||
|
$link = 'https://example.org';
|
||||||
|
$description = 'My description is <a href="https://example.org">better</a>';
|
||||||
|
$language = 'en-us';
|
||||||
|
$copyright = 'Shikiryu';
|
||||||
|
$managingEditor = 'editor';
|
||||||
|
$webMaster = 'Shikiryu';
|
||||||
|
$pubDate = (new DateTime())->format(DATE_RSS);
|
||||||
|
$lastBuildDate = $pubDate;
|
||||||
|
$category = new Category();
|
||||||
|
$category->domain = $link;
|
||||||
|
$category->value = 'Test Category';
|
||||||
|
$generator = 'SRSS';
|
||||||
|
$docs = $link;
|
||||||
|
$cloud = new Cloud();
|
||||||
|
$cloud->domain = $link;
|
||||||
|
$cloud->port = 80;
|
||||||
|
$cloud->path = '/test';
|
||||||
|
$ttl = 3660;
|
||||||
|
$image = new Image();
|
||||||
|
$image->link = $link;
|
||||||
|
$image->title = 'title of image';
|
||||||
|
$rating = 'yes';
|
||||||
|
$textInput = 'ignore';
|
||||||
|
$skipDays = 'monday';
|
||||||
|
$skipHours = '8';
|
||||||
|
|
||||||
|
$srss = SRSS::create();
|
||||||
|
$srss->title = $title;
|
||||||
|
$srss->link = $link;
|
||||||
|
$srss->description = $description;
|
||||||
|
$srss->language = $language;
|
||||||
|
$srss->copyright = $copyright;
|
||||||
|
$srss->managingEditor = $managingEditor;
|
||||||
|
$srss->webMaster = $webMaster;
|
||||||
|
$srss->pubDate = $pubDate;
|
||||||
|
$srss->lastBuildDate = $lastBuildDate;
|
||||||
|
$srss->category = $category;
|
||||||
|
$srss->generator = $generator;
|
||||||
|
$srss->docs = $docs;
|
||||||
|
$srss->cloud = $cloud;
|
||||||
|
$srss->ttl = $ttl;
|
||||||
|
$srss->image = $image;
|
||||||
|
$srss->rating = $rating;
|
||||||
|
$srss->textInput = $textInput;
|
||||||
|
$srss->skipDays = $skipDays;
|
||||||
|
$srss->skipHours = $skipHours;
|
||||||
|
|
||||||
|
$item_title = 'item title';
|
||||||
|
$item_link = 'https://example.com';
|
||||||
|
$item_description = 'item <strong>description</strong>';
|
||||||
|
$item_author = 'shikiryu@shikiryu.com';
|
||||||
|
$item_category = new \Shikiryu\SRSS\Entity\Item\Category();
|
||||||
|
$item_category->domain = 'https://shikiryu.com';
|
||||||
|
$item_category->value = 'category shikiryu';
|
||||||
|
$item_comments = $link.'/comments';
|
||||||
|
$item_enclosure = new Enclosure();
|
||||||
|
$item_enclosure->url = $item_link;
|
||||||
|
$item_enclosure->length = 5023;
|
||||||
|
$item_enclosure->type = 'audio/mp3';
|
||||||
|
$item_guid = '123456';
|
||||||
|
$item_pubdate = $pubDate;
|
||||||
|
$item_source = new Source();
|
||||||
|
$item_source->url = $item_link;
|
||||||
|
$item_source->value = 'source';
|
||||||
|
$item_media = new Shikiryu\SRSS\Entity\Media\Content();
|
||||||
|
$item_media->url = $item_link;
|
||||||
|
$item_media->type = 'image/jpg';
|
||||||
|
$item = new Shikiryu\SRSS\Entity\Item();
|
||||||
|
$item->title = $item_title;
|
||||||
|
$item->link = $item_link;
|
||||||
|
$item->description = $item_description;
|
||||||
|
$item->author = $item_author;
|
||||||
|
$item->category[] = $item_category;
|
||||||
|
$item->comments = $item_comments;
|
||||||
|
$item->enclosure = $item_enclosure;
|
||||||
|
$item->guid = $item_guid;
|
||||||
|
$item->pubDate = $item_pubdate;
|
||||||
|
$item->source = $item_source;
|
||||||
|
$item->medias[] = $item_media;
|
||||||
|
|
||||||
|
$srss->addItem($item);
|
||||||
|
|
||||||
|
self::assertTrue($srss->isValid(), var_export($srss->channel->validated + array_map(static fn ($item) => $item->validated, $srss->items), true));
|
||||||
|
}
|
||||||
|
}
|
29
tests/ExceptionTest.php
Normal file
29
tests/ExceptionTest.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\Exception\ChannelNotFoundInRSSException;
|
||||||
|
use Shikiryu\SRSS\Exception\PropertyNotFoundException;
|
||||||
|
use Shikiryu\SRSS\Exception\UnreadableRSSException;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class ExceptionTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testPropertyNotFound(): void
|
||||||
|
{
|
||||||
|
$srss = new SRSS();
|
||||||
|
$this->expectException(PropertyNotFoundException::class);
|
||||||
|
$srss->notfound = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRssNotFound(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnreadableRSSException::class);
|
||||||
|
SRSS::read('not_found.xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMissingChannel(): void
|
||||||
|
{
|
||||||
|
$this->expectException(ChannelNotFoundInRSSException::class);
|
||||||
|
SRSS::read(__DIR__ . '/resources/invalid-no-channel.xml');
|
||||||
|
}
|
||||||
|
}
|
31
tests/MediaTest.php
Normal file
31
tests/MediaTest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
|
||||||
|
class MediaTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testImages(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/media/cnn.xml');
|
||||||
|
self::assertEquals('CNN.com - RSS Channel - Entertainment', $rss->title);
|
||||||
|
|
||||||
|
$first_item = $rss->getFirst();
|
||||||
|
self::assertEquals('Kirstie Alley, \'Cheers\' and \'Veronica\'s Closet\' star, dead at 71', $first_item->title);
|
||||||
|
|
||||||
|
self::assertEquals('https://cdn.cnn.com/cnnnext/dam/assets/221205172141-kirstie-alley-2005-super-169.jpg', $first_item->medias[0]->url);
|
||||||
|
self::assertTrue($rss->isValid(), var_export($rss->channel->validated, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMusicVideo(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read(__DIR__.'/resources/media/music-video.xml');
|
||||||
|
self::assertEquals('Music Videos 101', $rss->title);
|
||||||
|
|
||||||
|
self::assertCount(1, $rss->items);
|
||||||
|
|
||||||
|
$first_item = $rss->getFirst();
|
||||||
|
self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url);
|
||||||
|
self::assertTrue($rss->isValid());
|
||||||
|
}
|
||||||
|
}
|
41
tests/OriginalReaderSRSSTest.php
Normal file
41
tests/OriginalReaderSRSSTest.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
use Shikiryu\SRSS\SRSSTools;
|
||||||
|
|
||||||
|
class OriginalReaderSRSSTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $original = __DIR__.'/resources/harvard.xml';
|
||||||
|
private string $saved = __DIR__.'/resources/tmp/build/rss.xml';
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
if (file_exists($this->saved)) {
|
||||||
|
unlink($this->saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOriginalReader(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::read($this->original);
|
||||||
|
self::assertEquals('Liftoff News', $rss->title);
|
||||||
|
|
||||||
|
$article1 = $rss->getItem(1);
|
||||||
|
$articleFirst = $rss->getFirst();
|
||||||
|
self::assertEquals($article1, $articleFirst);
|
||||||
|
|
||||||
|
$links = [];
|
||||||
|
foreach($rss as $article) {
|
||||||
|
$links[] = sprintf('<a href="%s">%s %s</a>', $article->link, SRSSTools::formatDate('d/m/y', $article->pubDate), $article->title);
|
||||||
|
}
|
||||||
|
self::assertCount(4, $links, var_export($links, true));
|
||||||
|
|
||||||
|
$rssArray = $rss->toArray();
|
||||||
|
self::assertCount(11, $rssArray, var_export($rssArray, true)); // 11 elements in RSS
|
||||||
|
|
||||||
|
$rss->save($this->saved);
|
||||||
|
|
||||||
|
self::assertFileExists($this->saved);
|
||||||
|
}
|
||||||
|
}
|
45
tests/OriginalWriterSRSSTest.php
Normal file
45
tests/OriginalWriterSRSSTest.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
use Shikiryu\SRSS\SRSS;
|
||||||
|
use Shikiryu\SRSS\SRSSTools;
|
||||||
|
|
||||||
|
class OriginalWriterSRSSTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testOriginalWriter(): void
|
||||||
|
{
|
||||||
|
$rss = SRSS::create();
|
||||||
|
$rss->title = 'My Awesome Blog';
|
||||||
|
$rss->link = 'http://shikiryu.com/devblog/';
|
||||||
|
$rss->description = 'is awesome';
|
||||||
|
|
||||||
|
$items = [
|
||||||
|
['title' => 'title 1', 'link' => 'http://shikiryu.com/devblog/article-1', 'pubDate' => SRSSTools::getRSSDate('2012-03-05 12:02:01'), 'description' => 'description 1'],
|
||||||
|
['title' => 'title 2', 'link' => 'http://shikiryu.com/devblog/article-2', 'pubDate' => SRSSTools::getRSSDate('2022-03-05 22:02:02'), 'description' => 'description 2'],
|
||||||
|
['title' => 'title 3', 'link' => 'http://shikiryu.com/devblog/article-3', 'pubDate' => SRSSTools::getRSSDate('2032-03-05 32:02:03'), 'description' => 'description 3'],
|
||||||
|
['title' => 'title 4', 'link' => 'http://shikiryu.com/devblog/article-4', 'pubDate' => SRSSTools::getRSSDate('2042-03-05 42:02:04'), 'description' => 'description 4'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach($items as $item){
|
||||||
|
$rss_item = new Item();
|
||||||
|
$rss_item->title = $item["title"];
|
||||||
|
$rss_item->link = $item['link'];
|
||||||
|
$rss_item->pubDate = $item["pubDate"];
|
||||||
|
$rss_item->description = $item["description"];
|
||||||
|
$rss->addItem($rss_item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstItem = new Item();
|
||||||
|
$firstItem->title = 'title 0';
|
||||||
|
$firstItem->link = 'http://shikiryu.com/devblog/article-0';
|
||||||
|
$firstItem->pubDate = SRSSTools::getRSSDate('2011-03-05 12:02:01');
|
||||||
|
$firstItem->description = 'description 0';
|
||||||
|
$rss->addItemBefore($firstItem);
|
||||||
|
|
||||||
|
self::assertCount(5, $rss->items, var_export($rss->items, true));
|
||||||
|
self::assertEquals('title 0', $rss->getFirst()->title, var_export($rss->items, true));
|
||||||
|
self::assertEquals('title 1', $rss->getItem(2)->title, var_export($rss->items, true));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
22
tests/resources/basic.xml
Normal file
22
tests/resources/basic.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title>test Home Page</title>
|
||||||
|
<link>https://www.test.com</link>
|
||||||
|
<description>Free web building tutorials</description>
|
||||||
|
<item>
|
||||||
|
<title>RSS Tutorial</title>
|
||||||
|
<source url="http://www.tomalak.org/links2.xml">Tomalak's Realm</source>
|
||||||
|
<link>https://www.test.com/xml/xml_rss.asp</link>
|
||||||
|
<description>New RSS tutorial on test</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>XML Tutorial</title>
|
||||||
|
<enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/>
|
||||||
|
<link>https://www.test.com/xml</link>
|
||||||
|
<description>New XML tutorial on test</description>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
|
||||||
|
</rss>
|
78
tests/resources/cloud.xml
Normal file
78
tests/resources/cloud.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- RSS generated by Radio UserLand v8.0.5 on Mon, 13 Oct 2003 18:54:10 GMT -->
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Dave's Handsome Radio Blog!</title>
|
||||||
|
<link>http://radio.weblogs.com/0001015/</link>
|
||||||
|
<description>A non-smoking weblog since June 14, 2002.</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<copyright>Copyright 2003 Dave Winer</copyright>
|
||||||
|
<lastBuildDate>Mon, 13 Oct 2003 18:54:10 GMT</lastBuildDate>
|
||||||
|
<docs>http://backend.userland.com/rss</docs>
|
||||||
|
<generator>Radio UserLand v8.0.5</generator>
|
||||||
|
<managingEditor>dave@userland.com</managingEditor>
|
||||||
|
<webMaster>dave@userland.com</webMaster>
|
||||||
|
<category domain="http://www.weblogs.com/rssUpdates/changes.xml">rssUpdates</category>
|
||||||
|
<skipHours>
|
||||||
|
<hour>0</hour>
|
||||||
|
<hour>23</hour>
|
||||||
|
<hour>1</hour>
|
||||||
|
<hour>2</hour>
|
||||||
|
<hour>3</hour>
|
||||||
|
<hour>22</hour>
|
||||||
|
<hour>17</hour>
|
||||||
|
<hour>11</hour>
|
||||||
|
</skipHours>
|
||||||
|
<cloud domain="radio.xmlstoragesystem.com" port="80" path="/RPC2" registerProcedure="xmlStorageSystem.rssPleaseNotify" protocol="xml-rpc"/>
|
||||||
|
<ttl>60</ttl>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/10/13.html#a1866</link>
|
||||||
|
<description><A href="http://andrea.editthispage.com/"><IMG height=100 alt="A picture named andrea.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/13/andrea.jpg" width=45 align=right vspace=5 border=0></A><A href="http://andrea.editthispage.com/"><IMG height=100 alt="A picture named andrea.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/13/andrea.jpg" width=45 align=right vspace=5 border=0></A><A href="http://andrea.editthispage.com/"><IMG height=100 alt="A picture named andrea.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/13/andrea.jpg" width=45 align=right vspace=5 border=0></A><A href="http://andrea.editthispage.com/"><IMG height=100 alt="A picture named andrea.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/13/andrea.jpg" width=45 align=right vspace=5 border=0></A><A href="http://andrea.editthispage.com/"><IMG height=100 alt="A picture named andrea.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/13/andrea.jpg" width=45 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/10/13.html#a1866</guid>
|
||||||
|
<pubDate>Mon, 13 Oct 2003 18:54:09 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1866&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F10%2F13.html%23a1866</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/10/12.html#a1865</link>
|
||||||
|
<description><A href="http://blogs.law.harvard.edu/crimson1/pictures/viewer$747"><IMG height=89 alt="A picture named cousinJoey.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/12/cousinJoey.jpg" width=65 align=right vspace=5 border=0></A><A href="http://blogs.law.harvard.edu/crimson1/pictures/viewer$747"><IMG height=89 alt="A picture named cousinJoey.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/12/cousinJoey.jpg" width=65 align=right vspace=5 border=0></A><A href="http://blogs.law.harvard.edu/crimson1/pictures/viewer$747"><IMG height=89 alt="A picture named cousinJoey.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/12/cousinJoey.jpg" width=65 align=right vspace=5 border=0></A><A href="http://blogs.law.harvard.edu/crimson1/pictures/viewer$747"><IMG height=89 alt="A picture named cousinJoey.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/12/cousinJoey.jpg" width=65 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/10/12.html#a1865</guid>
|
||||||
|
<pubDate>Sun, 12 Oct 2003 20:42:30 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1865&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F10%2F12.html%23a1865</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/10/08.html#a1864</link>
|
||||||
|
<description><A href="http://radio.weblogs.com/0001015/images/2002/06/04/yourarespecial.gif"><IMG height=52 alt="You are so special." hspace=15 src="http://radio.weblogs.com/0001015/images/2002/06/04/mrrogers.gif" width=45 align=right vspace=5 border=0></A><A href="http://radio.weblogs.com/0001015/images/2002/06/04/yourarespecial.gif"><IMG height=52 alt="You are so special." hspace=15 src="http://radio.weblogs.com/0001015/images/2002/06/04/mrrogers.gif" width=45 align=right vspace=5 border=0></A><A href="http://radio.weblogs.com/0001015/images/2002/06/04/yourarespecial.gif"><IMG height=52 alt="You are so special." hspace=15 src="http://radio.weblogs.com/0001015/images/2002/06/04/mrrogers.gif" width=45 align=right vspace=5 border=0></A><A href="http://radio.weblogs.com/0001015/images/2002/06/04/yourarespecial.gif"><IMG height=52 alt="You are so special." hspace=15 src="http://radio.weblogs.com/0001015/images/2002/06/04/mrrogers.gif" width=45 align=right vspace=5 border=0></A><A href="http://radio.weblogs.com/0001015/images/2002/06/04/yourarespecial.gif"><IMG height=52 alt="You are so special." hspace=15 src="http://radio.weblogs.com/0001015/images/2002/06/04/mrrogers.gif" width=45 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/10/08.html#a1864</guid>
|
||||||
|
<pubDate>Wed, 08 Oct 2003 13:22:16 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1864&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F10%2F08.html%23a1864</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/10/06.html#a1863</link>
|
||||||
|
<description><A href="http://www.state.gov/secretary/rm/2003/17300.htm"><IMG height=51 alt="A picture named powell.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/06/powell.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.state.gov/secretary/rm/2003/17300.htm"><IMG height=51 alt="A picture named powell.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/06/powell.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.state.gov/secretary/rm/2003/17300.htm"><IMG height=51 alt="A picture named powell.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/06/powell.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.state.gov/secretary/rm/2003/17300.htm"><IMG height=51 alt="A picture named powell.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/06/powell.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.state.gov/secretary/rm/2003/17300.htm"><IMG height=51 alt="A picture named powell.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/06/powell.jpg" width=45 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/10/06.html#a1863</guid>
|
||||||
|
<pubDate>Mon, 06 Oct 2003 13:37:40 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1863&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F10%2F06.html%23a1863</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/10/01.html#a1862</link>
|
||||||
|
<description><A href="http://127.0.0.1:5335/xxx"><IMG height=80 alt="A picture named retard.gif" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/01/retard.gif" width=53 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=80 alt="A picture named retard.gif" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/01/retard.gif" width=53 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=80 alt="A picture named retard.gif" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/01/retard.gif" width=53 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=80 alt="A picture named retard.gif" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/01/retard.gif" width=53 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=80 alt="A picture named retard.gif" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/10/01/retard.gif" width=53 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/10/01.html#a1862</guid>
|
||||||
|
<pubDate>Wed, 01 Oct 2003 18:26:11 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1862&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F10%2F01.html%23a1862</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/09/26.html#a1861</link>
|
||||||
|
<description><A href="http://127.0.0.1:5335/xxx"><IMG height=77 alt="A picture named allen.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/26/allen.jpg" width=65 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=77 alt="A picture named allen.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/26/allen.jpg" width=65 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=77 alt="A picture named allen.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/26/allen.jpg" width=65 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=77 alt="A picture named allen.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/26/allen.jpg" width=65 align=right vspace=5 border=0></A><A href="http://127.0.0.1:5335/xxx"><IMG height=77 alt="A picture named allen.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/26/allen.jpg" width=65 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/09/26.html#a1861</guid>
|
||||||
|
<pubDate>Fri, 26 Sep 2003 13:29:36 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1861&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F09%2F26.html%23a1861</comments>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<link>http://radio.weblogs.com/0001015/2003/09/25.html#a1860</link>
|
||||||
|
<description><A href="http://www.command-post.org/2004/2_archives/008524.html"><IMG height=61 alt="A picture named clark.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/25/clark.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.command-post.org/2004/2_archives/008524.html"><IMG height=61 alt="A picture named clark.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/25/clark.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.command-post.org/2004/2_archives/008524.html"><IMG height=61 alt="A picture named clark.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/25/clark.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.command-post.org/2004/2_archives/008524.html"><IMG height=61 alt="A picture named clark.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/25/clark.jpg" width=45 align=right vspace=5 border=0></A><A href="http://www.command-post.org/2004/2_archives/008524.html"><IMG height=61 alt="A picture named clark.jpg" hspace=15 src="http://radio.weblogs.com/0001015/images/2003/09/25/clark.jpg" width=45 align=right vspace=5 border=0></A></description>
|
||||||
|
<guid>http://radio.weblogs.com/0001015/2003/09/25.html#a1860</guid>
|
||||||
|
<pubDate>Thu, 25 Sep 2003 14:08:26 GMT</pubDate>
|
||||||
|
<comments>http://blogs.law.harvard.edu/comments?u=1015&amp;p=1860&amp;link=http%3A%2F%2Fradio.weblogs.com%2F0001015%2F2003%2F09%2F25.html%23a1860</comments>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
41
tests/resources/harvard.xml
Normal file
41
tests/resources/harvard.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Liftoff News</title>
|
||||||
|
<link>http://liftoff.msfc.nasa.gov/</link>
|
||||||
|
<description>Liftoff to Space Exploration.</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
|
||||||
|
<lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
|
||||||
|
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
|
||||||
|
<generator>Weblog Editor 2.0</generator>
|
||||||
|
<managingEditor>editor@example.com</managingEditor>
|
||||||
|
<webMaster>webmaster@example.com</webMaster>
|
||||||
|
<item>
|
||||||
|
<title>Star City</title>
|
||||||
|
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
|
||||||
|
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
|
||||||
|
<pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
|
||||||
|
<guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st.</description>
|
||||||
|
<pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
|
||||||
|
<guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>The Engine That Does More</title>
|
||||||
|
<link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
|
||||||
|
<description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
|
||||||
|
<pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
|
||||||
|
<guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Astronauts' Dirty Laundry</title>
|
||||||
|
<link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
|
||||||
|
<description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
|
||||||
|
<pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
|
||||||
|
<guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
18
tests/resources/invalid-no-channel.xml
Normal file
18
tests/resources/invalid-no-channel.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
|
||||||
|
<title>test Home Page</title>
|
||||||
|
<link>https://www.test.com</link>
|
||||||
|
<description>Free web building tutorials</description>
|
||||||
|
<item>
|
||||||
|
<title>RSS Tutorial</title>
|
||||||
|
<link>https://www.test.com/xml/xml_rss.asp</link>
|
||||||
|
<description>New RSS tutorial on test</description>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>XML Tutorial</title>
|
||||||
|
<link>https://www.test.com/xml</link>
|
||||||
|
<description>New XML tutorial on test</description>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</rss>
|
1383
tests/resources/media/cnn.xml
Normal file
1383
tests/resources/media/cnn.xml
Normal file
File diff suppressed because it is too large
Load Diff
32
tests/resources/media/music-video.xml
Normal file
32
tests/resources/media/music-video.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/">
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title>Music Videos 101</title>
|
||||||
|
<link>http://www.foo.com</link>
|
||||||
|
<description>Discussions of great videos</description>
|
||||||
|
<item>
|
||||||
|
<title>The latest video from an artist</title>
|
||||||
|
<link>http://www.foo.com/item1.htm</link>
|
||||||
|
<media:content url="http://www.foo.com/movie.mov" fileSize="12216320" type="video/quicktime"
|
||||||
|
expression="full">
|
||||||
|
<media:player url="http://www.foo.com/player?id=1111" height="200" width="400"/>
|
||||||
|
<media:hash algo="md5">dfdec888b72151965a34b4b59031290a</media:hash>
|
||||||
|
<media:credit role="producer">producer's name</media:credit>
|
||||||
|
<media:credit role="artist">artist's name</media:credit>
|
||||||
|
<media:category scheme="http://blah.com/scheme">
|
||||||
|
music/artistname/album/song
|
||||||
|
</media:category>
|
||||||
|
<media:text type="plain">
|
||||||
|
Oh, say, can you see, by the dawn's early light
|
||||||
|
</media:text>
|
||||||
|
<media:rating>nonadult</media:rating>
|
||||||
|
<dcterms:valid>
|
||||||
|
start=2002-10-13T09:00+01:00;
|
||||||
|
end=2002-10-17T17:00+01:00;
|
||||||
|
scheme=W3C-DTF
|
||||||
|
</dcterms:valid>
|
||||||
|
</media:content>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
19
tests/resources/media/trailer.xml
Normal file
19
tests/resources/media/trailer.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
|
||||||
|
xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule">
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title>My Movie Review Site</title>
|
||||||
|
<link>http://www.foo.com</link>
|
||||||
|
<description>I review movies.</description>
|
||||||
|
<item>
|
||||||
|
<title>Movie Title: Is this a good movie?</title>
|
||||||
|
<link>http://www.foo.com/item1.htm</link>
|
||||||
|
<media:content url="http://www.foo.com/trailer.mov" fileSize="12216320" type="video/quicktime"
|
||||||
|
expression="sample"/>
|
||||||
|
<creativeCommons:license>
|
||||||
|
http://www.creativecommons.org/licenses/by-nc/1.0
|
||||||
|
</creativeCommons:license>
|
||||||
|
<media:rating>nonadult</media:rating>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
0
tests/resources/tmp/build/.gitkeep
Normal file
0
tests/resources/tmp/build/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user