diff --git a/packages/framework/src/Framework/Features/BuildTasks/PostBuildTasks/GenerateRssFeed.php b/packages/framework/src/Framework/Features/BuildTasks/PostBuildTasks/GenerateRssFeed.php index 92d5af570a1..844001442bf 100644 --- a/packages/framework/src/Framework/Features/BuildTasks/PostBuildTasks/GenerateRssFeed.php +++ b/packages/framework/src/Framework/Features/BuildTasks/PostBuildTasks/GenerateRssFeed.php @@ -15,13 +15,13 @@ class GenerateRssFeed extends BuildTask public function run(): void { file_put_contents( - Hyde::sitePath(RssFeedService::getDefaultOutputFilename()), + Hyde::sitePath(RssFeedService::outputFilename()), RssFeedService::generateFeed() ); } public function then(): void { - $this->createdSiteFile('_site/'.RssFeedService::getDefaultOutputFilename())->withExecutionTime(); + $this->createdSiteFile('_site/'.RssFeedService::outputFilename())->withExecutionTime(); } } diff --git a/packages/framework/src/Framework/Features/Metadata/GlobalMetadataBag.php b/packages/framework/src/Framework/Features/Metadata/GlobalMetadataBag.php index c160ba75362..29a8f16d59d 100644 --- a/packages/framework/src/Framework/Features/Metadata/GlobalMetadataBag.php +++ b/packages/framework/src/Framework/Features/Metadata/GlobalMetadataBag.php @@ -31,7 +31,7 @@ public static function make(): static } if (Features::rss()) { - $metadataBag->add(Meta::link('alternate', Hyde::url(RssFeedService::getDefaultOutputFilename()), [ + $metadataBag->add(Meta::link('alternate', Hyde::url(RssFeedService::outputFilename()), [ 'type' => 'application/rss+xml', 'title' => RssFeedService::getDescription(), ])); } diff --git a/packages/framework/src/Framework/Services/RssFeedService.php b/packages/framework/src/Framework/Services/RssFeedService.php index 4169e92c75f..4b780974187 100644 --- a/packages/framework/src/Framework/Services/RssFeedService.php +++ b/packages/framework/src/Framework/Services/RssFeedService.php @@ -7,10 +7,16 @@ namespace Hyde\Framework\Services; +use function config; +use function date; use Exception; +use function extension_loaded; +use Hyde\Facades\Site; use Hyde\Hyde; use Hyde\Pages\MarkdownPost; +use Hyde\Support\Helpers\XML; use SimpleXMLElement; +use function throw_unless; /** * @see \Hyde\Framework\Testing\Feature\Services\RssFeedServiceTest @@ -21,28 +27,41 @@ class RssFeedService { public SimpleXMLElement $feed; + public static function generateFeed(): string + { + return (new static)->generate()->getXML(); + } + + public static function outputFilename(): string + { + return config('hyde.rss_filename', 'feed.xml'); + } + + public static function getDescription(): string + { + return XML::escape(config( + 'hyde.rss_description', + XML::escape(Site::name()).' RSS Feed' + )); + } + public function __construct() { - if (! extension_loaded('simplexml') || config('testing.mock_disabled_extensions', false) === true) { - throw new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.'); - } + throw_unless(extension_loaded('simplexml'), + new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.') + ); $this->feed = new SimpleXMLElement(' '); $this->feed->addChild('channel'); - $this->addInitialChannelItems(); + $this->addBaseChannelItems(); } - /** - * @throws \Exception - */ public function generate(): static { - /** @var \Hyde\Pages\MarkdownPost $post */ - foreach (MarkdownPost::getLatestPosts() as $post) { - $this->addItem($post); - } + MarkdownPost::getLatestPosts() + ->each(fn (MarkdownPost $post) => $this->addItem($post)); return $this; } @@ -56,17 +75,18 @@ protected function addItem(MarkdownPost $post): void { $item = $this->feed->channel->addChild('item'); $item->addChild('title', $post->title); - if ($post->canonicalUrl !== null) { - $item->addChild('link', $post->canonicalUrl); - $item->addChild('guid', $post->canonicalUrl); - } $item->addChild('description', $post->description); - $this->addAdditionalItemData($item, $post); + $this->addDynamicItemData($item, $post); } - protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $post): void + protected function addDynamicItemData(SimpleXMLElement $item, MarkdownPost $post): void { + if (isset($post->canonicalUrl)) { + $item->addChild('link', $post->canonicalUrl); + $item->addChild('guid', $post->canonicalUrl); + } + if (isset($post->date)) { $item->addChild('pubDate', $post->date->dateTimeObject->format(DATE_RSS)); } @@ -81,72 +101,37 @@ protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $p if (isset($post->image)) { $image = $item->addChild('enclosure'); - $image->addAttribute('url', Hyde::image((string) $post->image, true)); - $image->addAttribute('type', str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg'); - $image->addAttribute('length', (string) $post->image->getContentLength()); + $image->addAttribute('url', Hyde::image($post->image->getSource(), true)); + $image->addAttribute('type', $this->getImageType($post)); + $image->addAttribute('length', $this->getImageLength($post)); } } - protected function addInitialChannelItems(): void + protected function addBaseChannelItems(): void { - $this->feed->channel->addChild('title', static::getTitle()); - $this->feed->channel->addChild('link', static::getLink()); + $this->feed->channel->addChild('title', XML::escape(Site::name())); + $this->feed->channel->addChild('link', XML::escape(Site::url())); $this->feed->channel->addChild('description', static::getDescription()); - $atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom'); - $atomLink->addAttribute('href', static::getLink().'/'.static::getDefaultOutputFilename()); - $atomLink->addAttribute('rel', 'self'); - $atomLink->addAttribute('type', 'application/rss+xml'); - - $this->addAdditionalChannelData(); - } - - protected function addAdditionalChannelData(): void - { $this->feed->channel->addChild('language', config('site.language', 'en')); $this->feed->channel->addChild('generator', 'HydePHP '.Hyde::version()); $this->feed->channel->addChild('lastBuildDate', date(DATE_RSS)); - } - - protected static function xmlEscape(string $string): string - { - return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8'); - } - - public static function getDescription(): string - { - return static::xmlEscape( - config( - 'hyde.rss_description', - static::getTitle().' RSS Feed' - ) - ); - } - public static function getTitle(): string - { - return static::xmlEscape( - config('site.name', 'HydePHP') - ); - } - - public static function getLink(): string - { - return static::xmlEscape( - rtrim( - config('site.url') ?? 'http://localhost', - '/' - ) - ); + $atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom'); + $atomLink->addAttribute('href', XML::escape(Hyde::url(static::outputFilename()))); + $atomLink->addAttribute('rel', 'self'); + $atomLink->addAttribute('type', 'application/rss+xml'); } - public static function getDefaultOutputFilename(): string + protected function getImageType(MarkdownPost $post): string { - return config('hyde.rss_filename', 'feed.xml'); + /** @todo Add support for more types */ + return str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg'; } - public static function generateFeed(): string + protected function getImageLength(MarkdownPost $post): string { - return (new static)->generate()->getXML(); + /** @todo We might want to add a build warning if the length is zero */ + return (string) $post->image->getContentLength(); } } diff --git a/packages/framework/src/Framework/Services/SitemapService.php b/packages/framework/src/Framework/Services/SitemapService.php index f66e5d93de2..5cd7dda48c5 100644 --- a/packages/framework/src/Framework/Services/SitemapService.php +++ b/packages/framework/src/Framework/Services/SitemapService.php @@ -1,19 +1,28 @@ generate()->getXML(); + } public function __construct() { - if (! extension_loaded('simplexml') || config('testing.mock_disabled_extensions', false) === true) { - throw new Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.'); - } + throw_unless(extension_loaded('simplexml'), + new Exception('The ext-simplexml extension is not installed, but is required to generate sitemaps.') + ); - $this->time_start = microtime(true); + $this->timeStart = microtime(true); $this->xmlElement = new SimpleXMLElement(''); $this->xmlElement->addAttribute('generator', 'HydePHP '.Hyde::version()); @@ -39,7 +53,7 @@ public function __construct() public function generate(): static { - \Hyde\Facades\Route::all()->each(function ($route) { + Route::all()->each(function (Route $route): void { $this->addRoute($route); }); @@ -48,7 +62,7 @@ public function generate(): static public function getXML(): string { - $this->xmlElement->addAttribute('processing_time_ms', (string) round((microtime(true) - $this->time_start) * 1000, 2)); + $this->xmlElement->addAttribute('processing_time_ms', $this->getFormattedProcessingTime()); return (string) $this->xmlElement->asXML(); } @@ -56,19 +70,21 @@ public function getXML(): string public function addRoute(Route $route): void { $urlItem = $this->xmlElement->addChild('url'); - $urlItem->addChild('loc', htmlentities(Hyde::url($route->getOutputPath()))); - $urlItem->addChild('lastmod', htmlentities($this->getLastModDate($route->getSourcePath()))); + + $urlItem->addChild('loc', XML::escape(Hyde::url($route->getOutputPath()))); + $urlItem->addChild('lastmod', XML::escape($this->getLastModDate($route->getSourcePath()))); $urlItem->addChild('changefreq', 'daily'); + if (config('hyde.sitemap.dynamic_priority', true)) { - $urlItem->addChild('priority', $this->getPriority($route->getPageClass(), $route->getPage()->getIdentifier())); + $urlItem->addChild('priority', $this->getPriority( + $route->getPageClass(), $route->getPage()->getIdentifier() + )); } } protected function getLastModDate(string $file): string { - return date('c', filemtime( - $file - )); + return date('c', filemtime($file)); } protected function getPriority(string $pageClass, string $slug): string @@ -96,8 +112,8 @@ protected function getPriority(string $pageClass, string $slug): string return (string) $priority; } - public static function generateSitemap(): string + protected function getFormattedProcessingTime(): string { - return (new static)->generate()->getXML(); + return (string) round((microtime(true) - $this->timeStart) * 1000, 2); } } diff --git a/packages/framework/src/Support/Helpers/XML.php b/packages/framework/src/Support/Helpers/XML.php new file mode 100644 index 00000000000..5097bff88ec --- /dev/null +++ b/packages/framework/src/Support/Helpers/XML.php @@ -0,0 +1,18 @@ +assertInstanceOf('SimpleXMLElement', $service->feed); } - public function test_constructor_throws_exception_when_missing_simplexml_extension() - { - config(['testing.mock_disabled_extensions' => true]); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('The ext-simplexml extension is not installed, but is required to generate RSS feeds.'); - new RssFeedService(); - } - public function test_xml_root_element_is_set_to_rss_2_0() { $service = new RssFeedService(); diff --git a/packages/framework/tests/Feature/Services/SitemapServiceTest.php b/packages/framework/tests/Feature/Services/SitemapServiceTest.php index e27a09d4a96..5f0d7275eb6 100644 --- a/packages/framework/tests/Feature/Services/SitemapServiceTest.php +++ b/packages/framework/tests/Feature/Services/SitemapServiceTest.php @@ -30,15 +30,6 @@ public function test_service_instantiates_xml_element() $this->assertInstanceOf('SimpleXMLElement', $service->xmlElement); } - public function test_constructor_throws_exception_when_missing_simplexml_extension() - { - config(['testing.mock_disabled_extensions' => true]); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('The ext-simplexml extension is not installed, but is required to generate RSS feeds.'); - new SitemapService(); - } - public function test_generate_adds_default_pages_to_xml() { $service = new SitemapService();