From fec8c122e393cd75f7808b1a0fcbea20af654758 Mon Sep 17 00:00:00 2001 From: Shikiryu Date: Fri, 7 Apr 2023 00:35:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=BA=20Add=20validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/Entity/Channel.php | 104 ++++++++++++++++++----- src/SRSS.php | 19 ++--- src/SRSSParser.php | 5 +- src/SRSSTools.php | 37 ++++----- src/Validator/HasValidator.php | 11 +++ src/Validator/Validator.php | 146 +++++++++++++++++++++++++++++++++ tests/BasicReader.php | 4 + tests/MediaTest.php | 2 + 9 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 src/Validator/HasValidator.php create mode 100644 src/Validator/Validator.php diff --git a/.gitignore b/.gitignore index f45219c..4eb89f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ -/.idea \ No newline at end of file +/.idea +/tests/error.log diff --git a/src/Entity/Channel.php b/src/Entity/Channel.php index c0bae45..9700bad 100644 --- a/src/Entity/Channel.php +++ b/src/Entity/Channel.php @@ -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 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); } /** diff --git a/src/SRSS.php b/src/SRSS.php index addc078..954f5bb 100644 --- a/src/SRSS.php +++ b/src/SRSS.php @@ -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 { diff --git a/src/SRSSParser.php b/src/SRSSParser.php index 7aa83e3..70e5a04 100644 --- a/src/SRSSParser.php +++ b/src/SRSSParser.php @@ -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 */ diff --git a/src/SRSSTools.php b/src/SRSSTools.php index 1a2e2a9..e7d42a4 100644 --- a/src/SRSSTools.php +++ b/src/SRSSTools.php @@ -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.'), + }; } /** diff --git a/src/Validator/HasValidator.php b/src/Validator/HasValidator.php new file mode 100644 index 0000000..96f7de3 --- /dev/null +++ b/src/Validator/HasValidator.php @@ -0,0 +1,11 @@ +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'] + ); + } +} \ No newline at end of file diff --git a/tests/BasicReader.php b/tests/BasicReader.php index 1ce19a4..8e4834e 100644 --- a/tests/BasicReader.php +++ b/tests/BasicReader.php @@ -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()); } } \ No newline at end of file diff --git a/tests/MediaTest.php b/tests/MediaTest.php index ae1e96a..67511d0 100644 --- a/tests/MediaTest.php +++ b/tests/MediaTest.php @@ -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()); } } \ No newline at end of file