1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-17 20:11:46 +02:00

Upgrades to MarkupRSS module, add support for xmlns:content for option to include full HTML of content, plus fix processwire/processwire-issues#1427

This commit is contained in:
Ryan Cramer
2021-08-20 10:41:42 -04:00
parent 6871035292
commit 4123181bd5

View File

@@ -8,18 +8,27 @@
* *
* USAGE * USAGE
* ~~~~~~ * ~~~~~~
* $rss = $modules->get("MarkupRSS"); * $rss = $modules->get('MarkupRSS');
* $rss->title = "Latest updates"; * $rss->setArray([ // specify RSS feed settings
* $rss->description = "The most recent pages updated on my site"; * 'title' => 'Latest updates',
* $items = $pages->find("limit=10, sort=-modified"); // or any pages you want * 'description' => 'The most recent pages updated on my site',
* 'itemTitleField' => 'title',
* 'itemDateField' => 'created', // date field or 'created', 'published' or 'modified'
* 'itemDescriptionField' => 'summary',
* 'itemDescriptionLength' => 1000, // truncate description to this max length
* 'itemContentField' => 'body', // optional HTML full-content, or omit to exclude
* 'itemAuthorField' => 'author', // optional text or Page field containing author(s)
* ]);
* $items = $pages->find('limit=10, sort=-modified'); // or any pages you want
* $rss->render($items); * $rss->render($items);
* exit; // exit now, or if you dont then at least stop sending further output
* ~~~~~~ * ~~~~~~
* *
* See also the $defaultConfigData below (first thing in the class) to see what * See also the $defaultConfigData below (first thing in the class) to see what
* options you can change at runtime. * options you can change at runtime.
* *
* *
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @property string $title * @property string $title
@@ -31,6 +40,7 @@
* @property int $ttl * @property int $ttl
* @property bool $stripTags * @property bool $stripTags
* @property string $itemTitleField * @property string $itemTitleField
* @property string $itemContentField
* @property string $itemDateField * @property string $itemDateField
* @property string $itemDescriptionField * @property string $itemDescriptionField
* @property string $itemDescriptionLength * @property string $itemDescriptionLength
@@ -51,8 +61,9 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
public static function getModuleInfo() { public static function getModuleInfo() {
return array( return array(
'title' => 'Markup RSS Feed', 'title' => 'Markup RSS Feed',
'version' => 103, 'version' => 104,
'summary' => 'Renders an RSS feed. Given a PageArray, renders an RSS feed of them.', 'summary' => 'Renders an RSS feed. Given a PageArray, renders an RSS feed of them.',
'icon' => 'rss-square',
); );
} }
@@ -66,11 +77,12 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
'ttl' => 60, 'ttl' => 60,
'stripTags' => true, 'stripTags' => true,
'itemTitleField' => 'title', 'itemTitleField' => 'title',
'itemContentField' => '', // for <content:encoded>
'itemDescriptionField' => 'summary', 'itemDescriptionField' => 'summary',
'itemDescriptionLength' => 1024, 'itemDescriptionLength' => 1024,
'itemDateField' => 'created', 'itemDateField' => 'created',
'itemAuthorField' => '', // i.e. createdUser.title or leave blank to not use 'itemAuthorField' => '', // i.e. createdUser.title or leave blank to not use
'itemAuthorElement' => 'dc:creator', // may be 'dc:creator' or 'author' 'itemAuthorElement' => 'dc:creator', // may be 'dc:creator' or 'author' (author if email address)
'header' => 'Content-Type: application/xml; charset=utf-8;', 'header' => 'Content-Type: application/xml; charset=utf-8;',
'feedPages' => array(), 'feedPages' => array(),
); );
@@ -96,16 +108,29 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
* *
*/ */
protected function ent1($str) { protected function ent1($str) {
return $this->wire('sanitizer')->entities1($str); if(strpos($str, '&') !== false) $str = $this->wire()->sanitizer->unentities($str, true);
return $this->ent($str);
} }
/** /**
* @param string $str * @param string $str
* @return string * @return string
* *
*
*/ */
protected function ent($str) { protected function ent($str) {
return $this->wire('sanitizer')->entities($str); $str = htmlspecialchars($str, ENT_XML1, 'UTF-8');
$str = strtr($str, array(
// https://validator.w3.org/feed/
// recommends using hexadecimal entities here
'&gt;' => '&#x0003E;',
'&lt;' => '&#x0003C;',
'&amp;' => '&#x00026;',
'&quot;' => '&#x00022;',
'&apos;' => '&#x00027;',
'&#39;' => '&#x00027;',
));
return $str;
} }
/** /**
@@ -127,15 +152,25 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
$ttl = (int) $this->ttl; $ttl = (int) $this->ttl;
$copyright = $this->ent1($this->copyright); $copyright = $this->ent1($this->copyright);
$out = "<?xml version='1.0' encoding='utf-8' ?>\n"; $out = '<?xml version="1.0" encoding="utf-8" ?>' . "\n";
if($xsl) $out .= "<?xml-stylesheet type='text/xsl' href='$xsl' ?>\n"; if($xsl) $out .= "<?xml-stylesheet type='text/xsl' href='$xsl' ?>\n";
if($css) $out .= "<?xml-stylesheet type='text/css' href='$css' ?>\n"; if($css) $out .= "<?xml-stylesheet type='text/css' href='$css' ?>\n";
$xmlns = array(
'xmlns:atom="http://www.w3.org/2005/Atom"',
'xmlns:dc="http://purl.org/dc/elements/1.1/"'
);
if($this->itemContentField) {
$xmlns[] = 'xmlns:content="http://purl.org/rss/1.0/modules/content/"';
}
$xmlns = implode(' ', $xmlns);
$out .= $out .=
"<rss version='2.0' xmlns:dc='http://purl.org/dc/elements/1.1/'>\n" . "<rss version=\"2.0\" $xmlns>\n" .
"<channel>\n" . "<channel>\n" .
"\t<title>$title</title>\n" . "\t<title>$title</title>\n" .
"\t<link>$url</link>\n" . "\t<link>$url</link>\n" .
"\t<atom:link href=\"$url\" rel=\"self\" type=\"application/rss+xml\" />\n" .
"\t<description>$description</description>\n" . "\t<description>$description</description>\n" .
"\t<pubDate>$pubDate</pubDate>\n"; "\t<pubDate>$pubDate</pubDate>\n";
@@ -154,35 +189,66 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
*/ */
protected function renderItem(Page $page) { protected function renderItem(Page $page) {
$sanitizer = $this->wire()->sanitizer;
$title = strip_tags($page->get($this->itemTitleField)); $title = strip_tags($page->get($this->itemTitleField));
if(empty($title)) return ''; if(empty($title)) return '';
$title = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); $author = '';
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); $description = '';
$title = str_replace('&#039;', '&apos;', $title); $content = '';
$pubDate = '';
$title = $this->ent1($title);
if($this->itemDateField && ($ts = $page->getUnformatted($this->itemDateField))) { if($this->itemDateField && ($ts = $page->getUnformatted($this->itemDateField))) {
// date
$pubDate = "\t\t<pubDate>" . date(DATE_RSS, $ts) . "</pubDate>\n"; $pubDate = "\t\t<pubDate>" . date(DATE_RSS, $ts) . "</pubDate>\n";
} else {
$pubDate = '';
} }
$author = '';
if($this->itemAuthorField) { if($this->itemAuthorField) {
$author = $page->getUnformatted($this->itemAuthorField); // author
if($author instanceof Page) $author = $page->get('title|name'); $author = $page->get($this->itemAuthorField);
if($author instanceof Page) {
$author = $author->get('title|name');
} else if($author instanceof PageArray) {
$author = $author->implode(', ', 'title');
}
$author = (string) $author; $author = (string) $author;
if(strlen($author)) { if(strlen($author)) {
$author = $this->ent($author); $author = $this->ent1($author);
$author = "\t\t<$this->itemAuthorElement>$author</$this->itemAuthorElement>\n"; $author = "\t\t<$this->itemAuthorElement>$author</$this->itemAuthorElement>\n";
} else { } else {
$author = ''; $author = '';
} }
} }
$description = $page->get($this->itemDescriptionField); if($this->itemDescriptionField) {
$description = $description === null ? '' : $this->ent1($this->truncateDescription($description)); // description summary
$description = '<![CDATA[' . $description . ']]>'; $description = $page->get($this->itemDescriptionField);
if($description !== null) {
$description = $sanitizer->unentities($description, true);
$description = $this->truncateDescription($description);
$description = '<![CDATA[' . $this->ent($description) . ']]>';
} else {
$description = '';
}
}
if($this->itemContentField) {
// full HTML content, like that from CKEditor
$content = (string) $page->get($this->itemContentField);
$content = str_ireplace(array('<![CDATA[', ']]>'), array('&lt;![CDATA[', ']]&gt;'), $content);
$rootUrl = $this->wire()->config->urls->httpRoot;
if(strpos($content, '"/') !== false) {
// convert relative URLs to absolute with host
$content = str_ireplace(array(' href="/', ' src="/'), array(' href="' . $rootUrl, ' src="' . $rootUrl), $content);
}
if(strpos($content, 'href="#') !== false) {
// convert in-page #anchor links to page URL with anchor
$content = str_ireplace(' href="#', ' href="' . $page->httpUrl . '#', $content);
}
$content = "\t\t<content:encoded><![CDATA[" . $content . "]]></content:encoded>\n";
}
$out = $out =
"\t<item>\n" . "\t<item>\n" .
@@ -190,8 +256,9 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
"\t\t<description>$description</description>\n" . "\t\t<description>$description</description>\n" .
$pubDate . $pubDate .
$author . $author .
$content .
"\t\t<link>$page->httpUrl</link>\n" . "\t\t<link>$page->httpUrl</link>\n" .
"\n\t<guid>$page->httpUrl</guid>\n" . "\t\t<guid>$page->httpUrl</guid>\n" .
"\t</item>\n"; "\t</item>\n";
return $out; return $out;
@@ -288,12 +355,18 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
/** @var Modules $modules */ /** @var Modules $modules */
$modules = $this->wire('modules'); $modules = $this->wire('modules');
/** @var InputfieldWrapper $inputfields */ /** @var InputfieldWrapper $form */
$inputfields = $this->wire(new InputfieldWrapper()); $form = $this->wire(new InputfieldWrapper());
/** @var InputfieldFieldset $inputfields */
$inputfields = $modules->get('InputfieldFieldset');
$inputfields->attr('name', '_defaults');
$inputfields->label = 'RSS feed defaults';
$inputfields->icon = 'rss';
$inputfields->description = $inputfields->description =
"Select the default options for any given feed. Each of these may be overridden in the API, " . "Select the default options for any given feed. Each of these may be overridden in the API, " .
"so the options you select below should be considered defaults, unless you only have 1 feed. " . "so the options you select below should be considered defaults, unless you only have 1 feed. " .
"If you only need to support 1 feed, then you will not need to override any of these in the API."; "If you only need to support 1 feed, then you will not need to override any of these in the API.";
$form->add($inputfields);
foreach(self::$defaultConfigData as $key => $value) { foreach(self::$defaultConfigData as $key => $value) {
if(!isset($data[$key])) $data[$key] = $value; if(!isset($data[$key])) $data[$key] = $value;
@@ -303,104 +376,130 @@ class MarkupRSS extends WireData implements Module, ConfigurableModule {
$f = $modules->get('InputfieldText'); $f = $modules->get('InputfieldText');
$f->attr('name', 'title'); $f->attr('name', 'title');
$f->attr('value', $data['title']); $f->attr('value', $data['title']);
$f->label = "Default Feed Title"; $f->label = "Feed title";
$f->description = "The primary title of the RSS feed.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldURL $f */ /** @var InputfieldURL $f */
$f = $modules->get('InputfieldURL'); $f = $modules->get('InputfieldURL');
$f->attr('name', 'url'); $f->attr('name', 'url');
$f->attr('value', $data['url']); $f->attr('value', $data['url']);
$f->label = "Default Feed URL"; $f->label = "Feed URL";
$f->description = "The URL on your site that serves as a feed index. May also be left blank."; $f->description = "Optional URL on your site that serves as a feed index.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldText $f */ /** @var InputfieldText $f */
$f = $modules->get('InputfieldText'); $f = $modules->get('InputfieldText');
$f->attr('name', 'description'); $f->attr('name', 'description');
$f->attr('value', $data['description']); $f->attr('value', $data['description']);
$f->label = "Default Feed Description"; $f->label = "Feed description";
$f->description = "The default description for a feed. May also be left blank."; $f->description = "Optional default description for a feed.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldURL $f */ /** @var InputfieldURL $f */
$f = $modules->get('InputfieldURL'); $f = $modules->get('InputfieldURL');
$f->attr('name', 'xsl'); $f->attr('name', 'xsl');
$f->attr('value', $data['xsl']); $f->attr('value', $data['xsl']);
$f->label = "Default Link to XSL Stylesheet"; $f->label = "Link to XSL stylesheet";
$f->description = "Optional URL/link to an XSL stylesheet. Default is none."; $f->description = "Optional URL/link to an XSL stylesheet.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
$f = $modules->get('InputfieldURL'); $f = $modules->get('InputfieldURL');
$f->attr('name', 'css'); $f->attr('name', 'css');
$f->attr('value', $data['css']); $f->attr('value', $data['css']);
$f->label = "Default Link to CSS Stylesheet"; $f->label = "Link to CSS stylesheet";
$f->description = "Optional URL/link to a CSS stylesheet. Default is none."; $f->description = "Optional URL/link to a CSS stylesheet.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldText $f */ /** @var InputfieldText $f */
$f = $modules->get('InputfieldText'); $f = $modules->get('InputfieldText');
$f->attr('name', 'copyright'); $f->attr('name', 'copyright');
$f->attr('value', $data['copyright']); $f->attr('value', $data['copyright']);
$f->label = "Default Feed Copyright"; $f->label = "Feed copyright";
$f->description = "The default copyright statement for a feed. Default is blank."; $f->description = "Optional default copyright statement for a feed.";
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldInteger $f */
$f = $modules->get('InputfieldInteger');
$f->attr('name', 'ttl');
$f->attr('value', (int) $data['ttl']);
$f->label = "Default Feed TTL";
$f->description = "TTL stands for \"time to live\" in minutes. It indicates how long a channel can be cached before refreshing from the source. Default is 60.";
$inputfields->add($f);
/** @var InputfieldSelect $f1 */
$f1 = $modules->get('InputfieldSelect');
$f1->attr('name', 'itemTitleField');
$f1->attr('value', $data['itemTitleField']);
$f1->label = "Default Feed Item Title Field";
$f1->description = "The default field to use as an individual feed item's title.";
/** @var InputfieldSelect $f2 */
$f2 = $modules->get('InputfieldSelect');
$f2->attr('name', 'itemDescriptionField');
$f2->attr('value', $data['itemDescriptionField']);
$f2->label = "Default Feed Item Description Field";
$f2->description = "The default field to use as an individual feed item's description (typically a summary or body field).";
/** @var InputfieldInteger $f2a */
$f2a = $modules->get('InputfieldInteger');
$f2a->attr('name', 'itemDescriptionLength');
$f2a->attr('value', (int) $data['itemDescriptionLength']);
$f2a->label = "Maximum Characters for Item Description Field";
$f2a->description = "The item description will be truncated to be no longer than the max length provided. Specify '0' for no max length. When there is no max length, markup tags will not be stripped.";
/** @var InputfieldSelect $f3 */ /** @var InputfieldSelect $f3 */
$f3 = $modules->get('InputfieldSelect'); $f3 = $modules->get('InputfieldSelect');
$f3->attr('name', 'itemDateField'); $f3->attr('name', 'itemDateField');
$f3->attr('value', $data['itemDateField']); $f3->attr('value', $data['itemDateField']);
$f3->label = "Default Feed Item Date Field"; $f3->label = "Feed item date field";
$f3->description = "The default field to use as an individual feed item's date."; $f3->description = "The default field to use as an individual feed item's date.";
$f3->addOption('created'); $f3->addOption('created');
$f3->addOption('modified'); $f3->addOption('modified');
$f3->addOption('published'); $f3->addOption('published');
$f3->columnWidth = 50;
foreach($this->wire('fields') as $field) { /** @var InputfieldSelect $f1 */
$f1 = $modules->get('InputfieldSelect');
$f1->attr('name', 'itemTitleField');
$f1->attr('value', $data['itemTitleField']);
$f1->label = "Feed item title field";
$f1->description = "The default field to use as an individual feed item's title.";
$f1->columnWidth = 50;
if($field->type instanceof FieldtypeText) { /** @var InputfieldSelect $f2 */
$f2 = $modules->get('InputfieldSelect');
$f2->attr('name', 'itemDescriptionField');
$f2->attr('value', $data['itemDescriptionField']);
$f2->label = "Feed item description field";
$f2->columnWidth = 50;
$f2->description = "The default field to use as an individual feed item's description (typically a summary or body field). Note that HTML will be stripped out.";
/** @var InputfieldInteger $f2a */
$f2a = $modules->get('InputfieldInteger');
$f2a->attr('name', 'itemDescriptionLength');
$f2a->attr('value', (int) $data['itemDescriptionLength']);
$f2a->label = "Maximum characters for item description field";
$f2a->columnWidth = 50;
$f2a->description = "The item description will be truncated to be no longer than the max length provided. Specify '0' for no max length. When there is no max length, markup tags will not be stripped.";
/** @var InputfieldSelect $f4 */
$f4 = $modules->get('InputfieldSelect');
$f4->attr('name', 'itemContentField');
$f4->attr('value', $data['itemContentField']);
$f4->label = "HTML content/body field";
$f4->description = "Optional field that contains the entire article/bodycopy in HTML. Select only if you intend to include the entire content in the RSS feed, otherwise use just the description field.";
$f4->columnWidth = 50;
/** @var InputfieldInteger $ttl */
$ttl = $modules->get('InputfieldInteger');
$ttl->attr('name', 'ttl');
$ttl->attr('value', (int) $data['ttl']);
$ttl->label = "Feed TTL";
$ttl->description = "TTL stands for \"time to live\" in minutes. It indicates how long a channel can be cached before refreshing from the source. Default is 60.";
$ttl->columnWidth = 50;
foreach($this->wire()->fields as $field) {
$fieldtype = $field->type;
if($fieldtype instanceof FieldtypeTextarea) {
$f2->addOption($field->name);
$f4->addOption($field->name);
} else if($fieldtype instanceof FieldtypeText) {
$f1->addOption($field->name); $f1->addOption($field->name);
$f2->addOption($field->name); $f2->addOption($field->name);
} else if($field->type instanceof FieldtypeDatetime) { } else if($fieldtype instanceof FieldtypeDatetime) {
$f3->addOption($field->name); $f3->addOption($field->name);
} }
} }
$inputfields->add($f1); $inputfields->add($f1);
$inputfields->add($f3);
$inputfields->add($f2); $inputfields->add($f2);
$inputfields->add($f2a); $inputfields->add($f2a);
$inputfields->add($f3); $inputfields->add($f4);
$inputfields->add($ttl);
return $inputfields; return $form;
} }