diff options
| author | 2023-03-04 13:30:45 +0100 | |
|---|---|---|
| committer | 2023-03-04 13:30:45 +0100 | |
| commit | b3239256dc6d188cda970adab516b3fcf1b86129 (patch) | |
| tree | d8e65dd9784834ba2e82ce7ee94b4718f8af19ea /app | |
| parent | 27b71ffa99f7dff013fb8d51d020ed628e0d2ce6 (diff) | |
| parent | 0fe0ce894cbad09757d719dd4b400b9862c1a12a (diff) | |
Merge branch 'edge' into latest
Diffstat (limited to 'app')
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(']]>', ']]>', $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~&body=~LINK~', + 'transform' => array('rawurlencode'), + 'form' => 'simple', + 'method' => 'GET', + ), 'facebook' => array( 'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&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> </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" |
