aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
commitb3239256dc6d188cda970adab516b3fcf1b86129 (patch)
treed8e65dd9784834ba2e82ce7ee94b4718f8af19ea /app
parent27b71ffa99f7dff013fb8d51d020ed628e0d2ce6 (diff)
parent0fe0ce894cbad09757d719dd4b400b9862c1a12a (diff)
Merge branch 'edge' into latest
Diffstat (limited to 'app')
-rw-r--r--[-rwxr-xr-x]app/Controllers/configureController.php35
-rw-r--r--[-rwxr-xr-x]app/Controllers/entryController.php0
-rw-r--r--[-rwxr-xr-x]app/Controllers/feedController.php18
-rw-r--r--app/Controllers/importExportController.php2
-rw-r--r--[-rwxr-xr-x]app/Controllers/indexController.php2
-rw-r--r--[-rwxr-xr-x]app/Controllers/javascriptController.php8
-rw-r--r--app/Controllers/statsController.php37
-rw-r--r--app/Controllers/subscriptionController.php18
-rw-r--r--app/Controllers/updateController.php12
-rw-r--r--app/Controllers/userController.php3
-rw-r--r--app/FreshRSS.php30
-rw-r--r--app/Models/BooleanSearch.php8
-rw-r--r--app/Models/Category.php15
-rw-r--r--app/Models/CategoryDAO.php15
-rw-r--r--app/Models/ConfigurationSetter.php7
-rw-r--r--app/Models/Context.php20
-rw-r--r--app/Models/Days.php8
-rw-r--r--app/Models/Entry.php174
-rw-r--r--app/Models/EntryDAO.php25
-rw-r--r--app/Models/EntryDAOSQLite.php4
-rw-r--r--app/Models/Feed.php142
-rw-r--r--app/Models/FeedDAO.php6
-rw-r--r--app/Models/Searchable.php4
-rw-r--r--app/Models/SystemConfiguration.php6
-rw-r--r--app/Models/Tag.php55
-rw-r--r--app/Models/TagDAO.php40
-rw-r--r--app/Models/Themes.php1
-rw-r--r--app/Models/UserConfiguration.php8
-rw-r--r--app/Models/UserQuery.php98
-rw-r--r--app/Models/View.php1
-rw-r--r--app/SQL/install.sql.mysql.php5
-rw-r--r--app/SQL/install.sql.pgsql.php4
-rw-r--r--app/SQL/install.sql.sqlite.php7
-rw-r--r--app/Services/ExportService.php3
-rw-r--r--app/Services/ImportService.php445
-rw-r--r--app/Utils/feverUtil.php38
-rw-r--r--app/Utils/passwordUtil.php19
-rw-r--r--app/i18n/cz/conf.php10
-rw-r--r--app/i18n/cz/gen.php3
-rw-r--r--app/i18n/cz/sub.php1
-rw-r--r--app/i18n/de/conf.php10
-rw-r--r--app/i18n/de/gen.php3
-rw-r--r--app/i18n/de/sub.php1
-rw-r--r--app/i18n/el/conf.php10
-rw-r--r--app/i18n/el/gen.php3
-rw-r--r--app/i18n/el/sub.php1
-rw-r--r--app/i18n/en-us/conf.php10
-rw-r--r--app/i18n/en-us/gen.php3
-rw-r--r--app/i18n/en-us/sub.php1
-rw-r--r--app/i18n/en/conf.php10
-rw-r--r--app/i18n/en/gen.php3
-rw-r--r--app/i18n/en/sub.php1
-rw-r--r--[-rwxr-xr-x]app/i18n/es/admin.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/conf.php10
-rw-r--r--[-rwxr-xr-x]app/i18n/es/feedback.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/gen.php3
-rw-r--r--[-rwxr-xr-x]app/i18n/es/index.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/install.php0
-rw-r--r--[-rwxr-xr-x]app/i18n/es/sub.php1
-rw-r--r--app/i18n/fr/conf.php10
-rw-r--r--app/i18n/fr/gen.php3
-rw-r--r--app/i18n/fr/sub.php1
-rw-r--r--app/i18n/he/conf.php10
-rw-r--r--app/i18n/he/gen.php3
-rw-r--r--app/i18n/he/sub.php1
-rw-r--r--app/i18n/id/conf.php10
-rw-r--r--app/i18n/id/gen.php3
-rw-r--r--app/i18n/id/sub.php1
-rw-r--r--app/i18n/it/conf.php10
-rw-r--r--app/i18n/it/gen.php3
-rw-r--r--app/i18n/it/sub.php1
-rw-r--r--app/i18n/ja/conf.php10
-rw-r--r--app/i18n/ja/gen.php3
-rw-r--r--app/i18n/ja/sub.php1
-rw-r--r--app/i18n/ko/conf.php10
-rw-r--r--app/i18n/ko/gen.php3
-rw-r--r--app/i18n/ko/sub.php1
-rw-r--r--app/i18n/nl/conf.php10
-rw-r--r--app/i18n/nl/gen.php3
-rw-r--r--app/i18n/nl/sub.php1
-rw-r--r--app/i18n/oc/conf.php10
-rw-r--r--app/i18n/oc/gen.php3
-rw-r--r--app/i18n/oc/sub.php1
-rw-r--r--app/i18n/pl/conf.php10
-rw-r--r--app/i18n/pl/gen.php3
-rw-r--r--app/i18n/pl/sub.php1
-rw-r--r--app/i18n/pt-br/conf.php10
-rw-r--r--app/i18n/pt-br/gen.php3
-rw-r--r--app/i18n/pt-br/sub.php1
-rw-r--r--app/i18n/ru/conf.php10
-rw-r--r--app/i18n/ru/gen.php3
-rw-r--r--app/i18n/ru/sub.php1
-rw-r--r--app/i18n/sk/conf.php10
-rw-r--r--app/i18n/sk/gen.php3
-rw-r--r--app/i18n/sk/sub.php1
-rw-r--r--app/i18n/tr/conf.php10
-rw-r--r--app/i18n/tr/gen.php3
-rw-r--r--app/i18n/tr/sub.php1
-rw-r--r--app/i18n/zh-cn/admin.php76
-rw-r--r--app/i18n/zh-cn/conf.php100
-rw-r--r--app/i18n/zh-cn/feedback.php32
-rw-r--r--app/i18n/zh-cn/gen.php21
-rw-r--r--app/i18n/zh-cn/index.php6
-rw-r--r--app/i18n/zh-cn/install.php42
-rw-r--r--app/i18n/zh-cn/sub.php25
-rw-r--r--app/i18n/zh-cn/user.php18
-rw-r--r--app/i18n/zh-tw/conf.php10
-rw-r--r--app/i18n/zh-tw/gen.php3
-rw-r--r--app/i18n/zh-tw/sub.php1
-rw-r--r--app/install.php12
-rw-r--r--app/layout/aside_configure.phtml138
-rw-r--r--app/layout/aside_feed.phtml2
-rw-r--r--app/layout/aside_subscription.phtml57
-rw-r--r--app/layout/header.phtml88
-rw-r--r--app/layout/layout.phtml2
-rw-r--r--app/layout/nav_menu.phtml67
-rw-r--r--app/layout/simple.phtml2
-rw-r--r--app/shares.php15
-rw-r--r--app/views/auth/register.phtml12
-rw-r--r--app/views/configure/display.phtml47
-rw-r--r--app/views/configure/integration.phtml4
-rw-r--r--app/views/configure/system.phtml4
-rw-r--r--app/views/feed/add.phtml2
-rw-r--r--app/views/helpers/category/update.phtml2
-rw-r--r--app/views/helpers/configure/query.phtml4
-rw-r--r--app/views/helpers/export/opml.phtml66
-rw-r--r--app/views/helpers/feed/update.phtml7
-rw-r--r--app/views/helpers/index/normal/entry_header.phtml5
-rwxr-xr-xapp/views/helpers/stream-footer.phtml11
-rw-r--r--app/views/index/logs.phtml10
-rw-r--r--app/views/index/normal.phtml2
-rw-r--r--app/views/index/reader.phtml6
-rwxr-xr-xapp/views/index/rss.phtml22
-rw-r--r--app/views/subscription/add.phtml11
-rw-r--r--app/views/user/manage.phtml16
135 files changed, 1652 insertions, 885 deletions
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 613bacade..791d58d6d 100755..100644
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -25,6 +25,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* The options available on the page are:
* - language (default: en)
* - theme (default: Origin)
+ * - darkMode (default: no)
* - content width (default: thin)
* - display of read action in header
* - display of favorite action in header
@@ -42,7 +43,9 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
public function displayAction() {
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en');
+ FreshRSS_Context::$user_conf->timezone = Minz_Request::param('timezone', '');
FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme);
+ FreshRSS_Context::$user_conf->darkMode = Minz_Request::param('darkMode', 'no');
FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin');
FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false);
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
@@ -106,32 +109,32 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10);
FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal');
FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive');
- FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::param('show_fav_unread', false);
- FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false);
- FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false);
+ FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::paramBoolean('show_fav_unread');
+ FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::paramBoolean('auto_load_more');
+ FreshRSS_Context::$user_conf->display_posts = Minz_Request::paramBoolean('display_posts');
FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', 'active');
FreshRSS_Context::$user_conf->show_tags = Minz_Request::param('show_tags', '0');
FreshRSS_Context::$user_conf->show_tags_max = Minz_Request::param('show_tags_max', '0');
FreshRSS_Context::$user_conf->show_author_date = Minz_Request::param('show_author_date', '0');
FreshRSS_Context::$user_conf->show_feed_name = Minz_Request::param('show_feed_name', 't');
- FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false);
- FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false);
- FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false);
- FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::param('sides_close_article', false);
- FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false);
- FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false);
- FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false);
- FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false);
+ FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::paramBoolean('hide_read_feeds');
+ FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::paramBoolean('onread_jump_next');
+ FreshRSS_Context::$user_conf->lazyload = Minz_Request::paramBoolean('lazyload');
+ FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::paramBoolean('sides_close_article');
+ FreshRSS_Context::$user_conf->sticky_post = Minz_Request::paramBoolean('sticky_post');
+ FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
+ FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
+ FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC');
FreshRSS_Context::$user_conf->mark_when = array(
- 'article' => Minz_Request::param('mark_open_article', false),
- 'gone' => Minz_Request::param('read_upon_gone', false),
+ 'article' => Minz_Request::paramBoolean('mark_open_article'),
+ 'gone' => Minz_Request::paramBoolean('read_upon_gone'),
'max_n_unread' => Minz_Request::paramBoolean('enable_keep_max_n_unread') ? Minz_Request::param('keep_max_n_unread', false) : false,
- 'reception' => Minz_Request::param('mark_upon_reception', false),
+ 'reception' => Minz_Request::paramBoolean('mark_upon_reception'),
'same_title_in_feed' => Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') ?
Minz_Request::param('read_when_same_title_in_feed', false) : false,
- 'scroll' => Minz_Request::param('mark_scroll', false),
- 'site' => Minz_Request::param('mark_open_site', false),
+ 'scroll' => Minz_Request::paramBoolean('mark_scroll'),
+ 'site' => Minz_Request::paramBoolean('mark_open_site'),
);
FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
index 6750de43b..6750de43b 100755..100644
--- a/app/Controllers/entryController.php
+++ b/app/Controllers/entryController.php
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index 319faece8..84f38fe5e 100755..100644
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -81,6 +81,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
break;
case FreshRSS_Feed::KIND_HTML_XPATH:
+ case FreshRSS_Feed::KIND_XML_XPATH:
$feed->_website($url);
break;
}
@@ -172,7 +173,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$proxy_address = Minz_Request::param('curl_params', '');
$proxy_type = Minz_Request::param('proxy_type', '');
$opts = [];
- if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
+ if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
}
@@ -201,8 +202,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$timeout = intval(Minz_Request::param('timeout', 0));
$attributes['timeout'] = $timeout > 0 ? $timeout : null;
- $feed_kind = Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
- if ($feed_kind == FreshRSS_Feed::KIND_HTML_XPATH) {
+ $feed_kind = (int)Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
+ if ($feed_kind === FreshRSS_Feed::KIND_HTML_XPATH || $feed_kind === FreshRSS_Feed::KIND_XML_XPATH) {
$xPathSettings = [];
if (Minz_Request::param('xPathFeedTitle', '') != '') $xPathSettings['feedTitle'] = Minz_Request::param('xPathFeedTitle', '', true);
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
@@ -385,10 +386,15 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($simplePiePush) {
$simplePie = $simplePiePush; //Used by WebSub
} elseif ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
- $simplePie = $feed->loadHtmlXpath(false, $isNewFeed);
- if ($simplePie == null) {
+ $simplePie = $feed->loadHtmlXpath();
+ if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('HTML+XPath Web scraping failed for [' . $feed->url(false) . ']');
}
+ } elseif ($feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
+ $simplePie = $feed->loadHtmlXpath();
+ if ($simplePie === null) {
+ throw new FreshRSS_Feed_Exception('XML+XPath parsing failed for [' . $feed->url(false) . ']');
+ }
} else {
$simplePie = $feed->load(false, $isNewFeed);
}
@@ -949,7 +955,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$this->view->htmlContent = $fullContent;
} else {
$this->view->selectorSuccess = false;
- $this->view->htmlContent = $entry->content();
+ $this->view->htmlContent = $entry->content(false);
}
} catch (Exception $e) {
$this->view->fatalError = _t('feedback.sub.feed.selector_preview.http_error');
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index a1e1106c1..6c4b684e9 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -21,8 +21,6 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
Minz_Error::error(403);
}
- require_once(LIB_PATH . '/lib_opml.php');
-
$this->entryDAO = FreshRSS_Factory::createEntryDao();
$this->feedDAO = FreshRSS_Factory::createFeedDao();
}
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 7fced48af..968518e3f 100755..100644
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -237,8 +237,6 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
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');
diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php
index c2a5cb872..b4e769738 100755..100644
--- a/app/Controllers/javascriptController.php
+++ b/app/Controllers/javascriptController.php
@@ -1,11 +1,11 @@
<?php
class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
- public function firstAction() {
+ public function firstAction(): void {
$this->view->_layout(false);
}
- public function actualizeAction() {
+ public function actualizeAction(): void {
header('Content-Type: application/json; charset=UTF-8');
Minz_Session::_param('actualize_feeds', false);
@@ -16,7 +16,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
}
- public function nbUnreadsPerFeedAction() {
+ public function nbUnreadsPerFeedAction(): void {
header('Content-Type: application/json; charset=UTF-8');
$catDAO = FreshRSS_Factory::createCategoryDao();
$this->view->categories = $catDAO->listCategories(true, false);
@@ -25,7 +25,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
}
//For Web-form login
- public function nonceAction() {
+ public function nonceAction(): void {
header('Content-Type: application/json; charset=UTF-8');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
header('Expires: 0');
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index 1798ee3cf..16b09d702 100644
--- a/app/Controllers/statsController.php
+++ b/app/Controllers/statsController.php
@@ -10,7 +10,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* the common boiler plate for every action. It is triggered by the
* underlying framework.
*/
- public function firstAction() {
+ public function firstAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
@@ -32,27 +32,6 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
}
- private function convertToSeries($data) {
- $series = array();
-
- foreach ($data as $key => $value) {
- $series[] = array($key, $value);
- }
-
- return $series;
- }
-
- private function convertToPieSeries($data) {
- $series = array();
-
- foreach ($data as $value) {
- $value['data'] = array(array(0, (int) $value['data']));
- $series[] = $value;
- }
-
- return $series;
- }
-
/**
* This action handles the statistic main page.
*
@@ -64,7 +43,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* - number of article by category (entryByCategory)
* - list of most prolific feed (topFeed)
*/
- public function indexAction() {
+ public function indexAction(): void {
$statsDAO = FreshRSS_Factory::createStatsDAO();
FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/chart.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/chart.min.js')));
@@ -94,7 +73,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
$last30DaysLabels = [];
for ($i = 0; $i < 30; $i++) {
- $last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days'));
+ $last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days') ?: null);
}
$this->view->last30DaysLabels = $last30DaysLabels;
@@ -106,9 +85,9 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* to use the subscription controller to save it,
* but shows the stats idle page
*/
- public function feedAction() {
- $id = Minz_Request::param('id');
- $ajax = Minz_Request::param('ajax');
+ public function feedAction(): void {
+ $id = '' . Minz_Request::param('id', '');
+ $ajax = '' . Minz_Request::param('ajax', '');
if ($ajax) {
$url_redirect = array('c' => 'subscription', 'a' => 'feed', 'params' => array('id' => $id, 'from' => 'stats', 'ajax' => $ajax));
} else {
@@ -131,7 +110,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* - last month
* - last week
*/
- public function idleAction() {
+ public function idleAction(): void {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
$feed_dao = FreshRSS_Factory::createFeedDao();
$statsDAO = FreshRSS_Factory::createStatsDAO();
@@ -216,7 +195,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
* @todo verify that the metrics used here make some sense. Especially
* for the average.
*/
- public function repartitionAction() {
+ public function repartitionAction(): void {
$statsDAO = FreshRSS_Factory::createStatsDAO();
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$feedDAO = FreshRSS_Factory::createFeedDao();
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
index 4a63d1ee4..f0355a82a 100644
--- a/app/Controllers/subscriptionController.php
+++ b/app/Controllers/subscriptionController.php
@@ -118,8 +118,6 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$httpAuth = $user . ':' . $pass;
}
- $cat = intval(Minz_Request::param('category', 0));
-
$feed->_ttl(intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT)));
$feed->_mute(boolval(Minz_Request::param('mute', false)));
@@ -149,7 +147,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$proxy_address = Minz_Request::param('curl_params', '');
$proxy_type = Minz_Request::param('proxy_type', '');
$opts = [];
- if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
+ if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
}
@@ -205,7 +203,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
$feed->_kind(intval(Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS)));
- if ($feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH) {
+ if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_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);
@@ -230,7 +228,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
'website' => checkUrl(Minz_Request::param('website', '')),
'url' => checkUrl(Minz_Request::param('url', '')),
- 'category' => $cat,
+ 'category' => intval(Minz_Request::param('category', 0)),
'pathEntries' => Minz_Request::param('path_entries', ''),
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
'httpAuth' => $httpAuth,
@@ -258,12 +256,18 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
}
- if ($feedDAO->updateFeed($id, $values) !== false) {
- $feed->_categoryId($cat);
+ if ($values['url'] != '' && $feedDAO->updateFeed($id, $values) !== false) {
+ $feed->_categoryId($values['category']);
+ // update url and website values for faviconPrepare
+ $feed->_url($values['url'], false);
+ $feed->_website($values['website'], false);
$feed->faviconPrepare();
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
} else {
+ if ($values['url'] == '') {
+ Minz_Log::warning('Invalid feed URL!');
+ }
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
}
}
diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php
index 675bd7def..f638ce96c 100644
--- a/app/Controllers/updateController.php
+++ b/app/Controllers/updateController.php
@@ -14,7 +14,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
public static function migrateToGitEdge() {
$errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
- if (!is_writable(FRESHRSS_PATH . '/.git/')) {
+ if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
throw new Exception($errorMessage);
}
@@ -23,7 +23,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
if ($return != 0) {
throw new Exception($errorMessage);
}
- $line = is_array($output) ? implode('', $output) : $output;
+ $line = implode('', $output);
if ($line !== 'master' && $line !== 'dev') {
return true; // not on master or dev, nothing to do
}
@@ -54,14 +54,14 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
$output = [];
exec('git status -sb --porcelain remote', $output, $return);
} else {
- $line = is_array($output) ? implode('; ', $output) : $output;
+ $line = implode('; ', $output);
Minz_Log::warning('git fetch warning: ' . $line);
}
} catch (Exception $e) {
Minz_Log::warning('git fetch error: ' . $e->getMessage());
}
chdir($cwd);
- $line = is_array($output) ? implode('; ', $output) : $output;
+ $line = implode('; ', $output);
return $line == '' ||
strpos($line, '[behind') !== false || strpos($line, '[ahead') !== false || strpos($line, '[gone') !== false;
}
@@ -118,7 +118,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
if ($version == '') {
$version = 'unknown';
}
- if (is_writable(FRESHRSS_PATH)) {
+ if (touch(FRESHRSS_PATH . '/index.html')) {
$this->view->update_to_apply = true;
$this->view->message = array(
'status' => 'good',
@@ -217,7 +217,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
}
public function applyAction() {
- if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH) || Minz_Configuration::get('system')->disable_update) {
+ if (FreshRSS_Context::$system_conf->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
Minz_Request::forward(array('c' => 'update'), true);
}
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php
index 06dbab9fa..ac8f3be82 100644
--- a/app/Controllers/userController.php
+++ b/app/Controllers/userController.php
@@ -242,7 +242,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
if ($ok) {
if (!is_dir($homeDir)) {
- mkdir($homeDir);
+ mkdir($homeDir, 0770, true);
}
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
}
@@ -344,6 +344,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$ok = self::createUser($new_user_name, $email, $passwordPlain, array(
'language' => Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language),
+ 'timezone' => Minz_Request::param('new_user_timezone', ''),
'is_admin' => Minz_Request::paramBoolean('new_user_is_admin'),
'enabled' => true,
));
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index 602c46658..76ced841c 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -18,7 +18,7 @@ class FreshRSS extends Minz_FrontController {
* - Init notifications
* - Enable user extensions (need all the other initializations)
*/
- public function init() {
+ public function init(): void {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
@@ -71,10 +71,10 @@ class FreshRSS extends Minz_FrontController {
Minz_ExtensionManager::callHook('freshrss_init');
}
- private static function initAuth() {
+ private static function initAuth(): void {
FreshRSS_Auth::init();
if (Minz_Request::isPost()) {
- if (!(FreshRSS_Auth::isCsrfOk() ||
+ if (FreshRSS_Context::$system_conf == null || !(FreshRSS_Auth::isCsrfOk() ||
(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' && !FreshRSS_Auth::hasAccess('admin')) ||
(Minz_Request::controllerName() === 'feed' && Minz_Request::actionName() === 'actualize'
@@ -92,21 +92,30 @@ class FreshRSS extends Minz_FrontController {
}
}
- private static function initI18n() {
+ private static function initI18n(): void {
$userLanguage = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->language : null;
$systemLanguage = isset(FreshRSS_Context::$system_conf) ? FreshRSS_Context::$system_conf->language : null;
$language = Minz_Translate::getLanguage($userLanguage, Minz_Request::getPreferredLanguages(), $systemLanguage);
Minz_Session::_param('language', $language);
Minz_Translate::init($language);
+
+ $timezone = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->timezone : '';
+ if ($timezone == '') {
+ $timezone = FreshRSS_Context::defaultTimeZone();
+ }
+ date_default_timezone_set($timezone);
}
- private static function getThemeFileUrl($theme_id, $filename) {
+ private static function getThemeFileUrl(string $theme_id, string $filename): string {
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
return '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
}
- public static function loadStylesAndScripts() {
+ public static function loadStylesAndScripts(): void {
+ if (FreshRSS_Context::$user_conf == null) {
+ return;
+ }
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
if ($theme) {
foreach(array_reverse($theme['files']) as $file) {
@@ -140,22 +149,23 @@ class FreshRSS extends Minz_FrontController {
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
- private static function loadNotifications() {
+ private static function loadNotifications(): void {
$notif = Minz_Request::getNotification();
if ($notif) {
FreshRSS_View::_param('notification', $notif);
}
}
- public static function preLayout() {
+ public static function preLayout(): void {
header("X-Content-Type-Options: nosniff");
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
self::loadStylesAndScripts();
}
- private static function checkEmailValidated() {
- $email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
+ private static function checkEmailValidated(): void {
+ $email_not_verified = FreshRSS_Auth::hasAccess() &&
+ FreshRSS_Context::$user_conf !== null && FreshRSS_Context::$user_conf->email_validation_token !== '';
$action_is_allowed = (
Minz_Request::is('user', 'validateEmail') ||
Minz_Request::is('user', 'sendValidationEmail') ||
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
index b1c7bbd3b..279040a5a 100644
--- a/app/Models/BooleanSearch.php
+++ b/app/Models/BooleanSearch.php
@@ -118,8 +118,9 @@ class FreshRSS_BooleanSearch {
$nextOperator = 'AND';
while ($i < $length) {
$c = $input[$i];
+ $backslashed = $i >= 1 ? $input[$i - 1] === '\\' : false;
- if ($c === '(') {
+ if ($c === '(' && !$backslashed) {
$hasParenthesis = true;
$before = trim($before);
@@ -164,11 +165,12 @@ class FreshRSS_BooleanSearch {
$i++;
while ($i < $length) {
$c = $input[$i];
- if ($c === '(') {
+ $backslashed = $input[$i - 1] === '\\';
+ if ($c === '(' && !$backslashed) {
// One nested level deeper
$parentheses++;
$sub .= $c;
- } elseif ($c === ')') {
+ } elseif ($c === ')' && !$backslashed) {
$parentheses--;
if ($parentheses === 0) {
// Found the matching closing parenthesis
diff --git a/app/Models/Category.php b/app/Models/Category.php
index c4ca12fd3..b23e8da0a 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -103,9 +103,7 @@ class FreshRSS_Category extends Minz_Model {
$this->hasFeedsWithError |= $feed->inError();
}
- usort($this->feeds, function ($a, $b) {
- return strnatcasecmp($a->name(), $b->name());
- });
+ $this->sortFeeds();
}
return $this->feeds;
@@ -144,6 +142,7 @@ class FreshRSS_Category extends Minz_Model {
}
$this->feeds = $values;
+ $this->sortFeeds();
}
/**
@@ -155,6 +154,8 @@ class FreshRSS_Category extends Minz_Model {
$this->feeds = [];
}
$this->feeds[] = $feed;
+
+ $this->sortFeeds();
}
public function _attributes($key, $value) {
@@ -194,7 +195,7 @@ class FreshRSS_Category extends Minz_Model {
} else {
$dryRunCategory = new FreshRSS_Category();
$importService = new FreshRSS_Import_Service();
- $importService->importOpml($opml, $dryRunCategory, true, true);
+ $importService->importOpml($opml, $dryRunCategory, true);
if ($importService->lastStatus()) {
$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -245,4 +246,10 @@ class FreshRSS_Category extends Minz_Model {
return $ok;
}
+
+ private function sortFeeds() {
+ usort($this->feeds, static function ($a, $b) {
+ return strnatcasecmp($a->name(), $b->name());
+ });
+ }
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 20a92d52a..c855f1495 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -265,7 +265,7 @@ SQL;
return $categories;
}
- uasort($categories, function ($a, $b) {
+ uasort($categories, static function ($a, $b) {
$aPosition = $a->attributes('position');
$bPosition = $b->attributes('position');
if ($aPosition === $bPosition) {
@@ -310,9 +310,9 @@ SQL;
}
/** @return array<FreshRSS_Category> */
- public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0) {
+ public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
- . ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
+ . ($limit < 1 ? '' : ' LIMIT ' . $limit);
$stm = $this->pdo->prepare($sql);
if ($stm &&
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
@@ -387,7 +387,7 @@ SQL;
return $res[0]['count'];
}
- public function countFeed($id) {
+ public function countFeed(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
$stm = $this->pdo->prepare($sql);
$stm->bindParam(':id', $id, PDO::PARAM_INT);
@@ -396,7 +396,7 @@ SQL;
return $res[0]['count'];
}
- public function countNotRead($id) {
+ public function countNotRead(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
$stm = $this->pdo->prepare($sql);
$stm->bindParam(':id', $id, PDO::PARAM_INT);
@@ -409,7 +409,7 @@ SQL;
* @param array<FreshRSS_Category> $categories
* @param int $feed_id
*/
- public static function findFeed($categories, $feed_id) {
+ public static function findFeed(array $categories, int $feed_id) {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
@@ -422,9 +422,8 @@ SQL;
/**
* @param array<FreshRSS_Category> $categories
- * @param int $minPriority
*/
- public static function CountUnreads($categories, $minPriority = 0) {
+ public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index c822bcf4d..258c2ad58 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/app/Models/ConfigurationSetter.php
@@ -234,6 +234,13 @@ class FreshRSS_ConfigurationSetter {
$data['sticky_post'] = $this->handleBool($value);
}
+ private function _darkMode(&$data, $value) {
+ if (!in_array($value, [ 'no', 'auto'], true)) {
+ $value = 'no';
+ }
+ $data['darkMode'] = $value;
+ }
+
private function _bottomline_date(&$data, $value) {
$data['bottomline_date'] = $this->handleBool($value);
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index fed2a6767..734458d7f 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -58,12 +58,7 @@ class FreshRSS_Context {
public static function initSystem($reload = false) {
if ($reload || FreshRSS_Context::$system_conf == null) {
//TODO: Keep in session what we need instead of always reloading from disk
- Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
- /**
- * @var FreshRSS_SystemConfiguration $system_conf
- */
- $system_conf = Minz_Configuration::get('system');
- FreshRSS_Context::$system_conf = $system_conf;
+ FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
// Register the configuration setter for the system configuration
$configurationSetter = new FreshRSS_ConfigurationSetter();
FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter);
@@ -88,17 +83,12 @@ class FreshRSS_Context {
(!$userMustExist || FreshRSS_user_Controller::userExists($username))) {
try {
//TODO: Keep in session what we need instead of always reloading from disk
- Minz_Configuration::register('user',
+ FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init(
USERS_PATH . '/' . $username . '/config.php',
FRESHRSS_PATH . '/config-user.default.php',
FreshRSS_Context::$system_conf->configurationSetter());
Minz_Session::_param('currentUser', $username);
- /**
- * @var FreshRSS_UserConfiguration $user_conf
- */
- $user_conf = Minz_Configuration::get('user');
- FreshRSS_Context::$user_conf = $user_conf;
} catch (Exception $ex) {
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME);
}
@@ -163,7 +153,7 @@ class FreshRSS_Context {
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
- self::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
+ self::$total_unread = FreshRSS_CategoryDAO::countUnread(
self::$categories, 1
);
@@ -510,4 +500,8 @@ class FreshRSS_Context {
return false;
}
+ public static function defaultTimeZone(): string {
+ $timezone = ini_get('date.timezone');
+ return $timezone != '' ? $timezone : 'UTC';
+ }
}
diff --git a/app/Models/Days.php b/app/Models/Days.php
index 2d770c30b..d3f1ba075 100644
--- a/app/Models/Days.php
+++ b/app/Models/Days.php
@@ -1,7 +1,9 @@
<?php
+declare(strict_types=1);
+
class FreshRSS_Days {
- const TODAY = 0;
- const YESTERDAY = 1;
- const BEFORE_YESTERDAY = 2;
+ public const TODAY = 0;
+ public const YESTERDAY = 1;
+ public const BEFORE_YESTERDAY = 2;
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 47fcf3b4a..81ece1ce4 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -17,10 +17,14 @@ class FreshRSS_Entry extends Minz_Model {
*/
private $guid;
+ /** @var string */
private $title;
private $authors;
+ /** @var string */
private $content;
+ /** @var string */
private $link;
+ /** @var int */
private $date;
private $date_added = 0; //In microseconds
/**
@@ -67,14 +71,16 @@ class FreshRSS_Entry extends Minz_Model {
$dao['content'] = '';
}
if (!empty($dao['thumbnail'])) {
- $dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>';
+ $dao['attributes']['thumbnail'] = [
+ 'url' => $dao['thumbnail'],
+ ];
}
$entry = new FreshRSS_Entry(
$dao['id_feed'] ?? 0,
$dao['guid'] ?? '',
$dao['title'] ?? '',
$dao['author'] ?? '',
- $dao['content'] ?? '',
+ $dao['content'],
$dao['link'] ?? '',
$dao['date'] ?? 0,
$dao['is_read'] ?? false,
@@ -116,15 +122,117 @@ class FreshRSS_Entry extends Minz_Model {
return $this->authors;
}
}
- public function content(): string {
- return $this->content;
+
+ /**
+ * Basic test without ambition to catch all cases such as unquoted addresses, variants of entities, HTML comments, etc.
+ */
+ private static function containsLink(string $html, string $link): bool {
+ return preg_match('/(?P<delim>[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1;
+ }
+
+ private static function enclosureIsImage(array $enclosure): bool {
+ $elink = $enclosure['url'] ?? '';
+ $length = $enclosure['length'] ?? 0;
+ $medium = $enclosure['medium'] ?? '';
+ $mime = $enclosure['type'] ?? '';
+
+ return $elink != '' && $medium === 'image' || strpos($mime, 'image') === 0 ||
+ ($mime == '' && $length == 0 && preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink));
}
- /** @return array<array<string,string>> */
- public function enclosures(bool $searchBodyImages = false): array {
- $results = [];
+ /**
+ * @param bool $withEnclosures Set to true to include the enclosures in the returned HTML, false otherwise.
+ * @param bool $allowDuplicateEnclosures Set to false to remove obvious enclosure duplicates (based on simple string comparison), true otherwise.
+ * @return string HTML content
+ */
+ public function content(bool $withEnclosures = true, bool $allowDuplicateEnclosures = false): string {
+ if (!$withEnclosures) {
+ return $this->content;
+ }
+
+ $content = $this->content;
+
+ $thumbnail = $this->attributes('thumbnail');
+ if (!empty($thumbnail['url'])) {
+ $elink = $thumbnail['url'];
+ if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) {
+ $content .= <<<HTML
+<figure class="enclosure">
+ <p class="enclosure-content">
+ <img class="enclosure-thumbnail" src="{$elink}" alt="" />
+ </p>
+</figure>
+HTML;
+ }
+ }
+
+ $attributeEnclosures = $this->attributes('enclosures');
+ if (empty($attributeEnclosures)) {
+ return $content;
+ }
+
+ foreach ($attributeEnclosures as $enclosure) {
+ $elink = $enclosure['url'] ?? '';
+ if ($elink == '') {
+ continue;
+ }
+ if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) {
+ continue;
+ }
+ $credit = $enclosure['credit'] ?? '';
+ $description = $enclosure['description'] ?? '';
+ $length = $enclosure['length'] ?? 0;
+ $medium = $enclosure['medium'] ?? '';
+ $mime = $enclosure['type'] ?? '';
+ $thumbnails = $enclosure['thumbnails'] ?? [];
+ $etitle = $enclosure['title'] ?? '';
+
+ $content .= '<figure class="enclosure">';
+
+ foreach ($thumbnails as $thumbnail) {
+ $content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
+ }
+
+ if (self::enclosureIsImage($enclosure)) {
+ $content .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" title="' . $etitle . '" /></p>';
+ } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
+ $content .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
+ . ($length == null ? '' : '" data-length="' . intval($length))
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . '" controls="controls" title="' . $etitle . '"></audio> <a download="" href="' . $elink . '">💾</a></p>';
+ } elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
+ $content .= '<p class="enclosure-content"><video preload="none" src="' . $elink
+ . ($length == null ? '' : '" data-length="' . intval($length))
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . '" controls="controls" title="' . $etitle . '"></video> <a download="" href="' . $elink . '">💾</a></p>';
+ } else { //e.g. application, text, unknown
+ $content .= '<p class="enclosure-content"><a download="" href="' . $elink
+ . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
+ . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
+ . '" title="' . $etitle . '">💾</a></p>';
+ }
+
+ if ($credit != '') {
+ $content .= '<p class="enclosure-credits">© ' . $credit . '</p>';
+ }
+ if ($description != '') {
+ $content .= '<figcaption class="enclosure-description">' . $description . '</figcaption>';
+ }
+ $content .= "</figure>\n";
+ }
+
+ return $content;
+ }
+
+ /** @return iterable<array<string,string>> */
+ public function enclosures(bool $searchBodyImages = false) {
+ $attributeEnclosures = $this->attributes('enclosures');
+ if (is_array($attributeEnclosures)) {
+ // FreshRSS 1.20.1+: The enclosures are saved as attributes
+ yield from $attributeEnclosures;
+ }
try {
- $searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false;
+ $searchEnclosures = !is_array($attributeEnclosures) && (strpos($this->content, '<p class="enclosure-content') !== false);
$searchBodyImages &= (stripos($this->content, '<img') !== false);
$xpath = null;
if ($searchEnclosures || $searchBodyImages) {
@@ -133,6 +241,7 @@ class FreshRSS_Entry extends Minz_Model {
$xpath = new DOMXpath($dom);
}
if ($searchEnclosures) {
+ // Legacy code for database entries < FreshRSS 1.20.1
$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
foreach ($enclosures as $enclosure) {
$result = [
@@ -148,7 +257,7 @@ class FreshRSS_Entry extends Minz_Model {
case 'audio': $result['medium'] = 'audio'; break;
}
}
- $results[] = $result;
+ yield Minz_Helper::htmlspecialchars_utf8($result);
}
}
if ($searchBodyImages) {
@@ -159,26 +268,31 @@ class FreshRSS_Entry extends Minz_Model {
$src = $img->getAttribute('data-src');
}
if ($src != null) {
- $results[] = [
+ $result = [
'url' => $src,
- 'alt' => $img->getAttribute('alt'),
];
+ yield Minz_Helper::htmlspecialchars_utf8($result);
}
}
}
- return $results;
} catch (Exception $ex) {
- return $results;
+ Minz_Log::debug(__METHOD__ . ' ' . $ex->getMessage());
}
}
/**
* @return array<string,string>|null
*/
- public function thumbnail() {
- foreach ($this->enclosures(true) as $enclosure) {
- if (!empty($enclosure['url']) && empty($enclosure['type'])) {
- return $enclosure;
+ public function thumbnail(bool $searchEnclosures = true) {
+ $thumbnail = $this->attributes('thumbnail');
+ if (!empty($thumbnail['url'])) {
+ return $thumbnail;
+ }
+ if ($searchEnclosures) {
+ foreach ($this->enclosures(true) as $enclosure) {
+ if (self::enclosureIsImage($enclosure)) {
+ return $enclosure;
+ }
}
}
return null;
@@ -188,6 +302,7 @@ class FreshRSS_Entry extends Minz_Model {
public function link(): string {
return $this->link;
}
+ /** @return string|int */
public function date(bool $raw = false) {
if ($raw) {
return $this->date;
@@ -587,7 +702,7 @@ class FreshRSS_Entry extends Minz_Model {
if ($entry) {
// l’article existe déjà en BDD, en se contente de recharger ce contenu
- $this->content = $entry->content();
+ $this->content = $entry->content(false);
} else {
try {
// The article is not yet in the database, so let’s fetch it
@@ -629,7 +744,7 @@ class FreshRSS_Entry extends Minz_Model {
'guid' => $this->guid(),
'title' => $this->title(),
'author' => $this->authors(true),
- 'content' => $this->content(),
+ 'content' => $this->content(false),
'link' => $this->link(),
'date' => $this->date(true),
'hash' => $this->hash(),
@@ -677,7 +792,6 @@ class FreshRSS_Entry extends Minz_Model {
'published' => $this->date(true),
// 'updated' => $this->date(true),
'title' => $this->title(),
- 'summary' => ['content' => $this->content()],
'canonical' => [
['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)],
],
@@ -697,13 +811,16 @@ class FreshRSS_Entry extends Minz_Model {
if ($mode === 'compat') {
$item['title'] = escapeToUnicodeAlternative($this->title(), false);
unset($item['alternate'][0]['type']);
- if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) {
- $item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8');
- }
- } elseif ($mode === 'freshrss') {
+ $item['summary'] = [
+ 'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'),
+ ];
+ } else {
+ $item['content'] = [
+ 'content' => $this->content(false),
+ ];
+ }
+ if ($mode === 'freshrss') {
$item['guid'] = $this->guid();
- unset($item['summary']);
- $item['content'] = ['content' => $this->content()];
}
if ($category != null && $mode !== 'freshrss') {
$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES);
@@ -718,10 +835,11 @@ class FreshRSS_Entry extends Minz_Model {
}
}
foreach ($this->enclosures() as $enclosure) {
- if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
+ if (!empty($enclosure['url'])) {
$media = [
'href' => $enclosure['url'],
- 'type' => $enclosure['type'],
+ 'type' => $enclosure['type'] ?? $enclosure['medium'] ??
+ (self::enclosureIsImage($enclosure) ? 'image' : ''),
];
if (!empty($enclosure['length'])) {
$media['length'] = intval($enclosure['length']);
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index b63515223..3b7c1ac3f 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return true;
}
+ protected static function sqlConcat($s1, $s2) {
+ return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL
+ }
+
public static function sqlHexDecode(string $x): string {
return 'unhex(' . $x . ')';
}
@@ -943,8 +947,8 @@ SQL;
}
if ($filter->getTags()) {
foreach ($filter->getTags() as $tag) {
- $sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
- $values[] = "%{$tag}%";
+ $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
+ $values[] = "%{$tag} #%";
}
}
if ($filter->getInurl()) {
@@ -968,8 +972,8 @@ SQL;
}
if ($filter->getNotTags()) {
foreach ($filter->getNotTags() as $tag) {
- $sub_search .= 'AND ' . $alias . 'tags NOT LIKE ? ';
- $values[] = "%{$tag}%";
+ $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
+ $values[] = "%{$tag} #%";
}
}
if ($filter->getNotInurl()) {
@@ -1161,10 +1165,12 @@ SQL;
}
}
- public function listByIds($ids, $order = 'DESC') {
+ /** @param array<string> $ids */
+ public function listByIds(array $ids, string $order = 'DESC') {
if (count($ids) < 1) {
- yield false;
- } elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
+ return;
+ }
+ if (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
// Split a query with too many variables parameters
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($idsChunks as $idsChunk) {
@@ -1191,15 +1197,16 @@ SQL;
/**
* For API
+ * @return array<string>
*/
public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
- $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {
+ $order = 'DESC', $limit = 1, $firstId = '', $filters = null): array {
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
$stm->execute($values);
- return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+ return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: [];
}
public function listHashForFeedGuids($id_feed, $guids) {
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 8039581e6..35f3ef676 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
return false;
}
+ protected static function sqlConcat($s1, $s2) {
+ return $s1 . '||' . $s2;
+ }
+
public static function sqlHexDecode(string $x): string {
return $x;
}
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index f24ec1884..7c46199a5 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -18,6 +18,11 @@ class FreshRSS_Feed extends Minz_Model {
*/
const KIND_HTML_XPATH = 10;
/**
+ * Normal XML with XPath scraping
+ * @var int
+ */
+ const KIND_XML_XPATH = 15;
+ /**
* Normal JSON with XPath scraping
* @var int
*/
@@ -259,13 +264,14 @@ class FreshRSS_Feed extends Minz_Model {
}
public function _url(string $value, bool $validate = true) {
$this->hash = '';
+ $url = $value;
if ($validate) {
- $value = checkUrl($value);
+ $url = checkUrl($url);
}
- if ($value == '') {
+ if ($url == '') {
throw new FreshRSS_BadUrl_Exception($value);
}
- $this->url = $value;
+ $this->url = $url;
}
public function _kind(int $value) {
$this->kind = $value;
@@ -502,61 +508,46 @@ class FreshRSS_Feed extends Minz_Model {
$content = html_only_entity_decode($item->get_content());
- if ($item->get_enclosures() != null) {
- $elinks = array();
+ $attributeThumbnail = $item->get_thumbnail() ?? [];
+ if (empty($attributeThumbnail['url'])) {
+ $attributeThumbnail['url'] = '';
+ }
+
+ $attributeEnclosures = [];
+ if (!empty($item->get_enclosures())) {
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
- if ($elink != '' && empty($elinks[$elink])) {
- $content .= '<div class="enclosure">';
-
- if ($enclosure->get_title() != '') {
- $content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>';
- }
-
- $enclosureContent = '';
- $elinks[$elink] = true;
+ if ($elink != '') {
+ $etitle = $enclosure->get_title() ?? '';
+ $credit = $enclosure->get_credit() ?? null;
+ $description = $enclosure->get_description() ?? '';
$mime = strtolower($enclosure->get_type() ?? '');
$medium = strtolower($enclosure->get_medium() ?? '');
$height = $enclosure->get_height();
$width = $enclosure->get_width();
$length = $enclosure->get_length();
- if ($medium === 'image' || strpos($mime, 'image') === 0 ||
- ($mime == '' && $length == null && ($width != 0 || $height != 0 || preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)))) {
- $enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>';
- } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
- $enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
- . ($length == null ? '' : '" data-length="' . intval($length))
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
- } elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
- $enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
- . ($length == null ? '' : '" data-length="' . intval($length))
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
- } else { //e.g. application, text, unknown
- $enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink
- . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
- . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
- . '">💾</a></p>';
- }
- $thumbnailContent = '';
- if ($enclosure->get_thumbnails() != null) {
+ $attributeEnclosure = [
+ 'url' => $elink,
+ ];
+ if ($etitle != '') $attributeEnclosure['title'] = $etitle;
+ if ($credit != null) $attributeEnclosure['credit'] = $credit->get_name();
+ if ($description != '') $attributeEnclosure['description'] = $description;
+ if ($mime != '') $attributeEnclosure['type'] = $mime;
+ if ($medium != '') $attributeEnclosure['medium'] = $medium;
+ if ($length != '') $attributeEnclosure['length'] = intval($length);
+ if ($height != '') $attributeEnclosure['height'] = intval($height);
+ if ($width != '') $attributeEnclosure['width'] = intval($width);
+
+ if (!empty($enclosure->get_thumbnails())) {
foreach ($enclosure->get_thumbnails() as $thumbnail) {
- if (empty($elinks[$thumbnail])) {
- $elinks[$thumbnail] = true;
- $thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>';
+ if ($thumbnail !== $attributeThumbnail['url']) {
+ $attributeEnclosure['thumbnails'][] = $thumbnail;
}
}
}
- $content .= $thumbnailContent;
- $content .= $enclosureContent;
-
- if ($enclosure->get_description() != '') {
- $content .= '<p class="enclosure-description">' . $enclosure->get_description() . '</p>';
- }
- $content .= "</div>\n";
+ $attributeEnclosures[] = $attributeEnclosure;
}
}
}
@@ -586,6 +577,10 @@ class FreshRSS_Feed extends Minz_Model {
);
$entry->_tags($tags);
$entry->_feed($this);
+ if (!empty($attributeThumbnail['url'])) {
+ $entry->_attributes('thumbnail', $attributeThumbnail);
+ }
+ $entry->_attributes('enclosures', $attributeEnclosures);
$entry->hash(); //Must be computed before loading full content
$entry->loadCompleteContent(); // Optionally load full content for truncated feeds
@@ -596,7 +591,7 @@ class FreshRSS_Feed extends Minz_Model {
/**
* @return SimplePie|null
*/
- public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false) {
+ public function loadHtmlXpath() {
if ($this->url == '') {
return null;
}
@@ -624,8 +619,9 @@ class FreshRSS_Feed extends Minz_Model {
return null;
}
- $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), FreshRSS_Feed::KIND_HTML_XPATH);
- $html = httpGet($feedSourceUrl, $cachePath, 'html', $this->attributes());
+ $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind());
+ $html = httpGet($feedSourceUrl, $cachePath,
+ $this->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'xml' : 'html', $this->attributes());
if (strlen($html) <= 0) {
return null;
}
@@ -640,7 +636,18 @@ class FreshRSS_Feed extends Minz_Model {
$doc = new DOMDocument();
$doc->recover = true;
$doc->strictErrorChecking = false;
- $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+
+ switch ($this->kind()) {
+ case FreshRSS_Feed::KIND_HTML_XPATH:
+ $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+ break;
+ case FreshRSS_Feed::KIND_XML_XPATH:
+ $doc->loadXML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
+ break;
+ default:
+ return null;
+ }
+
$xpath = new DOMXPath($doc);
$view->rss_title = $xPathFeedTitle == '' ? $this->name() :
htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8');
@@ -653,7 +660,23 @@ class FreshRSS_Feed extends Minz_Model {
foreach ($nodes as $node) {
$item = [];
$item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node);
- $item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node);
+
+ $item['content'] = '';
+ if ($xPathItemContent != '') {
+ $result = @$xpath->evaluate($xPathItemContent, $node);
+ if ($result instanceof DOMNodeList) {
+ // List of nodes, save as HTML
+ $content = '';
+ foreach ($result as $child) {
+ $content .= $doc->saveHTML($child) . "\n";
+ }
+ $item['content'] = $content;
+ } else {
+ // Typed expression, save as-is
+ $item['content'] = strval($result);
+ }
+ }
+
$item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node);
$item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node);
$item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node);
@@ -679,8 +702,15 @@ class FreshRSS_Feed extends Minz_Model {
$item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']);
}
- if ($item['title'] . $item['content'] . $item['link'] != '') {
- $item = Minz_Helper::htmlspecialchars_utf8($item);
+ if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
+ // HTML-encoding/escaping of the relevant fields (all except 'content')
+ foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) {
+ if (!empty($item[$key])) {
+ $item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]);
+ }
+ }
+ // CDATA protection
+ $item['content'] = str_replace(']]>', ']]&gt;', $item['content']);
$view->entries[] = FreshRSS_Entry::fromArray($item);
}
}
@@ -763,8 +793,10 @@ class FreshRSS_Feed extends Minz_Model {
public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
$simplePie = customSimplePie($attributes);
$filename = $simplePie->get_cache_filename($url);
- if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) {
+ if ($kind === FreshRSS_Feed::KIND_HTML_XPATH) {
return CACHE_PATH . '/' . $filename . '.html';
+ } elseif ($kind === FreshRSS_Feed::KIND_XML_XPATH) {
+ return CACHE_PATH . '/' . $filename . '.xml';
} else {
return CACHE_PATH . '/' . $filename . '.spc';
}
@@ -966,14 +998,14 @@ class FreshRSS_Feed extends Minz_Model {
$key = $hubJson['key']; //To renew our lease
}
} else {
- @mkdir($path, 0777, true);
+ @mkdir($path, 0770, true);
$key = sha1($path . FreshRSS_Context::$system_conf->salt);
$hubJson = array(
'hub' => $this->hubUrl,
'key' => $key,
);
file_put_contents($hubFilename, json_encode($hubJson));
- @mkdir(PSHB_PATH . '/keys/');
+ @mkdir(PSHB_PATH . '/keys/', 0770, true);
file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', $this->selfUrl);
$text = 'WebSub prepared for ' . $this->url;
Minz_Log::debug($text);
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 5993f50dc..1aae5fee5 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -49,11 +49,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
$values = array(
- substr($valuesTmp['url'], 0, 511),
+ $valuesTmp['url'],
$valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
$valuesTmp['category'],
mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
- substr($valuesTmp['website'], 0, 255),
+ $valuesTmp['website'],
sanitizeHTML($valuesTmp['description'], '', 1023),
$valuesTmp['lastUpdate'],
isset($valuesTmp['priority']) ? intval($valuesTmp['priority']) : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
@@ -434,7 +434,7 @@ SQL;
. '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)'
. ($id != 0 ? ' WHERE id=:id' : '');
$stm = $this->pdo->prepare($sql);
- if ($id != 0) {
+ if ($stm && $id != 0) {
$stm->bindParam(':id', $id, PDO::PARAM_INT);
}
diff --git a/app/Models/Searchable.php b/app/Models/Searchable.php
index d5bcea49d..a15a44ed7 100644
--- a/app/Models/Searchable.php
+++ b/app/Models/Searchable.php
@@ -2,5 +2,9 @@
interface FreshRSS_Searchable {
+ /**
+ * @param int|string $id
+ * @return Minz_Model
+ */
public function searchById($id);
}
diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php
index ec5960c0e..9fc79969d 100644
--- a/app/Models/SystemConfiguration.php
+++ b/app/Models/SystemConfiguration.php
@@ -25,6 +25,10 @@
* @property string $unsafe_autologin_enabled
* @property-read array<string> $trusted_sources
*/
-class FreshRSS_SystemConfiguration extends Minz_Configuration {
+final class FreshRSS_SystemConfiguration extends Minz_Configuration {
+ public static function init($config_filename, $default_filename = null): FreshRSS_SystemConfiguration {
+ parent::register('system', $config_filename, $default_filename);
+ return parent::get('system');
+ }
}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 589648e26..c1290d192 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -5,40 +5,61 @@ class FreshRSS_Tag extends Minz_Model {
* @var int
*/
private $id = 0;
+ /**
+ * @var string
+ */
private $name;
+ /**
+ * @var array<string,mixed>
+ */
private $attributes = [];
+ /**
+ * @var int
+ */
private $nbEntries = -1;
+ /**
+ * @var int
+ */
private $nbUnread = -1;
- public function __construct($name = '') {
+ public function __construct(string $name = '') {
$this->_name($name);
}
- public function id() {
+ public function id(): int {
return $this->id;
}
- public function _id($value) {
+ /**
+ * @param int|string $value
+ */
+ public function _id($value): void {
$this->id = (int)$value;
}
- public function name() {
+ public function name(): string {
return $this->name;
}
- public function _name($value) {
+ public function _name(string $value): void {
$this->name = trim($value);
}
- public function attributes($key = '') {
+ /**
+ * @return mixed|string|array<string,mixed>|null
+ */
+ public function attributes(string $key = '') {
if ($key == '') {
return $this->attributes;
} else {
- return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+ return $this->attributes[$key] ?? null;
}
}
- public function _attributes($key, $value) {
+ /**
+ * @param mixed|string|array<string,mixed>|null $value
+ */
+ public function _attributes(string $key, $value = null): void {
if ($key == '') {
if (is_string($value)) {
$value = json_decode($value, true);
@@ -53,27 +74,33 @@ class FreshRSS_Tag extends Minz_Model {
}
}
- public function nbEntries() {
+ public function nbEntries(): int {
if ($this->nbEntries < 0) {
$tagDAO = FreshRSS_Factory::createTagDao();
- $this->nbEntries = $tagDAO->countEntries($this->id());
+ $this->nbEntries = $tagDAO->countEntries($this->id()) ?: 0;
}
return $this->nbEntries;
}
- public function _nbEntries($value) {
+ /**
+ * @param string|int $value
+ */
+ public function _nbEntries($value): void {
$this->nbEntries = (int)$value;
}
- public function nbUnread() {
+ public function nbUnread(): int {
if ($this->nbUnread < 0) {
$tagDAO = FreshRSS_Factory::createTagDao();
- $this->nbUnread = $tagDAO->countNotRead($this->id());
+ $this->nbUnread = $tagDAO->countNotRead($this->id()) ?: 0;
}
return $this->nbUnread;
}
- public function _nbUnread($value) {
+ /**
+ * @param string|int$value
+ */
+ public function _nbUnread($value): void {
$this->nbUnread = (int)$value;
}
}
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
index f232b2f9f..35123606b 100644
--- a/app/Models/TagDAO.php
+++ b/app/Models/TagDAO.php
@@ -267,12 +267,13 @@ SQL;
return $newestItemUsec;
}
+ /** @return int|false */
public function count() {
$sql = 'SELECT COUNT(*) AS count FROM `_tag`';
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ return (int)$res[0]['count'];
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -283,16 +284,27 @@ SQL;
}
}
- public function countEntries($id) {
+ /**
+ * @return int|false
+ */
+ public function countEntries(int $id) {
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=?';
- $stm = $this->pdo->prepare($sql);
$values = array($id);
- $stm->execute($values);
- $res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ if (($stm = $this->pdo->prepare($sql)) !== false &&
+ $stm->execute($values) &&
+ ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
+ return (int)$res[0]['count'];
+ } else {
+ $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
+ Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+ return false;
+ }
}
- public function countNotRead($id = null) {
+ /**
+ * @return int|false
+ */
+ public function countNotRead(?int $id = null) {
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` et '
. 'INNER JOIN `_entry` e ON et.id_entry=e.id '
. 'WHERE e.is_read=0';
@@ -303,11 +315,15 @@ SQL;
$values = [$id];
}
- $stm = $this->pdo->prepare($sql);
-
- $stm->execute($values);
- $res = $stm->fetchAll(PDO::FETCH_ASSOC);
- return $res[0]['count'];
+ if (($stm = $this->pdo->prepare($sql)) !== false &&
+ $stm->execute($values) &&
+ ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
+ return (int)$res[0]['count'];
+ } else {
+ $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
+ Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+ return false;
+ }
}
public function tagEntry($id_tag, $id_entry, $checked = true) {
diff --git a/app/Models/Themes.php b/app/Models/Themes.php
index d652ada5b..86125c5f5 100644
--- a/app/Models/Themes.php
+++ b/app/Models/Themes.php
@@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model {
static $alts = array(
'add' => '➕', //✚
'all' => '☰',
- 'bookmark' => '✨', //★
'bookmark-add' => '➕', //✚
'bookmark-tag' => '📑',
'category' => '🗂️', //☷
diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php
index 05c3c08ac..53b12cc2e 100644
--- a/app/Models/UserConfiguration.php
+++ b/app/Models/UserConfiguration.php
@@ -28,6 +28,7 @@
* @property-read string $is_admin
* @property int|null $keep_history_default
* @property string $language
+ * @property string $timezone
* @property bool $lazyload
* @property string $mail_login
* @property bool $mark_updated_article_unread
@@ -52,6 +53,7 @@
* @property bool $sides_close_article
* @property bool $sticky_post
* @property string $theme
+ * @property string $darkMode
* @property string $token
* @property bool $topline_date
* @property bool $topline_display_authors
@@ -66,6 +68,10 @@
* @property string $view_mode
* @property array<string,mixed> $volatile
*/
-class FreshRSS_UserConfiguration extends Minz_Configuration {
+final class FreshRSS_UserConfiguration extends Minz_Configuration {
+ public static function init($config_filename, $default_filename = null, $configuration_setter = null): FreshRSS_UserConfiguration {
+ parent::register('user', $config_filename, $default_filename, $configuration_setter);
+ return parent::get('user');
+ }
}
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 964324bf7..278074362 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -8,26 +8,35 @@
*/
class FreshRSS_UserQuery {
+ /** @var bool */
private $deprecated = false;
- private $get;
- private $get_name;
- private $get_type;
- private $name;
- private $order;
+ /** @var string */
+ private $get = '';
+ /** @var string */
+ private $get_name = '';
+ /** @var string */
+ private $get_type = '';
+ /** @var string */
+ private $name = '';
+ /** @var string */
+ private $order = '';
/** @var FreshRSS_BooleanSearch */
private $search;
- private $state;
- private $url;
+ /** @var int */
+ private $state = 0;
+ /** @var string */
+ private $url = '';
+ /** @var FreshRSS_FeedDAO|null */
private $feed_dao;
+ /** @var FreshRSS_CategoryDAO|null */
private $category_dao;
+ /** @var FreshRSS_TagDAO|null */
private $tag_dao;
/**
* @param array<string,string> $query
- * @param FreshRSS_Searchable $feed_dao
- * @param FreshRSS_Searchable $category_dao
*/
- public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) {
+ public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
$this->tag_dao = $tag_dao;
@@ -53,17 +62,17 @@ class FreshRSS_UserQuery {
}
// linked too deeply with the search object, need to use dependency injection
$this->search = new FreshRSS_BooleanSearch($query['search']);
- if (isset($query['state'])) {
- $this->state = $query['state'];
+ if (!empty($query['state'])) {
+ $this->state = intval($query['state']);
}
}
/**
* Convert the current object to an array.
*
- * @return array<string,string>
+ * @return array<string,string|int>
*/
- public function toArray() {
+ public function toArray(): array {
return array_filter(array(
'get' => $this->get,
'name' => $this->name,
@@ -75,29 +84,27 @@ class FreshRSS_UserQuery {
}
/**
- * Parse the get parameter in the query string to extract its name and
- * type
- *
- * @param string $get
+ * Parse the get parameter in the query string to extract its name and type
*/
- private function parseGet($get) {
+ private function parseGet(string $get): void {
$this->get = $get;
if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
+ $id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
$this->parseAll();
break;
case 'c':
- $this->parseCategory($matches['id']);
+ $this->parseCategory($id);
break;
case 'f':
- $this->parseFeed($matches['id']);
+ $this->parseFeed($id);
break;
case 's':
$this->parseFavorite();
break;
case 't':
- $this->parseTag($matches['id']);
+ $this->parseTag($id);
break;
}
}
@@ -106,7 +113,7 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is an "all" query
*/
- private function parseAll() {
+ private function parseAll(): void {
$this->get_name = 'all';
$this->get_type = 'all';
}
@@ -114,11 +121,10 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "category" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseCategory($id) {
- if (is_null($this->category_dao)) {
+ private function parseCategory(int $id): void {
+ if ($this->category_dao === null) {
throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
}
$category = $this->category_dao->searchById($id);
@@ -133,11 +139,10 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "feed" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseFeed($id) {
- if (is_null($this->feed_dao)) {
+ private function parseFeed(int $id): void {
+ if ($this->feed_dao === null) {
throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
}
$feed = $this->feed_dao->searchById($id);
@@ -152,10 +157,9 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "tag" query
*
- * @param integer $id
* @throws FreshRSS_DAO_Exception
*/
- private function parseTag($id) {
+ private function parseTag(int $id): void {
if ($this->tag_dao == null) {
throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
}
@@ -171,7 +175,7 @@ class FreshRSS_UserQuery {
/**
* Parse the query string when it is a "favorite" query
*/
- private function parseFavorite() {
+ private function parseFavorite(): void {
$this->get_name = 'favorite';
$this->get_type = 'favorite';
}
@@ -180,20 +184,16 @@ class FreshRSS_UserQuery {
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
* not existing.
- *
- * @return boolean
*/
- public function isDeprecated() {
+ public function isDeprecated(): bool {
return $this->deprecated;
}
/**
* Check if the user query has parameters.
* If the type is 'all', it is considered equal to no parameters
- *
- * @return boolean
*/
- public function hasParameters() {
+ public function hasParameters(): bool {
if ($this->get_type === 'all') {
return false;
}
@@ -214,42 +214,40 @@ class FreshRSS_UserQuery {
/**
* Check if there is a search in the search object
- *
- * @return boolean
*/
- public function hasSearch() {
- return $this->search->getRawInput() != "";
+ public function hasSearch(): bool {
+ return $this->search->getRawInput() !== '';
}
- public function getGet() {
+ public function getGet(): string {
return $this->get;
}
- public function getGetName() {
+ public function getGetName(): string {
return $this->get_name;
}
- public function getGetType() {
+ public function getGetType(): string {
return $this->get_type;
}
- public function getName() {
+ public function getName(): string {
return $this->name;
}
- public function getOrder() {
+ public function getOrder(): string {
return $this->order;
}
- public function getSearch() {
+ public function getSearch(): FreshRSS_BooleanSearch {
return $this->search;
}
- public function getState() {
+ public function getState(): int {
return $this->state;
}
- public function getUrl() {
+ public function getUrl(): string {
return $this->url;
}
diff --git a/app/Models/View.php b/app/Models/View.php
index ab1780405..309773c93 100644
--- a/app/Models/View.php
+++ b/app/Models/View.php
@@ -39,6 +39,7 @@ class FreshRSS_View extends Minz_View {
public $details;
public $disable_aside;
public $show_email_field;
+ /** @var string */
public $username;
public $users;
diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php
index d85bd3dc3..b8fff170a 100644
--- a/app/SQL/install.sql.mysql.php
+++ b/app/SQL/install.sql.mysql.php
@@ -18,11 +18,11 @@ 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,
+ `url` VARCHAR(32768) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`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,
+ `website` TEXT CHARACTER SET latin1 COLLATE latin1_bin,
`description` TEXT,
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
`priority` TINYINT(2) NOT NULL DEFAULT 10,
@@ -35,7 +35,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
`cache_nbUnreads` INT DEFAULT 0, -- v0.7
PRIMARY KEY (`id`),
FOREIGN KEY (`category`) REFERENCES `_category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
- UNIQUE KEY (`url`), -- v0.7
INDEX (`name`), -- v0.7
INDEX (`priority`) -- v0.7
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php
index c4da2afad..00a30a8c7 100644
--- a/app/SQL/install.sql.pgsql.php
+++ b/app/SQL/install.sql.pgsql.php
@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS `_category` (
CREATE TABLE IF NOT EXISTS `_feed` (
"id" SERIAL PRIMARY KEY,
- "url" VARCHAR(511) UNIQUE NOT NULL,
+ "url" VARCHAR(32768) NOT NULL,
"kind" SMALLINT DEFAULT 0, -- 1.20.0
"category" INT DEFAULT 0, -- 1.20.0
"name" VARCHAR(255) NOT NULL,
- "website" VARCHAR(255),
+ "website" VARCHAR(32768),
"description" TEXT,
"lastUpdate" INT DEFAULT 0,
"priority" SMALLINT NOT NULL DEFAULT 10,
diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php
index ccf256d6a..8762b33eb 100644
--- a/app/SQL/install.sql.sqlite.php
+++ b/app/SQL/install.sql.sqlite.php
@@ -16,11 +16,11 @@ CREATE TABLE IF NOT EXISTS `category` (
CREATE TABLE IF NOT EXISTS `feed` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
- `url` VARCHAR(511) NOT NULL,
+ `url` VARCHAR(32768) NOT NULL,
`kind` SMALLINT DEFAULT 0, -- 1.20.0
`category` INTEGER DEFAULT 0, -- 1.20.0
`name` VARCHAR(255) NOT NULL,
- `website` VARCHAR(255),
+ `website` VARCHAR(32768),
`description` TEXT,
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
`priority` TINYINT(2) NOT NULL DEFAULT 10,
@@ -31,8 +31,7 @@ CREATE TABLE IF NOT EXISTS `feed` (
`attributes` TEXT, -- v1.11.0
`cache_nbEntries` INT DEFAULT 0,
`cache_nbUnreads` INT DEFAULT 0,
- FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
- UNIQUE (`url`)
+ FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);
CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);
diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php
index ad0f5f5a8..6b0a3f178 100644
--- a/app/Services/ExportService.php
+++ b/app/Services/ExportService.php
@@ -21,6 +21,7 @@ class FreshRSS_Export_Service {
const FRSS_NAMESPACE = 'https://freshrss.org/opml';
const TYPE_HTML_XPATH = 'HTML+XPath';
+ const TYPE_XML_XPATH = 'XML+XPath';
const TYPE_RSS_ATOM = 'rss';
/**
@@ -43,8 +44,6 @@ class FreshRSS_Export_Service {
* @return array First item is the filename, second item is the content
*/
public function generateOpml() {
- require_once(LIB_PATH . '/lib_opml.php');
-
$view = new FreshRSS_View();
$day = date('Y-m-d');
$view->categories = $this->category_dao->listCategories(true, true);
diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php
index 28286a753..55aa28679 100644
--- a/app/Services/ImportService.php
+++ b/app/Services/ImportService.php
@@ -19,8 +19,6 @@ class FreshRSS_Import_Service {
* @param string $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);
}
@@ -34,153 +32,194 @@ class FreshRSS_Import_Service {
* This method parses and imports an OPML file.
*
* @param string $opml_file the OPML file content.
- * @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.
+ * @param FreshRSS_Category|null $forced_category force the feeds to be associated to this category.
+ * @param boolean $dry_run true to not create categories and feeds in database.
*/
- public function importOpml(string $opml_file, $parent_cat = null, $flatten = false, $dryRun = false) {
+ public function importOpml(string $opml_file, $forced_category = null, $dry_run = false) {
$this->lastStatus = true;
$opml_array = array();
try {
- $opml_array = libopml_parse_string($opml_file, false);
- } catch (LibOPML_Exception $e) {
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
- } else {
- Minz_Log::warning($e->getMessage());
- }
+ $libopml = new \marienfressinaud\LibOpml\LibOpml(false);
+ $opml_array = $libopml->parseString($opml_file);
+ } catch (\marienfressinaud\LibOpml\Exception $e) {
+ self::log($e->getMessage());
$this->lastStatus = false;
- return false;
+ return;
}
- return $this->addOpmlElements($opml_array['body'], $parent_cat, $flatten, $dryRun);
- }
+ $this->catDAO->checkDefault();
+ $default_category = $this->catDAO->getDefault();
+ if (!$default_category) {
+ self::log('Cannot get the default category');
+ $this->lastStatus = false;
+ return;
+ }
- /**
- * This method imports an OPML file based on its body.
- *
- * @param array $opml_elements an OPML element (body or outline).
- * @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, $flatten = false, $dryRun = false) {
+ // Get the categories by names so we can use this array to retrieve
+ // existing categories later.
+ $categories = $this->catDAO->listCategories(false);
+ $categories_by_names = [];
+ foreach ($categories as $category) {
+ $categories_by_names[$category->name()] = $category;
+ }
+
+ // Get current numbers of categories and feeds, and the limits to
+ // verify the user can import its categories/feeds.
+ $nb_categories = count($categories);
$nb_feeds = count($this->feedDAO->listFeeds());
- $nb_cats = count($this->catDAO->listCategories(false));
$limits = FreshRSS_Context::$system_conf->limits;
- //Sort with categories first
- usort($opml_elements, static function ($a, $b) {
- return strcmp(
- (isset($a['xmlUrl']) ? 'Z' : 'A') . (isset($a['text']) ? $a['text'] : ''),
- (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']));
- $this->lastStatus = false;
- continue;
- }
+ // Process the OPML outlines to get a list of categories and a list of
+ // feeds elements indexed by their categories names.
+ list (
+ $categories_elements,
+ $categories_to_feeds,
+ ) = $this->loadFromOutlines($opml_array['body'], '');
- if ($this->addFeedOpml($elt, $parent_cat, $dryRun)) {
- $nb_feeds++;
+ foreach ($categories_to_feeds as $category_name => $feeds_elements) {
+ $category_element = $categories_elements[$category_name] ?? null;
+
+ $category = null;
+ if ($forced_category) {
+ // If the category is forced, ignore the actual category name
+ $category = $forced_category;
+ } elseif (isset($categories_by_names[$category_name])) {
+ // If the category already exists, get it from $categories_by_names
+ $category = $categories_by_names[$category_name];
+ } elseif ($category_element) {
+ // Otherwise, create the category (if possible)
+ $limit_reached = $nb_categories >= $limits['max_categories'];
+ $can_create_category = FreshRSS_Context::$isCli || !$limit_reached;
+
+ if ($can_create_category) {
+ $category = $this->createCategory($category_element, $dry_run);
+ if ($category) {
+ $categories_by_names[$category->name()] = $category;
+ $nb_categories++;
+ }
} else {
- $this->lastStatus = false;
+ Minz_Log::warning(
+ _t('feedback.sub.category.over_max', $limits['max_categories'])
+ );
}
- } elseif (!empty($elt['text'])) {
- // No xmlUrl? It should be a category!
- $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']));
+ }
+
+ if (!$category) {
+ // Category can be null if the feeds weren't in a category
+ // outline, or if we weren't able to create the category.
+ $category = $default_category;
+ }
+
+ // Then, create the feeds one by one and attach them to the
+ // category we just got.
+ foreach ($feeds_elements as $feed_element) {
+ $limit_reached = $nb_feeds >= $limits['max_feeds'];
+ $can_create_feed = FreshRSS_Context::$isCli || !$limit_reached;
+ if (!$can_create_feed) {
+ Minz_Log::warning(
+ _t('feedback.sub.feed.over_max', $limits['max_feeds'])
+ );
$this->lastStatus = false;
- $flatten = true;
+ break;
}
- $category = $this->addCategoryOpml($elt, $parent_cat, $flatten, $dryRun);
-
- if ($category) {
- $nb_cats++;
- $categories[] = $category;
+ if ($this->createFeed($feed_element, $category, $dry_run)) {
+ // TODO what if the feed already exists in the database?
+ $nb_feeds++;
+ } else {
+ $this->lastStatus = false;
}
}
}
- return $categories;
+ return;
}
/**
- * This method imports an OPML feed element.
+ * Create a feed from a feed element (i.e. OPML outline).
*
- * @param array $feed_elt an OPML element (must be a feed element).
- * @param FreshRSS_Category|null $parent_cat the name of the parent category.
- * @return FreshRSS_Feed|null a feed.
+ * @param array<string, string> $feed_elt An OPML element (must be a feed element).
+ * @param FreshRSS_Category $category The category to associate to the feed.
+ * @param boolean $dry_run true to not create the feed in database.
+ *
+ * @return FreshRSS_Feed|null The created feed, or null if it failed.
*/
- private function addFeedOpml($feed_elt, $parent_cat, $dryRun = false) {
- if (empty($feed_elt['xmlUrl'])) {
- return null;
- }
- if ($parent_cat == null) {
- // This feed has no parent category so we get the default one
- $this->catDAO->checkDefault();
- $parent_cat = $this->catDAO->getDefault();
- if ($parent_cat == null) {
- $this->lastStatus = false;
- return null;
- }
- }
-
- // We get different useful information
+ private function createFeed($feed_elt, $category, $dry_run) {
$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
- $name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text'] ?? '');
+ $name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
+ $name = Minz_Helper::htmlspecialchars_utf8($name);
$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl'] ?? '');
$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description'] ?? '');
try {
// Create a Feed object and add it in DB
$feed = new FreshRSS_Feed($url);
- $feed->_categoryId($parent_cat->id());
- $parent_cat->addFeed($feed);
+ $feed->_categoryId($category->id());
+ $category->addFeed($feed);
$feed->_name($name);
$feed->_website($website);
$feed->_description($description);
- switch ($feed_elt['type'] ?? '') {
- case FreshRSS_Export_Service::TYPE_HTML_XPATH:
+ switch (strtolower($feed_elt['type'] ?? '')) {
+ case strtolower(FreshRSS_Export_Service::TYPE_HTML_XPATH):
$feed->_kind(FreshRSS_Feed::KIND_HTML_XPATH);
break;
- case FreshRSS_Export_Service::TYPE_RSS_ATOM:
+ case strtolower(FreshRSS_Export_Service::TYPE_XML_XPATH):
+ $feed->_kind(FreshRSS_Feed::KIND_XML_XPATH);
+ break;
+ case strtolower(FreshRSS_Export_Service::TYPE_RSS_ATOM):
default:
$feed->_kind(FreshRSS_Feed::KIND_RSS);
break;
}
+ if (isset($feed_elt['frss:cssFullContent'])) {
+ $feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($feed_elt['frss:cssFullContent']));
+ }
+
+ if (isset($feed_elt['frss:cssFullContentFilter'])) {
+ $feed->_attributes('path_entries_filter', $feed_elt['frss:cssFullContentFilter']);
+ }
+
+ if (isset($feed_elt['frss:filtersActionRead'])) {
+ $feed->_filtersAction(
+ 'read',
+ preg_split('/[\n\r]+/', $feed_elt['frss:filtersActionRead'])
+ );
+ }
+
$xPathSettings = [];
- foreach ($feed_elt as $key => $value) {
- if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
- switch ($key) {
- case 'cssFullContent': $feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($value['value'])); break;
- case 'cssFullContentFilter': $feed->_attributes('path_entries_filter', $value['value']); break;
- case 'filtersActionRead': $feed->_filtersAction('read', preg_split('/[\n\r]+/', $value['value'])); break;
- case 'xPathItem': $xPathSettings['item'] = $value['value']; break;
- case 'xPathItemTitle': $xPathSettings['itemTitle'] = $value['value']; break;
- case 'xPathItemContent': $xPathSettings['itemContent'] = $value['value']; break;
- case 'xPathItemUri': $xPathSettings['itemUri'] = $value['value']; break;
- case 'xPathItemAuthor': $xPathSettings['itemAuthor'] = $value['value']; break;
- case 'xPathItemTimestamp': $xPathSettings['itemTimestamp'] = $value['value']; break;
- case 'xPathItemTimeFormat': $xPathSettings['itemTimeFormat'] = $value['value']; break;
- case 'xPathItemThumbnail': $xPathSettings['itemThumbnail'] = $value['value']; break;
- case 'xPathItemCategories': $xPathSettings['itemCategories'] = $value['value']; break;
- case 'xPathItemUid': $xPathSettings['itemUid'] = $value['value']; break;
- }
- }
+ if (isset($feed_elt['frss:xPathItem'])) {
+ $xPathSettings['item'] = $feed_elt['frss:xPathItem'];
+ }
+ if (isset($feed_elt['frss:xPathItemTitle'])) {
+ $xPathSettings['itemTitle'] = $feed_elt['frss:xPathItemTitle'];
+ }
+ if (isset($feed_elt['frss:xPathItemContent'])) {
+ $xPathSettings['itemContent'] = $feed_elt['frss:xPathItemContent'];
+ }
+ if (isset($feed_elt['frss:xPathItemUri'])) {
+ $xPathSettings['itemUri'] = $feed_elt['frss:xPathItemUri'];
+ }
+ if (isset($feed_elt['frss:xPathItemAuthor'])) {
+ $xPathSettings['itemAuthor'] = $feed_elt['frss:xPathItemAuthor'];
+ }
+ if (isset($feed_elt['frss:xPathItemTimestamp'])) {
+ $xPathSettings['itemTimestamp'] = $feed_elt['frss:xPathItemTimestamp'];
+ }
+ if (isset($feed_elt['frss:xPathItemTimeFormat'])) {
+ $xPathSettings['itemTimeFormat'] = $feed_elt['frss:xPathItemTimeFormat'];
}
+ if (isset($feed_elt['frss:xPathItemThumbnail'])) {
+ $xPathSettings['itemThumbnail'] = $feed_elt['frss:xPathItemThumbnail'];
+ }
+ if (isset($feed_elt['frss:xPathItemCategories'])) {
+ $xPathSettings['itemCategories'] = $feed_elt['frss:xPathItemCategories'];
+ }
+ if (isset($feed_elt['frss:xPathItemUid'])) {
+ $xPathSettings['itemUid'] = $feed_elt['frss:xPathItemUid'];
+ }
+
if (!empty($xPathSettings)) {
$feed->_attributes('xpath', $xPathSettings);
}
@@ -188,9 +227,11 @@ class FreshRSS_Import_Service {
// Call the extension hook
/** @var FreshRSS_Feed|null */
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
- if ($dryRun) {
+
+ if ($dry_run) {
return $feed;
}
+
if ($feed != null) {
// addFeedObject checks if feed is already in DB
$id = $this->feedDAO->addFeedObject($feed);
@@ -202,81 +243,163 @@ class FreshRSS_Import_Service {
}
}
} catch (FreshRSS_Feed_Exception $e) {
- if (FreshRSS_Context::$isCli) {
- fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
- } else {
- Minz_Log::warning($e->getMessage());
- }
+ self::log($e->getMessage());
$this->lastStatus = false;
}
- 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());
- }
-
+ $clean_url = SimplePie_Misc::url_remove_credentials($url);
+ self::log("Cannot create {$clean_url} feed in category {$category->name()}");
return null;
}
/**
- * This method imports an OPML category element.
+ * Create and return a category.
+ *
+ * @param array<string, string> $category_element An OPML element (must be a category element).
+ * @param boolean $dry_run true to not create the category in database.
*
- * @param array $cat_elt an OPML element (must be a category element).
- * @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.
+ * @return FreshRSS_Category|null The created category, or null if it failed.
*/
- 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);
-
- 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;
- }
- }
+ private function createCategory($category_element, $dry_run) {
+ $name = $category_element['text'] ?? $category_element['title'] ?? '';
+ $name = Minz_Helper::htmlspecialchars_utf8($name);
+ $category = new FreshRSS_Category($name);
+
+ if (isset($category_element['frss:opmlUrl'])) {
+ $opml_url = checkUrl($category_element['frss:opmlUrl']);
+ if ($opml_url != '') {
+ $category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
+ $category->_attributes('opml_url', $opml_url);
}
+ }
- if (!$dryRun) {
- $id = $this->catDAO->addCategoryObject($cat);
- if ($id == false) {
- $this->lastStatus = false;
- $error = true;
- } else {
- $cat->_id($id);
+ if ($dry_run) {
+ return $category;
+ }
+
+ $id = $this->catDAO->addCategoryObject($category);
+ if ($id !== false) {
+ $category->_id($id);
+ return $category;
+ } else {
+ self::log("Cannot create category {$category->name()}");
+ $this->lastStatus = false;
+ return null;
+ }
+ }
+
+ /**
+ * Return the list of category and feed outlines by categories names.
+ *
+ * This method is applied to a list of outlines. It merges the different
+ * list of feeds from several outlines into one array.
+ *
+ * @param array $outlines
+ * The outlines from which to extract the outlines.
+ * @param string $parent_category_name
+ * The name of the parent category of the current outlines.
+ *
+ * @return array[]
+ */
+ private function loadFromOutlines($outlines, $parent_category_name) {
+ $categories_elements = [];
+ $categories_to_feeds = [];
+
+ foreach ($outlines as $outline) {
+ // Get the categories and feeds from the child outline (it may
+ // return several categories and feeds if the outline is a category).
+ list (
+ $outline_categories,
+ $outline_categories_to_feeds,
+ ) = $this->loadFromOutline($outline, $parent_category_name);
+
+ // Then, we merge the initial arrays with the arrays returned by
+ // the outline.
+ $categories_elements = array_merge($categories_elements, $outline_categories);
+
+ foreach ($outline_categories_to_feeds as $category_name => $feeds) {
+ if (!isset($categories_to_feeds[$category_name])) {
+ $categories_to_feeds[$category_name] = [];
}
+
+ $categories_to_feeds[$category_name] = array_merge(
+ $categories_to_feeds[$category_name],
+ $feeds
+ );
}
- 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);
- }
+ }
+
+ return [$categories_elements, $categories_to_feeds];
+ }
+
+ /**
+ * Return the list of category and feed outlines by categories names.
+ *
+ * This method is applied to a specific outline. If the outline represents
+ * a category (i.e. @outlines key exists), it will reapply loadFromOutlines()
+ * to its children. If the outline represents a feed (i.e. xmlUrl key
+ * exists), it will add the outline to an array accessible by its category
+ * name.
+ *
+ * @param array $outline
+ * The outline from which to extract the categories and feeds outlines.
+ * @param string $parent_category_name
+ * The name of the parent category of the current outline.
+ *
+ * @return array[]
+ */
+ private function loadFromOutline($outline, $parent_category_name) {
+ $categories_elements = [];
+ $categories_to_feeds = [];
+
+ if ($parent_category_name === '' && isset($outline['category'])) {
+ // The outline has no parent category, but its OPML category
+ // attribute is set, so we use it as the category name.
+ // lib_opml parses this attribute as an array of strings, so we
+ // rebuild a string here.
+ $parent_category_name = implode(', ', $outline['category']);
+ $categories_elements[$parent_category_name] = [
+ 'text' => $parent_category_name,
+ ];
+ }
+
+ if (isset($outline['@outlines'])) {
+ // The outline has children, it's probably a category
+ if (!empty($outline['text'])) {
+ $category_name = $outline['text'];
+ } elseif (!empty($outline['title'])) {
+ $category_name = $outline['title'];
} else {
- $parent_cat = $cat;
+ $category_name = $parent_category_name;
}
+
+ list (
+ $categories_elements,
+ $categories_to_feeds,
+ ) = $this->loadFromOutlines($outline['@outlines'], $category_name);
+
+ unset($outline['@outlines']);
+ $categories_elements[$category_name] = $outline;
}
- 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, so always flatten from here
- $this->addOpmlElements($cat_elt['@outlines'], $parent_cat, true, $dryRun);
+ // The xmlUrl means it's a feed URL: add the outline to the array if it
+ // exists.
+ if (isset($outline['xmlUrl'])) {
+ if (!isset($categories_to_feeds[$parent_category_name])) {
+ $categories_to_feeds[$parent_category_name] = [];
+ }
+
+ $categories_to_feeds[$parent_category_name][] = $outline;
}
- return $cat;
+ return [$categories_elements, $categories_to_feeds];
+ }
+
+ private static function log($message) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
+ } else {
+ Minz_Log::warning("Error during OPML import: {$message}");
+ }
}
}
diff --git a/app/Utils/feverUtil.php b/app/Utils/feverUtil.php
index a7d21dacb..0e4b712ce 100644
--- a/app/Utils/feverUtil.php
+++ b/app/Utils/feverUtil.php
@@ -1,19 +1,19 @@
<?php
class FreshRSS_fever_Util {
- const FEVER_PATH = DATA_PATH . '/fever';
+ private const FEVER_PATH = DATA_PATH . '/fever';
/**
* Make sure the fever path exists and is writable.
*
- * @return boolean true if the path is writable, else false.
+ * @return bool true if the path is writable, false otherwise.
*/
- public static function checkFeverPath() {
+ public static function checkFeverPath(): bool {
if (!file_exists(self::FEVER_PATH)) {
@mkdir(self::FEVER_PATH, 0770, true);
}
- $ok = is_writable(self::FEVER_PATH);
+ $ok = touch(self::FEVER_PATH . '/index.html'); // is_writable() is not reliable for a folder on NFS
if (!$ok) {
Minz_Log::error("Could not save Fever API credentials. The directory does not have write access.");
}
@@ -22,25 +22,21 @@ class FreshRSS_fever_Util {
/**
* Return the corresponding path for a fever key.
- *
- * @param string $feverKey
- * @return string
*/
- public static function getKeyPath($feverKey) {
+ public static function getKeyPath(string $feverKey): string {
+ if (FreshRSS_Context::$system_conf === null) {
+ throw new FreshRSS_Context_Exception('System configuration not initialised!');
+ }
$salt = sha1(FreshRSS_Context::$system_conf->salt);
return self::FEVER_PATH . '/.key-' . $salt . '-' . $feverKey . '.txt';
}
/**
* Update the fever key of a user.
- *
- * @param string $username
- * @param string $passwordPlain
* @return string|false the Fever key, or false if the update failed
*/
- public static function updateKey($username, $passwordPlain) {
- $ok = self::checkFeverPath();
- if (!$ok) {
+ public static function updateKey(string $username, string $passwordPlain) {
+ if (!self::checkFeverPath()) {
return false;
}
@@ -48,22 +44,20 @@ class FreshRSS_fever_Util {
$feverKey = strtolower(md5("{$username}:{$passwordPlain}"));
$feverKeyPath = self::getKeyPath($feverKey);
- $res = file_put_contents($feverKeyPath, $username);
- if ($res !== false) {
+ $result = file_put_contents($feverKeyPath, $username);
+ if (is_int($result) && $result > 0) {
return $feverKey;
- } else {
- Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
- return false;
}
+ Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
+ return false;
}
/**
* Delete the Fever key of a user.
*
- * @param string $username
- * @return boolean true if the deletion succeeded, else false.
+ * @return bool true if the deletion succeeded, else false.
*/
- public static function deleteKey($username) {
+ public static function deleteKey(string $username) {
$userConfig = get_user_configuration($username);
if ($userConfig === null) {
return false;
diff --git a/app/Utils/passwordUtil.php b/app/Utils/passwordUtil.php
index cff97d2bc..0edead213 100644
--- a/app/Utils/passwordUtil.php
+++ b/app/Utils/passwordUtil.php
@@ -3,26 +3,25 @@
class FreshRSS_password_Util {
// Will also have to be computed client side on mobile devices,
// so do not use a too high cost
- const BCRYPT_COST = 9;
+ public const BCRYPT_COST = 9;
/**
* Return a hash of a plain password, using BCRYPT
- *
- * @param string $passwordPlain
- * @return string
*/
- public static function hash($passwordPlain) {
+ public static function hash(string $passwordPlain): string {
$passwordHash = password_hash(
$passwordPlain,
PASSWORD_BCRYPT,
array('cost' => self::BCRYPT_COST)
);
- $passwordPlain = '';
// Compatibility with bcrypt.js
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);
- return $passwordHash == '' ? '' : $passwordHash;
+ if ($passwordHash === '' || $passwordHash === null) {
+ return '';
+ }
+ return $passwordHash;
}
/**
@@ -30,11 +29,9 @@ class FreshRSS_password_Util {
*
* A valid password is a string of at least 7 characters.
*
- * @param string $password
- *
- * @return boolean True if the password is valid, false otherwise
+ * @return bool True if the password is valid, false otherwise
*/
- public static function check($password) {
+ public static function check(string $password): bool {
return strlen($password) >= 7;
}
}
diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php
index 7b603555c..4411b5047 100644
--- a/app/i18n/cz/conf.php
+++ b/app/i18n/cz/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Zobrazení',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Spodní řádek',
'display_authors' => 'Autoři',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Časový limit HTML5 oznámení',
),
'show_nav_buttons' => 'Zobrazit navigační tlačítka',
- 'theme' => 'Motiv',
+ 'theme' => array(
+ '_' => 'Motiv',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Motiv „%s“ již není dostupný. Zvolte jiný motiv, prosím.',
'thumbnail' => array(
'label' => 'Náhled',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Na výšku',
'square' => 'Čtverec',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Zobrazení',
'width' => array(
'content' => 'Šířka obsahu',
diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php
index 55586b24c..1f75033fa 100644
--- a/app/i18n/cz/gen.php
+++ b/app/i18n/cz/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Uživatelské dotazy',
'reading' => 'Čtení',
'search' => 'Hledat slova nebo #štítky',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sdílení',
'shortcuts' => 'Zkratky',
'stats' => 'Statistika',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Známé základní stránky',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schránka',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php
index a11a9359d..3d08c315b 100644
--- a/app/i18n/cz/sub.php
+++ b/app/i18n/cz/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pro:',
),
'rss' => 'RSS / Atom (výchozí)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazat mezipaměť',
diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php
index 5e9cdb36e..8962123f4 100644
--- a/app/i18n/de/conf.php
+++ b/app/i18n/de/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Anzeige',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Fußzeile',
'display_authors' => 'Autoren',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung',
),
'show_nav_buttons' => 'Zeige Navigations-Buttons',
- 'theme' => 'Erscheinungsbild',
+ 'theme' => array(
+ '_' => 'Layout',
+ 'deprecated' => array(
+ '_' => 'Veraltet',
+ 'description' => 'Diese Layout wird nicht mehr länger aktualisiert und wir in einer <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">zukünftigen Version von FreshRSS</a> entfernt sein.',
+ ),
+ ),
'theme_not_available' => 'Das Erscheinungsbild „%s“ ist nicht mehr verfügbar. Bitte ein anderes auswählen.',
'thumbnail' => array(
'label' => 'Vorschaubild',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Hochformat',
'square' => 'Quadrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Anzeige',
'width' => array(
'content' => 'Inhaltsbreite',
diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php
index 59f532c74..fb35bc41c 100644
--- a/app/i18n/de/gen.php
+++ b/app/i18n/de/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Benutzerabfragen',
'reading' => 'Lesen',
'search' => 'Suche Worte oder #Tags',
+ 'search_help' => 'Siehe Dokumentation zu den <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">Suchparametern</a>',
'sharing' => 'Teilen',
'shortcuts' => 'Tastaturkürzel',
'stats' => 'Statistiken',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known-Seite (https://withknown.com)',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Zwischenablage',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-Mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php
index 580f7d348..b265c1b98 100644
--- a/app/i18n/de/sub.php
+++ b/app/i18n/de/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath für:',
),
'rss' => 'RSS / Atom (Standard)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Zwischenspeicher leeren',
diff --git a/app/i18n/el/conf.php b/app/i18n/el/conf.php
index 98f559d18..daacfe684 100644
--- a/app/i18n/el/conf.php
+++ b/app/i18n/el/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // TODO
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // TODO
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // TODO
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'Theme', // TODO
+ 'theme' => array(
+ '_' => 'Theme', // TODO
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Display', // TODO
'width' => array(
'content' => 'Content width', // TODO
diff --git a/app/i18n/el/gen.php b/app/i18n/el/gen.php
index a0c95ab39..03852a0c6 100644
--- a/app/i18n/el/gen.php
+++ b/app/i18n/el/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // TODO
'reading' => 'Reading', // TODO
'search' => 'Search words or #tags', // TODO
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing', // TODO
'shortcuts' => 'Shortcuts', // TODO
'stats' => 'Statistics', // TODO
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // TODO
'blogotext' => 'Blogotext', // TODO
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // TODO
'email' => 'Email', // TODO
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // TODO
'gnusocial' => 'GNU social', // TODO
'jdh' => 'Journal du hacker', // TODO
diff --git a/app/i18n/el/sub.php b/app/i18n/el/sub.php
index 424fafc7b..aae9ae412 100644
--- a/app/i18n/el/sub.php
+++ b/app/i18n/el/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/en-us/conf.php b/app/i18n/en-us/conf.php
index 8330e4970..afea0299a 100644
--- a/app/i18n/en-us/conf.php
+++ b/app/i18n/en-us/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // IGNORE
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // IGNORE
'display_authors' => 'Authors', // IGNORE
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // IGNORE
),
'show_nav_buttons' => 'Show the navigation buttons', // IGNORE
- 'theme' => 'Theme', // IGNORE
+ 'theme' => array(
+ '_' => 'Theme', // IGNORE
+ 'deprecated' => array(
+ '_' => 'Deprecated', // IGNORE
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // IGNORE
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // IGNORE
'thumbnail' => array(
'label' => 'Thumbnail', // IGNORE
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // IGNORE
'square' => 'Square', // IGNORE
),
+ 'timezone' => 'Time zone', // IGNORE
'title' => 'Display', // IGNORE
'width' => array(
'content' => 'Content width', // IGNORE
diff --git a/app/i18n/en-us/gen.php b/app/i18n/en-us/gen.php
index c5f92ad40..ca08ed27f 100644
--- a/app/i18n/en-us/gen.php
+++ b/app/i18n/en-us/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // IGNORE
'reading' => 'Reading', // IGNORE
'search' => 'Search words or #tags', // IGNORE
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // IGNORE
'sharing' => 'Sharing', // IGNORE
'shortcuts' => 'Shortcuts', // IGNORE
'stats' => 'Statistics', // IGNORE
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // IGNORE
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Clipboard', // IGNORE
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/en-us/sub.php b/app/i18n/en-us/sub.php
index a6b311084..92d75b81e 100644
--- a/app/i18n/en-us/sub.php
+++ b/app/i18n/en-us/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // IGNORE
),
'rss' => 'RSS / Atom (default)', // IGNORE
+ 'xml_xpath' => 'XML + XPath', // IGNORE
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // IGNORE
diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php
index fe03499ea..9899cf897 100644
--- a/app/i18n/en/conf.php
+++ b/app/i18n/en/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line',
'display_authors' => 'Authors',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout',
),
'show_nav_buttons' => 'Show the navigation buttons',
- 'theme' => 'Theme',
+ 'theme' => array(
+ '_' => 'Theme',
+ 'deprecated' => array(
+ '_' => 'Deprecated',
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>',
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.',
'thumbnail' => array(
'label' => 'Thumbnail',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait',
'square' => 'Square',
),
+ 'timezone' => 'Time zone',
'title' => 'Display',
'width' => array(
'content' => 'Content width',
diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php
index 8f7065a83..d3a36995f 100644
--- a/app/i18n/en/gen.php
+++ b/app/i18n/en/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries',
'reading' => 'Reading',
'search' => 'Search words or #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing',
'shortcuts' => 'Shortcuts',
'stats' => 'Statistics',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites',
+ 'archiveORG' => 'archive.org',
'archivePH' => 'archive.ph',
'blogotext' => 'Blogotext',
'clipboard' => 'Clipboard',
'diaspora' => 'Diaspora*',
'email' => 'Email',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook',
'gnusocial' => 'GNU social',
'jdh' => 'Journal du hacker',
diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php
index c7e100c25..04caaff05 100644
--- a/app/i18n/en/sub.php
+++ b/app/i18n/en/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:',
),
'rss' => 'RSS / Atom (default)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache',
diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php
index 868cac45d..868cac45d 100755..100644
--- a/app/i18n/es/admin.php
+++ b/app/i18n/es/admin.php
diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php
index c91b0205c..5137ff987 100755..100644
--- a/app/i18n/es/conf.php
+++ b/app/i18n/es/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Visualización',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Línea inferior',
'display_authors' => 'Autores/Autoras',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notificación de fin de espera HTML5',
),
'show_nav_buttons' => 'Mostrar los botones de navegación',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'El tema “%s” ya no está disponible. Por favor, elija otro tema.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Retrato',
'square' => 'Cuadrado',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Visualización',
'width' => array(
'content' => 'Ancho de contenido',
diff --git a/app/i18n/es/feedback.php b/app/i18n/es/feedback.php
index b7305e6d9..b7305e6d9 100755..100644
--- a/app/i18n/es/feedback.php
+++ b/app/i18n/es/feedback.php
diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php
index 209a40dac..5ea2fce23 100755..100644
--- a/app/i18n/es/gen.php
+++ b/app/i18n/es/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Peticiones de usuario',
'reading' => 'Lectura',
'search' => 'Buscar palabras o #etiquetas',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Compartir',
'shortcuts' => 'Atajos',
'stats' => 'Estadísticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sitios basados en conocidos',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Portapapeles',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php
index 610f677ed..610f677ed 100755..100644
--- a/app/i18n/es/index.php
+++ b/app/i18n/es/index.php
diff --git a/app/i18n/es/install.php b/app/i18n/es/install.php
index 3c46d6e58..3c46d6e58 100755..100644
--- a/app/i18n/es/install.php
+++ b/app/i18n/es/install.php
diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php
index 52d681067..4fd2fa393 100755..100644
--- a/app/i18n/es/sub.php
+++ b/app/i18n/es/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath para:',
),
'rss' => 'RSS / Atom (por defecto)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Borrar caché',
diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php
index 61306289c..3122e3be5 100644
--- a/app/i18n/fr/conf.php
+++ b/app/i18n/fr/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Affichage',
+ 'darkMode' => 'Mode sombre automatique (bêta)',
'icon' => array(
'bottom_line' => 'Ligne du bas',
'display_authors' => 'Auteurs',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Temps d’affichage de la notification HTML5',
),
'show_nav_buttons' => 'Afficher les boutons de navigation',
- 'theme' => 'Thème',
+ 'theme' => array(
+ '_' => 'Thème',
+ 'deprecated' => array(
+ '_' => 'Obsolète',
+ 'description' => 'Ce thème est obsolète et sera supprimé dans une <a href="https://freshrss.github.io/FreshRSS/fr/users/05_Configuration.html#th%C3%A8me" target="_blank">future version de FreshRSS</a>',
+ ),
+ ),
'theme_not_available' => 'Le thème <em>%s</em> n’est plus disponible. Veuillez choisir un autre thème.',
'thumbnail' => array(
'label' => 'Miniature',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // IGNORE
'square' => 'Carrée',
),
+ 'timezone' => 'Fuseau horaire',
'title' => 'Affichage',
'width' => array(
'content' => 'Largeur du contenu',
diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php
index 69d260063..53e7160a2 100644
--- a/app/i18n/fr/gen.php
+++ b/app/i18n/fr/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Filtres utilisateurs',
'reading' => 'Lecture',
'search' => 'Rechercher des mots ou des #tags',
+ 'search_help' => 'Voir <a href="https://freshrss.github.io/FreshRSS/fr/users/03_Main_view.html#gr%C3%A2ce-au-champ-de-recherche" target="_blank">la documentation pour la syntaxe des recherches avancées</a>',
'sharing' => 'Partage',
'shortcuts' => 'Raccourcis',
'stats' => 'Statistiques',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites basés sur Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Presse-papier',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Courriel',
+ 'email-webmail-firefox-fix' => 'Courriel (pour Webmail avec Firefox)',
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php
index f9df0dbcc..be6dc094d 100644
--- a/app/i18n/fr/sub.php
+++ b/app/i18n/fr/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pour :',
),
'rss' => 'RSS / Atom (par défaut)',
+ 'xml_xpath' => 'XML + XPath', // IGNORE
),
'maintenance' => array(
'clear_cache' => 'Vider le cache',
diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php
index ad479db44..c4a490a2d 100644
--- a/app/i18n/he/conf.php
+++ b/app/i18n/he/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'תצוגה',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'שורה תחתונה',
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 התראה פג תוקף',
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'ערכת נושא',
+ 'theme' => array(
+ '_' => 'ערכת נושא',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'תצוגה',
'width' => array(
'content' => 'רוחב התוכן',
diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php
index a8df3db6b..6345e66e9 100644
--- a/app/i18n/he/gen.php
+++ b/app/i18n/he/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'שאילתות',
'reading' => 'קריאה',
'search' => 'חיפוש מילים או #תגים',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'שיתוף',
'shortcuts' => 'קיצורי דרך',
'stats' => 'סטטיסטיקות',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'דואר אלקטרוני',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php
index 25552ffa1..bae5f5177 100644
--- a/app/i18n/he/sub.php
+++ b/app/i18n/he/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/id/conf.php b/app/i18n/id/conf.php
index b8a5b4fc1..8b1fa8dc6 100644
--- a/app/i18n/id/conf.php
+++ b/app/i18n/id/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Display', // TODO
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Bottom line', // TODO
'display_authors' => 'Authors', // TODO
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notification timeout', // TODO
),
'show_nav_buttons' => 'Show the navigation buttons', // TODO
- 'theme' => 'Theme', // TODO
+ 'theme' => array(
+ '_' => 'Theme', // TODO
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
'thumbnail' => array(
'label' => 'Thumbnail', // TODO
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portrait', // TODO
'square' => 'Square', // TODO
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Display', // TODO
'width' => array(
'content' => 'Content width', // TODO
diff --git a/app/i18n/id/gen.php b/app/i18n/id/gen.php
index 93f8b0afe..1fc2fa155 100644
--- a/app/i18n/id/gen.php
+++ b/app/i18n/id/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'User queries', // TODO
'reading' => 'Reading', // TODO
'search' => 'Search words or #tags', // TODO
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Sharing', // TODO
'shortcuts' => 'Shortcuts', // TODO
'stats' => 'Statistics', // TODO
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // TODO
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // TODO
'blogotext' => 'Blogotext', // TODO
'clipboard' => 'Clipboard', // TODO
'diaspora' => 'Diaspora*', // TODO
'email' => 'Email', // TODO
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // TODO
'gnusocial' => 'GNU social', // TODO
'jdh' => 'Journal du hacker', // TODO
diff --git a/app/i18n/id/sub.php b/app/i18n/id/sub.php
index 7fdf5c024..3f9a4916a 100644
--- a/app/i18n/id/sub.php
+++ b/app/i18n/id/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath for:', // TODO
),
'rss' => 'RSS / Atom (default)', // TODO
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Clear cache', // TODO
diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php
index 4597687cc..6f3540322 100644
--- a/app/i18n/it/conf.php
+++ b/app/i18n/it/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Visualizzazione',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Barra in fondo',
'display_authors' => 'Autori',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notifica timeout HTML5',
),
'show_nav_buttons' => 'Mostra i pulsanti di navigazione',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Il tema “%s” non è più disponibile. Si prega di selezionarne un altro.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Ritratto',
'square' => 'Squadrata',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Visualizzazione',
'width' => array(
'content' => 'Larghezza contenuto',
diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php
index e5458866c..f3edb57aa 100644
--- a/app/i18n/it/gen.php
+++ b/app/i18n/it/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Ricerche personali',
'reading' => 'Lettura',
'search' => 'Ricerca parole o #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Condivisione',
'shortcuts' => 'Comandi tastiera',
'stats' => 'Statistiche',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Siti basati su Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Appunti',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php
index 8614caca7..7ab83cf07 100644
--- a/app/i18n/it/sub.php
+++ b/app/i18n/it/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath per:',
),
'rss' => 'RSS / Atom (predefinito)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Svuota cache',
diff --git a/app/i18n/ja/conf.php b/app/i18n/ja/conf.php
index 5e9aabfa2..4dd939760 100644
--- a/app/i18n/ja/conf.php
+++ b/app/i18n/ja/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '表示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '行の下部',
'display_authors' => '著者',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 の通知タイムアウト時間',
),
'show_nav_buttons' => 'ナビゲーションボタンを表示する',
- 'theme' => 'テーマ',
+ 'theme' => array(
+ '_' => 'テーマ',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s”テーマはご利用いただけません。他のテーマをお選びください。',
'thumbnail' => array(
'label' => 'サムネイル',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'ポートレート',
'square' => '四角',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'ディスプレイ',
'width' => array(
'content' => 'コンテンツ幅',
diff --git a/app/i18n/ja/gen.php b/app/i18n/ja/gen.php
index 69fc8f9c9..f6cf2dcdf 100644
--- a/app/i18n/ja/gen.php
+++ b/app/i18n/ja/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'ユーザークエリ',
'reading' => 'リーディング',
'search' => '単語で検索するかハッシュタグで検索する',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '共有',
'shortcuts' => 'ショートカット',
'stats' => '統計',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'よく使われるサイト',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'クリップボード',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Eメール',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ja/sub.php b/app/i18n/ja/sub.php
index 80548c025..2425b21f3 100644
--- a/app/i18n/ja/sub.php
+++ b/app/i18n/ja/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPathは:',
),
'rss' => 'RSS / Atom (標準)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'キャッシュのクリア',
diff --git a/app/i18n/ko/conf.php b/app/i18n/ko/conf.php
index 279f2f4ad..a88fcf9e0 100644
--- a/app/i18n/ko/conf.php
+++ b/app/i18n/ko/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '표시',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '하단',
'display_authors' => '저자',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 알림 타임아웃',
),
'show_nav_buttons' => '내비게이션 버튼 보이기',
- 'theme' => '테마',
+ 'theme' => array(
+ '_' => '테마',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 테마는 더이상 사용할 수 없습니다. 다른 테마를 선택해 주세요.',
'thumbnail' => array(
'label' => '섬네일',
@@ -57,6 +64,7 @@ return array(
'portrait' => '세로 방향',
'square' => '정사각형',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '표시',
'width' => array(
'content' => '내용 표시 너비',
diff --git a/app/i18n/ko/gen.php b/app/i18n/ko/gen.php
index 4f6b6a228..da1f57e9c 100644
--- a/app/i18n/ko/gen.php
+++ b/app/i18n/ko/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => '사용자 쿼리',
'reading' => '읽기',
'search' => '단어 또는 #태그 검색',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '공유',
'shortcuts' => '단축키',
'stats' => '통계',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known based sites', // IGNORE
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '클립보드',
'diaspora' => 'Diaspora*', // IGNORE
'email' => '메일',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ko/sub.php b/app/i18n/ko/sub.php
index e0ef5990b..f376247d5 100644
--- a/app/i18n/ko/sub.php
+++ b/app/i18n/ko/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => '다음의 XPath:',
),
'rss' => 'RSS / Atom (기본값)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '캐쉬 지우기',
diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php
index 8b3d597b1..e02ca81cc 100644
--- a/app/i18n/nl/conf.php
+++ b/app/i18n/nl/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Opmaak',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Onderaan',
'display_authors' => 'Auteurs',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 notificatie stop',
),
'show_nav_buttons' => 'Toon navigatieknoppen',
- 'theme' => 'Thema',
+ 'theme' => array(
+ '_' => 'Thema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Het „%s” thema is niet meer beschikbaar. Kies een ander thema.',
'thumbnail' => array(
'label' => 'Miniatuur',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Staand',
'square' => 'Vierkant',
),
+ 'timezone' => 'Tijdzone',
'title' => 'Opmaak',
'width' => array(
'content' => 'Inhoud breedte',
diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php
index abd21f460..ad3379ece 100644
--- a/app/i18n/nl/gen.php
+++ b/app/i18n/nl/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Gebruikers informatie',
'reading' => 'Lezen',
'search' => 'Zoek woorden of #labels',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Delen',
'shortcuts' => 'Snelle toegang',
'stats' => 'Statistieken',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Known-gebaseerde sites',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Klembord',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php
index 0fa767171..631da9477 100644
--- a/app/i18n/nl/sub.php
+++ b/app/i18n/nl/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath voor:',
),
'rss' => 'RSS / Atom (standaard)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Cache leegmaken',
diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php
index c1834e9aa..4a3b483e7 100644
--- a/app/i18n/oc/conf.php
+++ b/app/i18n/oc/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Afichatge',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Linha enbàs',
'display_authors' => 'Autors',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Temps d’afichatge de las notificacions HTML5',
),
'show_nav_buttons' => 'Mostrar los botons de navigacion',
- 'theme' => 'Tèma',
+ 'theme' => array(
+ '_' => 'Tèma',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Lo tèma « %s » es pas pus disponible. Causissètz un autre tèma.',
'thumbnail' => array(
'label' => 'Vinheta',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Retrach',
'square' => 'Carrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Afichatge',
'width' => array(
'content' => 'Largor del contengut',
diff --git a/app/i18n/oc/gen.php b/app/i18n/oc/gen.php
index 41f2c1499..8e852a810 100644
--- a/app/i18n/oc/gen.php
+++ b/app/i18n/oc/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Filtres utilizaire',
'reading' => 'Lectura',
'search' => 'Recercar de mots o d’#etiquetas',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Partatge',
'shortcuts' => 'Acorchis',
'stats' => 'Estatisticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites basats sus Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Quicha-papiers.',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Corrièl',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/oc/sub.php b/app/i18n/oc/sub.php
index 92a73057c..008b4964d 100644
--- a/app/i18n/oc/sub.php
+++ b/app/i18n/oc/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath per :',
),
'rss' => 'RSS / Atom (defaut)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Escafar lo cache',
diff --git a/app/i18n/pl/conf.php b/app/i18n/pl/conf.php
index 31b0d238c..8700a1c13 100644
--- a/app/i18n/pl/conf.php
+++ b/app/i18n/pl/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Wyświetlanie',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Dolny margines',
'display_authors' => 'Autorzy',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Czas wyświetlania powiadomienia HTML5',
),
'show_nav_buttons' => 'Pokaż przyciski nawigacyjne',
- 'theme' => 'Motyw',
+ 'theme' => array(
+ '_' => 'Motyw',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Motyw “%s” nie jest już dostępny. Wybierz inny motyw.',
'thumbnail' => array(
'label' => 'Miniaturka',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portret',
'square' => 'Kwadrat',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Wyświetlanie',
'width' => array(
'content' => 'Rozmiar treści',
diff --git a/app/i18n/pl/gen.php b/app/i18n/pl/gen.php
index 1a7bd69a5..fc91d8bca 100644
--- a/app/i18n/pl/gen.php
+++ b/app/i18n/pl/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Zapisane zapytania',
'reading' => 'Czytanie',
'search' => 'Wyszukaj wyrazy lub #tagi',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Podawanie dalej',
'shortcuts' => 'Skróty klawiszowe',
'stats' => 'Statystyki',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Strony bazujące na usłudze Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schowek',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/pl/sub.php b/app/i18n/pl/sub.php
index b6121fcb7..565401982 100644
--- a/app/i18n/pl/sub.php
+++ b/app/i18n/pl/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath dla:',
),
'rss' => 'RSS / Atom (domyślne)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Wyczyść pamięć podręczną',
diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php
index b925aee21..f8ad55f14 100644
--- a/app/i18n/pt-br/conf.php
+++ b/app/i18n/pt-br/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Exibição',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Linha inferior',
'display_authors' => 'Autores',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Notificação em HTML5 de timeout',
),
'show_nav_buttons' => 'Mostrar botões de navegação',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'O tema “%s” não está mais disponível. Por favor escolha outro tema.',
'thumbnail' => array(
'label' => 'Miniatura',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Modo retrato',
'square' => 'Modo quadrado',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Exibição',
'width' => array(
'content' => 'Largura do conteúdo',
diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php
index 969056969..51c1eb327 100644
--- a/app/i18n/pt-br/gen.php
+++ b/app/i18n/pt-br/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Queries de usuário',
'reading' => 'Leitura',
'search' => 'Procurar por palavras ou #tags',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Compartilhamento',
'shortcuts' => 'Atalhos',
'stats' => 'Estatísticas',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Sites no Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Área de transferência',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php
index c9755755e..4cdee8681 100644
--- a/app/i18n/pt-br/sub.php
+++ b/app/i18n/pt-br/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath para:',
),
'rss' => 'RSS / Atom (padrão)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Limpar o cache',
diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php
index c0d25aec1..2c5dda544 100644
--- a/app/i18n/ru/conf.php
+++ b/app/i18n/ru/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Отображение',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Нижняя линия',
'display_authors' => 'Авторы',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Таймаут уведомлений HTML5',
),
'show_nav_buttons' => 'Показать кнопки навигации',
- 'theme' => 'Тема',
+ 'theme' => array(
+ '_' => 'Тема',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Тема “%s” больше не доступна. Пожалуйста выберите другю тему.',
'thumbnail' => array(
'label' => 'Эскиз',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Вертикальный',
'square' => 'Квадратный',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Отображение',
'width' => array(
'content' => 'Ширина содержимого',
diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php
index 3ed1ab1ac..ddfea7ca4 100644
--- a/app/i18n/ru/gen.php
+++ b/app/i18n/ru/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Пользовательские запросы',
'reading' => 'Чтение',
'search' => 'Искать слова или #теги',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Обмен',
'shortcuts' => 'Горячие клавиши',
'stats' => 'Статистика',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Сайты на Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Буфер обмена',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Электронная почта',
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php
index 5704b53b1..d13c4c4f0 100644
--- a/app/i18n/ru/sub.php
+++ b/app/i18n/ru/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath для:',
),
'rss' => 'RSS / Atom (по умолчанию)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Очистить кэш',
diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php
index 7efc3a75d..d4714b506 100644
--- a/app/i18n/sk/conf.php
+++ b/app/i18n/sk/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Zobrazenie',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Spodný riadok',
'display_authors' => 'Autori',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'Limit HTML5 oznámenia',
),
'show_nav_buttons' => 'Zobraziť tlačidlá oznámenia',
- 'theme' => 'Vzhľad',
+ 'theme' => array(
+ '_' => 'Vzhľad',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => 'Vzhľad “%s” už nie je dostupný. Prosím, vyberte si iný vzhľad.',
'thumbnail' => array(
'label' => 'Miniatúra',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Nastojato',
'square' => 'Štvorec',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Zobraziť',
'width' => array(
'content' => 'Šírka obsahu',
diff --git a/app/i18n/sk/gen.php b/app/i18n/sk/gen.php
index 6bb5e4161..d591266e4 100644
--- a/app/i18n/sk/gen.php
+++ b/app/i18n/sk/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Používateľské dopyty',
'reading' => 'Čítanie',
'search' => 'Hľadajte slová alebo #značky',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Zdieľanie',
'shortcuts' => 'Skratky',
'stats' => 'Štatistiky',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Stránky založené na Known',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Schránka',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'E-mail', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/sk/sub.php b/app/i18n/sk/sub.php
index f583f6ca0..3c980d202 100644
--- a/app/i18n/sk/sub.php
+++ b/app/i18n/sk/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath pre:',
),
'rss' => 'RSS / Atom (prednastavené)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Vymazať vyrovnáciu pamäť',
diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php
index 7220d6670..41f658879 100644
--- a/app/i18n/tr/conf.php
+++ b/app/i18n/tr/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => 'Görünüm',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => 'Alt çizgi',
'display_authors' => 'Yazarlar',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 bildirim zaman aşımı',
),
'show_nav_buttons' => 'Gezinti düğmelerini göster',
- 'theme' => 'Tema',
+ 'theme' => array(
+ '_' => 'Tema',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” teması şuan uygun değilç Lütfen başka bir tema seçin.',
'thumbnail' => array(
'label' => 'Önizleme',
@@ -57,6 +64,7 @@ return array(
'portrait' => 'Portre',
'square' => 'Kare',
),
+ 'timezone' => 'Time zone', // TODO
'title' => 'Görünüm',
'width' => array(
'content' => 'İçerik genişliği',
diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php
index 8839023e6..4b84d6c40 100644
--- a/app/i18n/tr/gen.php
+++ b/app/i18n/tr/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => 'Kullanıcı sorguları',
'reading' => 'Okuma',
'search' => 'Kelime veya #etiket ara',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => 'Paylaşım',
'shortcuts' => 'Kısayollar',
'stats' => 'İstatistikler',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => 'Bilinen siteler',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => 'Kopyala',
'diaspora' => 'Diaspora*', // IGNORE
'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php
index 056c059ac..3e03f667c 100644
--- a/app/i18n/tr/sub.php
+++ b/app/i18n/tr/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath:',
),
'rss' => 'RSS / Atom (varsayılan)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Önbelleği temizle',
diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php
index 46b4d190a..79bcde2e1 100644
--- a/app/i18n/zh-cn/admin.php
+++ b/app/i18n/zh-cn/admin.php
@@ -17,7 +17,7 @@ return array(
'api_enabled' => '允许 <abbr>API</abbr> 访问 <small>(用于手机应用)</small>',
'form' => '网页表单(传统方式, 需要 JavaScript)',
'http' => 'HTTP(面向启用 HTTPS 的高级用户)',
- 'none' => '无认证(危险)',
+ 'none' => '无(危险)',
'title' => '认证',
'token' => '认证口令',
'token_help' => '用于不经认证访问默认用户的 RSS 输出:',
@@ -26,7 +26,7 @@ return array(
),
'check_install' => array(
'cache' => array(
- 'nok' => '请检查 <em>./data/cache</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/cache</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'cache 目录权限正常',
),
'categories' => array(
@@ -39,27 +39,27 @@ return array(
),
'ctype' => array(
'nok' => '找不到字符类型检测库(php-ctype)',
- 'ok' => '已找到字符类型检测库 (php-ctype)',
+ 'ok' => '已找到字符类型检测库(ctype)',
),
'curl' => array(
- 'nok' => '找不到 cURL 库(php-cURL)',
- 'ok' => '已找到 cURL 库(php-cURL)',
+ 'nok' => '找不到 cURL 库(php-curl 包)',
+ 'ok' => '已找到 cURL 库',
),
'data' => array(
- 'nok' => '请检查 <em>./data</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'data 目录权限正常',
),
- 'database' => '数据库相关',
+ 'database' => '数据库安装',
'dom' => array(
- 'nok' => '找不到用于浏览 DOM 的库(php-xml)',
- 'ok' => '已找到用于浏览 DOM 的库(php-xml)',
+ 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)',
+ 'ok' => '已找到用于浏览 DOM 的库',
),
'entries' => array(
'nok' => 'Entry 表配置错误',
- 'ok' => 'Entry 表正常',
+ 'ok' => 'Entry 表配置正常',
),
'favicons' => array(
- 'nok' => '请检查 <em>./data/favicons</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/favicons</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'favicons 目录权限正常',
),
'feeds' => array(
@@ -67,46 +67,46 @@ return array(
'ok' => 'Feed 表正常',
),
'fileinfo' => array(
- 'nok' => '找不到 fileinfo 库(php-fileinfo)',
- 'ok' => '已找到 fileinfo 库(php-fileinfo)',
+ 'nok' => '找不到 PHP fileinfo 库(php-fileinfo 包)',
+ 'ok' => '已找到 fileinfo 库',
),
'files' => '文件相关',
'json' => array(
- 'nok' => '找不到 JSON 扩展(php-json )',
- 'ok' => '已找到 JSON 扩展(php-json)',
+ 'nok' => '找不到 JSON 扩展(php-json 包)',
+ 'ok' => '已找到 JSON 扩展',
),
'mbstring' => array(
- 'nok' => '找不到推荐的 Unicode 解析库(mbstring)',
- 'ok' => '已找到推荐的 Unicode 解析库(mbstring)',
+ 'nok' => '找不到推荐用于 Unicode 的 mbstring 库',
+ 'ok' => '已找到推荐用于 Unicode 的 mbstring 库',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库(php-pcre)',
- 'ok' => '已找到正则表达式解析库(php-pcre)',
+ 'ok' => '已找到正则表达式解析库(PCRE)',
),
'pdo' => array(
- 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
- 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
),
'php' => array(
- '_' => 'PHP 相关',
+ '_' => 'PHP 安装',
'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s',
'ok' => '你的 PHP 版本为 %s,与 FreshRSS 兼容',
),
'tables' => array(
'nok' => '数据库中缺少一个或多个表',
- 'ok' => '数据库中相关表存在',
+ 'ok' => '数据库中存在正确的表',
),
'title' => '环境检查',
'tokens' => array(
- 'nok' => '请检查 <em>./data/tokens</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/tokens</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'tokens 目录权限正常',
),
'users' => array(
- 'nok' => '请检查 <em>./data/users</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/users</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'users 目录权限正常',
),
'zip' => array(
- 'nok' => '找不到 ZIP 扩展(php-zip)',
+ 'nok' => '找不到 ZIP 扩展(php-zip 包)',
'ok' => '已找到 ZIP 扩展',
),
),
@@ -119,10 +119,10 @@ return array(
'enabled' => '已启用',
'latest' => '已安装',
'name' => '名称',
- 'no_configure_view' => '此扩展不能配置。',
+ 'no_configure_view' => '此扩展无法配置。',
'system' => array(
'_' => '系统扩展',
- 'no_rights' => '系统扩展(你无权修改)',
+ 'no_rights' => '系统扩展(你没有所需权限)',
),
'title' => '扩展',
'update' => '更新可用',
@@ -130,20 +130,20 @@ return array(
'version' => '版本',
),
'stats' => array(
- '_' => '统计',
+ '_' => '统计数据',
'all_feeds' => '所有订阅源',
'category' => '分类',
'entry_count' => '文章数',
'entry_per_category' => '各分类文章数',
- 'entry_per_day' => '近三十日每日文章数',
- 'entry_per_day_of_week' => '一周各日(平均:%.2f 条消息)',
- 'entry_per_hour' => '各小时(平均:%.2f 条消息)',
- 'entry_per_month' => '各月(平均:%.2f 条消息)',
+ 'entry_per_day' => '每日文章数(近三十日)',
+ 'entry_per_day_of_week' => '一周中(平均:%.2f 条消息)',
+ 'entry_per_hour' => '各小时(平均:%.2f 条消息)',
+ 'entry_per_month' => '各月(平均:%.2f 条消息)',
'entry_repartition' => '文章分布',
'feed' => '订阅源',
'feed_per_category' => '各分类订阅源数',
'idle' => '长期无更新订阅源',
- 'main' => '主要统计',
+ 'main' => '主要统计数据',
'main_stream' => '首页',
'no_idle' => '订阅源近期皆有更新!',
'number_entries' => '%d 篇文章',
@@ -158,9 +158,9 @@ return array(
),
'system' => array(
'_' => '系统配置',
- 'auto-update-url' => '自动升级服务器地址',
+ 'auto-update-url' => '自动更新服务器 URL',
'cookie-duration' => array(
- 'help' => '单位(秒)',
+ 'help' => '单位:秒',
'number' => '保持登录的时长',
),
'force_email_validation' => '强制验证邮箱地址',
@@ -178,8 +178,8 @@ return array(
),
),
'status' => array(
- 'disabled' => '注册表单禁用',
- 'enabled' => '注册表单启用',
+ 'disabled' => '注册表单已禁用',
+ 'enabled' => '注册表单已启用',
),
'title' => '用户注册表单',
),
@@ -191,7 +191,7 @@ return array(
'current_version' => '当前 FreshRSS 版本为 %s。',
'last' => '上次检查:%s',
'none' => '没有可用更新',
- 'title' => '系统更新',
+ 'title' => '更新系统',
),
'user' => array(
'admin' => '管理员',
diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php
index 8f8ef09ad..0be182cfb 100644
--- a/app/i18n/zh-cn/conf.php
+++ b/app/i18n/zh-cn/conf.php
@@ -13,17 +13,17 @@
return array(
'archiving' => array(
'_' => '归档',
- 'exception' => '高级清理策略',
- 'help' => '具体选项位于各订阅源的设置',
- 'keep_favourites' => '不清理已收藏的文章',
- 'keep_labels' => '不清理标签',
+ 'exception' => '清理例外',
+ 'help' => '更多可用选项位于各订阅源的设置',
+ 'keep_favourites' => '永不删除已收藏的文章',
+ 'keep_labels' => '永不删除标签',
'keep_max' => '最多保留的文章数',
'keep_min_by_feed' => '至少保留的文章数',
'keep_period' => '文章最多保留',
- 'keep_unreads' => '不清理未读文章',
- 'maintenance' => '优化',
+ 'keep_unreads' => '永不删除未读文章',
+ 'maintenance' => '维护',
'optimize' => '优化数据库',
- 'optimize_help' => '偶尔执行优化可以减少数据库大小',
+ 'optimize_help' => '偶尔执行可以减少数据库大小',
'policy' => '清理策略',
'policy_warning' => '如果未选择清理策略,则将保留全部文章。',
'purge_now' => '立即清除',
@@ -32,12 +32,13 @@ return array(
),
'display' => array(
'_' => '显示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '底栏',
'display_authors' => '作者',
'entry' => '文章图标',
'publication_date' => '更新日期',
- 'related_tags' => '相关标签',
+ 'related_tags' => '文章标签',
'sharing' => '分享',
'summary' => '摘要',
'top_line' => '顶栏',
@@ -48,15 +49,22 @@ return array(
'timeout' => 'HTML5 通知超时时间',
),
'show_nav_buttons' => '显示导航按钮',
- 'theme' => '主题',
+ 'theme' => array(
+ '_' => '主题',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 主题不再可用,请选择其他主题。',
'thumbnail' => array(
'label' => '缩略图',
- 'landscape' => '风景',
+ 'landscape' => '横向',
'none' => '无',
- 'portrait' => '肖像',
- 'square' => '方块',
+ 'portrait' => '纵向',
+ 'square' => '方形',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '显示',
'width' => array(
'content' => '内容宽度',
@@ -80,17 +88,17 @@ return array(
),
),
'profile' => array(
- '_' => '用户管理',
+ '_' => '账户管理',
'api' => 'API 管理',
'delete' => array(
'_' => '账户删除',
- 'warn' => '将删除你的帐户以及所有相关数据!',
+ 'warn' => '你的帐户以及所有相关数据将被删除。',
),
'email' => '邮箱地址',
'password_api' => 'API 密码<br /><small>(例如用于手机应用)</small>',
'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
'password_format' => '至少 7 个字符',
- 'title' => '用户帐户',
+ 'title' => '账户',
),
'query' => array(
'_' => '自定义查询',
@@ -135,67 +143,67 @@ return array(
),
'reading' => array(
'_' => '阅读',
- 'after_onread' => '「全部标记为已读」后',
+ 'after_onread' => '“全部标记为已读”后',
'always_show_favorites' => '默认显示收藏夹中所有的文章',
'article' => array(
'authors_date' => array(
'_' => '作者和日期',
- 'both' => '两者都显示',
- 'footer' => '仅页脚显示',
- 'header' => '仅页眉显示',
+ 'both' => '页脚与页眉',
+ 'footer' => '页脚',
+ 'header' => '页眉',
'none' => '不显示',
),
'feed_name' => array(
- 'above_title' => '在文章标题和标签上方',
+ 'above_title' => '在标题/标签上方',
'none' => '不显示',
'with_authors' => '与作者和日期一行',
),
'feed_title' => '订阅源标题',
'tags' => array(
'_' => '文章标签',
- 'both' => '两者都显示',
- 'footer' => '仅页脚显示',
- 'header' => '仅页眉显示',
+ 'both' => '页脚与页眉',
+ 'footer' => '页脚',
+ 'header' => '页眉',
'none' => '不显示',
),
'tags_max' => array(
'_' => '标签最多显示个数',
- 'help' => '0 标识显示所有标签',
+ 'help' => '0 表示:显示所有标签且不折叠',
),
),
'articles_per_page' => '每页文章数',
'auto_load_more' => '在页面底部载入更多文章',
'auto_remove_article' => '阅读后隐藏文章',
- 'confirm_enabled' => '「全部标记为已读」时显示确认对话框',
+ 'confirm_enabled' => '“全部标记为已读”时显示确认对话框',
'display_articles_unfolded' => '默认展开显示文章',
'display_categories_unfolded' => '展开的分类',
'headline' => array(
'articles' => '文章:打开/关闭',
'articles_header_footer' => '文章: 页眉/页脚',
- 'categories' => '左侧导航:分类',
+ 'categories' => '左侧导航栏:分类',
'mark_as_read' => '标为已读选项',
'misc' => '其它',
'view' => '浏览',
),
- 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源 (启用「显示所有文章」后不生效)',
+ 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源(启用“显示所有文章”后不生效)',
'img_with_lazyload' => '延迟加载图片',
'jump_next' => '跳转到下一未读项(订阅源或分类)',
- 'mark_updated_article_unread' => '将更新的文章设为未读',
+ 'mark_updated_article_unread' => '将有更新的文章设为未读',
'number_divided_when_reader' => '阅读视图中显示一半',
'read' => array(
'article_open_on_website' => '在打开原文章后',
'article_viewed' => '在文章被浏览后',
'keep_max_n_unread' => '未读最多保留 n 条',
'scroll' => '在滚动浏览后',
- 'upon_gone' => '在被原订阅源移除后',
+ 'upon_gone' => '在被原订阅源被移除后',
'upon_reception' => '在接收文章后',
'when' => '何时将文章标记为已读',
'when_same_title' => '已存在 n 条相同标题文章',
),
'show' => array(
'_' => '文章显示',
- 'active_category' => '激活的分类',
- 'adaptive' => '智能显示',
+ 'active_category' => '活跃的分类',
+ 'adaptive' => '自适应显示',
'all_articles' => '显示所有',
'all_categories' => '所有分类',
'no_category' => '无分类',
@@ -203,13 +211,13 @@ return array(
'unread' => '只显示未读',
),
'show_fav_unread_help' => '同样适用于标签',
- 'sides_close_article' => '点击文章区域外以关闭',
+ 'sides_close_article' => '点击文章文本区域外关闭文章',
'sort' => array(
'_' => '排列顺序',
'newer_first' => '由新至旧',
'older_first' => '由旧至新',
),
- 'sticky_post' => '打开文章时将其置于页首',
+ 'sticky_post' => '打开文章时将其置顶',
'title' => '阅读',
'view' => array(
'default' => '默认视图',
@@ -222,20 +230,20 @@ return array(
'_' => '分享',
'add' => '添加分享方式',
'blogotext' => 'Blogotext', // IGNORE
- 'deprecated' => '这项功能已废弃并在将来版本的 FreshRSS 中移除,详情请见 <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="Open documentation for more information" target="_blank">说明文档</a>.',
+ 'deprecated' => '此功能已被废弃并会在未来的 FreshRSS 版本中移除,详情见 <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="打开文档获更多信息" target="_blank">说明文档</a>.',
'diaspora' => 'Diaspora*', // IGNORE
- 'email' => '邮箱', // IGNORE
- 'facebook' => '脸书', // IGNORE
+ 'email' => 'Email', // IGNORE
+ 'facebook' => 'Facebook', // IGNORE
'more_information' => '更多信息',
'print' => '打印',
'raindrop' => 'Raindrop.io', // IGNORE
'remove' => '删除分享方式',
'shaarli' => 'Shaarli', // IGNORE
- 'share_name' => '名称',
- 'share_url' => '地址',
+ 'share_name' => '显示名称',
+ 'share_url' => '用于分享的 URL',
'title' => '分享',
- 'twitter' => '推特', // IGNORE
- 'wallabag' => 'Wallabag', // IGNORE
+ 'twitter' => 'Twitter', // IGNORE
+ 'wallabag' => 'wallabag', // IGNORE
),
'shortcut' => array(
'_' => '快捷键',
@@ -243,9 +251,9 @@ return array(
'auto_share' => '分享',
'auto_share_help' => '如果有多种分享方式,则会按照它们的序号依次访问。',
'close_dropdown' => '关闭菜单',
- 'collapse_article' => '收起文章',
+ 'collapse_article' => '折叠文章',
'first_article' => '打开第一篇文章',
- 'focus_search' => '聚焦到搜索框',
+ 'focus_search' => '访问搜索框',
'global_view' => '切换到全屏视图',
'help' => '显示帮助文档',
'javascript' => '若要使用快捷键,必须启用 JavaScript',
@@ -254,18 +262,18 @@ return array(
'mark_favorite' => '加入收藏',
'mark_read' => '设为已读',
'navigation' => '浏览',
- 'navigation_help' => '组合 <kbd>⇧ Shift</kbd> 键,浏览快捷键将生效于订阅源。<br/>组合 <kbd>Alt ⎇</kbd> 键,浏览快捷键将生效于分类。',
+ 'navigation_help' => '组合 <kbd>⇧ Shift</kbd> 键,导航快捷键将应用于订阅源。<br/>组合 <kbd>Alt ⎇</kbd> 键,导航快捷键将应用于分类。',
'navigation_no_mod_help' => '以下快捷键不支持组合键(Shift 或 Alt)',
'next_article' => '打开下一篇文章',
'next_unread_article' => '打开下一篇未读文章',
- 'non_standard' => '这些键 (<kbd>%s</kbd>) 可能不能作为快捷键',
+ 'non_standard' => '这些键(<kbd>%s</kbd>)可能不能作为快捷键',
'normal_view' => '切换到普通视图',
'other_action' => '其他操作',
'previous_article' => '打开上一篇文章',
'reading_view' => '切换到阅读视图',
'rss_view' => '切换到 RSS 视图',
'see_on_website' => '在原网站中查看',
- 'shift_for_all_read' => '组合 <kbd>Alt ⎇</kbd>键 将上方的文章标记为已读<br />组合 <kbd>⇧ Shift</kbd>按键 可以将全部文章设为已读',
+ 'shift_for_all_read' => '+ <kbd>Alt ⎇</kbd> 键将上方的文章标记为已读<br />+ <kbd>⇧ Shift</kbd> 键将所有文章设为已读',
'skip_next_article' => '跳转到下一篇文章而不打开',
'skip_previous_article' => '跳转到上一篇文章而不打开',
'title' => '快捷键',
@@ -275,7 +283,7 @@ return array(
'views' => '视图',
),
'user' => array(
- 'articles_and_size' => '%s 篇文章 (%s)',
+ 'articles_and_size' => '%s 篇文章(%s)',
'current' => '当前用户',
'is_admin' => '该用户为管理员',
'users' => '用户',
diff --git a/app/i18n/zh-cn/feedback.php b/app/i18n/zh-cn/feedback.php
index 020e70918..701471f4e 100644
--- a/app/i18n/zh-cn/feedback.php
+++ b/app/i18n/zh-cn/feedback.php
@@ -20,8 +20,8 @@ return array(
),
'api' => array(
'password' => array(
- 'failed' => '您的密码无法修改',
- 'updated' => '您的密码已修改',
+ 'failed' => '你的密码无法修改',
+ 'updated' => '你的密码已修改',
),
),
'auth' => array(
@@ -43,7 +43,7 @@ return array(
'already_enabled' => '%s 已启用',
'cannot_remove' => '无法删除 %s',
'disable' => array(
- 'ko' => '禁用 %s 失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
+ 'ko' => '无法禁用 %s。<a href="%s">检查 FreshRSS 日志</a> 查看详情。',
'ok' => '%s 现已禁用',
),
'enable' => array(
@@ -56,15 +56,15 @@ return array(
'removed' => '%s 已删除',
),
'import_export' => array(
- 'export_no_zip_extension' => '服务器未启用 ZIP 扩展。请尝试逐个导出文件。',
- 'feeds_imported' => '你的订阅已导入,即将刷新',
+ 'export_no_zip_extension' => '服务器未启用 ZIP 扩展,请尝试逐个导出文件。',
+ 'feeds_imported' => '你的订阅源已导入,即将刷新',
'feeds_imported_with_errors' => '你的订阅源已导入,但发生错误',
'file_cannot_be_uploaded' => '文件未能上传!',
'no_zip_extension' => '服务器未启用 ZIP 扩展。',
'zip_error' => '导入 ZIP 文件时出错',
),
'profile' => array(
- 'error' => '你的帐户修改失败',
+ 'error' => '你的帐户无法修改',
'updated' => '你的帐户已修改',
),
'sub' => array(
@@ -79,7 +79,7 @@ return array(
'emptied' => '已清空分类',
'error' => '更新分类失败',
'name_exists' => '分类名已存在',
- 'no_id' => '你必须明确分类编号',
+ 'no_id' => '你必须指定分类 ID',
'no_name' => '分类名不能为空',
'not_delete_default' => '你不能删除默认分类!',
'not_exist' => '分类不存在!',
@@ -94,21 +94,21 @@ return array(
'cache_cleared' => '<em>%s</em> 缓存已清理',
'deleted' => '已删除订阅源',
'error' => '订阅源更新失败',
- 'internal_problem' => '订阅源添加失败。<a href="%s">检查 FreshRSS 日志</a> 查看详情。你可以在地址链接后附加 <code>#force_feed</code> 从而尝试强制添加。',
- 'invalid_url' => '地址链接 <em>%s</em> 无效',
+ 'internal_problem' => '订阅源添加失败,<a href="%s">检查 FreshRSS 日志</a> 查看详情。你可以在 URL 后添加 <code>#force_feed</code> 尝试强制添加。',
+ 'invalid_url' => 'URL <em>%s</em> 无效',
'n_actualized' => '已更新 %d 个订阅源',
'n_entries_deleted' => '已删除 %d 篇文章',
- 'no_refresh' => '没有可刷新的订阅源…',
+ 'no_refresh' => '没有可刷新的订阅源',
'not_added' => '<em>%s</em> 添加失败',
'not_found' => '无法找到订阅',
'over_max' => '你已达到订阅源数上限(%d)',
- 'reloaded' => '<em>%s</em> 已重置',
+ 'reloaded' => '<em>%s</em> 已重新加载',
'selector_preview' => array(
'http_error' => '无法加载网站内容。',
- 'no_entries' => '您的订阅中没有任何条目。您至少需要一个条目来创建一个预览。',
+ 'no_entries' => '你的订阅中没有任何条目,你至少需要一个条目来创建一个预览。',
'no_feed' => '网络错误(订阅源不存在)',
- 'no_result' => '选择器没有匹配到任何东西。作为备用,原始的feed文本将被显示出来。',
- 'selector_empty' => '选择器是空的。你需要一个来创建预览。',
+ 'no_result' => '选择器没有匹配到任何东西,回退显示原始的订阅源文本。',
+ 'selector_empty' => '选择器是空的,你需要一个来创建预览。',
),
'updated' => '已更新订阅源',
),
@@ -122,10 +122,10 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS 将更新到 <strong>版本 %s</strong>。',
'error' => '更新出错:%s',
- 'file_is_nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'file_is_nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须拥有写入权限。',
'finished' => '更新完成!',
'none' => '没有可用更新',
- 'server_not_found' => '找不到更新服务器 [%s]',
+ 'server_not_found' => '找不到更新服务器。 [%s]',
),
'user' => array(
'created' => array(
diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php
index 2b2249db5..d4999e5b0 100644
--- a/app/i18n/zh-cn/gen.php
+++ b/app/i18n/zh-cn/gen.php
@@ -12,7 +12,7 @@
return array(
'action' => array(
- 'actualize' => '更新提要',
+ 'actualize' => '更新订阅源',
'add' => '添加',
'back' => '← 返回',
'back_to_rss_feeds' => '← 返回订阅源',
@@ -26,7 +26,7 @@ return array(
'export' => '导出',
'filter' => '过滤',
'import' => '导入',
- 'load_default_shortcuts' => '重置快捷键',
+ 'load_default_shortcuts' => '加载默认快捷键',
'manage' => '管理',
'mark_read' => '标记已读',
'open_url' => '打开链接',
@@ -38,7 +38,7 @@ return array(
'see_website' => '网站中查看',
'submit' => '提交',
'truncate' => '删除所有文章',
- 'update' => '更新订阅',
+ 'update' => '更新',
),
'auth' => array(
'accept_tos' => '我接受 <a href="%s">服务条款</a>',
@@ -127,7 +127,7 @@ return array(
'js' => array(
'category_empty' => '清空分类',
'confirm_action' => '你确定要执行此操作吗?这将不可撤销!',
- 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询。这将不可撤销!',
+ 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询,这将不可撤销!',
'feedback' => array(
'body_new_articles' => 'FreshRSS 中有 %%d 篇文章等待阅读。',
'body_unread_articles' => '(未读: %%d)',
@@ -174,13 +174,14 @@ return array(
'queries' => '自定义查询',
'reading' => '阅读',
'search' => '搜索内容或#标签',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '分享',
'shortcuts' => '快捷键',
'stats' => '统计',
'system' => '系统配置',
'update' => '更新',
'user_management' => '用户管理',
- 'user_profile' => '用户帐户',
+ 'user_profile' => '帐户',
),
'period' => array(
'days' => '天',
@@ -191,12 +192,14 @@ return array(
),
'share' => array(
'Known' => '基于 Known 的站点',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '剪贴板',
'diaspora' => 'Diaspora*', // IGNORE
- 'email' => '邮箱', // IGNORE
- 'facebook' => '脸书', // IGNORE
+ 'email' => 'Email', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - 兼容 Firefox)',
+ 'facebook' => 'Facebook', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
'lemmy' => 'Lemmy', // IGNORE
@@ -211,10 +214,10 @@ return array(
'raindrop' => 'Raindrop.io', // IGNORE
'reddit' => 'Reddit', // IGNORE
'shaarli' => 'Shaarli', // IGNORE
- 'twitter' => '推特', // IGNORE
+ 'twitter' => 'Twitter', // IGNORE
'wallabag' => 'Wallabag v1', // IGNORE
'wallabagv2' => 'Wallabag v2', // IGNORE
- 'web-sharing-api' => 'Web分享',
+ 'web-sharing-api' => '系统分享',
'whatsapp' => 'Whatsapp', // IGNORE
'xing' => 'Xing', // IGNORE
),
diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php
index 916140107..59d9ffb87 100644
--- a/app/i18n/zh-cn/index.php
+++ b/app/i18n/zh-cn/index.php
@@ -17,7 +17,7 @@ return array(
'bugs_reports' => '报告错误',
'credits' => '致谢',
'credits_content' => '某些设计元素来自于 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> ,尽管 FreshRSS 并没有使用此框架。<a href="https://gitlab.gnome.org/Archive/gnome-icon-theme-symbolic">图标</a> 来自于 <a href="https://www.gnome.org/">GNOME 项目</a>。<em>Open Sans</em> 字体出自 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> 之手。FreshRSS 基于 PHP 框架 <a href="https://framagit.org/marienfressinaud/MINZ">Minz</a>。',
- 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="https://github.com/LeedRSS/Leed">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。',
+ 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="https://github.com/LeedRSS/Leed">Leed</a>。 它不仅轻快易用,并且强大又易于配置。',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>',
'license' => '授权',
'project_website' => '项目网站',
@@ -25,8 +25,8 @@ return array(
'version' => '版本',
),
'feed' => array(
- 'add' => '你可以添加一些订阅源。',
- 'empty' => '暂时没有文章可显示。',
+ 'add' => '请添加一些订阅源。',
+ 'empty' => '没有文章可以显示。',
'rss_of' => '%s 的订阅源',
'title' => '首页',
'title_fav' => '收藏',
diff --git a/app/i18n/zh-cn/install.php b/app/i18n/zh-cn/install.php
index 8927674d2..1d9d61e38 100644
--- a/app/i18n/zh-cn/install.php
+++ b/app/i18n/zh-cn/install.php
@@ -21,7 +21,7 @@ return array(
'auth' => array(
'form' => '网页表单(传统方式, 依赖 JavaScript)',
'http' => 'HTTP(面向启用 HTTPS 的高级用户)',
- 'none' => '无认证(危险)',
+ 'none' => '无(危险)',
'password_form' => '密码<br /><small>(用于网页表单登录方式)</small>',
'password_format' => '至少 7 个字符',
'type' => '认证方式',
@@ -30,61 +30,61 @@ return array(
'_' => '数据库',
'conf' => array(
'_' => '数据库配置',
- 'ko' => '请验证你的数据库信息',
+ 'ko' => '验证你的数据库信息',
'ok' => '数据库配置已保存',
),
'host' => '主机',
- 'password' => '密码',
+ 'password' => '数据库密码',
'prefix' => '表前缀',
'type' => '数据库类型',
- 'username' => '用户名',
+ 'username' => '数据库用户名',
),
'check' => array(
'_' => '检查',
'already_installed' => '我们检测到 FreshRSS 已经安装!',
'cache' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'cache 目录权限正常',
),
'ctype' => array(
'nok' => '找不到字符类型检测库(php-ctype)',
- 'ok' => '已找到字符类型检测库',
+ 'ok' => '已找到字符类型检测库(ctype)',
),
'curl' => array(
- 'nok' => '找不到 cURL 库(php-curl)',
+ 'nok' => '找不到 cURL 库(php-curl 包)',
'ok' => '已找到 cURL 库',
),
'data' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'data 目录权限正常',
),
'dom' => array(
- 'nok' => '找不到用于浏览 DOM 的库(php-xml)',
+ 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)',
'ok' => '已找到用于浏览 DOM 的库',
),
'favicons' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>./data/favicons</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'favicons 目录权限正常',
),
'fileinfo' => array(
- 'nok' => '找不到 PHP fileinfo 库(php-fileinfo)',
+ 'nok' => '找不到 PHP fileinfo 库(fileinfo 包)',
'ok' => '已找到 fileinfo 库',
),
'json' => array(
- 'nok' => '找不到推荐的 JSON 解析库',
- 'ok' => '已找到推荐的 JSON 解析库',
+ 'nok' => '找不到 JSON 扩展(php-json 包)',
+ 'ok' => '已找到 JSON 扩展',
),
'mbstring' => array(
- 'nok' => '找不到推荐的 Unicode 解析库(mbstring)',
- 'ok' => '已找到推荐的 Unicode 解析库',
+ 'nok' => '找不到推荐用于 Unicode 的 mbstring 库',
+ 'ok' => '已找到推荐用于 Unicode 的 mbstring 库',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库(php-pcre)',
- 'ok' => '已找到正则表达式解析库',
+ 'ok' => '已找到正则表达式解析库(PCRE)',
),
'pdo' => array(
- 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
- 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
+ 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)',
),
'php' => array(
'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s',
@@ -92,12 +92,12 @@ return array(
),
'reload' => '再检查一遍',
'tmp' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => '缓存目录权限正常。',
),
'unknown_process_username' => '未知',
'users' => array(
- 'nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
+ 'nok' => '请检查 <em>%s</em> 目录权限,HTTP 服务器必须拥有写入权限。',
'ok' => 'users 目录权限正常',
),
'xml' => array(
@@ -114,7 +114,7 @@ return array(
'fix_errors_before' => '请在继续下一步前修复错误',
'javascript_is_better' => '启用 JavaScript 会使 FreshRSS 工作得更好',
'js' => array(
- 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置。你确定要继续吗?',
+ 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置,你确定要继续吗?',
),
'language' => array(
'_' => '语言',
diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php
index 4ad401329..5e6e570a9 100644
--- a/app/i18n/zh-cn/sub.php
+++ b/app/i18n/zh-cn/sub.php
@@ -16,9 +16,9 @@ return array(
'title' => 'API', // IGNORE
),
'bookmarklet' => array(
- 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮',
+ 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮。',
'label' => '订阅',
- 'title' => '书签应用',
+ 'title' => '书签',
),
'category' => array(
'_' => '分类',
@@ -26,18 +26,18 @@ return array(
'archiving' => '归档',
'dynamic_opml' => array(
'_' => '动态订阅',
- 'help' => '使用地址上的 <a href="http://opml.org/" target="_blank">OPML 文件</a> 中的订阅源填充这一分类',
+ 'help' => '使用 URL 上的 <a href="http://opml.org/" target="_blank">OPML 文件</a> 中的订阅源填充这一分类',
),
'empty' => '空分类',
'information' => '信息',
- 'opml_url' => 'OPML 地址',
+ 'opml_url' => 'OPML URL', // IGNORE
'position' => '显示位置',
'position_help' => '控制分类排列顺序',
'title' => '标题',
),
'feed' => array(
'accept_cookies' => '接受 Cookies',
- 'accept_cookies_help' => '允许提要服务器设置 Cookies(仅在请求期间存储在内存中)',
+ 'accept_cookies_help' => '允许订阅源服务器设置 Cookies(仅在请求期间存储在内存中)',
'add' => '添加订阅源',
'advanced' => '高级',
'archiving' => '归档',
@@ -77,8 +77,8 @@ return array(
'html_xpath' => array(
'_' => 'HTML + XPath (Web 抓取)',
'feed_title' => array(
- '_' => '提要标题',
- 'help' => '如 <code>//title</code> 或是静态字符串如 <code>"My custom feed"</code>',
+ '_' => '订阅源标题',
+ 'help' => '如 <code>//title</code> 或是静态字符串如: <code>"My custom feed"</code>',
),
'help' => '<dfn><a href="https://www.w3.org/TR/xpath-10/" target="_blank">XPath 1.0</a></dfn> 是为资深用户准备的标准查询语言,FreshRSS 用以实现 Web 抓取.',
'item' => array(
@@ -99,8 +99,8 @@ return array(
'help' => '例如 <code>descendant::img/@src</code>',
),
'item_timeFormat' => array(
- '_' => 'Custom date/time format', // TODO
- 'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>', // TODO
+ '_' => '自定义日期/时间格式',
+ 'help' => '可选项, 格式参见 <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> 例如 <code>d-m-Y H:i:s</code>',
),
'item_timestamp' => array(
'_' => '文章日期:',
@@ -111,7 +111,7 @@ return array(
'help' => '注意使用 <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath 轴</a> <code>descendant::</code>,例如 <code>descendant::h2</code>',
),
'item_uid' => array(
- '_' => '文章唯一标识',
+ '_' => '文章唯一 ID',
'help' => '可选,例如: <code>descendant::div/@data-uri</code>',
),
'item_uri' => array(
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath 定位:',
),
'rss' => 'RSS / Atom (默认)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '清理缓存',
@@ -175,8 +176,8 @@ return array(
'export_opml' => '导出订阅源列表(OPML)',
'export_starred' => '导出你的收藏',
'feed_list' => '%s 文章列表',
- 'file_to_import' => '需要导入的文件<br />(OPML、JSON 或 ZIP)',
- 'file_to_import_no_zip' => '需要导入的文件<br />(OPML 或 JSON)',
+ 'file_to_import' => '需要导入的文件 <br />(OPML、JSON 或 ZIP)',
+ 'file_to_import_no_zip' => '需要导入的文件 <br />(OPML 或 JSON)',
'import' => '导入',
'starred_list' => '收藏文章列表',
'title' => '导入/导出',
diff --git a/app/i18n/zh-cn/user.php b/app/i18n/zh-cn/user.php
index 8b4d35a7f..8a096b985 100644
--- a/app/i18n/zh-cn/user.php
+++ b/app/i18n/zh-cn/user.php
@@ -13,21 +13,21 @@
return array(
'email' => array(
'feedback' => array(
- 'invalid' => '电子邮箱地址无效',
+ 'invalid' => '邮箱地址无效',
'required' => '必须填写邮箱地址',
),
'validation' => array(
- 'change_email' => '您可以在 <a href="%s">用户管理</a> 中变更您的邮箱地址',
- 'email_sent_to' => '我们已通过 <strong>%s</strong> 发送验证邮件给您,请按其中指示来验证邮箱地址。',
+ 'change_email' => '你可以在 <a href="%s">用户管理</a> 中变更你的邮箱地址',
+ 'email_sent_to' => '我们已通过 <strong>%s</strong> 发送验证邮件给你,请按其中指示来验证邮箱地址。',
'feedback' => array(
- 'email_failed' => '由于服务器配置错误,我们无法向您发送邮件。',
- 'email_sent' => '邮件已发送到您的邮箱中',
+ 'email_failed' => '由于服务器配置错误,我们无法向你发送邮件。',
+ 'email_sent' => '邮件已发送到你的邮箱中',
'error' => '邮箱地址无法通过验证',
'ok' => '邮箱地址已成功通过验证',
'unnecessary' => '该邮箱地址已被验证',
'wrong_token' => '由于令牌错误,邮箱地址无法通过验证。',
),
- 'need_to' => '您需要先验证邮箱地址才能使用 %s',
+ 'need_to' => '你需要先验证邮箱地址才能使用 %s',
'resend_email' => '重发邮件',
'title' => '验证邮箱地址',
),
@@ -35,8 +35,8 @@ return array(
'mailer' => array(
'email_need_validation' => array(
'body' => '%s,欢迎',
- 'title' => '您需要验证您的帐户',
- 'welcome' => '您已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:',
+ 'title' => '你需要验证你的帐户',
+ 'welcome' => '你已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:',
),
),
'password' => array(
@@ -44,7 +44,7 @@ return array(
),
'tos' => array(
'feedback' => array(
- 'invalid' => '您必须接受服务条款才能注册',
+ 'invalid' => '你必须接受服务条款才能注册',
),
),
'username' => array(
diff --git a/app/i18n/zh-tw/conf.php b/app/i18n/zh-tw/conf.php
index 15fabaa40..34439c01b 100644
--- a/app/i18n/zh-tw/conf.php
+++ b/app/i18n/zh-tw/conf.php
@@ -32,6 +32,7 @@ return array(
),
'display' => array(
'_' => '顯示',
+ 'darkMode' => 'Automatic dark mode (beta)', // TODO
'icon' => array(
'bottom_line' => '底欄',
'display_authors' => '作者',
@@ -48,7 +49,13 @@ return array(
'timeout' => 'HTML5 通知超時時間',
),
'show_nav_buttons' => '顯示導航按鈕',
- 'theme' => '主題',
+ 'theme' => array(
+ '_' => '主題',
+ 'deprecated' => array(
+ '_' => 'Deprecated', // TODO
+ 'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
+ ),
+ ),
'theme_not_available' => '“%s” 主題不再可用,請選擇其他主題。',
'thumbnail' => array(
'label' => '縮圖',
@@ -57,6 +64,7 @@ return array(
'portrait' => '肖像',
'square' => '方塊',
),
+ 'timezone' => 'Time zone', // TODO
'title' => '顯示',
'width' => array(
'content' => '內容寬度',
diff --git a/app/i18n/zh-tw/gen.php b/app/i18n/zh-tw/gen.php
index 1dcd94eeb..3ef8bca44 100644
--- a/app/i18n/zh-tw/gen.php
+++ b/app/i18n/zh-tw/gen.php
@@ -174,6 +174,7 @@ return array(
'queries' => '自定義查詢',
'reading' => '閱讀',
'search' => '搜尋內容或#標簽',
+ 'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
'sharing' => '分享',
'shortcuts' => '快捷鍵',
'stats' => '統計',
@@ -191,11 +192,13 @@ return array(
),
'share' => array(
'Known' => '基於 Known 的站點',
+ 'archiveORG' => 'archive.org', // IGNORE
'archivePH' => 'archive.ph', // IGNORE
'blogotext' => 'Blogotext', // IGNORE
'clipboard' => '剪貼板',
'diaspora' => 'Diaspora*', // IGNORE
'email' => '郵箱', // IGNORE
+ 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
'facebook' => '臉書', // IGNORE
'gnusocial' => 'GNU social', // IGNORE
'jdh' => 'Journal du hacker', // IGNORE
diff --git a/app/i18n/zh-tw/sub.php b/app/i18n/zh-tw/sub.php
index dddcb2661..8a255645d 100644
--- a/app/i18n/zh-tw/sub.php
+++ b/app/i18n/zh-tw/sub.php
@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath 定位:',
),
'rss' => 'RSS / Atom (默認)',
+ 'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '清理暫存',
diff --git a/app/install.php b/app/install.php
index 9d0d855b8..3163367f4 100644
--- a/app/install.php
+++ b/app/install.php
@@ -283,11 +283,7 @@ function freshrss_already_installed() {
// A configuration file already exists, we try to load it.
$system_conf = null;
try {
- Minz_Configuration::register('system', $conf_path);
- /**
- * @var FreshRSS_SystemConfiguration $system_conf
- */
- $system_conf = Minz_Configuration::get('system');
+ $system_conf = FreshRSS_SystemConfiguration::init($conf_path);
} catch (Minz_FileNotExistException $e) {
return false;
}
@@ -295,7 +291,7 @@ function freshrss_already_installed() {
// ok, the global conf exists… but what about default user conf?
$current_user = $system_conf->default_user;
try {
- Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'));
+ FreshRSS_UserConfiguration::init(USERS_PATH . '/' . $current_user . '/config.php');
} catch (Minz_FileNotExistException $e) {
return false;
}
@@ -449,7 +445,7 @@ function printStep1() {
<?php } else { ?>
<p class="alert alert-error"><?= _t('install.action.fix_errors_before') ?></p>
<a id="actualize" class="btn" href="./index.php?step=1" title="<?= _t('install.check.reload') ?>">
- <img class="icon" src="../themes/icons/refresh.svg" alt="🔃" />
+ <img class="icon" src="../themes/icons/refresh.svg" alt="🔃" loading="lazy" />
</a>
<?php } ?>
<?php
@@ -680,7 +676,7 @@ if (_t('gen.dir') === 'rtl') {
<div class="item title">
<div id="logo-wrapper">
<a href="./">
- <img class="logo" src="../themes/icons/FreshRSS-logo.svg" alt="">
+ <img class="logo" src="../themes/icons/FreshRSS-logo.svg" alt="" loading="lazy">
</a>
</div>
</div>
diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml
index 5f1762834..03b8108f7 100644
--- a/app/layout/aside_configure.phtml
+++ b/app/layout/aside_configure.phtml
@@ -1,72 +1,90 @@
<nav class="nav nav-list aside" id="aside_feed">
<a class="toggle_aside" href="#close"><?= _i('close') ?></a>
-
+
<ul>
- <li class="nav-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8')?></li>
- <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'profile' ? ' active' : '' ?>">
- <a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a>
- </li>
- <li class="item">
- <a class="signout" href="<?= _url('auth', 'logout') ?>">
- <?php
- echo _t('gen.auth.logout'); ?> <?= _i('logout') ?></a>
- </li>
- <li class="nav-header"><?= _t('gen.menu.configuration') ?></li>
- <li class="item<?= Minz_Request::actionName() === 'display' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'reading' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'archiving' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'integration' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'shortcut' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'queries' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'extension' ? ' active' : '' ?>">
- <a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a>
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8')?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'profile' ? ' active' : '' ?>">
+ <a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a>
+ </li>
+ <li class="item">
+ <a class="signout" href="<?= _url('auth', 'logout') ?>">
+ <?php
+ echo _t('gen.auth.logout'); ?> <?= _i('logout') ?></a>
+ </li>
+ </ul>
</li>
- <?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.configuration') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'display' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'reading' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'archiving' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'integration' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'shortcut' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'queries' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'extension' ? ' active' : '' ?>">
+ <a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a>
+ </li>
+ <?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
+ <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ </li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
+ </ul>
</li>
- <?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="nav-header"><?= _t('gen.menu.admin') ?></li>
- <li class="item<?= Minz_Request::actionName() === 'system' ? ' active' : '' ?>">
- <a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'manage' ? ' active' : '' ?>">
- <a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'auth' ? ' active' : '' ?>">
- <a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a>
- </li>
- <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'checkInstall' ? ' active' : '' ?>">
- <a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a>
- </li>
- <?php if (!Minz_Configuration::get('system')->disable_update) { ?>
- <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
- <a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ <li class="item nav-section">
+ <div class="item nav-header"><?= _t('gen.menu.admin') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'system' ? ' active' : '' ?>">
+ <a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'manage' ? ' active' : '' ?>">
+ <a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'auth' ? ' active' : '' ?>">
+ <a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'checkInstall' ? ' active' : '' ?>">
+ <a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a>
+ </li>
+ <?php if (!FreshRSS_Context::$system_conf->disable_update) { ?>
+ <li class="item<?= Minz_Request::controllerName() === 'update' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() === 'logs' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a>
+ </li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ </ul>
</li>
<?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
- <?php } ?>
- <li class="nav-header"><!-- empty headline --></li>
- <li class="item<?= Minz_Request::actionName() === 'about' ? ' active' : '' ?>">
- <a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a>
+
+ <li class="item nav-section">
+ <div class="item nav-header"><!-- empty headline --></div>
+ <ul>
+ <li class="item<?= Minz_Request::actionName() === 'about' ? ' active' : '' ?>">
+ <a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a>
+ </li>
+ </ul>
</li>
</ul>
</nav>
diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml
index 3c4f1ec2e..bb9d678dc 100644
--- a/app/layout/aside_feed.phtml
+++ b/app/layout/aside_feed.phtml
@@ -37,7 +37,7 @@
<li class="tree-folder category favorites<?= FreshRSS_Context::isCurrentGet('s') ? ' active' : '' ?>">
<div class="tree-folder-title">
- <?= _i('bookmark') ?>
+ <?= _i('starred') ?>
<a class="title" data-unread="<?= format_number(FreshRSS_Context::$total_starred['unread']) ?>" href="<?= _url('index', $actual_view, 'get', 's') . $state_filter_manual ?>">
<?= _t('index.menu.favorites', format_number(FreshRSS_Context::$total_starred['all'])) ?>
</a>
diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml
index aa7857f74..e1f520f34 100644
--- a/app/layout/aside_subscription.phtml
+++ b/app/layout/aside_subscription.phtml
@@ -1,38 +1,45 @@
<nav class="nav nav-list aside" id="aside_feed">
<a class="toggle_aside" href="#close"><?= _i('close') ?></a>
<ul>
- <li class="nav-header"><?= _t('sub.menu.subscription_management') ?></li>
+ <li class="item nav-section">
+ <div class="nav-header"><?= _t('sub.menu.subscription_management') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'add' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.menu.add') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'add' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'add') ?>"><?= _t('sub.menu.add') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'index') ?>"><?= _t('sub.menu.subscription_management') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'index' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'index') ?>"><?= _t('sub.menu.subscription_management') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'tag' ? ' active' : '' ?>">
+ <a href="<?= _url('tag', 'index') ?>"><?= _t('sub.menu.label_management') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'tag' ? ' active' : '' ?>">
- <a href="<?= _url('tag', 'index') ?>"><?= _t('sub.menu.label_management') ?></a>
- </li>
+ <li class="item<?= Minz_Request::controllerName() === 'importExport' ? ' active' : '' ?>">
+ <a href="<?= _url('importExport', 'index') ?>"><?= _t('sub.menu.import_export') ?></a>
+ </li>
- <li class="item<?= Minz_Request::controllerName() === 'importExport' ? ' active' : '' ?>">
- <a href="<?= _url('importExport', 'index') ?>"><?= _t('sub.menu.import_export') ?></a>
+ <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'bookmarklet' ? ' active' : '' ?>">
+ <a href="<?= _url('subscription', 'bookmarklet') ?>"><?= _t('sub.menu.subscription_tools') ?></a>
+ </li>
+ </ul>
</li>
- <li class="item<?= Minz_Request::controllerName() === 'subscription' && Minz_Request::actionName() === 'bookmarklet' ? ' active' : '' ?>">
- <a href="<?= _url('subscription', 'bookmarklet') ?>"><?= _t('sub.menu.subscription_tools') ?></a>
- </li>
- <li class="nav-header"><?= _t('admin.stats') ?></li>
- <li class="item<?= Minz_Request::controllerName() == 'stats' && Minz_Request::actionName() == 'index' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'index') ?>"><?= _t('sub.menu.stats.main') ?></a>
+ <li class="item nav-section">
+ <div class="nav-header"><?= _t('admin.stats') ?></div>
+ <ul>
+ <li class="item<?= Minz_Request::controllerName() == 'stats' && Minz_Request::actionName() == 'index' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'index') ?>"><?= _t('sub.menu.stats.main') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() == 'idle' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'idle') ?>"><?= _t('sub.menu.stats.idle') ?></a>
+ </li>
+ <li class="item<?= Minz_Request::actionName() == 'repartition' ? ' active' : '' ?>">
+ <a href="<?= _url('stats', 'repartition') ?>"><?= _t('sub.menu.stats.repartition') ?></a>
+ </li>
+ </ul>
</li>
- <li class="item<?= Minz_Request::actionName() == 'idle' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'idle') ?>"><?= _t('sub.menu.stats.idle') ?></a>
- </li>
- <li class="item<?= Minz_Request::actionName() == 'repartition' ? ' active' : '' ?>">
- <a href="<?= _url('stats', 'repartition') ?>"><?= _t('sub.menu.stats.repartition') ?></a>
- </li>
-
</ul>
</nav>
<a class="close-aside" href="#close">❌</a>
diff --git a/app/layout/header.phtml b/app/layout/header.phtml
index f8e54c7ce..0a49d5992 100644
--- a/app/layout/header.phtml
+++ b/app/layout/header.phtml
@@ -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', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
+ <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
} else {
echo FreshRSS_Context::$system_conf->logo_html;
@@ -15,10 +15,15 @@
<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) { ?>
<form action="<?= _url('index', 'index') ?>" method="get">
<div class="stick">
- <input type="search" name="search" id="search" class="extend"
+ <input type="search" name="search" id="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
placeholder="<?= _t('gen.menu.search') ?>" />
+ <?php $param_a = Minz_Request::actionName(); ?>
+ <?php if (in_array($param_a, ['normal', 'global', 'reader'])) { ?>
+ <input type="hidden" name="a" value="<?= $param_a ?>" />
+ <?php } ?>
+
<?php $get = Minz_Request::param('get', ''); ?>
<?php if ($get != '') { ?>
<input type="hidden" name="get" value="<?= $get ?>" />
@@ -48,38 +53,59 @@
<ul class="dropdown-menu scrollbar-thin">
<li class="dropdown-header-close"><a class="toggle_aside" href="#close"><?= _i('close') ?></a></li>
- <li class="dropdown-header"><?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?></li>
- <li class="item"><a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a></li>
- <?php if (FreshRSS_Auth::accessNeedsAction()): ?>
- <li class="item"><a class="signout" href="<?= _url('auth', 'logout') ?>"><?= _t('gen.auth.logout'); ?><?= _i('logout') ?></a></li>
- <?php else: ?>
- <li class="item"><span class="signout">(<?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>)</span></li>
- <?php endif; ?>
- <li class="dropdown-header"><?= _t('gen.menu.configuration') ?></li>
- <li class="item"><a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a></li>
- <li class="item"><a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a></li>
- <li class="item"><a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a></li>
- <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
-
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.account') ?>: <?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('user', 'profile') ?>"><?= _t('gen.menu.user_profile') ?></a></li>
+ <?php if (FreshRSS_Auth::accessNeedsAction()): ?>
+ <li class="item"><a class="signout" href="<?= _url('auth', 'logout') ?>"><?= _t('gen.auth.logout'); ?><?= _i('logout') ?></a></li>
+ <?php else: ?>
+ <li class="item"><span class="signout">(<?= htmlspecialchars(Minz_Session::param('currentUser', '_'), ENT_NOQUOTES, 'UTF-8') ?>)</span></li>
+ <?php endif; ?>
+ </ul>
+ </li>
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.configuration') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('configure', 'display') ?>"><?= _t('gen.menu.display') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'reading') ?>"><?= _t('gen.menu.reading') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'archiving') ?>"><?= _t('gen.menu.archiving') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'integration') ?>"><?= _t('gen.menu.sharing') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'shortcut') ?>"><?= _t('gen.menu.shortcuts') ?></a></li>
+ <li class="item"><a href="<?= _url('configure', 'queries') ?>"><?= _t('gen.menu.queries') ?></a></li>
+ <li class="item"><a href="<?= _url('extension', 'index') ?>"><?= _t('gen.menu.extensions') ?></a></li>
+ <?= Minz_ExtensionManager::callHook('menu_configuration_entry') ?>
+ </ul>
+ </li>
<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
- <li class="dropdown-header"><?= _t('gen.menu.admin') ?></li>
- <li class="item"><a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a></li>
- <li class="item"><a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a></li>
- <li class="item"><a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a></li>
- <li class="item"><a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a></li>
- <?php if (!Minz_Configuration::get('system')->disable_update) { ?>
- <li class="item"><a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a></li>
- <?php } ?>
- <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ <li class="item dropdown-section">
+ <div class="dropdown-section-title">
+ <?= _t('gen.menu.admin') ?>
+ </div>
+ <ul>
+ <li class="item"><a href="<?= _url('configure', 'system') ?>"><?= _t('gen.menu.system') ?></a></li>
+ <li class="item"><a href="<?= _url('user', 'manage') ?>"><?= _t('gen.menu.user_management') ?></a></li>
+ <li class="item"><a href="<?= _url('auth', 'index') ?>"><?= _t('gen.menu.authentication') ?></a></li>
+ <li class="item"><a href="<?= _url('update', 'checkInstall') ?>"><?= _t('gen.menu.check_install') ?></a></li>
+ <?php if (!FreshRSS_Context::$system_conf->disable_update) { ?>
+ <li class="item"><a href="<?= _url('update', 'index') ?>"><?= _t('gen.menu.update') ?></a></li>
+ <?php } ?>
+ <?= Minz_ExtensionManager::callHook('menu_admin_entry') ?>
+ </ul>
+ </li>
<?php } ?>
- <li class="item"><a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a></li>
- <li class="item"><a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a></li>
- <?= Minz_ExtensionManager::callHook('menu_other_entry') ?>
+ <li class="item dropdown-section">
+ <ul>
+ <li class="item"><a href="<?= _url('index', 'logs') ?>"><?= _t('gen.menu.logs') ?></a></li>
+ <li class="item"><a href="<?= _url('index', 'about') ?>"><?= _t('gen.menu.about') ?></a></li>
+ <?= Minz_ExtensionManager::callHook('menu_other_entry') ?>
+ </ul>
+ </li>
</ul>
<a class="dropdown-close" href="#close">❌</a>
</div>
diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml
index 48ada7183..1e9ce6905 100644
--- a/app/layout/layout.phtml
+++ b/app/layout/layout.phtml
@@ -52,7 +52,7 @@ if (_t('gen.dir') === 'rtl') {
<meta name="robots" content="noindex,nofollow" />
<?php } ?>
</head>
- <body class="<?= Minz_Request::actionName() ?>">
+ <body class="<?= Minz_Request::actionName() ?><?= (FreshRSS_Context::$user_conf->darkMode === 'no') ? '' : ' darkMode_' . FreshRSS_Context::$user_conf->darkMode ?>">
<?php
if (!Minz_Request::param('ajax')) {
flush();
diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml
index e8c4170c9..848144568 100644
--- a/app/layout/nav_menu.phtml
+++ b/app/layout/nav_menu.phtml
@@ -29,10 +29,53 @@
href="<?= Minz_Url::display($url_state) ?>"><?= _i($state_str) ?></a>
<?php } ?>
+ <div class="dropdown only-mobile" id="dropdown-search-wrapper">
+ <input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+ <div id="dropdown-search" class="dropdown-target"></div>
+
+ <a id="toggle-search" class="dropdown-toggle btn<?= (strlen(FreshRSS_Context::$search) > 0) ? ' active' : ''; ?>" title="<?= _t('gen.menu.search') ?>"
+ href="#dropdown-search"><?= _i('search') ?></a>
+ <ul class="dropdown-menu">
+ <li class="item">
+ <span>
+ <form action="<?= _url('index', 'index') ?>" method="get">
+ <?php $param_a = Minz_Request::actionName(); ?>
+ <?php if (in_array($param_a, ['normal', 'global', 'reader'])) { ?>
+ <input type="hidden" name="a" value="<?= $param_a ?>" />
+ <?php } ?>
+
+ <?php $get = Minz_Request::param('get', ''); ?>
+ <?php if ($get != '') { ?>
+ <input type="hidden" name="get" value="<?= $get ?>" />
+ <?php } ?>
+
+ <?php $order = Minz_Request::param('order', ''); ?>
+ <?php if ($order != '') { ?>
+ <input type="hidden" name="order" value="<?= $order ?>" />
+ <?php } ?>
+
+ <?php $state = Minz_Request::param('state', ''); ?>
+ <?php if ($state != '') { ?>
+ <input type="hidden" name="state" value="<?= $state ?>" />
+ <?php } ?>
+
+ <div class="stick search">
+ <input type="search" name="search"
+ value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>"
+ placeholder="<?= _t('gen.menu.search') ?>" title="<?= _t('gen.menu.search') ?>" /><button class="btn" type="submit" title="<?= _t('index.menu.search_short') ?>"><?= _i('search') ?></button>
+ </div>
+ <p class="help"><?= _i('help') ?> <?= _t('gen.menu.search_help') ?></a></p>
+ </form>
+ </span>
+ </li>
+ </ul>
+ <a class="dropdown-close" href="#close">❌</a>
+ </div>
+
<div class="dropdown">
<div id="dropdown-query" class="dropdown-target"></div>
- <a class="dropdown-toggle btn" href="#dropdown-query" title="<?= _t('index.menu.queries') ?>"><?= _i('bookmark-tag') ?></a>
+ <a id="toggle-userqueries" class="dropdown-toggle btn" href="#dropdown-query" title="<?= _t('index.menu.queries') ?>"><?= _i('bookmark-tag') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header">
<?= _t('index.menu.queries') ?>
@@ -185,28 +228,6 @@
</div>
<?php } ?>
- <div class="item search">
- <form action="<?= _url('index', 'index') ?>" method="get">
- <input type="search" name="search" class="extend" value="<?php
- echo htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search, ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>" placeholder="<?= _t('index.menu.search_short') ?>" />
-
- <?php $get = Minz_Request::param('get', ''); ?>
- <?php if($get != '') { ?>
- <input type="hidden" name="get" value="<?= $get ?>" />
- <?php } ?>
-
- <?php $order = Minz_Request::param('order', ''); ?>
- <?php if($order != '') { ?>
- <input type="hidden" name="order" value="<?= $order ?>" />
- <?php } ?>
-
- <?php $state = Minz_Request::param('state', ''); ?>
- <?php if($state != '') { ?>
- <input type="hidden" name="state" value="<?= $state ?>" />
- <?php } ?>
- </form>
- </div>
-
<?php
if (FreshRSS_Context::$order === 'DESC') {
$order = 'ASC';
diff --git a/app/layout/simple.phtml b/app/layout/simple.phtml
index 8a2ee14bb..c9d209999 100644
--- a/app/layout/simple.phtml
+++ b/app/layout/simple.phtml
@@ -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', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" />
+ <img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
} else {
echo FreshRSS_Context::$system_conf->logo_html;
diff --git a/app/shares.php b/app/shares.php
index 8685dba5d..117cd4dce 100644
--- a/app/shares.php
+++ b/app/shares.php
@@ -26,6 +26,13 @@
*/
return array(
+ 'archiveORG' => array(
+ 'url' => 'https://web.archive.org/save/~LINK~',
+ 'transform' => array(),
+ 'help' => 'https://web.archive.org',
+ 'form' => 'simple',
+ 'method' => 'GET',
+ ),
'archivePH' => array(
'url' => 'https://archive.ph/submit/?url=~LINK~',
'transform' => array(),
@@ -61,6 +68,12 @@ return array(
'form' => 'simple',
'method' => 'GET',
),
+ 'email-webmail-firefox-fix' => array( // see https://github.com/FreshRSS/FreshRSS/issues/2666
+ 'url' => 'mailto:?subject=~TITLE~&amp;body=~LINK~',
+ 'transform' => array('rawurlencode'),
+ 'form' => 'simple',
+ 'method' => 'GET',
+ ),
'facebook' => array(
'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&amp;t=~TITLE~',
'transform' => array('rawurlencode'),
@@ -88,7 +101,7 @@ return array(
'method' => 'GET',
),
'lemmy' => array(
- 'url' => '~URL~/create_post?url=~LINK~&name=~TITLE~',
+ 'url' => '~URL~/create_post?url=~LINK~&title=~TITLE~',
'transform' => array('rawurlencode'),
'help' => 'https://join-lemmy.org/',
'form' => 'advanced',
diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml
index a56eff3ee..999b2406a 100644
--- a/app/views/auth/register.phtml
+++ b/app/views/auth/register.phtml
@@ -16,6 +16,18 @@
</div>
<div class="form-group">
+ <label for="new_user_timezone"><?= _t('conf.display.timezone') ?></label>
+ <select name="new_user_timezone" id="new_user_timezone">
+ <?php $timezones = array_merge([''], DateTimeZone::listIdentifiers()); ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= $timezone === '' ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+
+ <div class="form-group">
<label for="new_user_name"><?= _t('gen.auth.username') ?></label>
<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" autocomplete="off"
pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" autocapitalize="off" />
diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml
index e3e18f5a0..44ca242ad 100644
--- a/app/views/configure/display.phtml
+++ b/app/views/configure/display.phtml
@@ -26,6 +26,25 @@
</div>
<div class="form-group">
+ <label class="group-name" for="language"><?= _t('conf.display.timezone') ?></label>
+ <div class="group-controls">
+ <select name="timezone" id="timezone" data-leave-validation="<?= FreshRSS_Context::$user_conf->timezone ?>">
+ <?php
+ $timezones = array_merge([''], DateTimeZone::listIdentifiers());
+ if (!in_array(FreshRSS_Context::$user_conf->timezone, $timezones, true)) {
+ FreshRSS_Context::$user_conf->timezone = '';
+ }
+ ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= FreshRSS_Context::$user_conf->timezone === $timezone ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
<label class="group-name" for="theme"><?= _t('conf.display.theme') ?></label>
<div class="group-controls">
<ul class="slides">
@@ -41,7 +60,7 @@
data-leave-validation="<?= (FreshRSS_Context::$user_conf->theme === $theme['id']) ? 1 : 0 ?>" />
<li class="slide-container">
<div class="slide">
- <img src="<?= Minz_Url::display('/themes/' . $theme['id'] . '/thumbs/original.png') ?>" />
+ <img src="<?= Minz_Url::display('/themes/' . $theme['id'] . '/thumbs/original.png') ?>" loading="lazy" />
</div>
<div class="nav">
<?php if ($i !== 1) {?>
@@ -52,8 +71,18 @@
<?php } ?>
</div>
<div class="properties">
- <div><?= sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']) ?></div>
- <div><?= $theme['description'] ?></div>
+ <div>
+ <?php if (!empty($theme['deprecated'])) { ?>
+ <span class="deprecated error"><?= _t('conf.display.theme.deprecated') ?>:</span>
+ <?php } ?>
+ <?= sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']) ?>
+ </div>
+ <div>
+ <?php if (!empty($theme['deprecated'])) { ?>
+ <span class="deprecated"><?= _t('conf.display.theme.deprecated.description') ?></span><br />
+ <?php } ?>
+ <?= $theme['description'] ?>
+ </div>
<div class="page-number"><?= sprintf('%d/%d', $i, $slides) ?></div>
</div>
</li>
@@ -76,6 +105,16 @@
</div>
</div>
+ <div class="form-group">
+ <label class="group-name" for="darkMode"><?= _t('conf.display.darkMode') ?></label>
+ <div class="group-controls">
+ <select name="darkMode" id="darkMode" data-leave-validation="<?= FreshRSS_Context::$user_conf->darkMode ?>">
+ <option value="no"<?= FreshRSS_Context::$user_conf->darkMode === 'no' ? ' selected' : '' ?>>No</option>
+ <option value="auto"<?= FreshRSS_Context::$user_conf->darkMode === 'auto' ? ' selected' : '' ?>>Auto</option>
+ </select>
+ </div>
+ </div>
+
<?php $width = FreshRSS_Context::$user_conf->content_width; ?>
<div class="form-group">
<label class="group-name" for="content_width"><?= _t('conf.display.width.content') ?></label>
@@ -126,7 +165,7 @@
<tr>
<th> </th>
<th title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i('read') ?></th>
- <th title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i('bookmark') ?></th>
+ <th title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i('starred') ?></th>
<th><?= _t('conf.display.icon.related_tags') ?></th>
<th><?= _t('conf.display.icon.sharing') ?></th>
<th><?= _t('conf.display.icon.summary') ?></th>
diff --git a/app/views/configure/integration.phtml b/app/views/configure/integration.phtml
index c078ae709..34c10b3c3 100644
--- a/app/views/configure/integration.phtml
+++ b/app/views/configure/integration.phtml
@@ -15,7 +15,7 @@
<template id="simple-share">
<formgroup class="group-share dragbox">
<legend draggable="true">##label##</legend>
- <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+ <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" data-leave-validation="" />
<div class="form-group" id="group-share-##key##">
<label class="group-name" for="share_##key##_name"><?= _t('conf.sharing.share_name') ?></label>
<div class="group-controls">
@@ -32,7 +32,7 @@
<template id="advanced-share">
<formgroup class="group-share dragbox">
<legend draggable="true">##label##</legend>
- <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+ <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" data-leave-validation="" />
<input type="hidden" id="share_##key##_method" name="share[##key##][method]" value="##method##" />
<input type="hidden" id="share_##key##_field" name="share[##key##][field]" value="##field##" />
<div class="form-group" id="group-share-##key##">
diff --git a/app/views/configure/system.phtml b/app/views/configure/system.phtml
index 94fe34b94..bf14cac5d 100644
--- a/app/views/configure/system.phtml
+++ b/app/views/configure/system.phtml
@@ -16,7 +16,7 @@
<div class="form-group">
<label class="group-name" for="instance-name"><?= _t('admin.system.instance-name') ?></label>
<div class="group-controls">
- <input type="text" class="extend" id="instance-name" name="instance-name" value="<?= FreshRSS_Context::$system_conf->title ?>"
+ <input type="text" id="instance-name" name="instance-name" value="<?= FreshRSS_Context::$system_conf->title ?>"
data-leave-validation="<?= FreshRSS_Context::$system_conf->title ?>"/>
</div>
</div>
@@ -24,7 +24,7 @@
<div class="form-group">
<label class="group-name" for="auto-update-url"><?= _t('admin.system.auto-update-url') ?></label>
<div class="group-controls">
- <input type="text" class="extend" id="auto-update-url" name="auto-update-url" value="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"
+ <input type="text" id="auto-update-url" name="auto-update-url" value="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"
data-leave-validation="<?= FreshRSS_Context::$system_conf->auto_update_url ?>"/>
</div>
</div>
diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml
index 8f5594526..3461e6e61 100644
--- a/app/views/feed/add.phtml
+++ b/app/views/feed/add.phtml
@@ -67,7 +67,7 @@
<div class="form-group">
<label class="group-name" for="http_user"><?= _t('sub.feed.auth.username') ?></label>
<div class="group-controls">
- <input type="text" name="http_user" id="http_user" class="extend" value="<?= $auth['username'] ?>" autocomplete="off" />
+ <input type="text" name="http_user" id="http_user" value="<?= $auth['username'] ?>" autocomplete="off" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.auth.help') ?></p>
</div>
diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml
index 171ee20a4..a0986ff04 100644
--- a/app/views/helpers/category/update.phtml
+++ b/app/views/helpers/category/update.phtml
@@ -16,7 +16,7 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('sub.category.title') ?></label>
<div class="group-controls">
- <input type="text" name="name" id="name" class="extend" value="<?= $this->category->name() ?>" <?php
+ <input type="text" name="name" id="name" value="<?= $this->category->name() ?>" <?php
//Disallow changing the name of the default category
echo $this->category->id() == FreshRSS_CategoryDAO::DEFAULTCATEGORYID ? 'disabled="disabled"' : '';
?> />
diff --git a/app/views/helpers/configure/query.phtml b/app/views/helpers/configure/query.phtml
index a75333cfc..f8d51c193 100644
--- a/app/views/helpers/configure/query.phtml
+++ b/app/views/helpers/configure/query.phtml
@@ -12,7 +12,7 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('conf.query.name') ?></label>
<div class="group-controls">
- <input type="text" name="name" id="name" class="extend" value="<?= $this->query->getName() ?>" />
+ <input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
</div>
</div>
<legend><?= _t('conf.query.filter') ?></legend>
@@ -20,7 +20,7 @@
<div class="form-group">
<label class="group-name" for=""><?= _t('conf.query.filter.search') ?></label>
<div class="group-controls">
- <input type="text" id="query_search" name="query[search]" class="extend" value="<?= htmlspecialchars($this->query->getSearch(), ENT_COMPAT, 'UTF-8') ?>"/>
+ <input type="text" id="query_search" name="query[search]" value="<?= htmlspecialchars($this->query->getSearch(), ENT_COMPAT, 'UTF-8') ?>"/>
</div>
</div>
<div class="form-group">
diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml
index d97641fd2..64c83c960 100644
--- a/app/views/helpers/export/opml.phtml
+++ b/app/views/helpers/export/opml.phtml
@@ -9,6 +9,7 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
if ($feed->mute() && $excludeMutedFeeds) {
continue;
}
+
$outline = [
'text' => htmlspecialchars_decode($feed->name(), ENT_QUOTES),
'type' => FreshRSS_Export_Service::TYPE_RSS_ATOM,
@@ -16,49 +17,65 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
'htmlUrl' => htmlspecialchars_decode($feed->website(), ENT_QUOTES),
'description' => htmlspecialchars_decode($feed->description(), ENT_QUOTES),
];
- if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
- $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH;
+
+ if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
+ switch ($feed->kind()) {
+ case FreshRSS_Feed::KIND_HTML_XPATH:
+ $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH;
+ break;
+ case FreshRSS_Feed::KIND_XML_XPATH:
+ $outline['type'] = FreshRSS_Export_Service::TYPE_XML_XPATH;
+ break;
+ }
/** @var array<string,string> */
$xPathSettings = $feed->attributes('xpath');
- $outline['frss:xPathItem'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['item'] ?? null];
- $outline['frss:xPathItemTitle'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTitle'] ?? null];
- $outline['frss:xPathItemContent'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemContent'] ?? null];
- $outline['frss:xPathItemUri'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUri'] ?? null];
- $outline['frss:xPathItemAuthor'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemAuthor'] ?? null];
- $outline['frss:xPathItemTimestamp'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimestamp'] ?? null];
- $outline['frss:xPathItemTimeformat'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimeformat'] ?? null];
- $outline['frss:xPathItemThumbnail'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemThumbnail'] ?? null];
- $outline['frss:xPathItemCategories'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemCategories'] ?? null];
- $outline['frss:xPathItemUid'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUid'] ?? null];
+ $outline['frss:xPathItem'] = $xPathSettings['item'] ?? null;
+ $outline['frss:xPathItemTitle'] = $xPathSettings['itemTitle'] ?? null;
+ $outline['frss:xPathItemContent'] = $xPathSettings['itemContent'] ?? null;
+ $outline['frss:xPathItemUri'] = $xPathSettings['itemUri'] ?? null;
+ $outline['frss:xPathItemAuthor'] = $xPathSettings['itemAuthor'] ?? null;
+ $outline['frss:xPathItemTimestamp'] = $xPathSettings['itemTimestamp'] ?? null;
+ $outline['frss:xPathItemTimeformat'] = $xPathSettings['itemTimeformat'] ?? null;
+ $outline['frss:xPathItemThumbnail'] = $xPathSettings['itemThumbnail'] ?? null;
+ $outline['frss:xPathItemCategories'] = $xPathSettings['itemCategories'] ?? null;
+ $outline['frss:xPathItemUid'] = $xPathSettings['itemUid'] ?? null;
}
+
if (!empty($feed->filtersAction('read'))) {
$filters = '';
foreach ($feed->filtersAction('read') as $filterRead) {
$filters .= $filterRead->getRawInput() . "\n";
}
$filters = trim($filters);
- $outline['frss:filtersActionRead'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $filters];
+ $outline['frss:filtersActionRead'] = $filters;
}
+
if ($feed->pathEntries() != '') {
- $outline['frss:cssFullContent'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => htmlspecialchars_decode($feed->pathEntries(), ENT_QUOTES)];
+ $outline['frss:cssFullContent'] = htmlspecialchars_decode($feed->pathEntries(), ENT_QUOTES);
}
+
if ($feed->attributes('path_entries_filter') != '') {
- $outline['frss:cssFullContentFilter'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $feed->attributes('path_entries_filter')];
+ $outline['frss:cssFullContentFilter'] = $feed->attributes('path_entries_filter');
}
+
$outlines[] = $outline;
}
+
return $outlines;
}
/** @var FreshRSS_View $this */
-$opml_array = array(
- 'head' => array(
+$opml_array = [
+ 'namespaces' => [
+ 'frss' => FreshRSS_Export_Service::FRSS_NAMESPACE,
+ ],
+ 'head' => [
'title' => FreshRSS_Context::$system_conf->title,
- 'dateCreated' => date('D, d M Y H:i:s')
- ),
- 'body' => array()
-);
+ 'dateCreated' => new DateTime(),
+ ],
+ 'body' => [],
+];
if (!empty($this->categories)) {
foreach ($this->categories as $key => $cat) {
@@ -66,9 +83,11 @@ if (!empty($this->categories)) {
'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')];;
+ $outline['frss:opmlUrl'] = $cat->attributes('opml_url');
}
+
$opml_array['body'][$key] = $outline;
}
}
@@ -77,4 +96,5 @@ if (!empty($this->feeds)) {
$opml_array['body'][] = feedsToOutlines($this->feeds, $this->excludeMutedFeeds);
}
-echo libopml_render($opml_array);
+$libopml = new \marienfressinaud\LibOpml\LibOpml(true);
+echo $libopml->render($opml_array);
diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml
index 8dbba0ab0..0cd2ec0c3 100644
--- a/app/views/helpers/feed/update.phtml
+++ b/app/views/helpers/feed/update.phtml
@@ -391,8 +391,9 @@
<label class="group-name" for="feed_kind"><?= _t('sub.feed.kind') ?></label>
<div class="group-controls">
<select name="feed_kind" id="feed_kind" class="select-show w100">
- <option value="<?= FreshRSS_Feed::KIND_RSS ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_RSS ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.rss') ?></option>
- <option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_RSS ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_RSS ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.rss') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
</select>
</div>
</div>
@@ -602,7 +603,7 @@
<div class="form-group">
<label class="group-name" for="timeout"><?= _t('sub.feed.timeout') ?></label>
<div class="group-controls">
- <input type="number" name="timeout" id="timeout" class="w50" min="3" max="120" value="<?= $this->feed->attributes('timeout') ?>" placeholder="<?= _t('gen.short.by_default') ?>" />
+ <input type="number" name="timeout" id="timeout" class="w50" min="3" max="900" value="<?= $this->feed->attributes('timeout') ?>" placeholder="<?= _t('gen.short.by_default') ?>" />
</div>
</div>
diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml
index 43eeb7f8a..92eacf617 100644
--- a/app/views/helpers/index/normal/entry_header.phtml
+++ b/app/views/helpers/index/normal/entry_header.phtml
@@ -42,8 +42,7 @@
?><li class="item thumbnail <?= $topline_thumbnail ?> <?= $topline_summary ? '' : 'small' ?>"><?php
$thumbnail = $this->entry->thumbnail();
if ($thumbnail != null):
- ?><img src="<?= htmlspecialchars($thumbnail['url'], ENT_COMPAT, 'UTF-8') ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?><?=
- empty($thumbnail['alt']) ? '' : ' alt="' . htmlspecialchars(strip_tags($thumbnail['alt']), ENT_COMPAT, 'UTF-8') . '"' ?> /><?php
+ ?><img src="<?= $thumbnail['url'] ?>" class="item-element "<?= $lazyload ? ' loading="lazy"' : '' ?> alt="" /><?php
endif;
?></li><?php
endif; ?>
@@ -62,7 +61,7 @@
?></span><?php
endif;
if ($topline_summary):
- ?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content()), 0, 500, 'UTF-8')) ?></div><?php
+ ?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content(false)), 0, 500, 'UTF-8')) ?></div><?php
endif;
?></a></li>
<?php if ($topline_date) { ?><li class="item date"><time datetime="<?= $this->entry->machineReadableDate() ?>" class="item-element"><?= $this->entry->date() ?></time>&nbsp;</li><?php } ?>
diff --git a/app/views/helpers/stream-footer.phtml b/app/views/helpers/stream-footer.phtml
index ebce4d852..f37ba59d6 100755
--- a/app/views/helpers/stream-footer.phtml
+++ b/app/views/helpers/stream-footer.phtml
@@ -30,19 +30,20 @@ if ($hasAccess) { ?>
<?php if (FreshRSS_Context::$next_id) { ?>
<button id="load_more" type="submit" class="btn" formaction="<?= Minz_Url::display($url_next) ?>"><?= _t('gen.stream.load_more') ?></button>
<?php } elseif ($hasAccess) { ?>
+ <?= _t('gen.stream.nothing_to_load') ?><br />
<button id="bigMarkAsRead"
class="as-link <?= FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : '' ?>"
form="stream-footer"
formaction="<?= Minz_Url::display($url_mark_read) ?>"
type="submit">
- <?= _t('gen.stream.nothing_to_load') ?><br />
<span class="bigTick">✓</span><br />
- <?= _t('gen.stream.mark_all_read') ?>
+ <span class="markAllRead"><?= _t('gen.stream.mark_all_read') ?></span>
+ <?php if (FreshRSS_Context::$user_conf->onread_jump_next) { ?>
+ <div class="jumpNext"><?= _t('conf.reading.jump_next') ?></div>
+ <?php } ?>
</button>
<?php } else { ?>
- <div id="bigMarkAsRead">
- <?= _t('gen.stream.nothing_to_load') ?><br />
- </div>
+ <?= _t('gen.stream.nothing_to_load') ?><br />
<?php } ?>
</div>
<?php if ($hasAccess) { ?>
diff --git a/app/views/index/logs.phtml b/app/views/index/logs.phtml
index f6a76b922..896a19765 100644
--- a/app/views/index/logs.phtml
+++ b/app/views/index/logs.phtml
@@ -6,7 +6,7 @@
</div>
<h1><?= _t('index.log') ?></h1>
-
+
<?php
/** @var array<FreshRSS_Log> $items */
@@ -29,8 +29,8 @@
<?= _i($log->level()) ?>
</td>
<td class="log-date">
- <time datetime="<?= @date('Y-m-d H:i:s', @strtotime($log->date())) ?>">
- <?= @date('Y-m-d H:i:s', @strtotime($log->date())) ?>
+ <time datetime="<?= date('Y-m-d H:i:s', @strtotime($log->date()) ?: 0) ?>">
+ <?= date('Y-m-d H:i:s', @strtotime($log->date()) ?: 0) ?>
</time>
</td>
<td class="log-message">
@@ -42,7 +42,7 @@
</table>
</div>
<?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?>
-
+
<form method="post" action="<?= _url('index', 'logs') ?>">
@@ -58,5 +58,5 @@
<?php } else { ?>
<p class="alert alert-warn"><?= _t('index.log.empty') ?></p>
<?php } ?>
-
+
</main>
diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml
index 6f7c47677..847c307ab 100644
--- a/app/views/index/normal.phtml
+++ b/app/views/index/normal.phtml
@@ -162,7 +162,7 @@ $today = @strtotime('today');
<?php } ?>
</header>
<div class="text"><?php
- echo $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content();
+ echo $lazyload && $hidePosts ? lazyimg($this->entry->content(true)) : $this->entry->content(true);
?></div>
<?php
$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';
diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml
index 5789f229b..9dcd07435 100644
--- a/app/views/index/reader.phtml
+++ b/app/views/index/reader.phtml
@@ -87,8 +87,8 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
- <div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
+ <div id="dropdown-tags-<?= $this->entry->id() ?>" class="dropdown-target"></div>
+ <a class="dropdown-toggle" href="#dropdown-tags-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
@@ -136,7 +136,7 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
</header>
<div class="text">
- <?= $item->content() ?>
+ <?= $item->content(true) ?>
</div>
<?php
$display_authors_date = FreshRSS_Context::$user_conf->show_author_date === 'f' || FreshRSS_Context::$user_conf->show_author_date === 'b';
diff --git a/app/views/index/rss.phtml b/app/views/index/rss.phtml
index 0b07a02f3..0b3dc7955 100755
--- a/app/views/index/rss.phtml
+++ b/app/views/index/rss.phtml
@@ -29,29 +29,41 @@ foreach ($this->entries as $item) {
$authors = $item->authors();
if (is_array($authors)) {
foreach ($authors as $author) {
- echo "\t\t\t" , '<dc:creator>', $author, '</dc:creator>', "\n";
+ echo "\t\t\t", '<dc:creator>', $author, '</dc:creator>', "\n";
}
}
$categories = $item->tags();
if (is_array($categories)) {
foreach ($categories as $category) {
- echo "\t\t\t" , '<category>', $category, '</category>', "\n";
+ echo "\t\t\t", '<category>', $category, '</category>', "\n";
}
}
+ $thumbnail = $item->thumbnail(false);
+ if (!empty($thumbnail['url'])) {
+ // https://www.rssboard.org/media-rss#media-thumbnails
+ echo "\t\t\t", '<media:thumbnail url="' . $thumbnail['url']
+ . (empty($thumbnail['width']) ? '' : '" width="' . $thumbnail['width'])
+ . (empty($thumbnail['height']) ? '' : '" height="' . $thumbnail['height'])
+ . (empty($thumbnail['time']) ? '' : '" time="' . $thumbnail['time'])
+ . '" />', "\n";
+ }
$enclosures = $item->enclosures(false);
if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
// https://www.rssboard.org/media-rss
- echo "\t\t\t" , '<media:content url="' . $enclosure['url']
+ echo "\t\t\t", '<media:content url="' . $enclosure['url']
. (empty($enclosure['medium']) ? '' : '" medium="' . $enclosure['medium'])
. (empty($enclosure['type']) ? '' : '" type="' . $enclosure['type'])
. (empty($enclosure['length']) ? '' : '" length="' . $enclosure['length'])
- . '"></media:content>', "\n";
+ . '">'
+ . (empty($enclosure['title']) ? '' : '<media:title type="html">' . $enclosure['title'] . '</media:title>')
+ . (empty($enclosure['credit']) ? '' : '<media:credit>' . $enclosure['credit'] . '</media:credit>')
+ . '</media:content>', "\n";
}
}
?>
<description><![CDATA[<?php
- echo $item->content();
+ echo $item->content(false);
?>]]></description>
<pubDate><?= date('D, d M Y H:i:s O', $item->date(true)) ?></pubDate>
<guid isPermaLink="false"><?= $item->id() > 0 ? $item->id() : $item->guid() ?></guid>
diff --git a/app/views/subscription/add.phtml b/app/views/subscription/add.phtml
index 800093bed..4e9da877f 100644
--- a/app/views/subscription/add.phtml
+++ b/app/views/subscription/add.phtml
@@ -70,6 +70,7 @@
<select name="feed_kind" id="feed_kind" class="select-show">
<option value="<?= FreshRSS_Feed::KIND_RSS ?>" selected="selected"><?= _t('sub.feed.kind.rss') ?></option>
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
+ <option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
</select>
</div>
</div>
@@ -190,7 +191,7 @@
<div class="form-group">
<label class="group-name" for="curl_params_cookie"><?= _t('sub.feed.css_cookie') ?></label>
<div class="group-controls">
- <input type="text" name="curl_params_cookie" id="curl_params_cookie" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params_cookie" id="curl_params_cookie" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.css_cookie_help') ?></p>
<label for="curl_params_cookiefile">
<input type="checkbox" name="curl_params_cookiefile" id="curl_params_cookiefile" value="1" />
@@ -203,7 +204,7 @@
<div class="form-group">
<label class="group-name" for="curl_params_redirects"><?= _t('sub.feed.max_http_redir') ?></label>
<div class="group-controls">
- <input type="number" name="curl_params_redirects" id="curl_params_redirects" class="extend" min="-1" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="number" name="curl_params_redirects" id="curl_params_redirects" min="-1" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.max_http_redir_help') ?></p>
</div>
</div>
@@ -212,7 +213,7 @@
<label class="group-name" for="curl_params_useragent"><?= _t('sub.feed.useragent') ?></label>
<div class="group-controls">
<div class="stick">
- <input type="text" name="curl_params_useragent" id="curl_params_useragent" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params_useragent" id="curl_params_useragent" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
</div>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.useragent_help') ?></p>
</div>
@@ -228,7 +229,7 @@
?>
</select>
<div class="stick">
- <input type="text" name="curl_params" id="curl_params" class="extend" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
+ <input type="text" name="curl_params" id="curl_params" value="" placeholder="<?= _t('gen.short.blank_to_disable') ?>" />
</div>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.proxy_help') ?></p>
</div>
@@ -237,7 +238,7 @@
<div class="form-group">
<label class="group-name" for="timeout"><?= _t('sub.feed.timeout') ?></label>
<div class="group-controls">
- <input type="number" name="timeout" id="timeout" min="3" max="120" value="" placeholder="<?= _t('gen.short.by_default') ?>" />
+ <input type="number" name="timeout" id="timeout" min="3" max="900" value="" placeholder="<?= _t('gen.short.by_default') ?>" />
</div>
</div>
diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml
index 0d01d33d7..b996cdf2b 100644
--- a/app/views/user/manage.phtml
+++ b/app/views/user/manage.phtml
@@ -15,8 +15,6 @@
<input type="hidden" name="originController" value="<?= Minz_Request::controllerName() ?>" />
<input type="hidden" name="originAction" value="<?= Minz_Request::actionName() ?>" />
-
-
<div class="form-group">
<label class="group-name" for="new_user_language"><?= _t('admin.user.language') ?></label>
<div class="group-controls">
@@ -31,6 +29,20 @@
</div>
<div class="form-group">
+ <label class="group-name" for="new_user_timezone"><?= _t('conf.display.timezone') ?></label>
+ <div class="group-controls">
+ <select name="new_user_timezone" id="new_user_timezone">
+ <?php $timezones = array_merge([''], DateTimeZone::listIdentifiers()); ?>
+ <?php foreach ($timezones as $timezone): ?>
+ <option value="<?= $timezone ?>"<?= $timezone === '' ? ' selected="selected"' : '' ?>>
+ <?= $timezone == '' ? _t('gen.short.by_default') . ' (' . FreshRSS_Context::defaultTimeZone() . ')' : $timezone ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
<label class="group-name" for="new_user_name"><?= _t('admin.user.username') ?></label>
<div class="group-controls">
<input id="new_user_name" name="new_user_name" type="text" size="16" required="required" autocomplete="off"