diff --git a/common.php b/common.php
index 3babc23..a89adca 100644
--- a/common.php
+++ b/common.php
@@ -4,6 +4,7 @@ require __DIR__.'/check_setup.php';
require __DIR__.'/vendor/PicoTools/Translator.php';
require __DIR__.'/vendor/PicoDb/Database.php';
require __DIR__.'/vendor/PicoFeed/Client.php';
+require __DIR__.'/vendor/PicoFeed/Parser.php';
require __DIR__.'/models/config.php';
require __DIR__.'/models/user.php';
require __DIR__.'/models/feed.php';
diff --git a/helpers.php b/helpers.php
index 3ef7495..62afba8 100644
--- a/helpers.php
+++ b/helpers.php
@@ -2,6 +2,11 @@
namespace Helper;
+function isRTL($language)
+{
+ return \PicoFeed\Parser::isLanguageRTL($language);
+}
+
function css()
{
$theme = \Model\Config\get('theme');
@@ -17,3 +22,240 @@ function css()
return 'assets/css/app.css?version='.filemtime('assets/css/app.css');
}
+
+function get_current_base_url()
+{
+ $url = isset($_SERVER['HTTPS']) ? 'https://' : 'http://';
+ $url .= $_SERVER['SERVER_NAME'];
+ $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+ $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/';
+
+ return $url;
+}
+
+function escape($value)
+{
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
+}
+
+function flash($html)
+{
+ $data = '';
+
+ if (isset($_SESSION['flash_message'])) {
+ $data = sprintf($html, escape($_SESSION['flash_message']));
+ unset($_SESSION['flash_message']);
+ }
+
+ return $data;
+}
+
+function flash_error($html)
+{
+ $data = '';
+
+ if (isset($_SESSION['flash_error_message'])) {
+ $data = sprintf($html, escape($_SESSION['flash_error_message']));
+ unset($_SESSION['flash_error_message']);
+ }
+
+ return $data;
+}
+
+function format_bytes($size, $precision = 2)
+{
+ $base = log($size) / log(1024);
+ $suffixes = array('', 'k', 'M', 'G', 'T');
+
+ return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
+}
+
+function get_host_from_url($url)
+{
+ return escape(parse_url($url, PHP_URL_HOST)) ?: $url;
+}
+
+function summary($value, $min_length = 5, $max_length = 120, $end = '[...]')
+{
+ $length = strlen($value);
+
+ if ($length > $max_length) {
+ return substr($value, 0, strpos($value, ' ', $max_length)).' '.$end;
+ }
+ else if ($length < $min_length) {
+ return '';
+ }
+
+ return $value;
+}
+
+function in_list($id, array $listing)
+{
+ if (isset($listing[$id])) {
+ return escape($listing[$id]);
+ }
+
+ return '?';
+}
+
+function relative_time($timestamp, $fallback_date_format = '%e %B %Y %k:%M')
+{
+ $diff = time() - $timestamp;
+
+ if ($diff < 60) return \t('%d second'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ $diff = floor($diff / 60);
+ if ($diff < 60) return \t('%d minute'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ $diff = floor($diff / 60);
+ if ($diff < 24) return \t('%d hour'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ $diff = floor($diff / 24);
+ if ($diff < 7) return \t('%d day'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ $diff = floor($diff / 7);
+ if ($diff < 4) return \t('%d week'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ $diff = floor($diff / 4);
+ if ($diff < 12) return \t('%d month'.($diff > 1 ? 's' : '').' ago', $diff);
+
+ return \dt($fallback_date_format, $timestamp);
+}
+
+function error_class(array $errors, $name)
+{
+ return ! isset($errors[$name]) ? '' : ' form-error';
+}
+
+function error_list(array $errors, $name)
+{
+ $html = '';
+
+ if (isset($errors[$name])) {
+
+ $html .= '
>
= Helper\escape(Helper\summary(strip_tags($item['content']), 50, 300)) ?>
-
+
>
= $item['content'] ?>
diff --git a/vendor/PicoFeed/Clients/Stream.php b/vendor/PicoFeed/Clients/Stream.php
index eaa610f..e004b2f 100644
--- a/vendor/PicoFeed/Clients/Stream.php
+++ b/vendor/PicoFeed/Clients/Stream.php
@@ -4,14 +4,27 @@ namespace PicoFeed\Clients;
use \PicoFeed\Logging;
+/**
+ * Stream context HTTP client
+ *
+ * @author Frederic Guillot
+ * @package client
+ */
class Stream extends \PicoFeed\Client
{
+ /**
+ * Do the HTTP request
+ *
+ * @access public
+ * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
+ */
public function doRequest()
{
// Prepare HTTP headers for the request
$headers = array(
'Connection: close',
'User-Agent: '.$this->user_agent,
+ 'Accept-Encoding: gzip',
);
if ($this->etag) $headers[] = 'If-None-Match: '.$this->etag;
@@ -61,6 +74,10 @@ class Stream extends \PicoFeed\Client
$body = $this->decodeChunked($body);
}
+ if (isset($headers['Content-Encoding']) && $headers['Content-Encoding'] === 'gzip') {
+ $body = gzdecode($body);
+ }
+
return array(
'status' => $status,
'body' => $body,
@@ -68,7 +85,13 @@ class Stream extends \PicoFeed\Client
);
}
-
+ /**
+ * Decode a chunked body
+ *
+ * @access public
+ * @param string $str Raw body
+ * @return string Decoded body
+ */
public function decodeChunked($str)
{
for ($result = ''; ! empty($str); $str = trim($str)) {
@@ -84,4 +107,4 @@ class Stream extends \PicoFeed\Client
return $result;
}
-}
\ No newline at end of file
+}
diff --git a/vendor/PicoFeed/Import.php b/vendor/PicoFeed/Import.php
index 76469bf..096dffc 100644
--- a/vendor/PicoFeed/Import.php
+++ b/vendor/PicoFeed/Import.php
@@ -2,6 +2,8 @@
namespace PicoFeed;
+require_once __DIR__.'/Logging.php';
+
class Import
{
private $content = '';
diff --git a/vendor/PicoFeed/Parser.php b/vendor/PicoFeed/Parser.php
index d96aa89..5175cab 100644
--- a/vendor/PicoFeed/Parser.php
+++ b/vendor/PicoFeed/Parser.php
@@ -7,24 +7,69 @@ require_once __DIR__.'/Filter.php';
require_once __DIR__.'/Encoding.php';
require_once __DIR__.'/Grabber.php';
+/**
+ * Base parser class
+ *
+ * @author Frederic Guillot
+ * @package parser
+ */
abstract class Parser
{
+ /**
+ * Hash algorithm used to generate item id, any value supported by PHP, see hash_algos()
+ *
+ * @access public
+ * @static
+ * @var string
+ */
+ public static $hashAlgo = 'crc32b'; // crc32b seems to be faster and shorter than other hash algorithms
+
+ /**
+ * Feed content (XML data)
+ *
+ * @access protected
+ * @var string
+ */
protected $content = '';
+ /**
+ * Feed properties (values parsed)
+ *
+ * @access public
+ */
public $id = '';
public $url = '';
public $title = '';
public $updated = '';
+ public $language = '';
public $items = array();
+
+ /**
+ * Content grabber parameters
+ *
+ * @access public
+ */
public $grabber = false;
public $grabber_ignore_urls = array();
public $grabber_timeout = null;
public $grabber_user_agent = null;
-
+ /**
+ * Parse feed content
+ *
+ * @abstract
+ * @access public
+ * @return mixed
+ */
abstract public function execute();
-
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param string $content Feed content
+ * @param string $http_encoding HTTP encoding (headers)
+ */
public function __construct($content, $http_encoding = '')
{
$xml_encoding = Filter::getEncodingFromXmlTag($content);
@@ -45,7 +90,14 @@ abstract class Parser
$this->content = $this->normalizeData($this->content);
}
-
+ /**
+ * Filter HTML for entry content
+ *
+ * @access public
+ * @param string $item_content Item content
+ * @param string $item_url Item URL
+ * @return string Filtered content
+ */
public function filterHtml($item_content, $item_url)
{
$content = '';
@@ -64,7 +116,12 @@ abstract class Parser
return $content;
}
-
+ /**
+ * Get XML parser errors
+ *
+ * @access public
+ * @return string
+ */
public function getXmlErrors()
{
$errors = array();
@@ -82,8 +139,13 @@ abstract class Parser
return implode(', ', $errors);
}
-
- // Dirty quickfix before XML parsing
+ /**
+ * Dirty quickfixes before XML parsing
+ *
+ * @access public
+ * @param string $data Raw data
+ * @return string Normalized data
+ */
public function normalizeData($data)
{
$data = str_replace("\xc3\x20", '', $data);
@@ -91,8 +153,13 @@ abstract class Parser
return $data;
}
- // For each href attribute, replace & by &
- // Useful for broken XML feeds
+ /**
+ * Replace & by & for each href attribute (Fix broken feeds)
+ *
+ * @access public
+ * @param string $content Raw data
+ * @return string Normalized data
+ */
public function replaceEntityAttribute($content)
{
$content = preg_replace_callback('/href="[^"]+"/', function(array $matches) {
@@ -102,8 +169,13 @@ abstract class Parser
return $content;
}
-
- // Trim whitespace from the begining, the end and inside a string and don't break utf-8 string
+ /**
+ * Trim whitespace from the begining, the end and inside a string and don't break utf-8 string
+ *
+ * @access public
+ * @param string $value Raw data
+ * @return string Normalized data
+ */
public function stripWhiteSpace($value)
{
$value = str_replace("\r", "", $value);
@@ -112,14 +184,25 @@ abstract class Parser
return trim($value);
}
-
+ /**
+ * Generate a unique id for an entry (hash all arguments)
+ *
+ * @access public
+ * @param string $args Pieces of data to hash
+ * @return string Id
+ */
public function generateId()
{
- // crc32b seems to be faster and shorter than other hash algorithms
- return hash('crc32b', implode(func_get_args()));
+ return hash(self::$hashAlgo, implode(func_get_args()));
}
-
+ /**
+ * Try to parse all date format for broken feeds
+ *
+ * @access public
+ * @param string $value Original date format
+ * @return integer Timestamp
+ */
public function parseDate($value)
{
// Format => truncate to this length if not null
@@ -168,7 +251,14 @@ abstract class Parser
return time();
}
-
+ /**
+ * Get a valid date from a given format
+ *
+ * @access public
+ * @param string $format Date format
+ * @param string $value Original date value
+ * @return integer Timestamp
+ */
public function getValidDate($format, $value)
{
$date = \DateTime::createFromFormat($format, $value);
@@ -181,8 +271,13 @@ abstract class Parser
return 0;
}
-
- // Hardcoded list of hostname/token to exclude from id generation
+ /**
+ * Hardcoded list of hostname/token to exclude from id generation
+ *
+ * @access public
+ * @param string $url URL
+ * @return boolean
+ */
public function isExcludedFromId($url)
{
$exclude_list = array('ap.org', 'jacksonville.com');
@@ -193,4 +288,59 @@ abstract class Parser
return false;
}
+
+ /**
+ * Get xml:lang value
+ *
+ * @access public
+ * @param string $xml XML string
+ * @return string Language
+ */
+ public function getXmlLang($xml)
+ {
+ $dom = new \DOMDocument;
+ $dom->loadXML($this->content);
+
+ $xpath = new \DOMXPath($dom);
+ return $xpath->evaluate('string(//@xml:lang[1])') ?: '';
+ }
+
+ /**
+ * Return true if the given language is "Right to Left"
+ *
+ * @static
+ * @access public
+ * @param string $language Language: fr-FR, en-US
+ * @return bool
+ */
+ public static function isLanguageRTL($language)
+ {
+ $language = strtolower($language);
+
+ // Arabic (ar-**)
+ if (strpos($language, 'ar') === 0) return true;
+
+ // Farsi (fa-**)
+ if (strpos($language, 'fa') === 0) return true;
+
+ // Urdu (ur-**)
+ if (strpos($language, 'ur') === 0) return true;
+
+ // Pashtu (ps-**)
+ if (strpos($language, 'ps') === 0) return true;
+
+ // Syriac (syr-**)
+ if (strpos($language, 'syr') === 0) return true;
+
+ // Divehi (dv-**)
+ if (strpos($language, 'dv') === 0) return true;
+
+ // Hebrew (he-**)
+ if (strpos($language, 'he') === 0) return true;
+
+ // Yiddish (yi-**)
+ if (strpos($language, 'yi') === 0) return true;
+
+ return false;
+ }
}
diff --git a/vendor/PicoFeed/Parsers/Atom.php b/vendor/PicoFeed/Parsers/Atom.php
index b483cac..1d068e1 100644
--- a/vendor/PicoFeed/Parsers/Atom.php
+++ b/vendor/PicoFeed/Parsers/Atom.php
@@ -2,8 +2,20 @@
namespace PicoFeed\Parsers;
+/**
+ * Atom parser
+ *
+ * @author Frederic Guillot
+ * @package parser
+ */
class Atom extends \PicoFeed\Parser
{
+ /**
+ * Parse the document
+ *
+ * @access public
+ * @return mixed Atom instance or false
+ */
public function execute()
{
\PicoFeed\Logging::log(\get_called_class().': begin parsing');
@@ -17,6 +29,7 @@ class Atom extends \PicoFeed\Parser
return false;
}
+ $this->language = $this->getXmlLang($this->content);
$this->url = $this->getUrl($xml);
$this->title = $this->stripWhiteSpace((string) $xml->title) ?: $this->url;
$this->id = (string) $xml->id;
@@ -41,6 +54,7 @@ class Atom extends \PicoFeed\Parser
$item->updated = $this->parseDate((string) $entry->updated);
$item->author = $author;
$item->content = $this->filterHtml($this->getContent($entry), $item->url);
+ $item->language = $this->language;
if (empty($item->title)) $item->title = $item->url;
@@ -65,7 +79,13 @@ class Atom extends \PicoFeed\Parser
return $this;
}
-
+ /**
+ * Get the entry content
+ *
+ * @access public
+ * @param SimpleXMLElement $entry XML Entry
+ * @return string
+ */
public function getContent($entry)
{
if (isset($entry->content) && ! empty($entry->content)) {
@@ -84,7 +104,13 @@ class Atom extends \PicoFeed\Parser
return '';
}
-
+ /**
+ * Get the URL from a link tag
+ *
+ * @access public
+ * @param SimpleXMLElement $xml XML tag
+ * @return string
+ */
public function getUrl($xml)
{
foreach ($xml->link as $link) {
diff --git a/vendor/PicoFeed/Parsers/Rss20.php b/vendor/PicoFeed/Parsers/Rss20.php
index c871c59..8feb985 100644
--- a/vendor/PicoFeed/Parsers/Rss20.php
+++ b/vendor/PicoFeed/Parsers/Rss20.php
@@ -2,8 +2,20 @@
namespace PicoFeed\Parsers;
+/**
+ * RSS 2.0 Parser
+ *
+ * @author Frederic Guillot
+ * @package parser
+ */
class Rss20 extends \PicoFeed\Parser
{
+ /**
+ * Parse the document
+ *
+ * @access public
+ * @return mixed Rss20 instance or false
+ */
public function execute()
{
\PicoFeed\Logging::log(\get_called_class().': begin parsing');
@@ -26,7 +38,6 @@ class Rss20 extends \PicoFeed\Parser
$link = (string) $xml_link;
if ($link !== '') {
-
$this->url = (string) $link;
break;
}
@@ -37,6 +48,7 @@ class Rss20 extends \PicoFeed\Parser
$this->url = (string) $xml->channel->link;
}
+ $this->language = isset($xml->channel->language) ? (string) $xml->channel->language : '';
$this->title = $this->stripWhiteSpace((string) $xml->channel->title) ?: $this->url;
$this->id = $this->url;
$this->updated = $this->parseDate(isset($xml->channel->pubDate) ? (string) $xml->channel->pubDate : (string) $xml->channel->lastBuildDate);
@@ -60,6 +72,7 @@ class Rss20 extends \PicoFeed\Parser
$item->content = '';
$item->enclosure = '';
$item->enclosure_type = '';
+ $item->language = $this->language;
foreach ($namespaces as $name => $url) {
@@ -94,22 +107,18 @@ class Rss20 extends \PicoFeed\Parser
if (empty($item->author)) {
if (isset($entry->author)) {
-
$item->author = (string) $entry->author;
}
else if (isset($xml->channel->webMaster)) {
-
$item->author = (string) $xml->channel->webMaster;
}
}
if (isset($entry->guid) && isset($entry->guid['isPermaLink']) && (string) $entry->guid['isPermaLink'] != 'false') {
-
$id = (string) $entry->guid;
$item->id = $this->generateId($id !== '' && $id !== $item->url ? $id : $item->url, $this->isExcludedFromId($this->url) ? '' : $this->url);
}
else {
-
$item->id = $this->generateId($item->url, $this->isExcludedFromId($this->url) ? '' : $this->url);
}