Implement Web scraping "HTML + XPath" (#4220)

* More PHP type hints for Fever
Follow-up of https://github.com/FreshRSS/FreshRSS/pull/4201
Related to https://github.com/FreshRSS/FreshRSS/issues/4200

* Detail

* Draft

* Progress

* More draft

* Fix thumbnail PHP type hint
https://github.com/FreshRSS/FreshRSS/issues/4215

* More types

* A bit more

* Refactor FreshRSS_Entry::fromArray

* Progress

* Starts to work

* Categories

* Fonctional

* Layout update

* Fix relative URLs

* Cache system

* Forgotten files

* Remove a debug line

* Automatic form validation of XPath expressions

* data-leave-validation

* Fix reload action

* Simpler examples

* Fix column type for PostgreSQL

* Enforce HTTP encoding

* Readme

* Fix get full content

* target="_blank"

* gitignore

* htmlspecialchars_utf8

* Implement HTML <base>
And fix/revert `xml:base` support in SimplePie e49c578817

* SimplePie upstream PR merged
https://github.com/simplepie/simplepie/pull/723
pull/4241/head^2
Alexandre Alapetite 3 years ago committed by GitHub
parent fa23ae76ea
commit 1fe66ad020
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.fr.md
  2. 2
      README.md
  3. 48
      app/Controllers/feedController.php
  4. 2
      app/Controllers/indexController.php
  5. 18
      app/Controllers/subscriptionController.php
  6. 96
      app/Models/Entry.php
  7. 31
      app/Models/EntryDAO.php
  8. 4
      app/Models/EntryDAOPGSQL.php
  9. 4
      app/Models/EntryDAOSQLite.php
  10. 188
      app/Models/Feed.php
  11. 42
      app/Models/FeedDAO.php
  12. 2
      app/Models/FeedDAOSQLite.php
  13. 17
      app/Models/View.php
  14. 1
      app/SQL/install.sql.mysql.php
  15. 1
      app/SQL/install.sql.pgsql.php
  16. 1
      app/SQL/install.sql.sqlite.php
  17. 43
      app/i18n/cz/sub.php
  18. 43
      app/i18n/de/sub.php
  19. 43
      app/i18n/en-us/sub.php
  20. 43
      app/i18n/en/sub.php
  21. 43
      app/i18n/es/sub.php
  22. 6
      app/i18n/fr/admin.php
  23. 4
      app/i18n/fr/conf.php
  24. 6
      app/i18n/fr/install.php
  25. 45
      app/i18n/fr/sub.php
  26. 20
      app/i18n/fr/user.php
  27. 43
      app/i18n/he/sub.php
  28. 43
      app/i18n/it/sub.php
  29. 43
      app/i18n/ja/sub.php
  30. 43
      app/i18n/ko/sub.php
  31. 43
      app/i18n/nl/sub.php
  32. 43
      app/i18n/oc/sub.php
  33. 43
      app/i18n/pl/sub.php
  34. 43
      app/i18n/pt-br/sub.php
  35. 43
      app/i18n/ru/sub.php
  36. 43
      app/i18n/sk/sub.php
  37. 43
      app/i18n/tr/sub.php
  38. 43
      app/i18n/zh-cn/sub.php
  39. 2
      app/layout/layout.phtml
  40. 2
      app/views/helpers/export/articles.phtml
  41. 104
      app/views/helpers/feed/update.phtml
  42. 7
      app/views/index/normal.phtml
  43. 2
      app/views/index/reader.phtml
  44. 30
      app/views/index/rss.phtml
  45. 91
      app/views/subscription/add.phtml
  46. 4
      data/cache/.gitignore
  47. 7
      lib/Minz/Url.php
  48. 6
      lib/Minz/View.php
  49. 2
      lib/SimplePie/SimplePie.php
  50. 3
      lib/lib_phpQuery.php
  51. 127
      lib/lib_rss.php
  52. 2
      p/api/fever.php
  53. 1
      p/api/greader.php
  54. 45
      p/scripts/extra.js
  55. 8
      p/themes/base-theme/template.css
  56. 8
      p/themes/base-theme/template.rtl.css

@ -15,6 +15,8 @@ Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de
Grâce au standard [WebSub](https://www.w3.org/TR/websub/) (anciennement [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, telles [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).

@ -15,6 +15,8 @@ There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.
Thanks to the [WebSub](https://www.w3.org/TR/websub/) standard (formerly [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
FreshRSS is able to receive instant push notifications from compatible sources, such as [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
Finally, it supports [extensions](#extensions) for further tuning.
Feature requests, bug reports, and other contributions are welcome. The best way to contribute is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).

@ -38,7 +38,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
* @throws FreshRSS_Feed_Exception
* @throws Minz_FileNotExistException
*/
public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '', $attributes = array()) {
public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '', $attributes = array(), $kind = FreshRSS_Feed::KIND_RSS) {
FreshRSS_UserDAO::touch();
@set_time_limit(300);
@ -67,10 +67,19 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$cat_id = $cat == null ? FreshRSS_CategoryDAO::DEFAULTCATEGORYID : $cat->id();
$feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception
$feed->_kind($kind);
$feed->_attributes('', $attributes);
$feed->_httpAuth($http_auth);
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
$feed->_category($cat_id);
switch ($kind) {
case FreshRSS_Feed::KIND_RSS:
case FreshRSS_Feed::KIND_RSS_FORCED:
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
break;
case FreshRSS_Feed::KIND_HTML_XPATH:
$feed->_website($url);
break;
}
$feedDAO = FreshRSS_Factory::createFeedDao();
if ($feedDAO->searchByUrl($feed->url())) {
@ -85,8 +94,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$values = array(
'url' => $feed->url(),
'kind' => $feed->kind(),
'category' => $feed->category(),
'name' => $title != '' ? $title : $feed->name(),
'name' => $title != '' ? $title : $feed->name(true),
'website' => $feed->website(),
'description' => $feed->description(),
'lastUpdate' => 0,
@ -184,8 +194,25 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$timeout = intval(Minz_Request::param('timeout', 0));
$attributes['timeout'] = $timeout > 0 ? $timeout : null;
$feed_kind = Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
if ($feed_kind == FreshRSS_Feed::KIND_HTML_XPATH) {
$xPathSettings = [];
if (Minz_Request::param('xPathFeedTitle', '') != '') $xPathSettings['feedTitle'] = Minz_Request::param('xPathFeedTitle', '', true);
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
if (Minz_Request::param('xPathItemTitle', '') != '') $xPathSettings['itemTitle'] = Minz_Request::param('xPathItemTitle', '', true);
if (Minz_Request::param('xPathItemContent', '') != '') $xPathSettings['itemContent'] = Minz_Request::param('xPathItemContent', '', true);
if (Minz_Request::param('xPathItemUri', '') != '') $xPathSettings['itemUri'] = Minz_Request::param('xPathItemUri', '', true);
if (Minz_Request::param('xPathItemAuthor', '') != '') $xPathSettings['itemAuthor'] = Minz_Request::param('xPathItemAuthor', '', true);
if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
if (Minz_Request::param('xPathItemThumbnail', '') != '') $xPathSettings['itemThumbnail'] = Minz_Request::param('xPathItemThumbnail', '', true);
if (Minz_Request::param('xPathItemCategories', '') != '') $xPathSettings['itemCategories'] = Minz_Request::param('xPathItemCategories', '', true);
if (!empty($xPathSettings)) {
$attributes['xpath'] = $xPathSettings;
}
}
try {
$feed = self::addFeed($url, '', $cat, '', $http_auth, $attributes);
$feed = self::addFeed($url, '', $cat, '', $http_auth, $attributes, $feed_kind);
} catch (FreshRSS_BadUrl_Exception $e) {
// Given url was not a valid url!
Minz_Log::warning($e->getMessage());
@ -264,6 +291,14 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
}
/**
* @param int $feed_id
* @param string $feed_url
* @param bool $force
* @param SimplePie|null $simplePiePush
* @param bool $noCommit
* @param int $maxFeeds
*/
public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $noCommit = false, $maxFeeds = 10) {
@set_time_limit(300);
@ -338,6 +373,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
try {
if ($simplePiePush) {
$simplePie = $simplePiePush; //Used by WebSub
} elseif ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
$simplePie = $feed->loadHtmlXpath(false, $isNewFeed);
} else {
$simplePie = $feed->load(false, $isNewFeed);
}
@ -377,6 +414,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$oldGuids = array();
// Add entries in database if possible.
/** @var FreshRSS_Entry $entry */
foreach ($entries as $entry) {
if (isset($newGuids[$entry->guid()])) {
continue; //Skip subsequent articles with same GUID
@ -765,7 +803,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
//Re-fetch articles as if the feed was new.
$feedDAO->updateFeed($feed->id(), [ 'lastUpdate' => 0 ]);
self::actualizeFeed($feed_id, null, false, null, true);
self::actualizeFeed($feed_id, '', false);
//Extract all feed entries from database, load complete content and store them back in database.
$entries = $entryDAO->listWhere('f', $feed_id, FreshRSS_Entry::STATE_ALL, 'DESC', 0);

@ -160,7 +160,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
// No layout for RSS output.
$this->view->url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
$this->view->rss_url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$this->view->_layout(false);
header('Content-Type: application/rss+xml; charset=utf-8');

@ -192,8 +192,26 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
$feed_kind = Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
if ($feed_kind == FreshRSS_Feed::KIND_HTML_XPATH) {
$xPathSettings = [];
if (Minz_Request::param('xPathFeedTitle', '') != '') $xPathSettings['feedTitle'] = Minz_Request::param('xPathFeedTitle', '', true);
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
if (Minz_Request::param('xPathItemTitle', '') != '') $xPathSettings['itemTitle'] = Minz_Request::param('xPathItemTitle', '', true);
if (Minz_Request::param('xPathItemContent', '') != '') $xPathSettings['itemContent'] = Minz_Request::param('xPathItemContent', '', true);
if (Minz_Request::param('xPathItemUri', '') != '') $xPathSettings['itemUri'] = Minz_Request::param('xPathItemUri', '', true);
if (Minz_Request::param('xPathItemAuthor', '') != '') $xPathSettings['itemAuthor'] = Minz_Request::param('xPathItemAuthor', '', true);
if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
if (Minz_Request::param('xPathItemThumbnail', '') != '') $xPathSettings['itemThumbnail'] = Minz_Request::param('xPathItemThumbnail', '', true);
if (Minz_Request::param('xPathItemCategories', '') != '') $xPathSettings['itemCategories'] = Minz_Request::param('xPathItemCategories', '', true);
if (!empty($xPathSettings)) {
$feed->_attributes('xpath', $xPathSettings);
}
}
$values = array(
'name' => Minz_Request::param('name', ''),
'kind' => $feed_kind,
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
'website' => checkUrl(Minz_Request::param('website', '')),
'url' => checkUrl(Minz_Request::param('url', '')),

@ -59,6 +59,38 @@ class FreshRSS_Entry extends Minz_Model {
$this->_guid($guid);
}
/** @param array<string,mixed> $dao */
public static function fromArray(array $dao): FreshRSS_Entry {
if (!isset($dao['content'])) {
$dao['content'] = '';
}
if (isset($dao['thumbnail'])) {
$dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>';
}
$entry = new FreshRSS_Entry(
$dao['id_feed'] ?? 0,
$dao['guid'] ?? '',
$dao['title'] ?? '',
$dao['author'] ?? '',
$dao['content'] ?? '',
$dao['link'] ?? '',
$dao['date'] ?? 0,
$dao['is_read'] ?? false,
$dao['is_favorite'] ?? false,
$dao['tags'] ?? ''
);
if (isset($dao['id'])) {
$entry->_id($dao['id']);
}
if (!empty($dao['timestamp'])) {
$entry->_date(strtotime($dao['timestamp']));
}
if (!empty($dao['categories'])) {
$entry->_tags($dao['categories']);
}
return $entry;
}
public function id(): string {
return $this->id;
}
@ -83,6 +115,7 @@ class FreshRSS_Entry extends Minz_Model {
return $this->content;
}
/** @return array<array<string,string>> */
public function enclosures(bool $searchBodyImages = false): array {
$results = [];
try {
@ -97,11 +130,20 @@ class FreshRSS_Entry extends Minz_Model {
if ($searchEnclosures) {
$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
foreach ($enclosures as $enclosure) {
$results[] = [
$result = [
'url' => $enclosure->getAttribute('src'),
'type' => $enclosure->getAttribute('data-type'),
'medium' => $enclosure->getAttribute('data-medium'),
'length' => $enclosure->getAttribute('data-length'),
];
if (empty($result['medium'])) {
switch (strtolower($enclosure->nodeName)) {
case 'img': $result['medium'] = 'image'; break;
case 'video': $result['medium'] = 'video'; break;
case 'audio': $result['medium'] = 'audio'; break;
}
}
$results[] = $result;
}
}
if ($searchBodyImages) {
@ -432,52 +474,12 @@ class FreshRSS_Entry extends Minz_Model {
}
}
public static function getContentByParsing(string $url, string $path, array $attributes = array(), int $maxRedirs = 3): string {
$limits = FreshRSS_Context::$system_conf->limits;
$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
if (FreshRSS_Context::$system_conf->simplepie_syslog_enabled) {
syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
//CURLOPT_FAILONERROR => true;
CURLOPT_MAXREDIRS => 4,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', //Enable all encodings
]);
curl_setopt_array($ch, FreshRSS_Context::$system_conf->curl_options);
if (isset($attributes['curl_params']) && is_array($attributes['curl_params'])) {
curl_setopt_array($ch, $attributes['curl_params']);
}
if (isset($attributes['ssl_verify'])) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $attributes['ssl_verify'] ? true : false);
if (!$attributes['ssl_verify']) {
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1');
}
}
$html = curl_exec($ch);
$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$c_error = curl_error($ch);
curl_close($ch);
if ($c_status != 200 || $c_error != '') {
Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
}
if (is_string($html) && strlen($html) > 0) {
/**
* @param array<string,mixed> $attributes
*/
public static function getContentByParsing(string $url, string $path, array $attributes = [], int $maxRedirs = 3): string {
$html = getHtml($url, $attributes);
if (strlen($html) > 0) {
require_once(LIB_PATH . '/lib_phpQuery.php');
/**
* @var phpQueryObject @doc

@ -164,7 +164,7 @@ INSERT IGNORE INTO `_entry` (
)
SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
FROM `_entrytmp`
ORDER BY date;
ORDER BY date, id;
DELETE FROM `_entrytmp` WHERE id <= @rank;
SQL;
@ -658,6 +658,7 @@ SQL;
}
}
/** @return FreshRSS_Entry|null */
public function searchByGuid($id_feed, $guid) {
// un guid est unique pour un flux donné
$sql = 'SELECT id, guid, title, author, '
@ -669,9 +670,10 @@ SQL;
$stm->bindParam(':guid', $guid);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return isset($res[0]) ? self::daoToEntry($res[0]) : null;
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
/** @return FreshRSS_Entry|null */
public function searchById($id) {
$sql = 'SELECT id, guid, title, author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@ -681,7 +683,7 @@ SQL;
$stm->bindParam(':id', $id, PDO::PARAM_INT);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
return isset($res[0]) ? self::daoToEntry($res[0]) : null;
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
public function searchIdByGuid($id_feed, $guid) {
@ -1061,7 +1063,7 @@ SQL;
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
if ($stm) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
yield self::daoToEntry($row);
yield FreshRSS_Entry::fromArray($row);
}
} else {
yield false;
@ -1092,7 +1094,7 @@ SQL;
$stm = $this->pdo->prepare($sql);
$stm->execute($ids);
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
yield self::daoToEntry($row);
yield FreshRSS_Entry::fromArray($row);
}
}
@ -1251,23 +1253,4 @@ SQL;
$unread = empty($res[1]) ? 0 : intval($res[1]);
return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
}
public static function daoToEntry($dao) {
$entry = new FreshRSS_Entry(
$dao['id_feed'],
$dao['guid'],
$dao['title'],
$dao['author'],
$dao['content'],
$dao['link'],
$dao['date'],
$dao['is_read'],
$dao['is_favorite'],
isset($dao['tags']) ? $dao['tags'] : ''
);
if (isset($dao['id'])) {
$entry->_id($dao['id']);
}
return $entry;
}
}

@ -45,13 +45,13 @@ rank bigint := (SELECT maxrank - COUNT(*) FROM `_entrytmp`);
BEGIN
INSERT INTO `_entry`
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
(SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content,
(SELECT rank + row_number() OVER(ORDER BY date, id) AS id, guid, title, author, content,
link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
FROM `_entrytmp` AS etmp
WHERE NOT EXISTS (
SELECT 1 FROM `_entry` AS ereal
WHERE (etmp.id = ereal.id) OR (etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid))
ORDER BY date);
ORDER BY date, id);
DELETE FROM `_entrytmp` WHERE id <= maxrank;
END $$;';
$hadTransaction = $this->pdo->inTransaction();

@ -41,13 +41,13 @@ DROP TABLE IF EXISTS `tmp`;
CREATE TEMP TABLE `tmp` AS
SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
FROM `_entrytmp`
ORDER BY date;
ORDER BY date, id;
INSERT OR IGNORE INTO `_entry`
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id,
guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
FROM `tmp`
ORDER BY date;
ORDER BY date, id;
DELETE FROM `_entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
DROP TABLE IF EXISTS `tmp`;
';

@ -1,6 +1,28 @@
<?php
class FreshRSS_Feed extends Minz_Model {
/**
* Normal RSS or Atom feed
* @var int
*/
const KIND_RSS = 0;
/**
* Invalid RSS or Atom feed
* @var int
*/
const KIND_RSS_FORCED = 2;
/**
* Normal HTML with XPath scraping
* @var int
*/
const KIND_HTML_XPATH = 10;
/**
* Normal JSON with XPath scraping
* @var int
*/
const KIND_JSON_XPATH = 20;
const PRIORITY_MAIN_STREAM = 10;
const PRIORITY_NORMAL = 0;
const PRIORITY_ARCHIVED = -10;
@ -10,33 +32,50 @@ class FreshRSS_Feed extends Minz_Model {
const ARCHIVING_RETENTION_COUNT_LIMIT = 10000;
const ARCHIVING_RETENTION_PERIOD = 'P3M';
/**
* @var int
*/
/** @var int */
private $id = 0;
private $url;
/**
* @var int
*/
/** @var string */
private $url = '';
/** @var int */
private $kind = 0;
/** @var int */
private $category = 1;
/** @var int */
private $nbEntries = -1;
/** @var int */
private $nbNotRead = -1;
/** @var int */
private $nbPendingNotRead = 0;
/** @var string */
private $name = '';
/** @var string */
private $website = '';
/** @var string */
private $description = '';
/** @var int */
private $lastUpdate = 0;
/** @var int */
private $priority = self::PRIORITY_MAIN_STREAM;
/** @var string */
private $pathEntries = '';
/** @var string */
private $httpAuth = '';
/** @var bool */
private $error = false;
/** @var int */
private $ttl = self::TTL_DEFAULT;
private $attributes = [];
/** @var bool */
private $mute = false;
/** @var string */
private $hash = '';
/** @var string */
private $lockPath = '';
/** @var string */
private $hubUrl = '';
/** @var string */
private $selfUrl = '';
/** @var array<FreshRSS_FilterAction> $filterActions */
private $filterActions = null;
public function __construct(string $url, bool $validate = true) {
@ -47,6 +86,9 @@ class FreshRSS_Feed extends Minz_Model {
}
}
/**
* @return FreshRSS_Feed
*/
public static function example() {
$f = new FreshRSS_Feed('http://example.net/', false);
$f->faviconPrepare();
@ -71,6 +113,9 @@ class FreshRSS_Feed extends Minz_Model {
public function selfUrl(): string {
return $this->selfUrl;
}
public function kind(): int {
return $this->kind;
}
public function hubUrl(): string {
return $this->hubUrl;
}
@ -200,6 +245,9 @@ class FreshRSS_Feed extends Minz_Model {
}
$this->url = $value;
}
public function _kind($value) {
$this->kind = $value;
}
public function _category($value) {
$value = intval($value);
$this->category = $value >= 0 ? $value : 0;
@ -267,7 +315,7 @@ class FreshRSS_Feed extends Minz_Model {
* @return SimplePie|null
*/
public function load(bool $loadDetails = false, bool $noCache = false) {
if ($this->url !== null) {
if ($this->url != '') {
// @phpstan-ignore-next-line
if (CACHE_PATH === false) {
throw new Minz_FileNotExistException(
@ -347,6 +395,7 @@ class FreshRSS_Feed extends Minz_Model {
$guids = [];
$hasBadGuids = $this->attributes('hasBadGuids');
// TODO: Replace very slow $simplePie->get_item($i) by getting all items at once
for ($i = $simplePie->get_item_quantity() - 1; $i >= 0; $i--) {
$item = $simplePie->get_item($i);
if ($item == null) {
@ -375,6 +424,7 @@ class FreshRSS_Feed extends Minz_Model {
$hasBadGuids = $this->attributes('hasBadGuids');
// We want chronological order and SimplePie uses reverse order.
// TODO: Replace very slow $simplePie->get_item($i) by getting all items at once
for ($i = $simplePie->get_item_quantity() - 1; $i >= 0; $i--) {
$item = $simplePie->get_item($i);
if ($item == null) {
@ -428,15 +478,18 @@ class FreshRSS_Feed extends Minz_Model {
} elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
$enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
. ($length == null ? '' : '" data-length="' . intval($length))
. '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
. '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
} elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
$enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
. ($length == null ? '' : '" data-length="' . intval($length))
. '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
. '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
} else { //e.g. application, text, unknown
$enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink . '">💾</a></p>';
$enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
. ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
. '">💾</a></p>';
}
$thumbnailContent = '';
@ -489,6 +542,97 @@ class FreshRSS_Feed extends Minz_Model {
}
}
/**
* @param array<string,mixed> $attributes
* @return SimplePie|null
*/
public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false, array $attributes = []) {
if ($this->url == '') {
return null;
}
$feedSourceUrl = htmlspecialchars_decode($this->url, ENT_QUOTES);
if ($this->httpAuth != '') {
$feedSourceUrl = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $feedSourceUrl);
}
// Same naming conventions than https://github.com/RSS-Bridge/rss-bridge/wiki/XPathAbstract
// https://github.com/RSS-Bridge/rss-bridge/wiki/The-collectData-function
/** @var array<string,string> */
$xPathSettings = $this->attributes('xpath');
$xPathFeedTitle = $xPathSettings['feedTitle'] ?? '';
$xPathItem = $xPathSettings['item'] ?? '';
$xPathItemTitle = $xPathSettings['itemTitle'] ?? '';
$xPathItemContent = $xPathSettings['itemContent'] ?? '';
$xPathItemUri = $xPathSettings['itemUri'] ?? '';
$xPathItemAuthor = $xPathSettings['itemAuthor'] ?? '';
$xPathItemTimestamp = $xPathSettings['itemTimestamp'] ?? '';
$xPathItemThumbnail = $xPathSettings['itemThumbnail'] ?? '';
$xPathItemCategories = $xPathSettings['itemCategories'] ?? '';
if ($xPathItem == '') {
return null;
}
$html = getHtml($feedSourceUrl, $attributes);
if (strlen($html) <= 0) {
return null;
}
$view = new FreshRSS_View();
$view->_path('index/rss.phtml');
$view->internal_rendering = true;
$view->rss_url = $feedSourceUrl;
$view->entries = [];
try {
$doc = new DOMDocument();
$doc->recover = true;
$doc->strictErrorChecking = false;
$doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
$xpath = new DOMXPath($doc);
$view->rss_title = $xPathFeedTitle == '' ? '' : htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8');
$view->rss_base = htmlspecialchars(trim($xpath->evaluate('normalize-space(//base/@href)')), ENT_COMPAT, 'UTF-8');
$nodes = $xpath->query($xPathItem);
if (empty($nodes)) {
return null;
}
foreach ($nodes as $node) {
$item = [];
$item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node);
$item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node);
$item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node);
$item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node);
$item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node);
$item['thumbnail'] = $xPathItemThumbnail == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemThumbnail . ')', $node);
if ($xPathItemCategories != '') {
$itemCategories = @$xpath->query($xPathItemCategories);
if ($itemCategories) {
foreach ($itemCategories as $itemCategory) {
$item['categories'][] = $itemCategory->textContent;
}
}
}
if ($item['title'] . $item['content'] . $item['link'] != '') {
$item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']);
$item = Minz_Helper::htmlspecialchars_utf8($item);
$view->entries[] = FreshRSS_Entry::fromArray($item);
}
}
} catch (Exception $ex) {
Minz_Log::warning($ex->getMessage());
return null;
}
if (count($view->entries) < 1) {
return null;
}
$simplePie = customSimplePie();
$simplePie->set_raw_data($view->renderToString());
$simplePie->init();
return $simplePie;
}
/**
* To keep track of some new potentially unread articles since last commit+fetch from database
*/
@ -532,18 +676,23 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
protected function cacheFilename(): string {
$simplePie = customSimplePie($this->attributes());
$filename = $simplePie->get_cache_filename($this->url);
return CACHE_PATH . '/' . $filename . '.spc';
public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
$simplePie = customSimplePie($attributes);
$filename = $simplePie->get_cache_filename($url);
if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) {
return CACHE_PATH . '/' . $filename . '.html';
} else {
return CACHE_PATH . '/' . $filename . '.spc';
}
}
public function clearCache(): bool {
return @unlink($this->cacheFilename());
return @unlink(FreshRSS_Feed::cacheFilename($this->url, $this->attributes(), $this->kind));
}
/** @return int|false */
public function cacheModifiedTime() {
return @filemtime($this->cacheFilename());
return @filemtime(FreshRSS_Feed::cacheFilename($this->url, $this->attributes(), $this->kind));
}
public function lock(): bool {
@ -567,7 +716,7 @@ class FreshRSS_Feed extends Minz_Model {
* @return array<FreshRSS_FilterAction>
*/
public function filterActions(): array {
if ($this->filterActions == null) {
if (empty($this->filterActions)) {
$this->filterActions = array();
$filters = $this->attributes('filters');
if (is_array($filters)) {
@ -582,6 +731,9 @@ class FreshRSS_Feed extends Minz_Model {
return $this->filterActions;
}
/**
* @param array<FreshRSS_FilterAction> $filterActions
*/
private function _filterActions($filterActions) {
$this->filterActions = $filterActions;
if (is_array($this->filterActions) && !empty($this->filterActions)) {

@ -5,7 +5,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
protected function addColumn(string $name) {
Minz_Log::warning(__method__ . ': ' . $name);
try {
if ($name === 'attributes') { //v1.11.0
if ($name === 'kind') { //v1.20.0
return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
} elseif ($name === 'attributes') { //v1.11.0
return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN attributes TEXT') !== false;
}
} catch (Exception $e) {
@ -17,7 +19,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
protected function autoUpdateDb(array $errorInfo) {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
foreach (['attributes'] as $column) {
foreach (['attributes', 'kind'] as $column) {
if (stripos($errorInfo[2], $column) !== false) {
return $this->addColumn($column);
}
@ -32,6 +34,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
INSERT INTO `_feed`
(
url,
kind,
category,
name,
website,
@ -45,7 +48,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
attributes
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stm = $this->pdo->prepare($sql);
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
@ -59,6 +62,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$values = array(
substr($valuesTmp['url'], 0, 511),
$valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
$valuesTmp['category'],
mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
substr($valuesTmp['website'], 0, 255),
@ -84,7 +88,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
public function addFeedObject($feed): int {
public function addFeedObject(FreshRSS_Feed $feed): int {
// TODO: not sure if we should write this method in DAO since DAO
// should not be aware about feed class
@ -94,6 +98,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$values = array(
'id' => $feed->id(),
'url' => $feed->url(),
'kind' => $feed->kind(),
'category' => $feed->category(),
'name' => $feed->name(),
'website' => $feed->website(),
@ -252,7 +257,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
public function selectAll() {
$sql = <<<'SQL'
SELECT id, url, category, name, website, description, `lastUpdate`,
SELECT id, url, kind, category, name, website, description, `lastUpdate`,
priority, `pathEntries`, `httpAuth`, error, ttl, attributes
FROM `_feed`
SQL;
@ -346,7 +351,7 @@ SQL;
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0) {
$this->updateTTL();
$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
$sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
. 'FROM `_feed` '
. ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
. ' AND `lastUpdate` < (' . (time() + 60)
@ -557,20 +562,21 @@ SQL;
$category = $catID;
}
$myFeed = new FreshRSS_Feed(isset($dao['url']) ? $dao['url'] : '', false);
$myFeed = new FreshRSS_Feed($dao['url'] ?? '', false);
$myFeed->_kind($dao['kind'] ?? FreshRSS_Feed::KIND_RSS);
$myFeed->_category($category);
$myFeed->_name($dao['name']);
$myFeed->_website(isset($dao['website']) ? $dao['website'] : '', false);
$myFeed->_description(isset($dao['description']) ? $dao['description'] : '');
$myFeed->_lastUpdate(isset($dao['lastUpdate']) ? $dao['lastUpdate'] : 0);
$myFeed->_priority(isset($dao['priority']) ? $dao['priority'] : 10);
$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);
$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
$myFeed->_website($dao['website'] ?? '', false);
$myFeed->_description($dao['description'] ?? '');
$myFeed->_lastUpdate($dao['lastUpdate'] ?? 0);
$myFeed->_priority($dao['priority'] ?? 10);
$myFeed->_pathEntries($dao['pathEntries'] ?? '');
$myFeed->_httpAuth(base64_decode($dao['httpAuth'] ?? ''));
$myFeed->_error($dao['error'] ?? 0);
$myFeed->_ttl($dao['ttl'] ?? FreshRSS_Feed::TTL_DEFAULT);
$myFeed->_attributes('', $dao['attributes'] ?? '');
$myFeed->_nbNotRead($dao['cache_nbUnreads'] ?? 0);
$myFeed->_nbEntries($dao['cache_nbEntries'] ?? 0);
if (isset($dao['id'])) {
$myFeed->_id($dao['id']);
}

@ -5,7 +5,7 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
protected function autoUpdateDb(array $errorInfo) {
if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) {
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
foreach (['attributes'] as $column) {
foreach (['attributes', 'kind'] as $column) {
if (!in_array($column, $columns)) {
return $this->addColumn($column);
}

@ -7,12 +7,19 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
public $callbackBeforePagination;
public $categories;
/** @var FreshRSS_Category|null */
public $category;
/** @var string */
public $current_user;
/** @var array<FreshRSS_Entry> */
public $entries;
/** @var FreshRSS_Entry */
public $entry;
/** @var FreshRSS_Feed|null */
public $feed;
/** @var array<FreshRSS_Feed> */
public $feeds;
/** @var int */
public $nbUnreadTags;
public $tags;
@ -88,8 +95,14 @@ class FreshRSS_View extends Minz_View {
public $nbPage;
// RSS view
public $rss_title;
public $url;
/** @var string */
public $rss_title = '';
/** @var string */
public $rss_url = '';
/** @var string */
public $rss_base = '';
/** @var boolean */
public $internal_rendering = false;
// Content preview
public $fatalError;

@ -16,6 +16,7 @@ ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `_feed` (
`id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7
`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`kind` SMALLINT DEFAULT 0, -- 0.20.0
`category` SMALLINT DEFAULT 0, -- v0.7
`name` VARCHAR(191) NOT NULL,
`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,

@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `_category` (
CREATE TABLE IF NOT EXISTS `_feed` (
"id" SERIAL PRIMARY KEY,
"url" VARCHAR(511) UNIQUE NOT NULL,
"kind" SMALLINT DEFAULT 0, -- 0.20.0
"category" SMALLINT DEFAULT 0,
"name" VARCHAR(255) NOT NULL,
"website" VARCHAR(255),

@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `category` (
CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`url` VARCHAR(511) NOT NULL,
`kind` SMALLINT DEFAULT 0, -- 0.20.0
`category` SMALLINT DEFAULT 0,
`name` VARCHAR(255) NOT NULL,
`website` VARCHAR(255),

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informace',
'keep_min' => 'Minimální počet článků pro ponechání',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazat mezipaměť',
'clear_cache_help' => 'Vymazat mezipaměť pro tento kanál.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Information', // IGNORE
'keep_min' => 'Minimale Anzahl an Artikeln, die behalten wird',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Zwischenspeicher leeren',
'clear_cache_help' => 'Zwischenspeicher für diesen Feed leeren.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Information', // IGNORE
'keep_min' => 'Minimum number of articles to keep', // IGNORE
'kind' => array(
'_' => 'Type of feed source', // IGNORE
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // IGNORE
'feed_title' => array(
'_' => 'feed title', // IGNORE
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // IGNORE
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // IGNORE
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // IGNORE
'help' => 'Example: <code>//div[@class="news-item"]</code>', // IGNORE
),
'item_author' => array(
'_' => 'item author', // IGNORE
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // IGNORE
),
'item_categories' => 'items tags', // IGNORE
'item_content' => array(
'_' => 'item content', // IGNORE
'help' => 'Example to take the full item: <code>.</code>', // IGNORE
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // IGNORE
'help' => 'Example: <code>descendant::img/@src</code>', // IGNORE
),
'item_timestamp' => array(
'_' => 'item date', // IGNORE
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // IGNORE
),
'item_title' => array(
'_' => 'item title', // IGNORE
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // IGNORE
),
'item_uri' => array(
'_' => 'item link (URL)', // IGNORE
'help' => 'Example: <code>descendant::a/@href</code>', // IGNORE
),
'relative' => 'XPath (relative to item) for:', // IGNORE
'xpath' => 'XPath for:', // IGNORE
),
'rss' => 'RSS / Atom (default)', // IGNORE
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // IGNORE
'clear_cache_help' => 'Clear the cache for this feed.', // IGNORE

@ -61,6 +61,49 @@ return array(
),
'information' => 'Information',
'keep_min' => 'Minimum number of articles to keep',
'kind' => array(
'_' => 'Type of feed source',
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)',
'feed_title' => array(
'_' => 'feed title',
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>',
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.',
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>',
'help' => 'Example: <code>//div[@class="news-item"]</code>',
),
'item_author' => array(
'_' => 'item author',
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',
),
'item_categories' => 'items tags',
'item_content' => array(
'_' => 'item content',
'help' => 'Example to take the full item: <code>.</code>',
),
'item_thumbnail' => array(
'_' => 'item thumbnail',
'help' => 'Example: <code>descendant::img/@src</code>',
),
'item_timestamp' => array(
'_' => 'item date',
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',
),
'item_title' => array(
'_' => 'item title',
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',
),
'item_uri' => array(
'_' => 'item link (URL)',
'help' => 'Example: <code>descendant::a/@href</code>',
),
'relative' => 'XPath (relative to item) for:',
'xpath' => 'XPath for:',
),
'rss' => 'RSS / Atom (default)',
),
'maintenance' => array(
'clear_cache' => 'Clear cache',
'clear_cache_help' => 'Clear the cache for this feed.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Información',
'keep_min' => 'Número mínimo de artículos a conservar',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Borrar caché',
'clear_cache_help' => 'Borrar la memoria caché de esta fuente.',

@ -72,8 +72,8 @@ return array(
),
'files' => 'Installation des fichiers',
'json' => array(
'nok' => 'Vous ne disposez pas de l’extension recommendée JSON (paquet php-json).',
'ok' => 'Vous disposez de l’extension recommendée JSON.',
'nok' => 'Vous ne disposez pas de l’extension recommandée JSON (paquet php-json).',
'ok' => 'Vous disposez de l’extension recommandée JSON.',
),
'mbstring' => array(
'nok' => 'Impossible de trouver la librairie recommandée mbstring pour Unicode.',
@ -199,7 +199,7 @@ return array(
'back_to_manage' => '← Revenir à la liste des utilisateurs',
'create' => 'Créer un nouvel utilisateur',
'database_size' => 'Volumétrie',
'email' => 'Adresse email',
'email' => 'adresse électronique',
'enabled' => 'Actif',
'feed_count' => 'Flux',
'is_admin' => 'Admin',

@ -73,7 +73,7 @@ return array(
'_' => 'Suppression du compte',
'warn' => 'Le compte et toutes les données associées vont être supprimées.',
),
'email' => 'Adresse email',
'email' => 'adresse électronique',
'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
'password_format' => '7 caractères minimum',
@ -185,7 +185,7 @@ return array(
'email' => 'Courriel',
'facebook' => 'Facebook', // IGNORE
'more_information' => 'Plus d’informations',
'print' => 'Print', // IGNORE
'print' => 'Imprimer',
'raindrop' => 'Raindrop.io', // IGNORE
'remove' => 'Supprimer la méthode de partage',
'shaarli' => 'Shaarli', // IGNORE

@ -71,8 +71,8 @@ return array(
'ok' => 'Vous disposez de fileinfo.',
),
'json' => array(
'nok' => 'Vous ne disposez pas de l’extension recommendée JSON (paquet php-json).',
'ok' => 'Vous disposez de l’extension recommendée JSON.',
'nok' => 'Vous ne disposez pas de l’extension recommandée JSON (paquet php-json).',
'ok' => 'Vous disposez de l’extension recommandée JSON.',
),
'mbstring' => array(
'nok' => 'Impossible de trouver la librairie recommandée mbstring pour Unicode.',
@ -124,7 +124,7 @@ return array(
'missing_applied_migrations' => 'Quelque chose s’est mal passé, vous devriez créer le fichier <em>%s</em> à la main.',
'ok' => 'L’installation s’est bien passée.',
'session' => array(
'nok' => 'Le serveur Web semble mal configué pour les cookies nécessaires aux sessions PHP!',
'nok' => 'Le serveur Web semble mal configuré pour les cookies nécessaires aux sessions PHP!',
),
'step' => 'étape %d',
'steps' => 'Étapes',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informations',
'keep_min' => 'Nombre minimum d’articles à conserver',
'kind' => array(
'_' => 'Type de source de flux',
'html_xpath' => array(
'_' => 'HTML + XPath (Moissonnage du Web)',
'feed_title' => array(
'_' => 'titre de flux',
'help' => 'Exemple : <code>//title</code> ou un text statique : <code>"Mon flux personnalisé"</code>',
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> est un langage de requête pour les utilisateurs avancés, supporté par FreshRSS pour le moissonnage du Web (Web scraping).',
'item' => array(
'_' => 'trouver les <strong>articles</strong>',
'help' => 'Exemple : <code>//div[@class="article"]</code>',
),
'item_author' => array(
'_' => 'auteur de l’article',
'help' => 'Peut aussi être une chaîne de texte statique. Exemple : <code>"Anonyme"</code>',
),
'item_categories' => 'catégories (tags) de l’article',
'item_content' => array(
'_' => 'contenu de l’article',
'help' => 'Exemple pour prendre l’article complet : <code>.</code>',
),
'item_thumbnail' => array(
'_' => 'miniature de l’article',
'help' => 'Exemple : <code>descendant::img/@src</code>',
),
'item_timestamp' => array(
'_' => 'date de l’article',
'help' => 'Le résultat sera passé à la fonction <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',
),
'item_title' => array(
'_' => 'titre de l’article',
'help' => 'Utiliser en particulier l’<a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">axe XPath</a> <code>descendant::</code> comme <code>descendant::h2</code>',
),
'item_uri' => array(
'_' => 'lien (URL) de l’article',
'help' => 'Exemple : <code>descendant::a/@href</code>',
),
'relative' => 'XPath (relatif à l’article) pour :',
'xpath' => 'XPath pour :',
),
'rss' => 'RSS / Atom (par défaut)',
),
'maintenance' => array(
'clear_cache' => 'Vider le cache',
'clear_cache_help' => 'Supprime le cache de ce flux.',
@ -100,7 +143,7 @@ return array(
'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
'url' => 'URL du flux',
'useragent' => 'Sélectionner l’agent utilisateur pour télécharger ce flux',
'useragent_help' => 'Exemple: <kbd>Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0)</kbd>',
'useragent_help' => 'Exemple : <kbd>Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0)</kbd>',
'validator' => 'Vérifier la validité du flux',
'website' => 'URL du site',
'websub' => 'Notification instantanée par WebSub',

@ -13,28 +13,28 @@
return array(
'email' => array(
'feedback' => array(
'invalid' => 'L’adresse email est invalide.',
'required' => 'L’adresse email est requise.',
'invalid' => 'L’adresse électronique est invalide.',
'required' => 'L’adresse électronique est requise.',
),
'validation' => array(
'change_email' => 'Vous pouvez changer votre adresse email <a href="%s">dans votre profil</a>.',
'change_email' => 'Vous pouvez changer votre adresse électronique <a href="%s">dans votre profil</a>.',
'email_sent_to' => 'Nous venons d’envoyer un email à <strong>%s</strong>, veuillez suivre ses indications pour valider votre adresse.',
'feedback' => array(
'email_failed' => 'Nous n’avons pas pu vous envoyer d’email à cause d’une mauvaise configuration du serveur.',
'email_sent' => 'Un email a été envoyé à votre adresse.',
'error' => 'L’adresse email n’a pas pu être validée.',
'ok' => 'L’adresse email a été validée.',
'unnecessary' => 'L’adresse email a déjà été validée.',
'wrong_token' => 'L’adresse email n’a pas pu être validée à cause d’un mauvais token.',
'error' => 'L’adresse électronique n’a pas pu être validée.',
'ok' => 'L’adresse électronique a été validée.',
'unnecessary' => 'L’adresse électronique a déjà été validée.',
'wrong_token' => 'L’adresse électronique n’a pas pu être validée à cause d’un mauvais token.',
),
'need_to' => 'Vous devez valider votre adresse email avant de pouvoir utiliser %s.',
'need_to' => 'Vous devez valider votre adresse électronique avant de pouvoir utiliser %s.',
'resend_email' => 'Renvoyer l’email',
'title' => 'Validation de l’adresse email',
'title' => 'Validation de l’adresse électronique',
),
),
'mailer' => array(
'email_need_validation' => array(
'body' => 'Vous venez de vous inscrire sur %s mais vous devez encore valider votre adresse email. Pour cela, veuillez cliquer sur ce lien :',
'body' => 'Vous venez de vous inscrire sur %s mais vous devez encore valider votre adresse électronique. Pour cela, veuillez cliquer sur ce lien :',
'title' => 'Vous devez valider votre compte',
'welcome' => 'Bienvenue %s,',
),

@ -61,6 +61,49 @@ return array(
),
'information' => 'מידע',
'keep_min' => 'מסםר מינימלי של מאמרים לשמור',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
'clear_cache_help' => 'Clear the cache for this feed.', // TODO

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informazioni',
'keep_min' => 'Numero minimo di articoli da mantenere',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
'clear_cache_help' => 'Clear the cache for this feed.', // TODO

@ -61,6 +61,49 @@ return array(
),
'information' => 'インフォメーション',
'keep_min' => '最小数の記事は保持されます',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'キャッシュのクリア',
'clear_cache_help' => 'このフィードのキャッシュをクリアします。',

@ -61,6 +61,49 @@ return array(
),
'information' => '정보',
'keep_min' => '최소 유지 글 개수',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => '캐쉬 지우기',
'clear_cache_help' => '이 피드의 캐쉬 지우기.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informatie',
'keep_min' => 'Minimum aantal artikelen om te houden',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Cache leegmaken',
'clear_cache_help' => 'Cache voor deze feed leegmaken.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informacions',
'keep_min' => 'Nombre minimum d’articles de servar',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Escafar lo cache',
'clear_cache_help' => 'Escafar lo cache d’aqueste flux sul disc',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informacja',
'keep_min' => 'Minimalna liczba wiadomości do do przechowywania',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Wyczyść pamięć podręczną',
'clear_cache_help' => 'Czyści pamięć podręczną tego kanału.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informações',
'keep_min' => 'Número mínimo de artigos para manter',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Limpar o cache',
'clear_cache_help' => 'Limpar o cache em disco deste feed',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Информация',
'keep_min' => 'Оставлять статей не менее',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Очистить кэш',
'clear_cache_help' => 'Очистить кэш для этой ленты.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Informácia',
'keep_min' => 'Minimálny počet článkov na uchovanie',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazať vyrovnáciu pamäť',
'clear_cache_help' => 'Vymazať vyrovnáciu pamäť pre tento kanál.',

@ -61,6 +61,49 @@ return array(
),
'information' => 'Bilgi',
'keep_min' => 'En az tutulacak makale sayısı',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => 'Önbelleği temizle',
'clear_cache_help' => 'Bu akışın önbelleğini temizler.',

@ -61,6 +61,49 @@ return array(
),
'information' => '信息',
'keep_min' => '至少保存的文章数',
'kind' => array(
'_' => 'Type of feed source', // TODO
'html_xpath' => array(
'_' => 'HTML + XPath (Web scraping)', // TODO
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>//title</code> or a static string: <code>"My custom feed"</code>', // TODO
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> is a standard query language for advanced users, and which FreshRSS supports to enable Web scraping.', // TODO
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'Example: <code>//div[@class="news-item"]</code>', // TODO
),
'item_author' => array(
'_' => 'item author', // TODO
'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>', // TODO
),
'item_categories' => 'items tags', // TODO
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Example to take the full item: <code>.</code>', // TODO
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>descendant::img/@src</code>', // TODO
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
),
'item_title' => array(
'_' => 'item title', // TODO
'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>', // TODO
),
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>descendant::a/@href</code>', // TODO
),
'relative' => 'XPath (relative to item) for:', // TODO
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
),
'maintenance' => array(
'clear_cache' => '清理缓存',
'clear_cache_help' => '清除该feed的缓存',

@ -31,7 +31,7 @@ if (_t('gen.dir') === 'rtl') {
<?= FreshRSS_View::headTitle() ?>
<?php
$url_base = Minz_Request::currentRequest();
if (isset($this->rss_title)) {
if ($this->rss_title != '') {
$url_rss = $url_base;
$url_rss['a'] = 'rss';
if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {

@ -22,7 +22,7 @@ foreach ($this->entriesRaw as $entryRaw) {
if ($entryRaw == null) {
continue;
}
$entry = FreshRSS_EntryDAO::daoToEntry($entryRaw);
$entry = FreshRSS_Entry::fromArray($entryRaw);
if (!isset($this->feed)) {
$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed());
if ($feed === null) {

@ -373,6 +373,110 @@
</div>
</div>
<legend><?= _t('sub.feed.kind') ?></legend>
<div class="form-group">
<label class="group-name" for="feed_kind"><?= _t('sub.feed.kind') ?></label>
<div class="group-controls">
<select name="feed_kind" id="feed_kind" class="select-show">
<option value="<?= FreshRSS_Feed::KIND_RSS ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_RSS ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.rss') ?></option>
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
</select>
</div>
</div>
<fieldset id="html_xpath">
<?php
$xpath = Minz_Helper::htmlspecialchars_utf8($this->feed->attributes('xpath'));
?>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.help') ?></p>
<div class="form-group">
<label class="group-name" for="xPathFeedTitle"><small><?= _t('sub.feed.kind.html_xpath.xpath') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.feed_title') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathFeedTitle" id="xPathFeedTitle" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['feedTitle'] ?? '' ?>"><?= $xpath['feedTitle'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.feed_title.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItem"><small><?= _t('sub.feed.kind.html_xpath.xpath') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItem" id="xPathItem" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['item'] ?? '' ?>"><?= $xpath['item'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemTitle"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_title') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemTitle" id="xPathItemTitle" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemTitle'] ?? '' ?>"><?= $xpath['itemTitle'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_title.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemContent"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_content') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemContent" id="xPathItemContent" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemContent'] ?? '' ?>"><?= $xpath['itemContent'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_content.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemUri"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_uri') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemUri" id="xPathItemUri" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemUri'] ?? '' ?>"><?= $xpath['itemUri'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_uri.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemThumbnail"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_thumbnail') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemThumbnail" id="xPathItemThumbnail" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemThumbnail'] ?? '' ?>"><?= $xpath['itemThumbnail'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_thumbnail.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemAuthor"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_author') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemAuthor" id="xPathItemAuthor" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemAuthor'] ?? '' ?>"><?= $xpath['itemAuthor'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_author.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemTimestamp"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_timestamp') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemTimestamp" id="xPathItemTimestamp" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemTimestamp'] ?? '' ?>"><?= $xpath['itemTimestamp'] ?? '' ?></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timestamp.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemCategories"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_categories') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemCategories" id="xPathItemCategories" rows="2" cols="64" spellcheck="false"
data-leave-validation="<?= $xpath['itemCategories'] ?? '' ?>"><?= $xpath['itemCategories'] ?? '' ?></textarea>
</div>
</div>
</fieldset>
<div class="form-group form-actions">
<div class="group-controls">
<button class="btn btn-important"><?= _t('gen.action.submit') ?></button>
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
</div>
</div>
<legend><?= _t('sub.feed.advanced') ?></legend>
<div class="form-group">
<label class="group-name" for="path_entries"><?= _t('sub.feed.css_path') ?></label>

@ -21,14 +21,17 @@ $today = @strtotime('today');
</div><?php
$lastEntry = null;
$nbEntries = 0;
/** @var FreshRSS_Entry */
foreach ($this->entries as $item):
$lastEntry = $item;
$nbEntries++;
ob_flush();
$this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($this->entry == null) {
/** @var FreshRSS_Entry */
$item = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($item == null) {
continue;
}
$this->entry = $item;
// We most likely already have the feed object in cache
$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feed());

@ -15,10 +15,12 @@ $content_width = FreshRSS_Context::$user_conf->content_width;
</div><?php
$lastEntry = null;
$nbEntries = 0;
/** @var FreshRSS_Entry */
foreach ($this->entries as $item):
$lastEntry = $item;
$nbEntries++;
ob_flush();
/** @var FreshRSS_Entry */
$item = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($item == null) {
continue;

@ -1,15 +1,26 @@
<?php /** @var FreshRSS_View $this */ ?>
<?= '<?xml version="1.0" encoding="UTF-8" ?>'; ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
<?= $this->rss_base == '' ? '' : ' xml:base="' . $this->rss_base . '"' ?>
>
<channel>
<title><?= $this->rss_title ?></title>
<link><?= Minz_Url::display('', 'html', true) ?></link>
<link><?= $this->internal_rendering ? $this->rss_url : Minz_Url::display('', 'html', true) ?></link>
<description><?= _t('index.feed.rss_of', $this->rss_title) ?></description>
<pubDate><?= date('D, d M Y H:i:s O') ?></pubDate>
<lastBuildDate><?= gmdate('D, d M Y H:i:s') ?> GMT</lastBuildDate>
<atom:link href="<?= Minz_Url::display($this->url, 'html', true) ?>" rel="self" type="application/rss+xml" />
<atom:link href="<?= $this->internal_rendering ? $this->rss_url :
Minz_Url::display($this->rss_url, 'html', true) ?>" rel="self" type="application/rss+xml" />
<?php
/** @var FreshRSS_Entry */
foreach ($this->entries as $item) {
if (!$this->internal_rendering) {
/** @var FreshRSS_Entry */
$item = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($item == null) {
continue;
}
}
?>
<item>
<title><?= $item->title() ?></title>
@ -27,12 +38,23 @@ foreach ($this->entries as $item) {
echo "\t\t\t" , '<category>', $category, '</category>', "\n";
}
}
$enclosures = $item->enclosures(false);
if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
// https://www.rssboard.org/media-rss
echo "\t\t\t" , '<media:content url="' . $enclosure['url']
. (empty($enclosure['medium']) ? '' : '" medium="' . $enclosure['medium'])
. (empty($enclosure['type']) ? '' : '" type="' . $enclosure['type'])
. (empty($enclosure['length']) ? '' : '" length="' . $enclosure['length'])
. '"></media:content>', "\n";
}
}
?>
<description><![CDATA[<?php
echo $item->content();
?>]]></description>
<pubDate><?= date('D, d M Y H:i:s O', $item->date(true)) ?></pubDate>
<guid isPermaLink="false"><?= $item->id() ?></guid>
<guid isPermaLink="false"><?= $item->id() > 0 ? $item->id() : $item->guid() ?></guid>
</item>
<?php } ?>

@ -51,6 +51,97 @@
</div>
</div>
<details class="form-advanced">
<summary class="form-advanced-title">
<?= _t('sub.feed.kind') ?>
</summary>
<div class="form-group">
<label class="group-name" for="feed_kind"><?= _t('sub.feed.kind') ?></label>
<div class="group-controls">
<select name="feed_kind" id="feed_kind" class="select-show">
<option value="<?= FreshRSS_Feed::KIND_RSS ?>" selected="selected"><?= _t('sub.feed.kind.rss') ?></option>
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
</select>
</div>
</div>
<fieldset id="html_xpath">
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.help') ?></p>
<div class="form-group">
<label class="group-name" for="xPathFeedTitle"><small><?= _t('sub.feed.kind.html_xpath.xpath') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.feed_title') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathFeedTitle" id="xPathFeedTitle" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.feed_title.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItem"><small><?= _t('sub.feed.kind.html_xpath.xpath') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItem" id="xPathItem" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemTitle"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_title') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemTitle" id="xPathItemTitle" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_title.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemContent"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_content') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemContent" id="xPathItemContent" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_content.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemUri"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_uri') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemUri" id="xPathItemUri" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_uri.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemThumbnail"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_thumbnail') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemThumbnail" id="xPathItemThumbnail" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_thumbnail.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemAuthor"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_author') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemAuthor" id="xPathItemAuthor" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_author.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemTimestamp"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_timestamp') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemTimestamp" id="xPathItemTimestamp" rows="2" cols="64" spellcheck="false"></textarea>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timestamp.help') ?></p>
</div>
</div>
<div class="form-group">
<label class="group-name" for="xPathItemCategories"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
<?= _t('sub.feed.kind.html_xpath.item_categories') ?></label>
<div class="group-controls">
<textarea class="valid-xpath" name="xPathItemCategories" id="xPathItemCategories" rows="2" cols="64" spellcheck="false"></textarea>
</div>
</div>
</fieldset>
</details>
<details class="form-advanced">
<summary class="form-advanced-title">
<?= _t('sub.feed.advanced') ?>

@ -1 +1,3 @@
*.spc
*.spc
*.html
!index.html

@ -121,7 +121,8 @@ class Minz_Url {
/**
* @param string $controller
* @param string $action
* @param string ...$args
* @param string|int ...$args
* @return string|false
*/
function _url ($controller, $action, ...$args) {
$nb_args = count($args);
@ -132,8 +133,8 @@ function _url ($controller, $action, ...$args) {
$params = array ();
for ($i = 0; $i < $nb_args; $i += 2) {
$arg = $args[$i];
$params[$arg] = $args[$i + 1];
$arg = '' . $args[$i];
$params[$arg] = '' . $args[$i + 1];
}
return Minz_Url::display (array ('c' => $controller, 'a' => $action, 'params' => $params));

@ -112,6 +112,12 @@ class Minz_View {
}
}
public function renderToString(): string {
ob_start();
$this->render();
return ob_get_clean();
}
/**
* Ajoute un élément du layout
* @param string $part l'élément partial à ajouter

@ -2275,7 +2275,7 @@ class SimplePie
*/
public function get_base($element = array())
{
if (!($this->get_type() & SIMPLEPIE_TYPE_RSS_SYNDICATION) && !empty($element['xml_base_explicit']) && isset($element['xml_base']))
if (!empty($element['xml_base_explicit']) && isset($element['xml_base']))
{
return $element['xml_base'];
}

@ -436,7 +436,8 @@ class DOMDocumentWrapper {
}
protected function isXML($markup) {
// return strpos($markup, '<?xml') !== false && stripos($markup, 'xhtml') === false;
return strpos(substr($markup, 0, 100), '<'.'?xml') !== false;
$head = substr($markup, 0, 100);
return strpos($head, '<'.'?xml') !== false && stripos($head, '<html ') === false;
}
protected function contentTypeToArray($contentType) {
$matches = explode(';', trim(strtolower($contentType)));

@ -218,6 +218,7 @@ function customSimplePie($attributes = array()): SimplePie {
$simplePie->set_cache_name_function('sha1');
$simplePie->set_cache_location(CACHE_PATH);
$simplePie->set_cache_duration($limits['cache_duration']);
$simplePie->enable_order_by_date(false);
$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
$simplePie->set_timeout($feed_timeout > 0 ? $feed_timeout : $limits['timeout']);
@ -290,7 +291,10 @@ function customSimplePie($attributes = array()): SimplePie {
return $simplePie;
}
function sanitizeHTML($data, $base = '', $maxLength = false) {
/**
* @param int|false $maxLength
*/
function sanitizeHTML($data, string $base = '', $maxLength = false) {
if (!is_string($data) || ($maxLength !== false && $maxLength <= 0)) {
return '';
}
@ -311,6 +315,127 @@ function sanitizeHTML($data, $base = '', $maxLength = false) {
return $result;
}
function cleanCache(int $hours = 720) {
$files = glob(CACHE_PATH . '/*.{html,spc}', GLOB_BRACE | GLOB_NOSORT);
foreach ($files as $file) {
if (substr($file, -10) === 'index.html') {
continue;
}
$cacheMtime = @filemtime($file);
if ($cacheMtime !== false && $cacheMtime < time() - (3600 * $hours)) {
unlink($file);
}
}
}
/**
* Set an XML preamble to enforce the HTML content type charset received by HTTP.
* @param string $html the row downloaded HTML content
* @param string $contentType an HTTP Content-Type such as 'text/html; charset=utf-8'
* @return string an HTML string with XML encoding information for DOMDocument::loadHTML()
*/
function enforceHttpEncoding(string $html, string $contentType = ''): string {
$httpCharset = preg_match('/\bcharset=([0-9a-z_-]{2,12})$/i', $contentType, $matches) === false ? '' : $matches[1] ?? '';
if ($httpCharset == '') {
// No charset defined by HTTP, do nothing
return $html;
}
$httpCharsetNormalized = SimplePie_Misc::encoding($httpCharset);
if ($httpCharsetNormalized === 'windows-1252') {
// Default charset for HTTP, do nothing
return $html;
}
if (substr($html, 0, 3) === "\xEF\xBB\xBF" || // UTF-8 BOM
substr($html, 0, 2) === "\xFF\xFE" || // UTF-16 Little Endian BOM
substr($html, 0, 2) === "\xFE\xFF" || // UTF-16 Big Endian BOM
substr($html, 0, 4) === "\xFF\xFE\x00\x00" || // UTF-32 Little Endian BOM
substr($html, 0, 4) === "\x00\x00\xFE\xFF") { // UTF-32 Big Endian BOM
// Existing byte order mark, do nothing
return $html;
}
if (preg_match('/^<[?]xml[^>]+encoding\b/', substr($html, 0, 64))) {
// Existing XML declaration, do nothing
return $html;
}
return '<' . '?xml version="1.0" encoding="' . $httpCharsetNormalized . '" ?' . ">\n" . $html;
}
/**
* @param array<string,mixed> $attributes
*/
function getHtml(string $url, array $attributes = []): string {
$limits = FreshRSS_Context::$system_conf->limits;
$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
$cachePath = FreshRSS_Feed::cacheFilename($url, $attributes, FreshRSS_Feed::KIND_HTML_XPATH);
$cacheMtime = @filemtime($cachePath);
if ($cacheMtime !== false && $cacheMtime > time() - intval($limits['cache_duration'])) {
$html = @file_get_contents($cachePath);
if ($html != '') {
syslog(LOG_DEBUG, 'FreshRSS uses cache for ' . SimplePie_Misc::url_remove_credentials($url));
return $html;
}
}
if (mt_rand(0, 30) === 1) { // Remove old entries once in a while
cleanCache();
}
if (FreshRSS_Context::$system_conf->simplepie_syslog_enabled) {
syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
}
// TODO: Implement HTTP 1.1 conditional GET If-Modified-Since
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_REFERER => SimplePie_Misc::url_remove_credentials($url),
CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
//CURLOPT_FAILONERROR => true;
CURLOPT_MAXREDIRS => 4,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', //Enable all encodings
]);
curl_setopt_array($ch, FreshRSS_Context::$system_conf->curl_options);
if (isset($attributes['curl_params']) && is_array($attributes['curl_params'])) {
curl_setopt_array($ch, $attributes['curl_params']);
}
if (isset($attributes['ssl_verify'])) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $attributes['ssl_verify'] ? 2 : 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $attributes['ssl_verify'] ? true : false);
if (!$attributes['ssl_verify']) {
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1');
}
}
$html = curl_exec($ch);
$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$c_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); //TODO: Check if that may be null
$c_error = curl_error($ch);
curl_close($ch);
if ($c_status != 200 || $c_error != '' || $html === false) {
Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
}
if ($html == false) {
$html = '';
} else {
$html = enforceHttpEncoding($html, $c_content_type);
}
if (file_put_contents($cachePath, $html) === false) {
Minz_Log::warning("Error saving cache $cachePath for $url");
}
return $html;
}
/**
* Validate an email address, supports internationalized addresses.
*

@ -114,7 +114,7 @@ class FeverDAO extends Minz_ModelPdo
$entries = array();
foreach ($result as $dao) {
$entries[] = FreshRSS_EntryDAO::daoToEntry($dao);
$entries[] = FreshRSS_Entry::fromArray($dao);
}
return $entries;

@ -536,6 +536,7 @@ function entriesToArray($entries) {
$items = array();
foreach ($entries as $item) {
/** @var FreshRSS_Entry $entry */
$entry = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($entry == null) {
continue;

@ -213,6 +213,49 @@ function init_configuration_alert() {
};
}
/**
* Allow a <select class="select-show"> to hide/show elements defined by <option data-show="elem-id"></option>
*/
function init_select_show() {
const listener = (select) => {
const options = select.querySelectorAll('option[data-show]');
for (const option of options) {
const elem = document.getElementById(option.dataset.show);
if (elem) {
elem.style.display = option.selected ? 'block' : 'none';
}
}
};
const selects = document.querySelectorAll('select.select-show');
for (const select of selects) {
select.addEventListener('change', (e) => listener(e.target));
listener(select);
}
}
/**
* Automatically validate XPath textarea fields
*/
function init_valid_xpath() {
const listener = (textarea) => {
const evaluator = new XPathEvaluator();
try {
if (textarea.value === '' || evaluator.createExpression(textarea.value) != null) {
textarea.setCustomValidity('');
}
} catch (ex) {
textarea.setCustomValidity(ex);
}
};
const textareas = document.querySelectorAll('textarea.valid-xpath');
for (const textarea of textareas) {
textarea.addEventListener('change', (e) => listener(e.target));
listener(textarea);
}
}
function init_extra() {
if (!window.context) {
if (window.console) {
@ -227,6 +270,8 @@ function init_extra() {
init_slider_observers();
init_configuration_alert();
fix_popup_preview_selector();
init_select_show();
init_valid_xpath();
}
if (document.readyState && document.readyState !== 'loading') {

@ -160,6 +160,14 @@ input, select, textarea {
font-size: 0.8rem;
}
textarea[rows="2"] {
height: 3em;
}
textarea:invalid {
border: 2px dashed red;
}
input[type="radio"],
input[type="checkbox"] {
width: 15px !important;

@ -160,6 +160,14 @@ input, select, textarea {
font-size: 0.8rem;
}
textarea[rows="2"] {
height: 3em;
}
textarea:invalid {
border: 2px dashed red;
}
input[type="radio"],
input[type="checkbox"] {
width: 15px !important;

Loading…
Cancel
Save