1
0
mirror of https://github.com/Chouchen/ShikiryuRSS.git synced 2024-11-24 20:28:51 +01:00

🦺 Add validator

This commit is contained in:
Shikiryu 2023-04-07 00:35:20 +02:00
parent bc0e818bbc
commit fec8c122e3
9 changed files with 276 additions and 55 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/vendor/
/.idea
/tests/error.log

View File

@ -2,39 +2,103 @@
namespace Shikiryu\SRSS\Entity;
use ReflectionException;
use Shikiryu\SRSS\Entity\Channel\Image;
use Shikiryu\SRSS\Validator\HasValidator;
use Shikiryu\SRSS\Validator\Validator;
class Channel implements SRSSElement
/**
* 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;
public ?string $language;
public ?string $copyright;
public ?string $managingEditor;
public ?string $webMaster;
public ?string $pubDate;
public ?string $lastBuildDate;
public ?string $category;
public ?string $generator;
public ?string $docs;
public ?string $cloud;
public ?string $ttl;
public ?Image $image;
public ?string $rating;
public ?string $textInput;
public ?string $skipHours;
public ?string $skipDays;
public array $required = ['title', 'link', '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;
/**
* TODO should be an array
*/
public ?string $category = null;
/**
* @nohtml
*/
public ?string $generator = null;
/**
* @url
*/
public ?string $docs = null;
/**
* @var string|null
* TODO validator
*/
public ?string $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 count(array_filter($this->required, fn($field) => !empty($this->{$field}))) === 0;
$annotation_validation = new Validator();
return $annotation_validation->isObjectValid($this);
}
/**

View File

@ -8,11 +8,10 @@ use Shikiryu\SRSS\Entity\Item;
class SRSS implements Iterator
{
public array $items; // array of SRSSItems
protected $attr; // array of RSS attributes
private $position; // Iterator position
public Channel $channel;
public array $items; // array of SRSSItems
private int $position; // Iterator position
// lists of possible attributes for RSS
protected $possibleAttr = [
@ -43,7 +42,6 @@ class SRSS implements Iterator
*/
public function __construct()
{
$this->attr = [];
$this->items = [];
$this->position = 0;
}
@ -76,9 +74,8 @@ class SRSS implements Iterator
/**
* check if current RSS is a valid one (based on specifications)
* @return bool
* TODO use required
*/
public function isValid()
public function isValid(): bool
{
$valid = true;
$items = $this->getItems();
@ -115,7 +112,7 @@ class SRSS implements Iterator
*/
public function __set($name, $val)
{
if (!array_key_exists($name, $this->possibleAttr)) {
if (!property_exists(Channel::class, $name)) {
throw new SRSSException($name . ' is not a possible item');
}
$flag = $this->possibleAttr[$name];
@ -148,7 +145,7 @@ class SRSS implements Iterator
/**
* current from Iterator
*/
public function current()
public function current(): mixed
{
return $this->items[$this->position];
}
@ -179,7 +176,7 @@ class SRSS implements Iterator
/**
* getter of 1st item
* @return Item
* @return Item|null
*/
public function getFirst(): ?Item
{
@ -214,7 +211,6 @@ class SRSS implements Iterator
/**
* getter of all items
* @return Item[]
* @throws SRSSException
*/
public function getItems(): array
{
@ -224,7 +220,6 @@ class SRSS implements Iterator
/**
* transform current object into an array
* @return array
* @throws SRSSException
*/
public function toArray(): array
{

View File

@ -7,6 +7,7 @@ use DOMNodeList;
use DOMXPath;
use Shikiryu\SRSS\Entity\Channel;
use Shikiryu\SRSS\Entity\Channel\Image;
use Shikiryu\SRSS\Entity\Item;
class SRSSParser extends DomDocument
{
@ -57,10 +58,10 @@ class SRSSParser extends DomDocument
}
/**
* @return array|mixed
* @return Item[]
* @throws \Shikiryu\SRSS\SRSSException
*/
private function getItems()
private function getItems(): mixed
{
$channel = $this->_getChannel();
/** @var DOMNodeList $items */

View File

@ -6,26 +6,23 @@ class SRSSTools
{
public static function check($check, $flag)
{
switch($flag){
case 'nohtml': return self::noHTML($check);
case 'link': return self::checkLink($check);
case 'html': return self::HTML4XML($check);
/*case 'lang':
return self::noHTML($check);
*/
case 'date': return self::getRSSDate($check);
case 'email': return self::checkEmail($check);
case 'int': return self::checkInt($check);
case 'hour': return self::checkHour($check);
case 'day': return self::checkDay($check);
case 'folder': return [];
case 'media_type': return self::checkMediaType($check);
case 'media_medium': return self::checkMediaMedium($check);
case 'bool': return self::checkBool($check);
case 'medium_expression': return self::checkMediumExpression($check);
case '': return $check;
default: throw new SRSSException('flag '.$flag.' does not exist.');
}
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),
'folder' => [],
'media_type' => self::checkMediaType($check),
'media_medium' => self::checkMediaMedium($check),
'bool' => self::checkBool($check),
'medium_expression' => self::checkMediumExpression($check),
'' => $check,
default => throw new SRSSException('flag ' . $flag . ' does not exist.'),
};
}
/**

View File

@ -0,0 +1,11 @@
<?php
namespace Shikiryu\SRSS\Validator;
abstract class HasValidator
{
/**
* @var bool[]
*/
public array $validated = [];
}

146
src/Validator/Validator.php Normal file
View File

@ -0,0 +1,146 @@
<?php
namespace Shikiryu\SRSS\Validator;
use DateTimeInterface;
use ReflectionClass;
use ReflectionException;
class Validator
{
/**
* @throws ReflectionException
*/
public function isObjectValid($object): bool
{
if (!$object->validated) {
$object = $this->validateObject($object);
}
return !in_array(false, $object->validated);
}
/**
* @throws ReflectionException
*/
public function validateObject($object)
{
$properties = $this->_getClassProperties(get_class($object));
foreach ($properties as $property) {
$propertyValue = $object->{$property->name};
$propertyAnnotations = $this->_getPropertyAnnotations($property, get_class($object));
if (!in_array('required', $propertyAnnotations) && empty($propertyValue)) {
continue;
}
foreach ($propertyAnnotations as $propertyAnnotation) {
$annotation = explode(' ', $propertyAnnotation);
$object->validated[$property->name] = $this->_validateProperty($annotation, $propertyValue);
}
}
return $object;
}
private function _validateProperty(array $annotation, $property): bool
{
if (count($annotation) === 1) {
return call_user_func([$this, sprintf('_validate%s', ucfirst($annotation[0]))], $property);
}
return true; // TODO check
}
/**
* @throws ReflectionException
*/
private function _getClassProperties($class): array
{
$ReflectionClass = new ReflectionClass($class);
return $ReflectionClass->getProperties();
}
private function _getPropertyAnnotations($property, $className): array
{
preg_match_all('#@(.*?)\n#s', $property->getDocComment(), $annotations);
return array_map(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));
}
/**
* @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']
);
}
}

View File

@ -13,6 +13,8 @@ class BasicReader extends TestCase
$first_item = $rss->getFirst();
self::assertNotNull($first_item);
self::assertEquals('RSS Tutorial', $first_item->title);
self::assertTrue($rss->channel->isValid());
}
public function testRssNotFound()
@ -38,5 +40,7 @@ class BasicReader extends TestCase
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->channel->isValid());
}
}

View File

@ -14,6 +14,7 @@ class MediaTest extends TestCase
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->channel->isValid(), var_export($rss->channel->validated, true));
}
public function testMusicVideo()
@ -25,5 +26,6 @@ class MediaTest extends TestCase
$first_item = $rss->getFirst();
self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url);
self::assertTrue($rss->channel->isValid());
}
}