mirror of https://github.com/FreshRSS/FreshRSS.git
Dynamic OPML (#4407)
* Dynamic OPML draft #fix https://github.com/FreshRSS/FreshRSS/issues/4191 * Export dynamic OPML http://opml.org/spec2.opml#1629043127000 * Restart with simpler approach * Minor revert * Export dynamic OPML also for single feeds * Special category type for importing dynamic OPML * Parameter for excludeMutedFeeds * Details * More draft * i18n * Fix update * Draft manual import working * Working manual refresh * Draft automatic update * Working Web refresh + fixes * Import/export dynamic OPML settings * Annoying numerous lines in SQL logs * Fix minor JavaScript error * Fix auto adding new columns * Add require * Add missing 🗲 * Missing space * Disable adding new feeds to dynamic categories * Link from import * i18n typo * Improve theme icon function * Fix pink-dark
This commit is contained in:
parent
57d571230e
commit
509c8cae63
|
@ -19,6 +19,9 @@ indent_style = tab
|
|||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[*.svg]
|
||||
indent_style = tab
|
||||
|
||||
[*.xml]
|
||||
indent_style = tab
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
ot = "ot"
|
||||
Ths2 = "Ths2"
|
||||
|
||||
[default.extend-words]
|
||||
ba = "ba"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"*.fr.md",
|
||||
|
|
|
@ -40,8 +40,8 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
|||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$cat_name = Minz_Request::param('new-category');
|
||||
if (!$cat_name) {
|
||||
$cat_name = trim(Minz_Request::param('new-category', ''));
|
||||
if ($cat_name == '') {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
|
||||
}
|
||||
|
||||
|
@ -51,12 +51,16 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
|||
Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect);
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'id' => $cat->id(),
|
||||
'name' => $cat->name(),
|
||||
);
|
||||
$opml_url = checkUrl(Minz_Request::param('opml_url', ''));
|
||||
if ($opml_url != '') {
|
||||
$cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$cat->_attributes('opml_url', $opml_url);
|
||||
} else {
|
||||
$cat->_kind(FreshRSS_Category::KIND_NORMAL);
|
||||
$cat->_attributes('opml_url', null);
|
||||
}
|
||||
|
||||
if ($catDAO->addCategory($values)) {
|
||||
if ($catDAO->addCategoryObject($cat)) {
|
||||
$url_redirect['a'] = 'index';
|
||||
Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect);
|
||||
} else {
|
||||
|
@ -156,6 +160,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
|||
*
|
||||
* Request parameter is:
|
||||
* - id (of a category)
|
||||
* - muted (truthy to remove only muted feeds, or falsy otherwise)
|
||||
*/
|
||||
public function emptyAction() {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
@ -169,10 +174,15 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
|||
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
|
||||
}
|
||||
|
||||
// List feeds to remove then related user queries.
|
||||
$feeds = $feedDAO->listByCategory($id);
|
||||
$muted = Minz_Request::param('muted', null);
|
||||
if ($muted !== null) {
|
||||
$muted = boolval($muted);
|
||||
}
|
||||
|
||||
if ($feedDAO->deleteFeedByCategory($id)) {
|
||||
// List feeds to remove then related user queries.
|
||||
$feeds = $feedDAO->listByCategory($id, $muted);
|
||||
|
||||
if ($feedDAO->deleteFeedByCategory($id, $muted)) {
|
||||
// TODO: Delete old favicons
|
||||
|
||||
// Remove related queries
|
||||
|
@ -190,4 +200,62 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
|||
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameter is:
|
||||
* - id (of a category)
|
||||
*/
|
||||
public function refreshOpmlAction() {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'index');
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
if (!$id) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
|
||||
}
|
||||
|
||||
$category = $catDAO->searchById($id);
|
||||
if ($category == null) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
|
||||
}
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$ok = $category->refreshDynamicOpml();
|
||||
|
||||
if (Minz_Request::param('ajax')) {
|
||||
Minz_Request::setGoodNotification(_t('feedback.sub.category.updated'));
|
||||
$this->view->_layout(false);
|
||||
} else {
|
||||
if ($ok) {
|
||||
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
|
||||
}
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,int> */
|
||||
public static function refreshDynamicOpmls() {
|
||||
$successes = 0;
|
||||
$errors = 0;
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::$user_conf->dynamic_opml_ttl_default ?? 86400);
|
||||
foreach ($categories as $category) {
|
||||
if ($category->refreshDynamicOpml()) {
|
||||
$successes++;
|
||||
} else {
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'successes' => $successes,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,10 @@ 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
|
||||
$title = trim($title);
|
||||
if ($title != '') {
|
||||
$feed->_name($title);
|
||||
}
|
||||
$feed->_kind($kind);
|
||||
$feed->_attributes('', $attributes);
|
||||
$feed->_httpAuth($http_auth);
|
||||
|
@ -92,19 +96,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
|||
throw new FreshRSS_FeedNotAdded_Exception($url);
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'url' => $feed->url(),
|
||||
'kind' => $feed->kind(),
|
||||
'category' => $feed->category(),
|
||||
'name' => $title != '' ? $title : $feed->name(true),
|
||||
'website' => $feed->website(),
|
||||
'description' => $feed->description(),
|
||||
'lastUpdate' => 0,
|
||||
'httpAuth' => $feed->httpAuth(),
|
||||
'attributes' => $feed->attributes(),
|
||||
);
|
||||
|
||||
$id = $feedDAO->addFeed($values);
|
||||
$id = $feedDAO->addFeedObject($feed);
|
||||
if (!$id) {
|
||||
// There was an error in database… we cannot say what here.
|
||||
throw new FreshRSS_FeedNotAdded_Exception($url);
|
||||
|
@ -469,7 +461,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
|||
}
|
||||
|
||||
if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull!
|
||||
$text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url .
|
||||
$text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' .
|
||||
SimplePie_Misc::url_remove_credentials($url) .
|
||||
' GUID ' . $entry->guid();
|
||||
Minz_Log::warning($text, PSHB_LOG);
|
||||
Minz_Log::warning($text);
|
||||
|
@ -528,7 +521,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
|||
}
|
||||
}
|
||||
} elseif ($feed->url() !== $url) { // HTTP 301 Moved Permanently
|
||||
Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url(false));
|
||||
Minz_Log::notice('Feed ' . SimplePie_Misc::url_remove_credentials($url) .
|
||||
' moved permanently to ' . SimplePie_Misc::url_remove_credentials($feed->url(false)));
|
||||
$feedProperties['url'] = $feed->url();
|
||||
}
|
||||
|
||||
|
@ -629,6 +623,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
|||
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
|
||||
$databaseDAO->minorDbMaintenance();
|
||||
} else {
|
||||
FreshRSS_category_Controller::refreshDynamicOpmls();
|
||||
list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, $noCommit, $maxFeeds);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
*/
|
||||
class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/** @var FreshRSS_EntryDAO */
|
||||
private $entryDAO;
|
||||
|
||||
/** @var FreshRSS_FeedDAO */
|
||||
private $feedDAO;
|
||||
|
||||
/**
|
||||
|
@ -96,7 +99,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
|
|||
$importService = new FreshRSS_Import_Service($username);
|
||||
|
||||
foreach ($list_files['opml'] as $opml_file) {
|
||||
if (!$importService->importOpml($opml_file)) {
|
||||
$importService->importOpml($opml_file);
|
||||
if (!$importService->lastStatus()) {
|
||||
$ok = false;
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
|
||||
|
@ -520,7 +524,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
|
|||
$feed->_name($name);
|
||||
$feed->_website($website);
|
||||
if (!empty($origin['disable'])) {
|
||||
$feed->_ttl(-1 * FreshRSS_Context::$user_conf->ttl_default);
|
||||
$feed->_mute(true);
|
||||
}
|
||||
|
||||
// Call the extension hook
|
||||
|
|
|
@ -174,6 +174,76 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
|||
header('Content-Type: application/rss+xml; charset=utf-8');
|
||||
}
|
||||
|
||||
public function opmlAction() {
|
||||
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
|
||||
$token = FreshRSS_Context::$user_conf->token;
|
||||
$token_param = Minz_Request::param('token', '');
|
||||
$token_is_ok = ($token != '' && $token === $token_param);
|
||||
|
||||
// Check if user has access.
|
||||
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous && !$token_is_ok) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->updateContext();
|
||||
} catch (FreshRSS_Context_Exception $e) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
$get = FreshRSS_Context::currentGet(true);
|
||||
if (is_array($get)) {
|
||||
$type = $get[0];
|
||||
$id = $get[1];
|
||||
} else {
|
||||
$type = $get;
|
||||
$id = '';
|
||||
}
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$categories = $catDAO->listCategories(true, true);
|
||||
$this->view->excludeMutedFeeds = true;
|
||||
|
||||
switch ($type) {
|
||||
case 'a':
|
||||
$this->view->categories = $categories;
|
||||
break;
|
||||
case 'c':
|
||||
$cat = $categories[$id] ?? null;
|
||||
if ($cat == null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
$this->view->categories = [ $cat ];
|
||||
break;
|
||||
case 'f':
|
||||
// We most likely already have the feed object in cache
|
||||
$feed = FreshRSS_CategoryDAO::findFeed($categories, $id);
|
||||
if ($feed == null) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$feed = $feedDAO->searchById($id);
|
||||
if ($feed == null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->view->feeds = [ $feed ];
|
||||
break;
|
||||
case 's':
|
||||
case 't':
|
||||
case 'T':
|
||||
default:
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
// No layout for OPML output.
|
||||
$this->view->_layout(false);
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* This action updates the Context object by using request parameters.
|
||||
*
|
||||
|
|
|
@ -8,6 +8,10 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
|
|||
public function actualizeAction() {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
Minz_Session::_param('actualize_feeds', false);
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->view->categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::$user_conf->dynamic_opml_ttl_default);
|
||||
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
|||
|
||||
$catDAO->checkDefault();
|
||||
$feedDAO->updateTTL();
|
||||
$this->view->categories = $catDAO->listSortedCategories(false);
|
||||
$this->view->categories = $catDAO->listSortedCategories(false, true, true);
|
||||
$this->view->default_category = $catDAO->getDefault();
|
||||
|
||||
$signalError = false;
|
||||
|
@ -120,11 +120,8 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
|||
|
||||
$cat = intval(Minz_Request::param('category', 0));
|
||||
|
||||
$mute = Minz_Request::param('mute', false);
|
||||
$ttl = intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT));
|
||||
if ($mute && FreshRSS_Feed::TTL_DEFAULT === $ttl) {
|
||||
$ttl = FreshRSS_Context::$user_conf->ttl_default;
|
||||
}
|
||||
$feed->_ttl(intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT)));
|
||||
$feed->_mute(boolval(Minz_Request::param('mute', false)));
|
||||
|
||||
$feed->_attributes('read_upon_gone', Minz_Request::paramTernary('read_upon_gone'));
|
||||
$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
|
||||
|
@ -196,8 +193,8 @@ 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) {
|
||||
$feed->_kind(intval(Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS)));
|
||||
if ($feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
$xPathSettings = [];
|
||||
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
|
||||
if (Minz_Request::param('xPathItemTitle', '') != '') $xPathSettings['itemTitle'] = Minz_Request::param('xPathItemTitle', '', true);
|
||||
|
@ -214,7 +211,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
|||
|
||||
$values = array(
|
||||
'name' => Minz_Request::param('name', ''),
|
||||
'kind' => $feed_kind,
|
||||
'kind' => $feed->kind(),
|
||||
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
|
||||
'website' => checkUrl(Minz_Request::param('website', '')),
|
||||
'url' => checkUrl(Minz_Request::param('url', '')),
|
||||
|
@ -222,7 +219,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
|||
'pathEntries' => Minz_Request::param('path_entries', ''),
|
||||
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
|
||||
'httpAuth' => $httpAuth,
|
||||
'ttl' => $ttl * ($mute ? -1 : 1),
|
||||
'ttl' => $feed->ttl(true),
|
||||
'attributes' => $feed->attributes(),
|
||||
);
|
||||
|
||||
|
@ -300,7 +297,17 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
|||
$position = Minz_Request::param('position');
|
||||
$category->_attributes('position', '' === $position ? null : (int) $position);
|
||||
|
||||
$opml_url = checkUrl(Minz_Request::param('opml_url', ''));
|
||||
if ($opml_url != '') {
|
||||
$category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$category->_attributes('opml_url', $opml_url);
|
||||
} else {
|
||||
$category->_kind(FreshRSS_Category::KIND_NORMAL);
|
||||
$category->_attributes('opml_url', null);
|
||||
}
|
||||
|
||||
$values = [
|
||||
'kind' => $category->kind(),
|
||||
'name' => Minz_Request::param('name', ''),
|
||||
'attributes' => $category->attributes(),
|
||||
];
|
||||
|
|
|
@ -1,17 +1,38 @@
|
|||
<?php
|
||||
|
||||
class FreshRSS_Category extends Minz_Model {
|
||||
|
||||
/**
|
||||
* Normal
|
||||
* @var int
|
||||
*/
|
||||
const KIND_NORMAL = 0;
|
||||
|
||||
/**
|
||||
* Category tracking a third-party Dynamic OPML
|
||||
* @var int
|
||||
*/
|
||||
const KIND_DYNAMIC_OPML = 2;
|
||||
|
||||
const TTL_DEFAULT = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id = 0;
|
||||
/** @var int */
|
||||
private $kind = 0;
|
||||
private $name;
|
||||
private $nbFeeds = -1;
|
||||
private $nbNotRead = -1;
|
||||
/** @var array<FreshRSS_Feed>|null */
|
||||
private $feeds = null;
|
||||
private $hasFeedsWithError = false;
|
||||
private $isDefault = false;
|
||||
private $attributes = [];
|
||||
/** @var int */
|
||||
private $lastUpdate = 0;
|
||||
/** @var bool */
|
||||
private $error = false;
|
||||
|
||||
public function __construct(string $name = '', $feeds = null) {
|
||||
$this->_name($name);
|
||||
|
@ -30,11 +51,26 @@ class FreshRSS_Category extends Minz_Model {
|
|||
public function id(): int {
|
||||
return $this->id;
|
||||
}
|
||||
public function kind(): int {
|
||||
return $this->kind;
|
||||
}
|
||||
public function name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
public function lastUpdate(): int {
|
||||
return $this->lastUpdate;
|
||||
}
|
||||
public function _lastUpdate(int $value) {
|
||||
$this->lastUpdate = $value;
|
||||
}
|
||||
public function inError(): bool {
|
||||
return $this->error;
|
||||
}
|
||||
public function _error($value) {
|
||||
$this->error = (bool)$value;
|
||||
}
|
||||
public function isDefault(): bool {
|
||||
return $this->isDefault;
|
||||
return $this->id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID;
|
||||
}
|
||||
public function nbFeeds(): int {
|
||||
if ($this->nbFeeds < 0) {
|
||||
|
@ -52,6 +88,8 @@ class FreshRSS_Category extends Minz_Model {
|
|||
|
||||
return $this->nbNotRead;
|
||||
}
|
||||
|
||||
/** @return array<FreshRSS_Feed> */
|
||||
public function feeds(): array {
|
||||
if ($this->feeds === null) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
@ -90,12 +128,15 @@ class FreshRSS_Category extends Minz_Model {
|
|||
$this->_name(_t('gen.short.default_category'));
|
||||
}
|
||||
}
|
||||
|
||||
public function _kind(int $kind) {
|
||||
$this->kind = $kind;
|
||||
}
|
||||
|
||||
public function _name($value) {
|
||||
$this->name = mb_strcut(trim($value), 0, 255, 'UTF-8');
|
||||
}
|
||||
public function _isDefault($value) {
|
||||
$this->isDefault = $value;
|
||||
}
|
||||
/** @param array<FreshRSS_Feed>|FreshRSS_Feed $values */
|
||||
public function _feeds($values) {
|
||||
if (!is_array($values)) {
|
||||
$values = array($values);
|
||||
|
@ -104,6 +145,17 @@ class FreshRSS_Category extends Minz_Model {
|
|||
$this->feeds = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* To manually add feeds to this category (not committing to database).
|
||||
* @param FreshRSS_Feed $feed
|
||||
*/
|
||||
public function addFeed($feed) {
|
||||
if ($this->feeds === null) {
|
||||
$this->feeds = [];
|
||||
}
|
||||
$this->feeds[] = $feed;
|
||||
}
|
||||
|
||||
public function _attributes($key, $value) {
|
||||
if ('' == $key) {
|
||||
if (is_string($value)) {
|
||||
|
@ -118,4 +170,78 @@ class FreshRSS_Category extends Minz_Model {
|
|||
$this->attributes[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public static function cacheFilename(string $url, array $attributes): string {
|
||||
$simplePie = customSimplePie($attributes);
|
||||
$filename = $simplePie->get_cache_filename($url);
|
||||
return CACHE_PATH . '/' . $filename . '.opml.xml';
|
||||
}
|
||||
|
||||
public function refreshDynamicOpml(): bool {
|
||||
$url = $this->attributes('opml_url');
|
||||
if ($url == '') {
|
||||
return false;
|
||||
}
|
||||
$ok = true;
|
||||
$attributes = []; //TODO
|
||||
$cachePath = self::cacheFilename($url, $attributes);
|
||||
$opml = httpGet($url, $cachePath, 'opml', $attributes);
|
||||
if ($opml == '') {
|
||||
Minz_Log::warning('Error getting dynamic OPML for category ' . $this->id() . '! ' .
|
||||
SimplePie_Misc::url_remove_credentials($url));
|
||||
$ok = false;
|
||||
} else {
|
||||
$dryRunCategory = new FreshRSS_Category();
|
||||
$importService = new FreshRSS_Import_Service();
|
||||
$importService->importOpml($opml, $dryRunCategory, true, true);
|
||||
if ($importService->lastStatus()) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
/** @var array<string,FreshRSS_Feed> */
|
||||
$dryRunFeeds = [];
|
||||
foreach ($dryRunCategory->feeds() as $dryRunFeed) {
|
||||
$dryRunFeeds[$dryRunFeed->url()] = $dryRunFeed;
|
||||
}
|
||||
|
||||
/** @var array<string,FreshRSS_Feed> */
|
||||
$existingFeeds = [];
|
||||
foreach ($this->feeds() as $existingFeed) {
|
||||
$existingFeeds[$existingFeed->url()] = $existingFeed;
|
||||
if (empty($dryRunFeeds[$existingFeed->url()])) {
|
||||
// The feed does not exist in the new dynamic OPML, so mute (disable) that feed
|
||||
$existingFeed->_mute(true);
|
||||
$ok &= ($feedDAO->updateFeed($existingFeed->id(), [
|
||||
'ttl' => $existingFeed->ttl(true),
|
||||
]) !== false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dryRunCategory->feeds() as $dryRunFeed) {
|
||||
if (empty($existingFeeds[$dryRunFeed->url()])) {
|
||||
// The feed does not exist in the current category, so add that feed
|
||||
$dryRunFeed->_category($this->id());
|
||||
$ok &= ($feedDAO->addFeedObject($dryRunFeed) !== false);
|
||||
} else {
|
||||
$existingFeed = $existingFeeds[$dryRunFeed->url()];
|
||||
if ($existingFeed->mute()) {
|
||||
// The feed already exists in the current category but was muted (disabled), so unmute (enable) again
|
||||
$existingFeed->_mute(false);
|
||||
$ok &= ($feedDAO->updateFeed($existingFeed->id(), [
|
||||
'ttl' => $existingFeed->ttl(true),
|
||||
]) !== false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ok = false;
|
||||
Minz_Log::warning('Error loading dynamic OPML for category ' . $this->id() . '! ' .
|
||||
SimplePie_Misc::url_remove_credentials($url));
|
||||
}
|
||||
}
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$catDAO->updateLastUpdate($this->id(), !$ok);
|
||||
|
||||
return $ok;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
protected function addColumn($name) {
|
||||
Minz_Log::warning(__method__ . ': ' . $name);
|
||||
try {
|
||||
if ('attributes' === $name) { //v1.15.0
|
||||
if ($name === 'kind') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
|
||||
} elseif ($name === 'lastUpdate') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN `lastUpdate` BIGINT DEFAULT 0') !== false;
|
||||
} elseif ($name === 'error') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN error SMALLINT DEFAULT 0') !== false;
|
||||
} elseif ('attributes' === $name) { //v1.15.0
|
||||
$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
|
||||
|
||||
$stm = $this->pdo->query('SELECT * FROM `_feed`');
|
||||
|
@ -69,8 +75,9 @@ class FreshRSS_CategoryDAO 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) {
|
||||
if (stripos($errorInfo[2], $column) !== false) {
|
||||
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
|
||||
foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {
|
||||
if (stripos($errorLines[0], $column) !== false) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
|
@ -79,12 +86,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
return false;
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function addCategory($valuesTmp) {
|
||||
// TRIM() to provide a type hint as text
|
||||
// No tag of the same name
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO `_category`(name, attributes)
|
||||
SELECT * FROM (SELECT TRIM(?) AS name, TRIM(?) AS attributes) c2
|
||||
INSERT INTO `_category`(kind, name, attributes)
|
||||
SELECT * FROM (SELECT ABS(?) AS kind, TRIM(?) AS name, TRIM(?) AS attributes) c2
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))
|
||||
SQL;
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -94,6 +102,7 @@ SQL;
|
|||
$valuesTmp['attributes'] = [];
|
||||
}
|
||||
$values = array(
|
||||
$valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL,
|
||||
$valuesTmp['name'],
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
|
||||
$valuesTmp['name'],
|
||||
|
@ -111,13 +120,18 @@ SQL;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FreshRSS_Category $category
|
||||
* @return int|false
|
||||
*/
|
||||
public function addCategoryObject($category) {
|
||||
$cat = $this->searchByName($category->name());
|
||||
if (!$cat) {
|
||||
// Category does not exist yet in DB so we add it before continue
|
||||
$values = array(
|
||||
$values = [
|
||||
'kind' => $category->kind(),
|
||||
'name' => $category->name(),
|
||||
);
|
||||
'attributes' => $category->attributes(),
|
||||
];
|
||||
return $this->addCategory($values);
|
||||
}
|
||||
|
||||
|
@ -127,7 +141,7 @@ SQL;
|
|||
public function updateCategory($id, $valuesTmp) {
|
||||
// No tag of the same name
|
||||
$sql = <<<'SQL'
|
||||
UPDATE `_category` SET name=?, attributes=? WHERE id=?
|
||||
UPDATE `_category` SET name=?, kind=?, attributes=? WHERE id=?
|
||||
AND NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = ?)
|
||||
SQL;
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -138,6 +152,7 @@ SQL;
|
|||
}
|
||||
$values = array(
|
||||
$valuesTmp['name'],
|
||||
$valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL,
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
|
||||
$id,
|
||||
$valuesTmp['name'],
|
||||
|
@ -155,6 +170,24 @@ SQL;
|
|||
}
|
||||
}
|
||||
|
||||
public function updateLastUpdate(int $id, bool $inError = false, int $mtime = 0) {
|
||||
$sql = 'UPDATE `_category` SET `lastUpdate`=?, error=? WHERE id=?';
|
||||
$values = [
|
||||
$mtime <= 0 ? time() : $mtime,
|
||||
$inError ? 1 : 0,
|
||||
$id,
|
||||
];
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
if ($stm && $stm->execute($values)) {
|
||||
return $stm->rowCount();
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCategory($id) {
|
||||
if ($id <= self::DEFAULTCATEGORYID) {
|
||||
return false;
|
||||
|
@ -172,7 +205,7 @@ SQL;
|
|||
}
|
||||
|
||||
public function selectAll() {
|
||||
$sql = 'SELECT id, name, attributes FROM `_category`';
|
||||
$sql = 'SELECT id, name, kind, `lastUpdate`, error, attributes FROM `_category`';
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm != false) {
|
||||
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
@ -181,15 +214,14 @@ SQL;
|
|||
} else {
|
||||
$info = $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
foreach ($this->selectAll() as $category) { // `yield from` requires PHP 7+
|
||||
yield $category;
|
||||
}
|
||||
yield from $this->selectAll();
|
||||
}
|
||||
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return FreshRSS_Category|null */
|
||||
public function searchById($id) {
|
||||
$sql = 'SELECT * FROM `_category` WHERE id=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -204,7 +236,9 @@ SQL;
|
|||
return null;
|
||||
}
|
||||
}
|
||||
public function searchByName($name) {
|
||||
|
||||
/** @return FreshRSS_Category|null|false */
|
||||
public function searchByName(string $name) {
|
||||
$sql = 'SELECT * FROM `_category` WHERE name=:name';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($stm == false) {
|
||||
|
@ -246,7 +280,7 @@ SQL;
|
|||
|
||||
public function listCategories($prePopulateFeeds = true, $details = false) {
|
||||
if ($prePopulateFeeds) {
|
||||
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.attributes AS c_attributes, '
|
||||
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
|
||||
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
|
||||
. 'FROM `_category` c '
|
||||
. 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
|
||||
|
@ -272,6 +306,27 @@ SQL;
|
|||
}
|
||||
}
|
||||
|
||||
/** @return array<FreshRSS_Category> */
|
||||
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0) {
|
||||
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
|
||||
. ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($stm &&
|
||||
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
|
||||
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
|
||||
$stm->execute()) {
|
||||
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
} else {
|
||||
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->listCategoriesOrderUpdate($defaultCacheDuration, $limit);
|
||||
}
|
||||
Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @return FreshRSS_Category|null */
|
||||
public function getDefault() {
|
||||
$sql = 'SELECT * FROM `_category` WHERE id=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -290,6 +345,8 @@ SQL;
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int|bool */
|
||||
public function checkDefault() {
|
||||
$def_cat = $this->searchById(self::DEFAULTCATEGORYID);
|
||||
|
||||
|
@ -345,6 +402,10 @@ SQL;
|
|||
return $res[0]['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
* @param int $feed_id
|
||||
*/
|
||||
public static function findFeed($categories, $feed_id) {
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
|
@ -356,6 +417,10 @@ SQL;
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
* @param int $minPriority
|
||||
*/
|
||||
public static function CountUnreads($categories, $minPriority = 0) {
|
||||
$n = 0;
|
||||
foreach ($categories as $category) {
|
||||
|
@ -386,6 +451,7 @@ SQL;
|
|||
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
|
||||
);
|
||||
$cat->_id($previousLine['c_id']);
|
||||
$cat->_kind($previousLine['c_kind']);
|
||||
$cat->_attributes('', $previousLine['c_attributes']);
|
||||
$list[$previousLine['c_id']] = $cat;
|
||||
|
||||
|
@ -403,6 +469,9 @@ SQL;
|
|||
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
|
||||
);
|
||||
$cat->_id($previousLine['c_id']);
|
||||
$cat->_kind($previousLine['c_kind']);
|
||||
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
|
||||
$cat->_error($previousLine['c_error'] ?? false);
|
||||
$cat->_attributes('', $previousLine['c_attributes']);
|
||||
$list[$previousLine['c_id']] = $cat;
|
||||
}
|
||||
|
@ -422,8 +491,10 @@ SQL;
|
|||
$dao['name']
|
||||
);
|
||||
$cat->_id($dao['id']);
|
||||
$cat->_kind($dao['kind']);
|
||||
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
|
||||
$cat->_error($dao['error'] ?? false);
|
||||
$cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
|
||||
$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
|
||||
$list[$key] = $cat;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
|
|||
protected function autoUpdateDb(array $errorInfo) {
|
||||
if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
|
||||
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
|
||||
foreach (['attributes'] as $column) {
|
||||
foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {
|
||||
if (!in_array($column, $columns)) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
|
|
|
@ -197,6 +197,20 @@ class FreshRSS_Context {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets all feeds (main view), false otherwise.
|
||||
*/
|
||||
public static function isAll(): bool {
|
||||
return self::$current_get['all'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets a category, false otherwise.
|
||||
*/
|
||||
public static function isCategory(): bool {
|
||||
return self::$current_get['category'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets a feed (and not a category or all articles), false otherwise.
|
||||
*/
|
||||
|
@ -251,8 +265,7 @@ class FreshRSS_Context {
|
|||
*/
|
||||
public static function _get($get) {
|
||||
$type = $get[0];
|
||||
$id = substr($get, 2);
|
||||
$nb_unread = 0;
|
||||
$id = intval(substr($get, 2));
|
||||
|
||||
if (empty(self::$categories)) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
|
|
|
@ -488,7 +488,8 @@ class FreshRSS_Entry extends Minz_Model {
|
|||
* @param array<string,mixed> $attributes
|
||||
*/
|
||||
public static function getContentByParsing(string $url, string $path, array $attributes = [], int $maxRedirs = 3): string {
|
||||
$html = getHtml($url, $attributes);
|
||||
$cachePath = FreshRSS_Feed::cacheFilename($url, $attributes, FreshRSS_Feed::KIND_HTML_XPATH);
|
||||
$html = httpGet($url, $cachePath, 'html', $attributes);
|
||||
if (strlen($html) > 0) {
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
|
|
|
@ -162,9 +162,21 @@ class FreshRSS_Feed extends Minz_Model {
|
|||
public function inError(): bool {
|
||||
return $this->error;
|
||||
}
|
||||
public function ttl(): int {
|
||||
|
||||
/**
|
||||
* @param bool $raw true for database version combined with mute information, false otherwise
|
||||
*/
|
||||
public function ttl(bool $raw = false): int {
|
||||
if ($raw) {
|
||||
$ttl = $this->ttl;
|
||||
if ($this->mute && FreshRSS_Feed::TTL_DEFAULT === $ttl) {
|
||||
$ttl = FreshRSS_Context::$user_conf ? FreshRSS_Context::$user_conf->ttl_default : 3600;
|
||||
}
|
||||
return $ttl * ($this->mute ? -1 : 1);
|
||||
}
|
||||
return $this->ttl;
|
||||
}
|
||||
|
||||
public function attributes($key = '') {
|
||||
if ($key == '') {
|
||||
return $this->attributes;
|
||||
|
@ -172,19 +184,11 @@ class FreshRSS_Feed extends Minz_Model {
|
|||
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
|
||||
}
|
||||
}
|
||||
|
||||
public function mute(): bool {
|
||||
return $this->mute;
|
||||
}
|
||||
// public function ttlExpire() {
|
||||
// $ttl = $this->ttl;
|
||||
// if ($ttl == self::TTL_DEFAULT) { //Default
|
||||
// $ttl = FreshRSS_Context::$user_conf->ttl_default;
|
||||
// }
|
||||
// if ($ttl == -1) { //Never
|
||||
// $ttl = 64000000; //~2 years. Good enough for PubSubHubbub logic
|
||||
// }
|
||||
// return $this->lastUpdate + $ttl;
|
||||
// }
|
||||
|
||||
public function nbEntries(): int {
|
||||
if ($this->nbEntries < 0) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
@ -248,10 +252,13 @@ class FreshRSS_Feed extends Minz_Model {
|
|||
public function _kind(int $value) {
|
||||
$this->kind = $value;
|
||||
}
|
||||
|
||||
/** @param int $value */
|
||||
public function _category($value) {
|
||||
$value = intval($value);
|
||||
$this->category = $value >= 0 ? $value : 0;
|
||||
}
|
||||
|
||||
public function _name(string $value) {
|
||||
$this->name = $value == '' ? '' : trim($value);
|
||||
}
|
||||
|
@ -282,6 +289,9 @@ class FreshRSS_Feed extends Minz_Model {
|
|||
public function _error($value) {
|
||||
$this->error = (bool)$value;
|
||||
}
|
||||
public function _mute(bool $value) {
|
||||
$this->mute = $value;
|
||||
}
|
||||
public function _ttl($value) {
|
||||
$value = intval($value);
|
||||
$value = min($value, 100000000);
|
||||
|
@ -584,7 +594,8 @@ class FreshRSS_Feed extends Minz_Model {
|
|||
return null;
|
||||
}
|
||||
|
||||
$html = getHtml($feedSourceUrl, $attributes);
|
||||
$cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $attributes, FreshRSS_Feed::KIND_HTML_XPATH);
|
||||
$html = httpGet($feedSourceUrl, $cachePath, 'html', $attributes);
|
||||
if (strlen($html) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@ 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) {
|
||||
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
|
||||
foreach (['attributes', 'kind'] as $column) {
|
||||
if (stripos($errorInfo[2], $column) !== false) {
|
||||
if (stripos($errorLines[0], $column) !== false) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
|
@ -29,26 +30,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function addFeed(array $valuesTmp) {
|
||||
$sql = '
|
||||
INSERT INTO `_feed`
|
||||
(
|
||||
url,
|
||||
kind,
|
||||
category,
|
||||
name,
|
||||
website,
|
||||
description,
|
||||
`lastUpdate`,
|
||||
priority,
|
||||
`pathEntries`,
|
||||
`httpAuth`,
|
||||
error,
|
||||
ttl,
|
||||
attributes
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
$sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
|
||||
|
@ -88,10 +73,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/** @return int|false */
|
||||
public function addFeedObject(FreshRSS_Feed $feed) {
|
||||
// Add feed only if we don’t find it in DB
|
||||
$feed_search = $this->searchByUrl($feed->url());
|
||||
if (!$feed_search) {
|
||||
|
@ -106,13 +89,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
'lastUpdate' => 0,
|
||||
'pathEntries' => $feed->pathEntries(),
|
||||
'httpAuth' => $feed->httpAuth(),
|
||||
'ttl' => $feed->ttl(true),
|
||||
'attributes' => $feed->attributes(),
|
||||
);
|
||||
if ($feed->mute() || (
|
||||
FreshRSS_Context::$user_conf != null && //When creating a new user
|
||||
$feed->ttl() != FreshRSS_Context::$user_conf->ttl_default)) {
|
||||
$values['ttl'] = $feed->ttl() * ($feed->mute() ? -1 : 1);
|
||||
}
|
||||
|
||||
$id = $this->addFeed($values);
|
||||
if ($id) {
|
||||
|
@ -121,11 +100,36 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
} else {
|
||||
// The feed already exists so make sure it is not muted
|
||||
$feed->_ttl($feed_search->ttl());
|
||||
$feed->_mute(false);
|
||||
|
||||
return $feed_search->id();
|
||||
// Merge existing and import attributes
|
||||
$existingAttributes = $feed_search->attributes();
|
||||
$importAttributes = $feed->attributes();
|
||||
$feed->_attributes('', array_merge_recursive($existingAttributes, $importAttributes));
|
||||
|
||||
// Update some values of the existing feed using the import
|
||||
$values = [
|
||||
'kind' => $feed->kind(),
|
||||
'name' => $feed->name(),
|
||||
'website' => $feed->website(),
|
||||
'description' => $feed->description(),
|
||||
'pathEntries' => $feed->pathEntries(),
|
||||
'ttl' => $feed->ttl(true),
|
||||
'attributes' => $feed->attributes(),
|
||||
];
|
||||
|
||||
if (!$this->updateFeed($feed_search->id(), $values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $feed_search->id();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function updateFeed(int $id, array $valuesTmp) {
|
||||
if (isset($valuesTmp['name'])) {
|
||||
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
|
||||
|
@ -193,7 +197,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
return $stm->rowCount();
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error updateLastUpdate: ' . $info[2]);
|
||||
Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -227,6 +231,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
}
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function deleteFeed(int $id) {
|
||||
$sql = 'DELETE FROM `_feed` WHERE id=?';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -241,8 +246,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
public function deleteFeedByCategory(int $id) {
|
||||
|
||||
/**
|
||||
* @param bool|null $muted to include only muted feeds
|
||||
* @return int|false
|
||||
*/
|
||||
public function deleteFeedByCategory(int $id, $muted = null) {
|
||||
$sql = 'DELETE FROM `_feed` WHERE category=?';
|
||||
if ($muted) {
|
||||
$sql .= ' AND ttl < 0';
|
||||
}
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$values = array($id);
|
||||
|
@ -349,6 +362,7 @@ SQL;
|
|||
|
||||
/**
|
||||
* Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
|
||||
* @return array<FreshRSS_Feed>
|
||||
*/
|
||||
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0) {
|
||||
$this->updateTTL();
|
||||
|
@ -365,7 +379,7 @@ SQL;
|
|||
} else {
|
||||
$info = $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->listFeedsOrderUpdate($defaultCacheDuration);
|
||||
return $this->listFeedsOrderUpdate($defaultCacheDuration, $limit);
|
||||
}
|
||||
Minz_Log::error('SQL error listFeedsOrderUpdate: ' . $info[2]);
|
||||
return array();
|
||||
|
@ -386,10 +400,14 @@ SQL;
|
|||
}
|
||||
|
||||
/**
|
||||
* @param bool|null $muted to include only muted feeds
|
||||
* @return array<FreshRSS_Feed>
|
||||
*/
|
||||
public function listByCategory(int $cat): array {
|
||||
public function listByCategory(int $cat, $muted = null): array {
|
||||
$sql = 'SELECT * FROM `_feed` WHERE category=?';
|
||||
if ($muted) {
|
||||
$sql .= ' AND ttl < 0';
|
||||
}
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$stm->execute(array($cat));
|
||||
|
|
|
@ -68,6 +68,13 @@ class FreshRSS_Themes extends Minz_Model {
|
|||
return $infos;
|
||||
}
|
||||
|
||||
public static function title($name) {
|
||||
static $titles = [
|
||||
'opml-dyn' => 'sub.category.dynamic_opml',
|
||||
];
|
||||
return $titles[$name] ?? '';
|
||||
}
|
||||
|
||||
public static function alt($name) {
|
||||
static $alts = array(
|
||||
'add' => '➕', //✚
|
||||
|
@ -94,6 +101,7 @@ class FreshRSS_Themes extends Minz_Model {
|
|||
'next' => '⏩',
|
||||
'non-starred' => '☆',
|
||||
'notice' => 'ℹ️', //ⓘ
|
||||
'opml-dyn' => '🗲',
|
||||
'prev' => '⏪',
|
||||
'read' => '☑️', //☑
|
||||
'rss' => '📣', //☄
|
||||
|
@ -115,7 +123,13 @@ class FreshRSS_Themes extends Minz_Model {
|
|||
return isset($name) ? $alts[$name] : '';
|
||||
}
|
||||
|
||||
public static function icon($name, $urlOnly = false) {
|
||||
// TODO: Change for enum in PHP 8.1+
|
||||
const ICON_DEFAULT = 0;
|
||||
const ICON_IMG = 1;
|
||||
const ICON_URL = 2;
|
||||
const ICON_EMOJI = 3;
|
||||
|
||||
public static function icon(string $name, int $type = self::ICON_DEFAULT): string {
|
||||
$alt = self::alt($name);
|
||||
if ($alt == '') {
|
||||
return '';
|
||||
|
@ -124,14 +138,29 @@ class FreshRSS_Themes extends Minz_Model {
|
|||
$url = $name . '.svg';
|
||||
$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) : (self::$defaultIconsUrl . $url);
|
||||
|
||||
if ($urlOnly) {
|
||||
return Minz_Url::display($url);
|
||||
$title = self::title($name);
|
||||
if ($title != '') {
|
||||
$title = ' title="' . _t($title) . '"';
|
||||
}
|
||||
|
||||
if (FreshRSS_Context::$user_conf && FreshRSS_Context::$user_conf->icons_as_emojis) {
|
||||
return '<span class="icon">' . $alt . '</span>';
|
||||
if ($type == self::ICON_DEFAULT) {
|
||||
if ((FreshRSS_Context::$user_conf && FreshRSS_Context::$user_conf->icons_as_emojis) ||
|
||||
// default to emoji alternate for some icons
|
||||
in_array($name, [ 'opml-dyn' ])) {
|
||||
$type = self::ICON_EMOJI;
|
||||
} else {
|
||||
$type = self::ICON_IMG;
|
||||
}
|
||||
}
|
||||
|
||||
return '<img class="icon" src="' . Minz_Url::display($url) . '" loading="lazy" alt="' . $alt . '" />';
|
||||
switch ($type) {
|
||||
case self::ICON_URL:
|
||||
return Minz_Url::display($url);
|
||||
case self::ICON_IMG:
|
||||
return '<img class="icon" src="' . Minz_Url::display($url) . '" loading="lazy" alt="' . $alt . '"' . $title . ' />';
|
||||
case self::ICON_EMOJI:
|
||||
default:
|
||||
return '<span class="icon"' . $title . '>' . $alt . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
* @property bool $topline_summary
|
||||
* @property string $topline_thumbnail
|
||||
* @property int $ttl_default
|
||||
* @property int $dynamic_opml_ttl_default
|
||||
* @property-read bool $unsafe_autologin_enabled
|
||||
* @property string $view_mode
|
||||
* @property array<string,mixed> $volatile
|
||||
|
|
|
@ -25,6 +25,8 @@ class FreshRSS_View extends Minz_View {
|
|||
public $tags;
|
||||
/** @var array<string,string> */
|
||||
public $notification;
|
||||
/** @var bool */
|
||||
public $excludeMutedFeeds;
|
||||
|
||||
// Substriptions
|
||||
public $default_category;
|
||||
|
|
|
@ -7,6 +7,9 @@ $GLOBALS['SQL_CREATE_TABLES'] = <<<'SQL'
|
|||
CREATE TABLE IF NOT EXISTS `_category` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT, -- v0.7
|
||||
`name` VARCHAR(191) NOT NULL, -- Max index length for Unicode is 191 characters (767 bytes) FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`lastUpdate` BIGINT DEFAULT 0, -- 1.20.0
|
||||
`error` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`attributes` TEXT, -- v1.15.0
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY (`name`) -- v0.7
|
||||
|
@ -16,7 +19,7 @@ ENGINE = INNODB;
|
|||
CREATE TABLE IF NOT EXISTS `_feed` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT, -- v0.7
|
||||
`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`category` INT DEFAULT 0, -- 1.20.0
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
|
||||
|
|
|
@ -7,6 +7,9 @@ $GLOBALS['SQL_CREATE_TABLES'] = <<<'SQL'
|
|||
CREATE TABLE IF NOT EXISTS `_category` (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"name" VARCHAR(255) UNIQUE NOT NULL,
|
||||
"kind" SMALLINT DEFAULT 0, -- 1.20.0
|
||||
"lastUpdate" BIGINT DEFAULT 0, -- 1.20.0
|
||||
"error" SMALLINT DEFAULT 0, -- 1.20.0
|
||||
"attributes" TEXT -- v1.15.0
|
||||
);
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ $GLOBALS['SQL_CREATE_TABLES'] = <<<'SQL'
|
|||
CREATE TABLE IF NOT EXISTS `category` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`lastUpdate` BIGINT DEFAULT 0, -- 1.20.0
|
||||
`error` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`attributes` TEXT, -- v1.15.0
|
||||
UNIQUE (`name`)
|
||||
);
|
||||
|
@ -14,7 +17,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, -- 1.20.0
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`category` INTEGER DEFAULT 0, -- 1.20.0
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`website` VARCHAR(255),
|
||||
|
|
|
@ -47,7 +47,8 @@ class FreshRSS_Export_Service {
|
|||
|
||||
$view = new FreshRSS_View();
|
||||
$day = date('Y-m-d');
|
||||
$view->categories = $this->category_dao->listCategories(true);
|
||||
$view->categories = $this->category_dao->listCategories(true, true);
|
||||
$view->excludeMutedFeeds = false;
|
||||
|
||||
return [
|
||||
"feeds_{$day}.opml.xml",
|
||||
|
|
|
@ -10,25 +10,36 @@ class FreshRSS_Import_Service {
|
|||
/** @var FreshRSS_FeedDAO */
|
||||
private $feedDAO;
|
||||
|
||||
/** @var bool true if success, false otherwise */
|
||||
private $lastStatus;
|
||||
|
||||
/**
|
||||
* Initialize the service for the given user.
|
||||
*
|
||||
* @param string $username
|
||||
*/
|
||||
public function __construct($username) {
|
||||
public function __construct($username = null) {
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
|
||||
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
|
||||
}
|
||||
|
||||
/** @return bool true if success, false otherwise */
|
||||
public function lastStatus(): bool {
|
||||
return $this->lastStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method parses and imports an OPML file.
|
||||
*
|
||||
* @param string $opml_file the OPML file content.
|
||||
* @return boolean false if an error occurred, true otherwise.
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return array<FreshRSS_Category>|false an array of categories containing some feeds, or false if an error occurred.
|
||||
*/
|
||||
public function importOpml($opml_file) {
|
||||
public function importOpml(string $opml_file, $parent_cat = null, $flatten = false, $dryRun = false) {
|
||||
$this->lastStatus = true;
|
||||
$opml_array = array();
|
||||
try {
|
||||
$opml_array = libopml_parse_string($opml_file, false);
|
||||
|
@ -38,24 +49,22 @@ class FreshRSS_Import_Service {
|
|||
} else {
|
||||
Minz_Log::warning($e->getMessage());
|
||||
}
|
||||
$this->lastStatus = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->catDAO->checkDefault();
|
||||
|
||||
return $this->addOpmlElements($opml_array['body']);
|
||||
return $this->addOpmlElements($opml_array['body'], $parent_cat, $flatten, $dryRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML file based on its body.
|
||||
*
|
||||
* @param array $opml_elements an OPML element (body or outline).
|
||||
* @param string $parent_cat the name of the parent category.
|
||||
* @return boolean false if an error occurred, true otherwise.
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return array<FreshRSS_Category> an array of categories containing some feeds
|
||||
*/
|
||||
private function addOpmlElements($opml_elements, $parent_cat = null) {
|
||||
$isOkStatus = true;
|
||||
|
||||
private function addOpmlElements($opml_elements, $parent_cat = null, $flatten = false, $dryRun = false) {
|
||||
$nb_feeds = count($this->feedDAO->listFeeds());
|
||||
$nb_cats = count($this->catDAO->listCategories(false));
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
|
@ -67,64 +76,61 @@ class FreshRSS_Import_Service {
|
|||
(isset($b['xmlUrl']) ? 'Z' : 'A') . (isset($b['text']) ? $b['text'] : ''));
|
||||
});
|
||||
|
||||
$categories = [];
|
||||
|
||||
foreach ($opml_elements as $elt) {
|
||||
if (isset($elt['xmlUrl'])) {
|
||||
// If xmlUrl exists, it means it is a feed
|
||||
if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
|
||||
Minz_Log::warning(_t('feedback.sub.feed.over_max',
|
||||
$limits['max_feeds']));
|
||||
$isOkStatus = false;
|
||||
$this->lastStatus = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->addFeedOpml($elt, $parent_cat)) {
|
||||
if ($this->addFeedOpml($elt, $parent_cat, $dryRun)) {
|
||||
$nb_feeds++;
|
||||
} else {
|
||||
$isOkStatus = false;
|
||||
$this->lastStatus = false;
|
||||
}
|
||||
} elseif (!empty($elt['text'])) {
|
||||
// No xmlUrl? It should be a category!
|
||||
$limit_reached = ($nb_cats >= $limits['max_categories']);
|
||||
$limit_reached = !$flatten && ($nb_cats >= $limits['max_categories']);
|
||||
if (!FreshRSS_Context::$isCli && $limit_reached) {
|
||||
Minz_Log::warning(_t('feedback.sub.category.over_max',
|
||||
$limits['max_categories']));
|
||||
$isOkStatus = false;
|
||||
continue;
|
||||
$this->lastStatus = false;
|
||||
$flatten = true;
|
||||
}
|
||||
|
||||
if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
|
||||
$category = $this->addCategoryOpml($elt, $parent_cat, $flatten, $dryRun);
|
||||
|
||||
if ($category) {
|
||||
$nb_cats++;
|
||||
} else {
|
||||
$isOkStatus = false;
|
||||
$categories[] = $category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isOkStatus;
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML feed element.
|
||||
*
|
||||
* @param array $feed_elt an OPML element (must be a feed element).
|
||||
* @param string $parent_cat the name of the parent category.
|
||||
* @return boolean false if an error occurred, true otherwise.
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @return FreshRSS_Feed|null a feed.
|
||||
*/
|
||||
private function addFeedOpml($feed_elt, $parent_cat) {
|
||||
private function addFeedOpml($feed_elt, $parent_cat, $dryRun = false) {
|
||||
if ($parent_cat == null) {
|
||||
// This feed has no parent category so we get the default one
|
||||
$this->catDAO->checkDefault();
|
||||
$default_cat = $this->catDAO->getDefault();
|
||||
$parent_cat = $default_cat->name();
|
||||
}
|
||||
|
||||
$cat = $this->catDAO->searchByName($parent_cat);
|
||||
if ($cat == null) {
|
||||
// If there is not $cat, it means parent category does not exist in
|
||||
// database.
|
||||
// If it happens, take the default category.
|
||||
$this->catDAO->checkDefault();
|
||||
$cat = $this->catDAO->getDefault();
|
||||
$parent_cat = $this->catDAO->getDefault();
|
||||
if ($parent_cat == null) {
|
||||
$this->lastStatus = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// We get different useful information
|
||||
|
@ -139,11 +145,11 @@ class FreshRSS_Import_Service {
|
|||
$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
|
||||
}
|
||||
|
||||
$error = false;
|
||||
try {
|
||||
// Create a Feed object and add it in DB
|
||||
$feed = new FreshRSS_Feed($url);
|
||||
$feed->_category($cat->id());
|
||||
$feed->_category($parent_cat->id());
|
||||
$parent_cat->addFeed($feed);
|
||||
$feed->_name($name);
|
||||
$feed->_website($website);
|
||||
$feed->_description($description);
|
||||
|
@ -180,14 +186,20 @@ class FreshRSS_Import_Service {
|
|||
}
|
||||
|
||||
// Call the extension hook
|
||||
/** @var FreshRSS_Feed|null */
|
||||
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
|
||||
if ($dryRun) {
|
||||
return $feed;
|
||||
}
|
||||
if ($feed != null) {
|
||||
// addFeedObject checks if feed is already in DB so nothing else to
|
||||
// check here
|
||||
// addFeedObject checks if feed is already in DB
|
||||
$id = $this->feedDAO->addFeedObject($feed);
|
||||
$error = ($id == false);
|
||||
} else {
|
||||
$error = true;
|
||||
if ($id == false) {
|
||||
$this->lastStatus = false;
|
||||
} else {
|
||||
$feed->_id($id);
|
||||
return $feed;
|
||||
}
|
||||
}
|
||||
} catch (FreshRSS_Feed_Exception $e) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
|
@ -195,54 +207,76 @@ class FreshRSS_Import_Service {
|
|||
} else {
|
||||
Minz_Log::warning($e->getMessage());
|
||||
}
|
||||
$error = true;
|
||||
$this->lastStatus = false;
|
||||
}
|
||||
|
||||
if ($error) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
|
||||
}
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' .
|
||||
SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id() . "\n");
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML feed import from URL: ' .
|
||||
SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id());
|
||||
}
|
||||
|
||||
return !$error;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML category element.
|
||||
*
|
||||
* @param array $cat_elt an OPML element (must be a category element).
|
||||
* @param string $parent_cat the name of the parent category.
|
||||
* @param boolean $cat_limit_reached indicates if category limit has been reached.
|
||||
* if yes, category is not added (but we try for feeds!)
|
||||
* @return boolean false if an error occurred, true otherwise.
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return FreshRSS_Category|null a new category containing some feeds, or null if no category was created, or false if an error occurred.
|
||||
*/
|
||||
private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
|
||||
// Create a new Category object
|
||||
$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
|
||||
$cat = new FreshRSS_Category($catName);
|
||||
private function addCategoryOpml($cat_elt, $parent_cat, $flatten = false, $dryRun = false) {
|
||||
$error = false;
|
||||
$cat = null;
|
||||
if (!$flatten) {
|
||||
$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
|
||||
$cat = new FreshRSS_Category($catName);
|
||||
|
||||
$error = true;
|
||||
if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
|
||||
$id = $this->catDAO->addCategoryObject($cat);
|
||||
$error = ($id === false);
|
||||
}
|
||||
if ($error) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
|
||||
foreach ($cat_elt as $key => $value) {
|
||||
if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
|
||||
switch ($key) {
|
||||
case 'opmlUrl':
|
||||
$opml_url = checkUrl($value['value']);
|
||||
if ($opml_url != '') {
|
||||
$cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$cat->_attributes('opml_url', $opml_url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$id = $this->catDAO->addCategoryObject($cat);
|
||||
if ($id == false) {
|
||||
$this->lastStatus = false;
|
||||
$error = true;
|
||||
} else {
|
||||
$cat->_id($id);
|
||||
}
|
||||
}
|
||||
if ($error) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
|
||||
}
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
|
||||
$parent_cat = $cat;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($cat_elt['@outlines'])) {
|
||||
// Our cat_elt contains more categories or more feeds, so we
|
||||
// add them recursively.
|
||||
// Note: FreshRSS does not support yet category arborescence
|
||||
$error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
|
||||
// Note: FreshRSS does not support yet category arborescence, so always flatten from here
|
||||
$this->addOpmlElements($cat_elt['@outlines'], $parent_cat, true, $dryRun);
|
||||
}
|
||||
|
||||
return !$error;
|
||||
return $cat;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Jít zpět na vaše kanály RSS',
|
||||
'cancel' => 'Zrušit',
|
||||
'create' => 'Vytvořit',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Snížit úroveň',
|
||||
'disable' => 'Zakázat',
|
||||
'empty' => 'Vyprázdnit',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Zvýšit úroveň',
|
||||
'purge' => 'Vymazat',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Odebrat',
|
||||
'rename' => 'Přejmenovat',
|
||||
'see_website' => 'Zobrazit webovou stránku',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Kategorie',
|
||||
'add' => 'Přidat kategorii',
|
||||
'archiving' => 'Archivace',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Vyprázdit kategorii',
|
||||
'information' => 'Informace',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Zobrazit pozici',
|
||||
'position_help' => 'Pro ovládání pořadí řazení kategorií',
|
||||
'title' => 'Název',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Správa odběrů',
|
||||
'add' => 'Přidat kanál nebo kategorii',
|
||||
'add_category' => 'Přidat kategorii',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Přidat kanál',
|
||||
'add_label' => 'Přidat popisek',
|
||||
'delete_label' => 'Odstranit popisek',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Zurück zu Ihren RSS-Feeds gehen',
|
||||
'cancel' => 'Abbrechen',
|
||||
'create' => 'Erstellen',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Zurückstufen',
|
||||
'disable' => 'Deaktivieren',
|
||||
'empty' => 'Leeren',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'URL öffnen',
|
||||
'promote' => 'Hochstufen',
|
||||
'purge' => 'Bereinigen',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Entfernen',
|
||||
'rename' => 'Umbenennen',
|
||||
'see_website' => 'Website ansehen',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Kategorie',
|
||||
'add' => 'Kategorie hinzufügen',
|
||||
'archiving' => 'Archivierung',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Leere Kategorie',
|
||||
'information' => 'Information', // IGNORE
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Reihenfolge',
|
||||
'position_help' => 'Steuert die Kategoriesortierung',
|
||||
'title' => 'Titel',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Abonnementverwaltung',
|
||||
'add' => 'Feed oder Kategorie hinzufügen',
|
||||
'add_category' => 'Kategorie hinzufügen',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Feed hinzufügen',
|
||||
'add_label' => 'Label hinzufügen',
|
||||
'delete_label' => 'Label löschen',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Go back to your RSS feeds', // IGNORE
|
||||
'cancel' => 'Cancel', // IGNORE
|
||||
'create' => 'Create', // IGNORE
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Demote', // IGNORE
|
||||
'disable' => 'Disable', // IGNORE
|
||||
'empty' => 'Empty', // IGNORE
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // IGNORE
|
||||
'promote' => 'Promote', // IGNORE
|
||||
'purge' => 'Purge', // IGNORE
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Remove', // IGNORE
|
||||
'rename' => 'Rename', // IGNORE
|
||||
'see_website' => 'See website', // IGNORE
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Category', // IGNORE
|
||||
'add' => 'Add a category', // IGNORE
|
||||
'archiving' => 'Archiving', // IGNORE
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Empty category', // IGNORE
|
||||
'information' => 'Information', // IGNORE
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Display position', // IGNORE
|
||||
'position_help' => 'To control category sort order', // IGNORE
|
||||
'title' => 'Title', // IGNORE
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Subscription management', // IGNORE
|
||||
'add' => 'Add a feed or category', // IGNORE
|
||||
'add_category' => 'Add a category', // IGNORE
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Add a feed', // IGNORE
|
||||
'add_label' => 'Add a label', // IGNORE
|
||||
'delete_label' => 'Delete a label', // IGNORE
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Go back to your RSS feeds',
|
||||
'cancel' => 'Cancel',
|
||||
'create' => 'Create',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Demote',
|
||||
'disable' => 'Disable',
|
||||
'empty' => 'Empty',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL',
|
||||
'promote' => 'Promote',
|
||||
'purge' => 'Purge',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Remove',
|
||||
'rename' => 'Rename',
|
||||
'see_website' => 'See website',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Category',
|
||||
'add' => 'Add a category',
|
||||
'archiving' => 'Archiving',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Empty category',
|
||||
'information' => 'Information',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Display position',
|
||||
'position_help' => 'To control category sort order',
|
||||
'title' => 'Title',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Subscription management',
|
||||
'add' => 'Add a feed or category',
|
||||
'add_category' => 'Add a category',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Add a feed',
|
||||
'add_label' => 'Add a label',
|
||||
'delete_label' => 'Delete a label',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← regresar a tus fuentes RSS',
|
||||
'cancel' => 'Cancelar',
|
||||
'create' => 'Crear',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Degradar',
|
||||
'disable' => 'Desactivar',
|
||||
'empty' => 'Vaciar',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Promover',
|
||||
'purge' => 'Eliminar',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Borrar',
|
||||
'rename' => 'Cambiar el nombre a',
|
||||
'see_website' => 'Ver web',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Categoría',
|
||||
'add' => 'Añadir categoría',
|
||||
'archiving' => 'Archivo',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Vaciar categoría',
|
||||
'information' => 'Información',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Posición de visualización',
|
||||
'position_help' => 'Para controlar el orden de clasificación de categorías',
|
||||
'title' => 'Título',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Administración de suscripciones',
|
||||
'add' => 'Agregar un feed o una categoría',
|
||||
'add_category' => 'Agregar una categoría',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Añadir un feed',
|
||||
'add_label' => 'Añadir una etiqueta',
|
||||
'delete_label' => 'Eliminar una etiqueta',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Retour à vos flux RSS',
|
||||
'cancel' => 'Annuler',
|
||||
'create' => 'Créer',
|
||||
'delete_muted_feeds' => 'Supprimer les flux désactivés',
|
||||
'demote' => 'Rétrograder',
|
||||
'disable' => 'Désactiver',
|
||||
'empty' => 'Vider',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Ouvrir l’URL',
|
||||
'promote' => 'Promouvoir',
|
||||
'purge' => 'Purger',
|
||||
'refresh_opml' => 'Rafraîchir OPML',
|
||||
'remove' => 'Supprimer',
|
||||
'rename' => 'Renommer',
|
||||
'see_website' => 'Voir le site',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Catégorie',
|
||||
'add' => 'Ajouter catégorie',
|
||||
'archiving' => 'Archivage',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'OPML dynamique',
|
||||
'help' => 'Fournir l’URL d’un <a href=http://opml.org/ target=_blank>fichier OPML</a> qui donnera dynamiquement la liste des flux de cette catégorie',
|
||||
),
|
||||
'empty' => 'Catégorie vide',
|
||||
'information' => 'Informations',
|
||||
'opml_url' => 'URL de l’OPML',
|
||||
'position' => 'Position d’affichage',
|
||||
'position_help' => 'Pour contrôler l’ordre de tri des catégories',
|
||||
'title' => 'Titre',
|
||||
|
@ -112,7 +117,7 @@ return array(
|
|||
'title' => 'Maintenance', // IGNORE
|
||||
),
|
||||
'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
|
||||
'mute' => 'muet',
|
||||
'mute' => 'désactivé',
|
||||
'no_selected' => 'Aucun flux sélectionné.',
|
||||
'number_entries' => '%d articles', // IGNORE
|
||||
'priority' => array(
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Gestion des abonnements',
|
||||
'add' => 'Ajouter un flux/une catégorie',
|
||||
'add_category' => 'Ajouter une catégorie',
|
||||
'add_dynamic_opml' => 'Ajouter un OPML dynamique',
|
||||
'add_feed' => 'Ajouter un flux',
|
||||
'add_label' => 'Ajouter une étiquette',
|
||||
'delete_label' => 'Supprimer une étiquette',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← חזרה להזנות הRSS שלך',
|
||||
'cancel' => 'ביטול',
|
||||
'create' => 'יצירה',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Demote', // TODO
|
||||
'disable' => 'Disable', // TODO
|
||||
'empty' => 'Empty', // TODO
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Promote', // TODO
|
||||
'purge' => 'Purge', // TODO
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Remove', // TODO
|
||||
'rename' => 'Rename', // TODO
|
||||
'see_website' => 'ראו אתר',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'קטגוריה',
|
||||
'add' => 'Add a category', // TODO
|
||||
'archiving' => 'ארכוב',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Empty category', // TODO
|
||||
'information' => 'מידע',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Display position', // TODO
|
||||
'position_help' => 'To control category sort order', // TODO
|
||||
'title' => 'כותרת',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'ניהול הרשמות',
|
||||
'add' => 'Add a feed or category', // TODO
|
||||
'add_category' => 'Add a category', // TODO
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Add a feed', // TODO
|
||||
'add_label' => 'Add a label', // TODO
|
||||
'delete_label' => 'Delete a label', // TODO
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Indietro',
|
||||
'cancel' => 'Annulla',
|
||||
'create' => 'Crea',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Demote', // TODO
|
||||
'disable' => 'Disabilita',
|
||||
'empty' => 'Vuoto',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Promote', // TODO
|
||||
'purge' => 'Purge', // TODO
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Rimuovi',
|
||||
'rename' => 'Rename', // TODO
|
||||
'see_website' => 'Vai al sito',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Categoria',
|
||||
'add' => 'Aggiungi categoria',
|
||||
'archiving' => 'Archiviazione',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Categoria vuota',
|
||||
'information' => 'Informazioni',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Display position', // TODO
|
||||
'position_help' => 'To control category sort order', // TODO
|
||||
'title' => 'Titolo',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Gestione sottoscrizioni',
|
||||
'add' => 'Add a feed or category', // TODO
|
||||
'add_category' => 'Add a category', // TODO
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Add a feed', // TODO
|
||||
'add_label' => 'Add a label', // TODO
|
||||
'delete_label' => 'Delete a label', // TODO
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← RSSフィードに戻る',
|
||||
'cancel' => 'キャンセル',
|
||||
'create' => '作成',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => '寄付',
|
||||
'disable' => '無効',
|
||||
'empty' => '空',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'プロモート',
|
||||
'purge' => '不要なデータの削除',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => '消去',
|
||||
'rename' => 'リネーム',
|
||||
'see_website' => 'webサイトを閲覧してください',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'カテゴリ',
|
||||
'add' => 'カテゴリを追加する',
|
||||
'archiving' => 'アーカイブ',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'からのカテゴリ',
|
||||
'information' => 'インフォメーション',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => '表示位置',
|
||||
'position_help' => 'カテゴリの表示順を操作する',
|
||||
'title' => 'タイトル',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => '購読されたものの管理',
|
||||
'add' => 'フィードあるいはカテゴリを追加します',
|
||||
'add_category' => 'カテゴリの追加',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'フィードの追加',
|
||||
'add_label' => 'ラベルの追加',
|
||||
'delete_label' => 'ラベルの削除',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← RSS 피드로 돌아가기',
|
||||
'cancel' => '취소',
|
||||
'create' => '생성',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => '목록 수준 내리기',
|
||||
'disable' => '비활성화',
|
||||
'empty' => '비우기',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => '목록 수준 올리기',
|
||||
'purge' => '제거',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => '삭제',
|
||||
'rename' => '이름 바꾸기',
|
||||
'see_website' => '웹사이트 열기',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => '카테고리',
|
||||
'add' => '카테고리 추가',
|
||||
'archiving' => '보관',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => '빈 카테고리',
|
||||
'information' => '정보',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => '표시 위치',
|
||||
'position_help' => '정렬 순서 제어',
|
||||
'title' => '제목',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => '구독 관리',
|
||||
'add' => '피드 혹은 카테고리 추가',
|
||||
'add_category' => '카테고리 추가',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => '피드 추가',
|
||||
'add_label' => '라벨 추가',
|
||||
'delete_label' => '라벨 삭제',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Ga terug naar je RSS feeds',
|
||||
'cancel' => 'Annuleren',
|
||||
'create' => 'Opslaan',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Degraderen',
|
||||
'disable' => 'Uitzetten',
|
||||
'empty' => 'Leeg',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Bevorderen',
|
||||
'purge' => 'Zuiveren',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Verwijderen',
|
||||
'rename' => 'Hernoemen',
|
||||
'see_website' => 'Bekijk website',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Categorie',
|
||||
'add' => 'Voeg categorie',
|
||||
'archiving' => 'Archiveren',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Lege categorie',
|
||||
'information' => 'Informatie',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Weergavepositie',
|
||||
'position_help' => 'Om de categorieweergave-sorteervolgorde te controleren',
|
||||
'title' => 'Titel',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Abonnementenbeheer',
|
||||
'add' => 'Feed of categorie toevoegen',
|
||||
'add_category' => 'Categorie toevoegen',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Feed toevoegen',
|
||||
'add_label' => 'Label toevoegen',
|
||||
'delete_label' => 'Label verwijderen',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Tornar a vòstres fluxes RSS',
|
||||
'cancel' => 'Anullar',
|
||||
'create' => 'Crear',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Retrogradar',
|
||||
'disable' => 'Desactivar',
|
||||
'empty' => 'Voidar',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Promòure',
|
||||
'purge' => 'Purgar',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Levar',
|
||||
'rename' => 'Renomenar',
|
||||
'see_website' => 'Veire lo site',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Categoria',
|
||||
'add' => 'Ajustar categoria',
|
||||
'archiving' => 'Archivar',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Categoria voida',
|
||||
'information' => 'Informacions',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Mostrar la posicion',
|
||||
'position_help' => 'Per contrarotlar l’òrdre de tria de la categoria',
|
||||
'title' => 'Títol',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Gestion dels abonaments',
|
||||
'add' => 'Apondon de flux o categoria',
|
||||
'add_category' => 'Ajustar una categoria',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Ajustar un flux',
|
||||
'add_label' => 'Ajustar una etiqueta',
|
||||
'delete_label' => 'Suprimir una etiqueta',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Wróć do subskrybowanych kanałów RSS',
|
||||
'cancel' => 'Anuluj',
|
||||
'create' => 'Stwórz',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Zdegraduj',
|
||||
'disable' => 'Wyłącz',
|
||||
'empty' => 'Opróżnij',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Awansuj',
|
||||
'purge' => 'Oczyść',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Usuń',
|
||||
'rename' => 'Zmień nazwę',
|
||||
'see_website' => 'Przejdź na stronę',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Kategoria',
|
||||
'add' => 'Dodaj kategoria',
|
||||
'archiving' => 'Archiwizacja',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Pusta kategoria',
|
||||
'information' => 'Informacje',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Miejsce wyświetlania',
|
||||
'position_help' => 'Kontrola porządku sortowania kategorii',
|
||||
'title' => 'Tytuł',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Zarządzanie subskrypcjami',
|
||||
'add' => 'Dodaj kanał lub kategorię',
|
||||
'add_category' => 'Dodaj kategorię',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Dodaj kanał',
|
||||
'add_label' => 'Dodaj etykietę',
|
||||
'delete_label' => 'Usuń etykietę',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Volte para o seu feeds RSS',
|
||||
'cancel' => 'Cancelar',
|
||||
'create' => 'Criar',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Despromover',
|
||||
'disable' => 'Desabilitar',
|
||||
'empty' => 'Vazio',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Promover',
|
||||
'purge' => 'Limpar',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Remover',
|
||||
'rename' => 'Renomear',
|
||||
'see_website' => 'Ver o site',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Categoria',
|
||||
'add' => 'Adicionar categoria',
|
||||
'archiving' => 'Arquivar',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Categoria vazia',
|
||||
'information' => 'Informações',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Posição de exibição',
|
||||
'position_help' => 'Para controlar a ordem de exibição',
|
||||
'title' => 'Título',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Gerenciamento de inscrições',
|
||||
'add' => 'Adicionar um feed ou categoria',
|
||||
'add_category' => 'Adicionar uma categoria',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Adicionar um feed',
|
||||
'add_label' => 'Adicionar uma etiqueta',
|
||||
'delete_label' => 'Deletar uma etiqueta',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Вернуться к вашим RSS-лентам',
|
||||
'cancel' => 'Отменить',
|
||||
'create' => 'Создать',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Понизить',
|
||||
'disable' => 'Отключить',
|
||||
'empty' => 'Опустошить',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Открыть URL',
|
||||
'promote' => 'Продвинуть',
|
||||
'purge' => 'Запустить очистку',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Удалить',
|
||||
'rename' => 'Переименовать',
|
||||
'see_website' => 'Посмотреть на сайте',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Категория',
|
||||
'add' => 'Добавить категория',
|
||||
'archiving' => 'Архивирование',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Пустая категория',
|
||||
'information' => 'Информация',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Положение отображения',
|
||||
'position_help' => 'Влияет на порядок отображения категорий',
|
||||
'title' => 'Заголовок',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Управление подписками',
|
||||
'add' => 'Добавить ленту или категорию',
|
||||
'add_category' => 'Добавить категорию',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Добавить ленту',
|
||||
'add_label' => 'Добавить метку',
|
||||
'delete_label' => 'Удалить метку',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← Späť na vaše RSS kanály',
|
||||
'cancel' => 'Zrušiť',
|
||||
'create' => 'Vytvoriť',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Degradovať',
|
||||
'disable' => 'Zakázať',
|
||||
'empty' => 'Vyprázdniť',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Podporiť',
|
||||
'purge' => 'Vymazať',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Odstrániť',
|
||||
'rename' => 'Premenovať',
|
||||
'see_website' => 'Zobraziť webovú stránku',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Kategória',
|
||||
'add' => 'Pridať kategória',
|
||||
'archiving' => 'Archív',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Prázdna kategória',
|
||||
'information' => 'Informácia',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Zobrazť pozíciu',
|
||||
'position_help' => 'Na kontrolu zoradenia kategórií',
|
||||
'title' => 'Názov',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Správa odoberaných kanálov',
|
||||
'add' => 'Pridať kanál alebo kategóriu',
|
||||
'add_category' => 'Pridať kategóriu',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Pridať kanál',
|
||||
'add_label' => 'Pridať štítok',
|
||||
'delete_label' => 'Zmazať štítok',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← RSS akışlarınız için geri gidin',
|
||||
'cancel' => 'İptal',
|
||||
'create' => 'Oluştur',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => 'Yöneticilikten al',
|
||||
'disable' => 'Pasif',
|
||||
'empty' => 'Boş',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => 'Open URL', // TODO
|
||||
'promote' => 'Yöneticilik ata',
|
||||
'purge' => 'Temizle',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => 'Sil',
|
||||
'rename' => 'Yeniden adlandır',
|
||||
'see_website' => 'Siteyi gör',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => 'Kategori',
|
||||
'add' => 'Kategori ekle',
|
||||
'archiving' => 'Arşiv',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => 'Boş kategori',
|
||||
'information' => 'Bilgi',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => 'Konumu göster',
|
||||
'position_help' => 'Kategori sıralama düzenini kontrol etmek için',
|
||||
'title' => 'Başlık',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => 'Abonelik yönetimi',
|
||||
'add' => 'Kategori veya akış ekle',
|
||||
'add_category' => 'Kategori ekle',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => 'Akış ekle',
|
||||
'add_label' => 'Etiket ekle',
|
||||
'delete_label' => 'Etiket sil',
|
||||
|
|
|
@ -18,6 +18,7 @@ return array(
|
|||
'back_to_rss_feeds' => '← 返回订阅源',
|
||||
'cancel' => '取消',
|
||||
'create' => '创建',
|
||||
'delete_muted_feeds' => 'Delete muted feeds', // TODO
|
||||
'demote' => '撤销管理员',
|
||||
'disable' => '禁用',
|
||||
'empty' => '清空',
|
||||
|
@ -31,6 +32,7 @@ return array(
|
|||
'open_url' => '打开链接',
|
||||
'promote' => '设为管理员',
|
||||
'purge' => '清理',
|
||||
'refresh_opml' => 'Refresh OPML', // TODO
|
||||
'remove' => '删除',
|
||||
'rename' => '重命名',
|
||||
'see_website' => '网站中查看',
|
||||
|
|
|
@ -24,8 +24,13 @@ return array(
|
|||
'_' => '分类',
|
||||
'add' => '添加分类',
|
||||
'archiving' => '归档',
|
||||
'dynamic_opml' => array(
|
||||
'_' => 'Dynamic OPML', // TODO
|
||||
'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds', // TODO
|
||||
),
|
||||
'empty' => '空分类',
|
||||
'information' => '信息',
|
||||
'opml_url' => 'OPML URL', // TODO
|
||||
'position' => '显示位置',
|
||||
'position_help' => '控制分类排列顺序',
|
||||
'title' => '标题',
|
||||
|
@ -181,6 +186,7 @@ return array(
|
|||
'_' => '订阅管理',
|
||||
'add' => '添加订阅源或分类',
|
||||
'add_category' => '添加分类',
|
||||
'add_dynamic_opml' => 'Add dynamic OPML', // TODO
|
||||
'add_feed' => '添加订阅源',
|
||||
'add_label' => '添加标签',
|
||||
'delete_label' => '删除标签',
|
||||
|
|
|
@ -89,7 +89,9 @@
|
|||
<div class="tree-folder-title">
|
||||
<a class="dropdown-toggle" href="#"><?= _i($c_show ? 'up' : 'down') ?></a>
|
||||
<a class="title<?= $cat->hasFeedsWithError() ? ' error' : '' ?>" data-unread="<?=
|
||||
format_number($cat->nbNotRead()) ?>" href="<?= _url('index', $actual_view, 'get', 'c_' . $cat->id()) . $state_filter_manual ?>"><?= $cat->name() ?></a>
|
||||
format_number($cat->nbNotRead()) ?>" href="<?= _url('index', $actual_view, 'get', 'c_' . $cat->id()) . $state_filter_manual ?>"><?=
|
||||
$cat->name()
|
||||
?><?php if ($cat->kind() == FreshRSS_Category::KIND_DYNAMIC_OPML) { echo _i('opml-dyn'); } ?></a>
|
||||
</div>
|
||||
|
||||
<ul class="tree-folder-items<?= $c_show ? ' active' : '' ?>">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="item title">
|
||||
<a href="<?= _url('index', 'index') ?>">
|
||||
<?php if (FreshRSS_Context::$system_conf->logo_html == '') { ?>
|
||||
<img class="logo" src="<?= _i('FreshRSS-logo', true) ?>" alt="FreshRSS" />
|
||||
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
|
||||
<?php
|
||||
} else {
|
||||
echo FreshRSS_Context::$system_conf->logo_html;
|
||||
|
|
|
@ -34,11 +34,18 @@ if (_t('gen.dir') === 'rtl') {
|
|||
if ($this->rss_title != '') {
|
||||
$url_rss = $url_base;
|
||||
$url_rss['a'] = 'rss';
|
||||
unset($url_rss['params']['rid']);
|
||||
if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) {
|
||||
$url_rss['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss;
|
||||
}
|
||||
?>
|
||||
<link rel="alternate" type="application/rss+xml" title="<?= $this->rss_title ?>" href="<?= Minz_Url::display($url_rss) ?>" />
|
||||
<?php } if (FreshRSS_Context::isAll() || FreshRSS_Context::isCategory() || FreshRSS_Context::isFeed()) {
|
||||
$opml_rss = $url_base;
|
||||
$opml_rss['a'] = 'opml';
|
||||
unset($opml_rss['params']['rid']);
|
||||
?>
|
||||
<link rel="outline" type="text/x-opml" title="OPML" href="<?= Minz_Url::display($opml_rss) ?>" />
|
||||
<?php } if (FreshRSS_Context::$system_conf->allow_robots) { ?>
|
||||
<meta name="description" content="<?= htmlspecialchars(FreshRSS_Context::$name . ' | ' . FreshRSS_Context::$description, ENT_COMPAT, 'UTF-8') ?>" />
|
||||
<?php } else { ?>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<div class="item title">
|
||||
<a href="<?= _url('index', 'index') ?>">
|
||||
<?php if (FreshRSS_Context::$system_conf->logo_html == '') { ?>
|
||||
<img class="logo" src="<?= _i('FreshRSS-logo', true) ?>" alt="FreshRSS" />
|
||||
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
|
||||
<?php
|
||||
} else {
|
||||
echo FreshRSS_Context::$system_conf->logo_html;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
OK
|
|
@ -0,0 +1 @@
|
|||
OK
|
|
@ -1,6 +1,9 @@
|
|||
<?php /** @var FreshRSS_View $this */ ?>
|
||||
<div class="post">
|
||||
<h2><?= $this->category->name() ?></h2>
|
||||
<h2>
|
||||
<?= $this->category->name() ?>
|
||||
<?php if ($this->category->kind() == FreshRSS_Category::KIND_DYNAMIC_OPML) { echo _i('opml-dyn'); } ?>
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<a href="<?= _url('index', 'index', 'get', 'c_' . $this->category->id()) ?>"><?= _i('link') ?> <?= _t('gen.action.filter') ?></a>
|
||||
|
@ -31,10 +34,37 @@
|
|||
<div class="group-controls">
|
||||
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
|
||||
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$this->category->isDefault()): ?>
|
||||
<legend><?= _t('sub.category.dynamic_opml') ?> <?= _i('opml-dyn') ?></legend>
|
||||
<div class="form-group">
|
||||
<label class="group-name" for="opml_url"><?= _t('sub.category.opml_url') ?></label>
|
||||
<div class="group-controls">
|
||||
<div class="stick">
|
||||
<input id="opml_url" name="opml_url" type="url" autocomplete="off" class="long" data-disable-update="refreshOpml" value="<?= $this->category->attributes('opml_url') ?>" />
|
||||
<button type="submit" class="btn" id="refreshOpml" formmethod="post" formaction="<?= _url('category', 'refreshOpml', 'id', $this->category->id()) ?>">
|
||||
<?= _i('refresh') ?> <?= _t('gen.action.refresh_opml') ?>
|
||||
</button>
|
||||
<a class="btn open-url" target="_blank" rel="noreferrer" href="" data-input="opml_url" title="<?= _t('gen.action.open_url') ?>"><?= _i('link') ?></a>
|
||||
</div>
|
||||
<p class="help"><?= _i('help') ?> <?= _t('gen.short.blank_to_disable') ?></p>
|
||||
<p class="help"><?= _i('help') ?> <?= _t('sub.category.dynamic_opml.help') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-actions">
|
||||
<div class="group-controls">
|
||||
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
|
||||
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
|
||||
<button type="submit" class="btn btn-attention confirm"
|
||||
data-str-confirm="<?= _t('gen.js.confirm_action_feed_cat') ?>"
|
||||
formaction="<?= _url('category', 'empty', 'id', $this->category->id(), 'muted', 1) ?>"
|
||||
formmethod="post"><?= _t('gen.action.delete_muted_feeds') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<legend><?= _t('sub.category.archiving') ?></legend>
|
||||
<?php
|
||||
$archiving = $this->category->attributes('archiving');
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
<?php
|
||||
/** @var FreshRSS_View $this */
|
||||
|
||||
$opml_array = array(
|
||||
'head' => array(
|
||||
'title' => FreshRSS_Context::$system_conf->title,
|
||||
'dateCreated' => date('D, d M Y H:i:s')
|
||||
),
|
||||
'body' => array()
|
||||
);
|
||||
|
||||
foreach ($this->categories as $key => $cat) {
|
||||
$opml_array['body'][$key] = array(
|
||||
'text' => htmlspecialchars_decode($cat->name(), ENT_QUOTES),
|
||||
'@outlines' => array()
|
||||
);
|
||||
|
||||
foreach ($cat->feeds() as $feed) {
|
||||
/**
|
||||
* @param array<FreshRSS_Feed> $feeds
|
||||
*/
|
||||
function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
|
||||
$outlines = [];
|
||||
foreach ($feeds as $feed) {
|
||||
if ($feed->mute() && $excludeMutedFeeds) {
|
||||
continue;
|
||||
}
|
||||
$outline = [
|
||||
'text' => htmlspecialchars_decode($feed->name(), ENT_QUOTES),
|
||||
'type' => FreshRSS_Export_Service::TYPE_RSS_ATOM,
|
||||
|
@ -47,8 +40,36 @@ foreach ($this->categories as $key => $cat) {
|
|||
if ($feed->pathEntries() != '') {
|
||||
$outline['frss:cssFullContent'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $feed->pathEntries()];
|
||||
}
|
||||
$opml_array['body'][$key]['@outlines'][] = $outline;
|
||||
$outlines[] = $outline;
|
||||
}
|
||||
return $outlines;
|
||||
}
|
||||
|
||||
/** @var FreshRSS_View $this */
|
||||
|
||||
$opml_array = array(
|
||||
'head' => array(
|
||||
'title' => FreshRSS_Context::$system_conf->title,
|
||||
'dateCreated' => date('D, d M Y H:i:s')
|
||||
),
|
||||
'body' => array()
|
||||
);
|
||||
|
||||
if (!empty($this->categories)) {
|
||||
foreach ($this->categories as $key => $cat) {
|
||||
$outline = [
|
||||
'text' => htmlspecialchars_decode($cat->name(), ENT_QUOTES),
|
||||
'@outlines' => feedsToOutlines($cat->feeds(), $this->excludeMutedFeeds),
|
||||
];
|
||||
if ($cat->kind() === FreshRSS_Category::KIND_DYNAMIC_OPML) {
|
||||
$outline['frss:opmlUrl'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $cat->attributes('opml_url')];;
|
||||
}
|
||||
$opml_array['body'][$key] = $outline;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->feeds)) {
|
||||
$opml_array['body'][] = feedsToOutlines($this->feeds, $this->excludeMutedFeeds);
|
||||
}
|
||||
|
||||
echo libopml_render($opml_array);
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
|
||||
<h1><?= _t('sub.menu.import_export') ?></h1>
|
||||
|
||||
<h2><?= _t('sub.category.dynamic_opml') ?></h2>
|
||||
<div class="form-group form-actions">
|
||||
<div class="group-controls">
|
||||
<ul>
|
||||
<li><a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.title.add_dynamic_opml') ?> <?= _i('opml-dyn') ?></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2><?= _t('sub.import_export.import') ?></h2>
|
||||
<form method="post" action="<?= _url('importExport', 'import') ?>" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
/** @var FreshRSS_View $this */
|
||||
$this->renderHelper('export/opml');
|
|
@ -1,5 +1,14 @@
|
|||
<?php /** @var FreshRSS_View $this */ ?>
|
||||
<?php
|
||||
/** @var FreshRSS_View $this */
|
||||
|
||||
$categories = [];
|
||||
foreach ($this->categories as $category) {
|
||||
$categories[] = [
|
||||
'url' => Minz_Url::display(array('c' => 'category', 'a' => 'refreshOpml', 'params' => array('id' => $category->id(), 'ajax' => '1')), 'php'),
|
||||
'title' => $category->name(),
|
||||
];
|
||||
}
|
||||
|
||||
$feeds = array();
|
||||
foreach ($this->feeds as $feed) {
|
||||
$feeds[] = array(
|
||||
|
@ -8,6 +17,7 @@ foreach ($this->feeds as $feed) {
|
|||
);
|
||||
}
|
||||
echo json_encode(array(
|
||||
'categories' => $categories,
|
||||
'feeds' => $feeds,
|
||||
'feedback_no_refresh' => _t('feedback.sub.feed.no_refresh'),
|
||||
'feedback_actualize' => _t('feedback.sub.actualize'),
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="form-group">
|
||||
<label class="group-name" for="new-category"><?= _t('sub.category') ?></label>
|
||||
<div class="group-controls">
|
||||
<input id="new-category" name="new-category" type="text" autocomplete="off"/>
|
||||
<input id="new-category" name="new-category" type="text" required="required" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -45,7 +45,12 @@
|
|||
<label class="group-name" for="category"><?= _t('sub.category') ?></label>
|
||||
<div class="group-controls">
|
||||
<select name="category" id="category">
|
||||
<?php foreach ($this->categories as $cat) { ?>
|
||||
<?php
|
||||
foreach ($this->categories as $cat) {
|
||||
if ($cat->kind() == FreshRSS_Category::KIND_DYNAMIC_OPML) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<option value="<?= $cat->id() ?>"<?= $cat->id() == ( Minz_Request::param('cat_id') ?: 1 ) ? ' selected="selected"' : '' ?>>
|
||||
<?= $cat->name() ?>
|
||||
</option>
|
||||
|
@ -218,4 +223,35 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>
|
||||
<?= _t('sub.title.add_dynamic_opml') ?>
|
||||
<?= _i('opml-dyn') ?>
|
||||
</h2>
|
||||
<form action="<?= _url('category', 'create') ?>" method="post">
|
||||
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
|
||||
<div class="form-group">
|
||||
<label class="group-name" for="new-category"><?= _t('sub.category') ?></label>
|
||||
<div class="group-controls">
|
||||
<input id="new-category" name="new-category" type="text" required="required" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="group-name" for="opml_url"><?= _t('sub.category.opml_url') ?></label>
|
||||
<div class="group-controls">
|
||||
<div class="stick">
|
||||
<input id="opml_url" name="opml_url" type="url" required="required" autocomplete="off" class="long" />
|
||||
<a class="btn open-url" target="_blank" rel="noreferrer" href="" data-input="opml_url" title="<?= _t('gen.action.open_url') ?>"><?= _i('link') ?></a>
|
||||
</div>
|
||||
<p class="help"><?= _i('help') ?> <?= _t('sub.category.dynamic_opml.help') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-actions">
|
||||
<div class="group-controls">
|
||||
<button type="submit" class="btn btn-important"><?= _t('gen.action.add') ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<div class="box-title">
|
||||
<a class="configure open-slider" href="<?= _url('subscription', 'category', 'id', $cat->id()) ?>"><?= _i('configure') ?></a>
|
||||
<?= $cat->name() ?>
|
||||
<?php if ($cat->kind() == FreshRSS_Category::KIND_DYNAMIC_OPML) { echo _i('opml-dyn'); } ?>
|
||||
</div>
|
||||
<ul class="box-content drop-zone" dropzone="move" data-cat-id="<?= $cat->id() ?>">
|
||||
<?php
|
||||
|
@ -60,7 +61,9 @@
|
|||
?>
|
||||
<li class="item feed disabled"><div class="alert-warn"><?= _t('sub.category.empty') ?></div></li>
|
||||
<?php } ?>
|
||||
<li class="item feed">✚ <a href="<?= _url('subscription', 'add', 'cat_id', $cat->id()) ?>"><?= _t('sub.feed.add') ?></a></li>
|
||||
<?php if ($cat->kind() != FreshRSS_Category::KIND_DYNAMIC_OPML): ?>
|
||||
<li class="item feed">✚ <a href="<?= _url('subscription', 'add', 'cat_id', $cat->id()) ?>"><?= _t('sub.feed.add') ?></a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
|
|
@ -18,6 +18,16 @@ $username = cliInitUser($options['user']);
|
|||
|
||||
fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
|
||||
|
||||
$result = FreshRSS_category_Controller::refreshDynamicOpmls();
|
||||
if (!empty($result['errors'])) {
|
||||
$errors = $result['errors'];
|
||||
fwrite(STDERR, "FreshRSS error refreshing $errors dynamic OPMLs!\n");
|
||||
}
|
||||
if (!empty($result['successes'])) {
|
||||
$successes = $result['successes'];
|
||||
echo "FreshRSS refreshed $successes dynamic OPMLs for $username\n";
|
||||
}
|
||||
|
||||
list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
|
||||
|
||||
echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username ($nbNewArticles new articles)\n";
|
||||
|
|
|
@ -16,6 +16,7 @@ return array (
|
|||
'keep_unreads' => false,
|
||||
],
|
||||
'ttl_default' => 3600,
|
||||
'dynamic_opml_ttl_default' => 43200,
|
||||
'mail_login' => '',
|
||||
'email_validation_token' => '',
|
||||
'token' => '',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
*.spc
|
||||
*.html
|
||||
*.xml
|
||||
!index.html
|
||||
|
|
|
@ -46,6 +46,10 @@ The following attributes are using similar naming conventions than [RSS-Bridge](
|
|||
* Example: `div.main`
|
||||
* `frss:filtersActionRead`: List (separated by a new line) of search queries to automatically mark a new article as read.
|
||||
|
||||
### Dynamic OPML (reading lists)
|
||||
|
||||
* `frss:opmlUrl`: If non-empty, indicates that this outline (category) should be dynamically populated from a remote OPML at the specified URL.
|
||||
|
||||
### Example
|
||||
|
||||
```xml
|
||||
|
|
|
@ -377,19 +377,19 @@ function enforceHttpEncoding(string $html, string $contentType = ''): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $type {html,opml}
|
||||
* @param array<string,mixed> $attributes
|
||||
*/
|
||||
function getHtml(string $url, array $attributes = []): string {
|
||||
function httpGet(string $url, string $cachePath, string $type = 'html', 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 != '') {
|
||||
$body = @file_get_contents($cachePath);
|
||||
if ($body != '') {
|
||||
syslog(LOG_DEBUG, 'FreshRSS uses cache for ' . SimplePie_Misc::url_remove_credentials($url));
|
||||
return $html;
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,14 +398,25 @@ function getHtml(string $url, array $attributes = []): string {
|
|||
}
|
||||
|
||||
if (FreshRSS_Context::$system_conf->simplepie_syslog_enabled) {
|
||||
syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
|
||||
syslog(LOG_INFO, 'FreshRSS GET ' . $type . ' ' . SimplePie_Misc::url_remove_credentials($url));
|
||||
}
|
||||
|
||||
$accept = '*/*;q=0.8';
|
||||
switch ($type) {
|
||||
case 'opml':
|
||||
$accept = 'text/x-opml,text/xml;q=0.9,application/xml;q=0.9,*/*;q=0.8';
|
||||
break;
|
||||
case 'html':
|
||||
default:
|
||||
$accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Implement HTTP 1.1 conditional GET If-Modified-Since
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_HTTPHEADER => array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
|
||||
CURLOPT_HTTPHEADER => array('Accept: ' . $accept),
|
||||
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
|
||||
CURLOPT_CONNECTTIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
|
||||
CURLOPT_TIMEOUT => $feed_timeout > 0 ? $feed_timeout : $limits['timeout'],
|
||||
|
@ -428,27 +439,28 @@ function getHtml(string $url, array $attributes = []): string {
|
|||
curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1');
|
||||
}
|
||||
}
|
||||
$html = curl_exec($ch);
|
||||
$body = 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) {
|
||||
if ($c_status != 200 || $c_error != '' || $body === false) {
|
||||
Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
|
||||
$body = '';
|
||||
// TODO: Implement HTTP 410 Gone
|
||||
}
|
||||
if ($html == false) {
|
||||
$html = '';
|
||||
if ($body == false) {
|
||||
$body = '';
|
||||
} else {
|
||||
$html = enforceHttpEncoding($html, $c_content_type);
|
||||
$body = enforceHttpEncoding($body, $c_content_type);
|
||||
}
|
||||
|
||||
if (file_put_contents($cachePath, $html) === false) {
|
||||
if (file_put_contents($cachePath, $body) === false) {
|
||||
Minz_Log::warning("Error saving cache $cachePath for $url");
|
||||
}
|
||||
|
||||
return $html;
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -770,8 +782,8 @@ function remove_query_by_get($get, $queries) {
|
|||
return $final_queries;
|
||||
}
|
||||
|
||||
function _i($icon, $url_only = false) {
|
||||
return FreshRSS_Themes::icon($icon, $url_only);
|
||||
function _i(string $icon, int $type = FreshRSS_Themes::ICON_DEFAULT): string {
|
||||
return FreshRSS_Themes::icon($icon, $type);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -307,8 +307,8 @@ function subscriptionExport() {
|
|||
function subscriptionImport($opml) {
|
||||
$user = Minz_Session::param('currentUser', '_');
|
||||
$importService = new FreshRSS_Import_Service($user);
|
||||
$ok = $importService->importOpml($opml);
|
||||
if ($ok) {
|
||||
$importService->importOpml($opml);
|
||||
if ($importService->lastStatus()) {
|
||||
list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
|
||||
invalidateHttpCache($user);
|
||||
exit('OK');
|
||||
|
|
|
@ -202,8 +202,8 @@ function updateHref(ev) {
|
|||
}
|
||||
|
||||
// set event listener on "show url" buttons
|
||||
function init_url_observers() {
|
||||
document.querySelectorAll('.open-url').forEach(function (btn) {
|
||||
function init_url_observers(parent) {
|
||||
parent.querySelectorAll('.open-url').forEach(function (btn) {
|
||||
btn.addEventListener('mouseover', updateHref);
|
||||
btn.addEventListener('click', updateHref);
|
||||
});
|
||||
|
@ -276,7 +276,6 @@ function init_extra_afterDOM() {
|
|||
if (!['normal', 'global', 'reader'].includes(context.current_view)) {
|
||||
init_crypto_form();
|
||||
init_password_observers(document.body);
|
||||
init_url_observers();
|
||||
init_select_observers();
|
||||
init_configuration_alert();
|
||||
|
||||
|
@ -284,8 +283,10 @@ function init_extra_afterDOM() {
|
|||
if (slider) {
|
||||
init_slider(slider);
|
||||
init_archiving(slider);
|
||||
init_url_observers(slider);
|
||||
} else {
|
||||
init_archiving(document.body);
|
||||
init_url_observers(document.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
|
||||
'use strict';
|
||||
/* globals init_archiving, init_configuration_alert, init_password_observers, init_slider */
|
||||
/* globals init_archiving, init_configuration_alert, init_password_observers, init_slider, init_url_observers */
|
||||
|
||||
// <popup>
|
||||
let popup = null;
|
||||
|
@ -64,6 +64,22 @@ function init_popup_preview_selector() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a <select class="select-show"> to hide/show elements defined by <option data-show="elem-id"></option>
|
||||
*/
|
||||
function init_disable_elements_on_update(parent) {
|
||||
const inputs = parent.querySelectorAll('input[data-disable-update]');
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('input', (e) => {
|
||||
const elem = document.getElementById(e.target.dataset.disableUpdate);
|
||||
if (elem) {
|
||||
elem.disabled = true;
|
||||
elem.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a <select class="select-show"> to hide/show elements defined by <option data-show="elem-id"></option>
|
||||
*/
|
||||
|
@ -120,7 +136,9 @@ function init_feed_afterDOM() {
|
|||
init_popup();
|
||||
init_popup_preview_selector();
|
||||
init_select_show(slider);
|
||||
init_disable_elements_on_update(slider);
|
||||
init_password_observers(slider);
|
||||
init_url_observers(slider);
|
||||
init_valid_xpath(slider);
|
||||
});
|
||||
init_slider(slider);
|
||||
|
@ -130,6 +148,7 @@ function init_feed_afterDOM() {
|
|||
init_popup();
|
||||
init_popup_preview_selector();
|
||||
init_select_show(document.body);
|
||||
init_disable_elements_on_update(document.body);
|
||||
init_password_observers(document.body);
|
||||
init_valid_xpath(document.body);
|
||||
}
|
||||
|
|
|
@ -115,9 +115,10 @@ function incUnreadsFeed(article, feed_id, nb) {
|
|||
}
|
||||
|
||||
// Update unread: category
|
||||
elem = document.getElementById(feed_id).closest('.category');
|
||||
feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
|
||||
elem = document.getElementById(feed_id);
|
||||
elem = elem ? elem.closest('.category') : null;
|
||||
if (elem) {
|
||||
feed_unreads = str2int(elem.getAttribute('data-unread'));
|
||||
elem.setAttribute('data-unread', feed_unreads + nb);
|
||||
elem = elem.querySelector('.title');
|
||||
if (elem) {
|
||||
|
@ -147,7 +148,7 @@ function incUnreadsFeed(article, feed_id, nb) {
|
|||
// Update unread: title
|
||||
document.title = document.title.replace(/^((?:\([\s0-9]+\) )?)/, function (m, p1) {
|
||||
const feed = document.getElementById(feed_id);
|
||||
if (article || feed.closest('.active')) {
|
||||
if (article || (feed && feed.closest('.active'))) {
|
||||
isCurrentView = true;
|
||||
return incLabel(p1, nb, true);
|
||||
} else if (document.querySelector('.all.active')) {
|
||||
|
@ -1287,9 +1288,11 @@ function loadDynamicTags(div) {
|
|||
}
|
||||
|
||||
// <actualize>
|
||||
let feed_processed = 0;
|
||||
let feeds_processed = 0;
|
||||
let categories_processed = 0;
|
||||
let to_process = 0;
|
||||
|
||||
function updateFeed(feeds, feeds_count) {
|
||||
function refreshFeed(feeds, feeds_count) {
|
||||
const feed = feeds.pop();
|
||||
if (!feed) {
|
||||
return;
|
||||
|
@ -1297,14 +1300,15 @@ function updateFeed(feeds, feeds_count) {
|
|||
const req = new XMLHttpRequest();
|
||||
req.open('POST', feed.url, true);
|
||||
req.onloadend = function (e) {
|
||||
feeds_processed++;
|
||||
if (this.status != 200) {
|
||||
return badAjax(false);
|
||||
badAjax(false);
|
||||
} else {
|
||||
const div = document.getElementById('actualizeProgress');
|
||||
div.querySelector('.progress').innerHTML = (categories_processed + feeds_processed) + ' / ' + to_process;
|
||||
div.querySelector('.title').innerHTML = feed.title;
|
||||
}
|
||||
feed_processed++;
|
||||
const div = document.getElementById('actualizeProgress');
|
||||
div.querySelector('.progress').innerHTML = feed_processed + ' / ' + feeds_count;
|
||||
div.querySelector('.title').innerHTML = feed.title;
|
||||
if (feed_processed === feeds_count) {
|
||||
if (feeds_processed === feeds_count) {
|
||||
// Empty request to commit new articles
|
||||
const req2 = new XMLHttpRequest();
|
||||
req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
|
||||
|
@ -1317,7 +1321,7 @@ function updateFeed(feeds, feeds_count) {
|
|||
noCommit: 0,
|
||||
}));
|
||||
} else {
|
||||
updateFeed(feeds, feeds_count);
|
||||
refreshFeed(feeds, feeds_count);
|
||||
}
|
||||
};
|
||||
req.setRequestHeader('Content-Type', 'application/json');
|
||||
|
@ -1327,8 +1331,73 @@ function updateFeed(feeds, feeds_count) {
|
|||
}));
|
||||
}
|
||||
|
||||
function refreshFeeds(json) {
|
||||
feeds_processed = 0;
|
||||
if (!json.feeds || json.feeds.length === 0) {
|
||||
// Empty request to commit new articles
|
||||
const req2 = new XMLHttpRequest();
|
||||
req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
|
||||
req2.onloadend = function (e) {
|
||||
context.ajax_loading = false;
|
||||
};
|
||||
req2.setRequestHeader('Content-Type', 'application/json');
|
||||
req2.send(JSON.stringify({
|
||||
_csrf: context.csrf,
|
||||
noCommit: 0,
|
||||
}));
|
||||
} else {
|
||||
const feeds_count = json.feeds.length;
|
||||
for (let i = 10; i > 0; i--) {
|
||||
refreshFeed(json.feeds, feeds_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDynamicOpml(categories, categories_count, next) {
|
||||
const category = categories.pop();
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
const req = new XMLHttpRequest();
|
||||
req.open('POST', category.url, true);
|
||||
req.onloadend = function (e) {
|
||||
categories_processed++;
|
||||
if (this.status != 200) {
|
||||
badAjax(false);
|
||||
} else {
|
||||
const div = document.getElementById('actualizeProgress');
|
||||
div.querySelector('.progress').innerHTML = (categories_processed + feeds_processed) + ' / ' + to_process;
|
||||
div.querySelector('.title').innerHTML = category.title;
|
||||
}
|
||||
if (categories_processed === categories_count) {
|
||||
if (next) { next(); }
|
||||
} else {
|
||||
refreshDynamicOpml(categories, categories_count, next);
|
||||
}
|
||||
};
|
||||
req.setRequestHeader('Content-Type', 'application/json');
|
||||
req.send(JSON.stringify({
|
||||
_csrf: context.csrf,
|
||||
noCommit: 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function refreshDynamicOpmls(json, next) {
|
||||
categories_processed = 0;
|
||||
if (json.categories && json.categories.length > 0) {
|
||||
const categories_count = json.categories.length;
|
||||
for (let i = 10; i > 0; i--) {
|
||||
refreshDynamicOpml(json.categories, categories_count, next);
|
||||
}
|
||||
} else {
|
||||
if (next) { next(); }
|
||||
}
|
||||
}
|
||||
|
||||
function init_actualize() {
|
||||
let auto = false;
|
||||
let nbCategoriesFirstRound = 0;
|
||||
let skipCategories = false;
|
||||
|
||||
const actualize = document.getElementById('actualize');
|
||||
if (!actualize) {
|
||||
|
@ -1352,33 +1421,29 @@ function init_actualize() {
|
|||
if (!json) {
|
||||
return badAjax(false);
|
||||
}
|
||||
if (auto && json.feeds.length < 1) {
|
||||
if (auto && json.categories.length < 1 && json.feeds.length < 1) {
|
||||
auto = false;
|
||||
context.ajax_loading = false;
|
||||
return false;
|
||||
}
|
||||
if (json.feeds.length === 0) {
|
||||
openNotification(json.feedback_no_refresh, 'good');
|
||||
// Empty request to commit new articles
|
||||
const req2 = new XMLHttpRequest();
|
||||
req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
|
||||
req2.onloadend = function (e) {
|
||||
context.ajax_loading = false;
|
||||
};
|
||||
req2.setRequestHeader('Content-Type', 'application/json');
|
||||
req2.send(JSON.stringify({
|
||||
_csrf: context.csrf,
|
||||
noCommit: 0,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// Progress bar
|
||||
const feeds_count = json.feeds.length;
|
||||
document.body.insertAdjacentHTML('beforeend', '<div id="actualizeProgress" class="notification good">' +
|
||||
to_process = json.categories.length + json.feeds.length + nbCategoriesFirstRound;
|
||||
if (json.categories.length + json.feeds.length > 0 && !document.getElementById('actualizeProgress')) {
|
||||
document.body.insertAdjacentHTML('beforeend', '<div id="actualizeProgress" class="notification good">' +
|
||||
json.feedback_actualize + '<br /><span class="title">/</span><br /><span class="progress">0 / ' +
|
||||
feeds_count + '</span></div>');
|
||||
for (let i = 10; i > 0; i--) {
|
||||
updateFeed(json.feeds, feeds_count);
|
||||
to_process + '</span></div>');
|
||||
} else {
|
||||
openNotification(json.feedback_no_refresh, 'good');
|
||||
}
|
||||
if (json.categories.length > 0 && !skipCategories) {
|
||||
skipCategories = true; // To avoid risk of infinite loop
|
||||
nbCategoriesFirstRound = json.categories.length;
|
||||
// If some dynamic OPML categories are refreshed, need to reload the list of feeds before updating them
|
||||
refreshDynamicOpmls(json, () => {
|
||||
context.ajax_loading = false;
|
||||
actualize.click();
|
||||
});
|
||||
} else {
|
||||
refreshFeeds(json);
|
||||
}
|
||||
};
|
||||
req.setRequestHeader('Content-Type', 'application/json');
|
||||
|
|
|
@ -117,6 +117,7 @@ input:focus {
|
|||
.icon[src*="/sort-up"],
|
||||
.icon[src*="/sort-down"],
|
||||
.icon[src*="/key"],
|
||||
.icon[src*="/opml-dyn"],
|
||||
.icon[src*="/configure"],
|
||||
.icon[src*="/category"] {
|
||||
/* Color light grey icons */
|
||||
|
|
|
@ -117,6 +117,7 @@ input:focus {
|
|||
.icon[src*="/sort-up"],
|
||||
.icon[src*="/sort-down"],
|
||||
.icon[src*="/key"],
|
||||
.icon[src*="/opml-dyn"],
|
||||
.icon[src*="/configure"],
|
||||
.icon[src*="/category"] {
|
||||
/* Color light grey icons */
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="93.619" height="93.619" viewBox="0 0 24.77 24.77" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(33.09 -98.68)">
|
||||
<path style="fill:#000;stroke-width:.264583" d="M98.217 120.656V96.711H132.084v47.89H98.217Zm19.925 5.03c5.583-1.455 9.438-6.373 9.438-12.041 0-7.464-6.765-13.365-14.031-12.24-9.125 1.412-13.645 11.61-8.549 19.289 2.769 4.17 8.27 6.26 13.142 4.992zm-5.108-3.822c-1.295-.395-2.793-1.303-3.807-2.306-3.247-3.214-3.255-8.545-.018-11.837 1.622-1.65 3.547-2.412 6.074-2.402 2.59.009 4.158.678 5.999 2.558 1.733 1.77 2.423 3.685 2.272 6.297-.2 3.452-2.21 6.207-5.398 7.4-1.464.547-3.839.682-5.122.29zm3.713-5.181c.92-.476 1.578-1.634 1.578-2.774 0-2.794-3.33-4.117-5.331-2.117-.626.625-.754.977-.754 2.061 0 2.57 2.263 3.99 4.507 2.83z"/>
|
||||
<path style="opacity:1;fill:#666;stroke:none;stroke-width:1.12498;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;paint-order:markers fill stroke" d="M-20.705 98.68a12.385 12.385 0 0 0-12.384 12.385 12.385 12.385 0 0 0 12.384 12.385 12.385 12.385 0 0 0 12.386-12.385A12.385 12.385 0 0 0-20.705 98.68zm0 3.616a8.77 8.77 0 0 1 8.77 8.77 8.77 8.77 0 0 1-8.77 8.77 8.77 8.77 0 0 1-8.77-8.77 8.77 8.77 0 0 1 8.77-8.77z"/>
|
||||
<circle style="opacity:1;fill:#666;stroke:none;stroke-width:.276252;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;paint-order:markers fill stroke" cx="-20.704" cy="111.065" r="3.041"/>
|
||||
<path style="fill:#666;stroke-width:.00362641;fill-opacity:1" d="M-17.936 117.039c-4.746 1.911-6.857-.321-8.826-3.398.058-.03.967-.532 1.018-.558 1.935 2.8 3.752 4.62 7.194 2.712l-.482-.983c.787.2 1.619.415 2.465.64-.322.801-.639 1.604-.966 2.402l-.403-.815zM-23.767 105.435c4.745-1.911 6.856.321 8.825 3.398-.058.03-.966.532-1.017.558-1.935-2.8-3.753-4.62-7.195-2.712l.482.983c-.787-.2-1.618-.415-2.465-.64.322-.801.64-1.604.967-2.402.134.272.267.543.403.815z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,7 @@
|
|||
<svg width="93.619" height="93.619" viewBox="0 0 24.77 24.77" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(33.09 -98.68)">
|
||||
<path style="fill:#000;stroke-width:.264583" d="M98.217 120.656V96.711H132.084v47.89H98.217Zm19.925 5.03c5.583-1.455 9.438-6.373 9.438-12.041 0-7.464-6.765-13.365-14.031-12.24-9.125 1.412-13.645 11.61-8.549 19.289 2.769 4.17 8.27 6.26 13.142 4.992zm-5.108-3.822c-1.295-.395-2.793-1.303-3.807-2.306-3.247-3.214-3.255-8.545-.018-11.837 1.622-1.65 3.547-2.412 6.074-2.402 2.59.009 4.158.678 5.999 2.558 1.733 1.77 2.423 3.685 2.272 6.297-.2 3.452-2.21 6.207-5.398 7.4-1.464.547-3.839.682-5.122.29zm3.713-5.181c.92-.476 1.578-1.634 1.578-2.774 0-2.794-3.33-4.117-5.331-2.117-.626.625-.754.977-.754 2.061 0 2.57 2.263 3.99 4.507 2.83z"/>
|
||||
<path style="opacity:1;fill:#666;stroke:none;stroke-width:1.12498;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;paint-order:markers fill stroke" d="M-20.705 98.68a12.385 12.385 0 0 0-12.384 12.385 12.385 12.385 0 0 0 12.384 12.385 12.385 12.385 0 0 0 12.386-12.385A12.385 12.385 0 0 0-20.705 98.68zm0 3.616a8.77 8.77 0 0 1 8.77 8.77 8.77 8.77 0 0 1-8.77 8.77 8.77 8.77 0 0 1-8.77-8.77 8.77 8.77 0 0 1 8.77-8.77z"/>
|
||||
<circle style="opacity:1;fill:#666;stroke:none;stroke-width:.276252;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-opacity:1;paint-order:markers fill stroke" cx="-20.704" cy="111.065" r="3.041"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
Reference in New Issue