rss-bridge/lib/XPathAbstract.php
2022-06-19 02:19:32 +02:00

586 lines
17 KiB
PHP

<?php
/**
* An alternative abstract class for bridges utilizing XPath expressions
*
* This class is meant as an alternative base class for bridge implementations.
* It offers preliminary functionality for generating feeds based on XPath
* expressions.
* As a minimum, extending classes should define XPath expressions pointing
* to the feed items contents in the class constants below. In case there is
* more manual fine tuning required, it offers a bunch of methods which can
* be overridden, for example in order to specify formatting of field values
* or more flexible definition of dynamic XPath expressions.
*
* This class extends {@see BridgeAbstract}, which means it incorporates and
* extends all of its functionality.
**/
abstract class XPathAbstract extends BridgeAbstract {
/**
* Source Web page URL (should provide either HTML or XML content)
* You can specify any website URL which serves data suited for display in RSS feeds
* (for example a news blog).
*
* Use {@see XPathAbstract::getSourceUrl()} to read this parameter
*/
const FEED_SOURCE_URL = '';
/**
* XPath expression for extracting the feed title from the source page.
* If this is left blank or does not provide any data {@see BridgeAbstract::getName()}
* is used instead as the feed's title.
*
* Use {@see XPathAbstract::getExpressionTitle()} to read this parameter
*/
const XPATH_EXPRESSION_FEED_TITLE = './/title';
/**
* XPath expression for extracting the feed favicon URL from the source page.
* If this is left blank or does not provide any data {@see BridgeAbstract::getIcon()}
* is used instead as the feed's favicon URL.
*
* Use {@see XPathAbstract::getExpressionIcon()} to read this parameter
*/
const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href';
/**
* XPath expression for extracting the feed items from the source page
* Enter an XPath expression matching a list of dom nodes, each node containing one
* feed article item in total (usually a surrounding <div> or <span> tag). This will
* be the context nodes for all of the following expressions. This expression usually
* starts with a single forward slash.
*
* Use {@see XPathAbstract::getExpressionItem()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM = '';
/**
* XPath expression for extracting an item title from the item context
* This expression should match a node contained within each article item node
* containing the article headline. It should start with a dot followed by two
* forward slashes, referring to any descendant nodes of the article item node.
*
* Use {@see XPathAbstract::getExpressionItemTitle()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_TITLE = '';
/**
* XPath expression for extracting an item's content from the item context
* This expression should match a node contained within each article item node
* containing the article content or description. It should start with a dot
* followed by two forward slashes, referring to any descendant nodes of the
* article item node.
*
* Use {@see XPathAbstract::getExpressionItemContent()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_CONTENT = '';
/**
* XPath expression for extracting an item link from the item context
* This expression should match a node's attribute containing the article URL
* (usually the href attribute of an <a> tag). It should start with a dot
* followed by two forward slashes, referring to any descendant nodes of
* the article item node. Attributes can be selected by prepending an @ char
* before the attributes name.
*
* Use {@see XPathAbstract::getExpressionItemUri()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_URI = '';
/**
* XPath expression for extracting an item author from the item context
* This expression should match a node contained within each article item
* node containing the article author's name. It should start with a dot
* followed by two forward slashes, referring to any descendant nodes of
* the article item node.
*
* Use {@see XPathAbstract::getExpressionItemAuthor()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_AUTHOR = '';
/**
* XPath expression for extracting an item timestamp from the item context
* This expression should match a node or node's attribute containing the
* article timestamp or date (parsable by PHP's strtotime function). It
* should start with a dot followed by two forward slashes, referring to
* any descendant nodes of the article item node. Attributes can be
* selected by prepending an @ char before the attributes name.
*
* Use {@see XPathAbstract::getExpressionItemTimestamp()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_TIMESTAMP = '';
/**
* XPath expression for extracting item enclosures (media content like
* images or movies) from the item context
* This expression should match a node's attribute containing an article
* image URL (usually the src attribute of an <img> tag or a style
* attribute). It should start with a dot followed by two forward slashes,
* referring to any descendant nodes of the article item node. Attributes
* can be selected by prepending an @ char before the attributes name.
*
* Use {@see XPathAbstract::getExpressionItemEnclosures()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_ENCLOSURES = '';
/**
* XPath expression for extracting an item category from the item context
* This expression should match a node or node's attribute contained
* within each article item node containing the article category. This
* could be inside <div> or <span> tags or sometimes be hidden
* in a data attribute. It should start with a dot followed by two
* forward slashes, referring to any descendant nodes of the article
* item node. Attributes can be selected by prepending an @ char
* before the attributes name.
*
* Use {@see XPathAbstract::getExpressionItemCategories()} to read this parameter
*/
const XPATH_EXPRESSION_ITEM_CATEGORIES = '';
/**
* Fix encoding
* Set this to true for fixing feed encoding by invoking PHP's utf8_decode
* function on all extracted texts. Try this in case you see "broken" or
* "weird" characters in your feed where you'd normally expect umlauts
* or any other non-ascii characters.
*
* Use {@see XPathAbstract::getSettingFixEncoding()} to read this parameter
*/
const SETTING_FIX_ENCODING = false;
/**
* Internal storage for resulting feed name, automatically detected
* @var string
*/
private $feedName;
/**
* Internal storage for resulting feed name, automatically detected
* @var string
*/
private $feedUri;
/**
* Internal storage for resulting feed favicon, automatically detected
* @var string
*/
private $feedIcon;
public function getName(){
return $this->feedName ?: parent::getName();
}
public function getURI() {
return $this->feedUri ?: parent::getURI();
}
public function getIcon() {
return $this->feedIcon ?: parent::getIcon();
}
/**
* Source Web page URL (should provide either HTML or XML content)
* @return string
*/
protected function getSourceUrl(){
return static::FEED_SOURCE_URL;
}
/**
* XPath expression for extracting the feed title from the source page
* @return string
*/
protected function getExpressionTitle(){
return static::XPATH_EXPRESSION_FEED_TITLE;
}
/**
* XPath expression for extracting the feed favicon from the source page
* @return string
*/
protected function getExpressionIcon(){
return static::XPATH_EXPRESSION_FEED_ICON;
}
/**
* XPath expression for extracting the feed items from the source page
* @return string
*/
protected function getExpressionItem(){
return static::XPATH_EXPRESSION_ITEM;
}
/**
* XPath expression for extracting an item title from the item context
* @return string
*/
protected function getExpressionItemTitle(){
return static::XPATH_EXPRESSION_ITEM_TITLE;
}
/**
* XPath expression for extracting an item's content from the item context
* @return string
*/
protected function getExpressionItemContent(){
return static::XPATH_EXPRESSION_ITEM_CONTENT;
}
/**
* XPath expression for extracting an item link from the item context
* @return string
*/
protected function getExpressionItemUri(){
return static::XPATH_EXPRESSION_ITEM_URI;
}
/**
* XPath expression for extracting an item author from the item context
* @return string
*/
protected function getExpressionItemAuthor(){
return static::XPATH_EXPRESSION_ITEM_AUTHOR;
}
/**
* XPath expression for extracting an item timestamp from the item context
* @return string
*/
protected function getExpressionItemTimestamp(){
return static::XPATH_EXPRESSION_ITEM_TIMESTAMP;
}
/**
* XPath expression for extracting item enclosures (media content like
* images or movies) from the item context
* @return string
*/
protected function getExpressionItemEnclosures(){
return static::XPATH_EXPRESSION_ITEM_ENCLOSURES;
}
/**
* XPath expression for extracting an item category from the item context
* @return string
*/
protected function getExpressionItemCategories(){
return static::XPATH_EXPRESSION_ITEM_CATEGORIES;
}
/**
* Fix encoding
* @return string
*/
protected function getSettingFixEncoding(){
return static::SETTING_FIX_ENCODING;
}
/**
* Internal helper method for quickly accessing all the user defined constants
* in derived classes
*
* @param $name
* @return bool|string
*/
private function getParam($name){
switch($name) {
case 'url':
return $this->getSourceUrl();
case 'feed_title':
return $this->getExpressionTitle();
case 'feed_icon':
return $this->getExpressionIcon();
case 'item':
return $this->getExpressionItem();
case 'title':
return $this->getExpressionItemTitle();
case 'content':
return $this->getExpressionItemContent();
case 'uri':
return $this->getExpressionItemUri();
case 'author':
return $this->getExpressionItemAuthor();
case 'timestamp':
return $this->getExpressionItemTimestamp();
case 'enclosures':
return $this->getExpressionItemEnclosures();
case 'categories':
return $this->getExpressionItemCategories();
case 'fix_encoding':
return $this->getSettingFixEncoding();
}
}
/**
* Should provide the source website HTML content
* can be easily overwritten for example if special headers or auth infos are required
* @return string
*/
protected function provideWebsiteContent() {
return getContents($this->feedUri);
}
/**
* Should provide the feeds title
*
* @param DOMXPath $xpath
* @return string
*/
protected function provideFeedTitle(DOMXPath $xpath) {
$title = $xpath->query($this->getParam('feed_title'));
if(count($title) === 1) {
return $this->getItemValueOrNodeValue($title);
}
}
/**
* Should provide the URL of the feed's favicon
*
* @param DOMXPath $xpath
* @return string
*/
protected function provideFeedIcon(DOMXPath $xpath) {
$icon = $xpath->query($this->getParam('feed_icon'));
if(count($icon) === 1) {
return $this->cleanMediaUrl($this->getItemValueOrNodeValue($icon));
}
}
/**
* Should provide the feed's items.
*
* @param DOMXPath $xpath
* @return DOMNodeList
*/
protected function provideFeedItems(DOMXPath $xpath) {
return @$xpath->query($this->getParam('item'));
}
public function collectData() {
$this->feedUri = $this->getParam('url');
$webPageHtml = new DOMDocument();
libxml_use_internal_errors(true);
$webPageHtml->loadHTML($this->provideWebsiteContent());
libxml_clear_errors();
libxml_use_internal_errors(false);
$xpath = new DOMXPath($webPageHtml);
$this->feedName = $this->provideFeedTitle($xpath);
$this->feedIcon = $this->provideFeedIcon($xpath);
$entries = $this->provideFeedItems($xpath);
if($entries === false) {
return;
}
foreach ($entries as $entry) {
$item = new \FeedItem();
foreach(array('title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories') as $param) {
$expression = $this->getParam($param);
if('' === $expression) {
continue;
}
//can be a string or DOMNodeList, depending on the expression result
$typedResult = @$xpath->evaluate($expression, $entry);
if ($typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0)
|| (is_string($typedResult) && strlen(trim($typedResult)) === 0)) {
continue;
}
$item->__set($param, $this->formatParamValue($param, $this->getItemValueOrNodeValue($typedResult)));
}
$itemId = $this->generateItemId($item);
if(null !== $itemId) {
$item->setUid($itemId);
}
$this->items[] = $item;
}
}
/**
* @param $param
* @param $value
* @return string|array
*/
protected function formatParamValue($param, $value)
{
$value = $this->fixEncoding($value);
switch ($param) {
case 'title':
return $this->formatItemTitle($value);
case 'content':
return $this->formatItemContent($value);
case 'uri':
return $this->formatItemUri($value);
case 'author':
return $this->formatItemAuthor($value);
case 'timestamp':
return $this->formatItemTimestamp($value);
case 'enclosures':
return $this->formatItemEnclosures($value);
case 'categories':
return $this->formatItemCategories($value);
}
return $value;
}
/**
* Formats the title of a feed item. Takes extracted raw title and returns it formatted
* as string.
* Can be easily overwritten for in case the value needs to be transformed into something
* else.
* @param string $value
* @return string
*/
protected function formatItemTitle($value) {
return $value;
}
/**
* Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
* timestamp as integer.
* Can be easily overwritten for example if a special format has to be expected on the
* source website.
* @param string $value
* @return string
*/
protected function formatItemContent($value) {
return $value;
}
/**
* Formats the URI of a feed item. Takes extracted raw URI and returns it formatted
* as string.
* Can be easily overwritten for in case the value needs to be transformed into something
* else.
* @param string $value
* @return string
*/
protected function formatItemUri($value) {
if(strlen($value) === 0) {
return '';
}
if(strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0) {
return $value;
}
return urljoin($this->feedUri, $value);
}
/**
* Formats the author of a feed item. Takes extracted raw author and returns it formatted
* as string.
* Can be easily overwritten for in case the value needs to be transformed into something
* else.
* @param string $value
* @return string
*/
protected function formatItemAuthor($value) {
return $value;
}
/**
* Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix
* timestamp as integer.
* Can be easily overwritten for example if a special format has to be expected on the
* source website.
* @param string $value
* @return false|int
*/
protected function formatItemTimestamp($value) {
return strtotime($value);
}
/**
* Formats the enclosures of a feed item. Takes extracted raw enclosures and returns them
* formatted as array.
* Can be easily overwritten for in case the values need to be transformed into something
* else.
* @param string $value
* @return array
*/
protected function formatItemEnclosures($value) {
return array($this->cleanMediaUrl($value));
}
/**
* Formats the categories of a feed item. Takes extracted raw categories and returns them
* formatted as array.
* Can be easily overwritten for in case the values need to be transformed into something
* else.
* @param string $value
* @return array
*/
protected function formatItemCategories($value) {
return array($value);
}
/**
* @param $mediaUrl
* @return string|void
*/
protected function cleanMediaUrl($mediaUrl)
{
$pattern = '~(?:http(?:s)?:)?[\/a-zA-Z0-9\-=_,\.\%]+\.(?:jpg|gif|png|jpeg|ico|mp3|webp){1}~i';
$result = preg_match($pattern, $mediaUrl, $matches);
if(1 !== $result) {
return;
}
return urljoin($this->feedUri, $matches[0]);
}
/**
* @param $typedResult
* @return string
*/
protected function getItemValueOrNodeValue($typedResult)
{
if($typedResult instanceof DOMNodeList) {
$item = $typedResult->item(0);
if ($item instanceof DOMElement) {
return trim($item->nodeValue);
} elseif ($item instanceof DOMAttr) {
return trim($item->value);
} elseif ($item instanceof DOMText) {
return trim($item->wholeText);
}
} elseif(is_string($typedResult) && strlen($typedResult) > 0) {
return trim($typedResult);
}
returnServerError('Unknown type of XPath expression result.');
}
/**
* Fixes feed encoding by invoking PHP's utf8_decode function on extracted texts.
* Useful in case of "broken" or "weird" characters in the feed where you'd normally
* expect umlauts.
*
* @param $input
* @return string
*/
protected function fixEncoding($input)
{
return $this->getParam('fix_encoding') ? utf8_decode($input) : $input;
}
/**
* Allows overriding default mechanism determining items Uid's
*
* @param FeedItem $item
* @return string|null
*/
protected function generateItemId(\FeedItem $item) {
return null; //auto generation
}
}