diff --git a/CHANGELOG.md b/CHANGELOG.md index caed250ca..620bccc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 2017-06-03 FreshRSS 1.7.0 +* Features: + * Deferred insertion of new articles, for better chronological order [#530](https://github.com/FreshRSS/FreshRSS/issues/530) + * Better search: + * Possibility to use multiple `intitle:`, `inurl:`, `author:` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478) + * Negative searches with `!` or `-` [#1381](https://github.com/FreshRSS/FreshRSS/issues/1381) + * Examples: `!intitle:unwanted`, `-intitle:unwanted`, `-inurl:unwanted`, `-author:unwanted`, `-#unwanted`, `-unwanted` + * Allow double-quotes, such as `author:"some name"`, in addition to single-quotes such as `author:'some name'` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478) + * Multi-user tokens (to access RSS outputs of any user) [#1390](https://github.com/FreshRSS/FreshRSS/issues/1390) +* Compatibility: + * Add support for PHP 7.1 [#1471](https://github.com/FreshRSS/FreshRSS/issues/1471) + * PostgreSQL is not experimental anymore [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476) +* Bug fixing + * Fix PubSubHubbub bugs when deleting users, and improved behaviour when removing feeds [#1495](https://github.com/FreshRSS/FreshRSS/pull/1495) + * Fix SQL uniqueness bug with PostgreSQL [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476) + * (Require manual update for existing installations) + * Do not require PHP extension `fileinfo` for favicons [#1461](https://github.com/FreshRSS/FreshRSS/issues/1461) + * Fix UI lowest subscription popup hidden [#1479](https://github.com/FreshRSS/FreshRSS/issues/1479) + * Fix update system via ZIP archive [#1498](https://github.com/FreshRSS/FreshRSS/pull/1498) + * Work around for IE / Edge bug in username pattern in version 1.6.3 [#1511](https://github.com/FreshRSS/FreshRSS/issues/1511) + * Fix *mark as read* articles when adding a new feed [#1535](https://github.com/FreshRSS/FreshRSS/issues/1535) + * Change load order of CSS and JS to help CustomCSS and CustomJS extensions [Extensions#13](https://github.com/FreshRSS/Extensions/issues/13), [#1547](https://github.com/FreshRSS/FreshRSS/pull/1547) +* UI + * New option for not closing the article when clicking outside its area [#1539](https://github.com/FreshRSS/FreshRSS/pull/1539) + * Add shortcut in reader view to open the original page [#1564](https://github.com/FreshRSS/FreshRSS/pull/1564) + * Download icon 💾 for other MIME types (e.g. `application/*`) [#1522](https://github.com/FreshRSS/FreshRSS/pull/1522) +* I18n + * Simplified Chinese [#1541](https://github.com/FreshRSS/FreshRSS/pull/1541) + * Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465) + * Improve Dutch [#1559](https://github.com/FreshRSS/FreshRSS/pull/1559) +* Security + * Do not require write access to check availability of new versions [#1450](https://github.com/FreshRSS/FreshRSS/issues/1450) +* Misc. + * Move [documentation](./docs/) into FreshRSS code [#1510](https://github.com/FreshRSS/FreshRSS/pull/1510) + * Moved `./data/force-https.default.txt` to `./force-https.default.txt`, + `./data/config.default.php` to `./config.default.php`, + and `./data/users/_/config.default.php` to `./config-user.default.php` [#1531](https://github.com/FreshRSS/FreshRSS/issues/1531) + * Fall back to article URL when the article GUID is empty [#1482](https://github.com/FreshRSS/FreshRSS/issues/1482) + * Rewritten Favicon library using cURL [#1504](https://github.com/FreshRSS/FreshRSS/pull/1504) + * Fix SimplePie option to disable syslog [#1528](https://github.com/FreshRSS/FreshRSS/pull/1528) + + ## 2017-03-11 FreshRSS 1.6.3 * Features diff --git a/CREDITS.md b/CREDITS.md index 57635669a..97651ab20 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -17,10 +17,14 @@ People are sorted by name so please keep this order. * [dswd](https://github.com/dswd): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:dswd) * [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed) * [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/) +* [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong) * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/) * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/) * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb) +* [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc) * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/) +* [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler) +* [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=cez81) * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/) * [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/) * [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/) @@ -31,6 +35,7 @@ People are sorted by name so please keep this order. * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie) * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/) * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop) +* [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu) * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/) * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/) * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi) @@ -39,3 +44,4 @@ People are sorted by name so please keep this order. * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/) * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue) * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo) +* [mszkb](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=mszkb) diff --git a/README.fr.md b/README.fr.md index b0b46bf65..b7a28fd91 100644 --- a/README.fr.md +++ b/README.fr.md @@ -14,7 +14,7 @@ Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de per * Démo : http://demo.freshrss.org/ * Licence : [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) - + # Téléchargement Voir la [liste des versions](../../releases). @@ -35,11 +35,15 @@ Nous sommes une communauté amicale. * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances) * Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql) * Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés) -* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental) +* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL 9.2+ * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari. * Fonctionne aussi sur mobile - + + +# Documentation +* http://doc.freshrss.org/fr/ +* https://github.com/FreshRSS/documentation # Installation 1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](../releases) @@ -48,7 +52,7 @@ Nous sommes une communauté amicale. 4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation * ou utilisez [l’interface en ligne de commande](./cli/README.md) 5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à [nous contacter](https://github.com/FreshRSS/FreshRSS/issues). -6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php). +6. Des paramètres de configuration avancée peuvent être vues dans [config.default.php](./config.default.php) et modifiées dans `data/config.php`. ## Installation automatisée * [](https://dfabric.github.io/DPlatform-ShellCore) @@ -130,7 +134,8 @@ Créer `/etc/cron.d/FreshRSS` avec : * Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`. * En particulier, les données personnelles se trouvent dans le répertoire `./data/`. * Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici. -* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`. +* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/users/*/log*.txt`. + * Le répertoire spécial `./data/users/_/` contient la partie des logs partagés par tous les utilisateurs. # Sauvegarde @@ -154,7 +159,6 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio * [MINZ](https://github.com/marienfressinaud/MINZ) * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) * [jQuery](http://jquery.com/) -* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon) * [lib_opml](https://github.com/marienfressinaud/lib_opml) * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/) * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) diff --git a/README.md b/README.md index d599afaa4..8b87ecb8f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Finally, it supports [extensions](#extensions) for further tuning. * Demo: http://demo.freshrss.org/ * License: [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) - + # Releases See the [list of releases](../../releases). @@ -35,11 +35,15 @@ We are a friendly community. * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance) * Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql) * Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds) -* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental) +* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL 9.2+ * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari. * Works on mobile - + + +# Documentation +* http://doc.freshrss.org/en/ +* https://github.com/FreshRSS/documentation # Installation 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip) @@ -48,7 +52,7 @@ We are a friendly community. 4. Access FreshRSS with your browser and follow the installation process * or use the [Command-Line Interface](./cli/README.md) 5. Everything should be working :) If you encounter any problem, feel free [contact us](https://github.com/FreshRSS/FreshRSS/issues). -6. Advanced configuration settings can be seen in [config.php](./data/config.default.php). +6. Advanced configuration settings can be seen in [config.default.php](./config.default.php) and modified in `data/config.php`. ## Automated install * [](https://cloudron.io/button.html?app=org.freshrss.cloudronapp) @@ -101,6 +105,7 @@ cd /usr/share/FreshRSS sudo git pull sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/ ``` +See more commands and git commands in the [Command-Line Interface documentation](./cli/README.md). ## Access control It is needed for the multi-user mode to limit access to FreshRSS. You can: @@ -128,10 +133,11 @@ Create `/etc/cron.d/FreshRSS` with: # Advices -* For a better security, expose only the `./p/` folder on the web. +* For a better security, expose only the `./p/` folder on the Web. * Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it. * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here. -* If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files. +* If you encounter any problem, logs are accessible from the interface or manually in `./data/users/*/log*.txt` files. + * The special folder `./data/users/_/` contains the part of the logs that are shared by all users. # Backup @@ -155,7 +161,6 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E * [MINZ](https://github.com/marienfressinaud/MINZ) * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) * [jQuery](http://jquery.com/) -* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon) * [lib_opml](https://github.com/marienfressinaud/lib_opml) * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/) * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 1398e4e49..5ad1a51d9 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -27,11 +27,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController { if (Minz_Request::isPost()) { $ok = true; - $current_token = FreshRSS_Context::$user_conf->token; - $token = Minz_Request::param('token', $current_token); - FreshRSS_Context::$user_conf->token = $token; - $ok &= FreshRSS_Context::$user_conf->save(); - $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); $anon_refresh = Minz_Request::param('anon_refresh', false); @@ -123,7 +118,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $challenge = Minz_Request::param('challenge', ''); $conf = get_user_configuration($username); - if (is_null($conf)) { + if ($conf == null) { Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false); return; } @@ -164,7 +159,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { } $conf = get_user_configuration($username); - if (is_null($conf)) { + if ($conf == null) { return; } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index e73f106a6..155221d19 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -109,6 +109,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { 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); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index f71f26a4e..c9b6deaa7 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -226,7 +226,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) { + public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false, $noCommit = false) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -254,6 +254,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. $updated_feeds = 0; + $nb_new_articles = 0; $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { $url = $feed->url(); //For detection of HTTP 301 @@ -308,6 +309,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // -2 means we take the default value from configuration $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } + $needFeedCacheRefresh = false; // We want chronological order and SimplePie uses reverse order. $entries = array_reverse($feed->entries()); @@ -333,6 +335,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . //', old hash ' . $existingHash . ', new hash ' . $entry->hash()); //TODO: Make an updated/is_read policy by feed, in addition to the global one. + $needFeedCacheRefresh = FreshRSS_Context::$user_conf->mark_updated_article_unread; $entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy. if (!$entryDAO->inTransaction()) { $entryDAO->beginTransaction(); @@ -345,6 +348,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { if ($isNewFeed) { $id = min(time(), $entry_date) . uSecString(); + $entry->_isRead($is_read); } elseif ($entry_date < $date_min) { $id = min(time(), $entry_date) . uSecString(); $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read @@ -372,6 +376,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->beginTransaction(); } $entryDAO->addEntry($entry->toArray()); + $nb_new_articles++; } } $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); @@ -388,12 +393,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $date_min, max($feed_history, count($entries) + 10)); if ($nb > 0) { + $needFeedCacheRefresh = true; Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']'); } } - $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime); + $feedDAO->updateLastUpdate($feed->id(), false, $mtime); + if ($needFeedCacheRefresh) { + $feedDAO->updateCachedValue($feed->id()); + } if ($entryDAO->inTransaction()) { $entryDAO->commit(); } @@ -434,7 +443,17 @@ class FreshRSS_feed_Controller extends Minz_ActionController { break; } } - return array($updated_feeds, reset($feeds)); + if (!$noCommit) { + if (!$entryDAO->inTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->commitNewEntries(); + $feedDAO->updateCachedValues(); + if ($entryDAO->inTransaction()) { + $entryDAO->commit(); + } + } + return array($updated_feeds, reset($feeds), $nb_new_articles); } /** @@ -444,6 +463,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { * - id (default: false): Feed ID * - url (default: false): Feed URL * - force (default: false) + * - noCommit (default: 0): Set to 1 to prevent committing the new articles to the main database * If id and url are not specified, all the feeds are actualized. But if force is * false, process stops at 10 feeds to avoid time execution problem. */ @@ -452,8 +472,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); $url = Minz_Request::param('url'); $force = Minz_Request::param('force'); + $noCommit = Minz_Request::fetchPOST('noCommit', 0) == 1; - list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force); + if ($id == -1 && !$noCommit) { //Special request only to commit & refresh DB cache + $updated_feeds = 0; + $entryDAO = FreshRSS_Factory::createEntryDao(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $entryDAO->beginTransaction(); + $entryDAO->commitNewEntries(); + $feedDAO->updateCachedValues(); + $entryDAO->commit(); + } else { + list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit); + } if (Minz_Request::param('ajax')) { // Most of the time, ajax request is for only one feed. But since diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 6ae89defb..2bc68848c 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -464,18 +464,22 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $values = $entry->toArray(); + $ok = false; if (isset($existingHashForGuids[$entry->guid()])) { - $id = $this->entryDAO->updateEntry($values); + $ok = $this->entryDAO->updateEntry($values); } else { - $id = $this->entryDAO->addEntry($values); + $ok = $this->entryDAO->addEntry($values); } + $error |= ($ok === false); - if (!$error && ($id === false)) { - $error = true; - } } $this->entryDAO->commit(); + $this->entryDAO->beginTransaction(); + $this->entryDAO->commitNewEntries(); + $this->feedDAO->updateCachedValues(); + $this->entryDAO->commit(); + return !$error; } diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index b4e8a0bff..7a8a3d6c0 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -59,24 +59,26 @@ class FreshRSS_update_Controller extends Minz_ActionController { public function indexAction() { Minz_View::prependTitle(_t('admin.update.title') . ' · '); - if (!is_writable(FRESHRSS_PATH)) { - $this->view->message = array( - 'status' => 'bad', - 'title' => _t('gen.short.damn'), - 'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH) - ); - } elseif (file_exists(UPDATE_FILENAME)) { + if (file_exists(UPDATE_FILENAME)) { // There is an update file to apply! $version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt')); - if (empty($version)) { + if ($version == '') { $version = 'unknown'; } - $this->view->update_to_apply = true; - $this->view->message = array( - 'status' => 'good', - 'title' => _t('gen.short.ok'), - 'body' => _t('feedback.update.can_apply', $version) - ); + if (is_writable(FRESHRSS_PATH)) { + $this->view->update_to_apply = true; + $this->view->message = array( + 'status' => 'good', + 'title' => _t('gen.short.ok'), + 'body' => _t('feedback.update.can_apply', $version), + ); + } else { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.file_is_nok', $version, FRESHRSS_PATH), + ); + } } } @@ -190,6 +192,7 @@ class FreshRSS_update_Controller extends Minz_ActionController { if (self::isGit()) { $res = self::gitPull(); } else { + require(UPDATE_FILENAME); if (Minz_Request::isPost()) { save_info_update(); } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index f910cecd9..3cbbd8633 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * The username is also used as folder name, file name, and part of SQL table name. * '_' is a reserved internal username. */ - const USERNAME_PATTERN = '[0-9a-zA-Z]|[0-9a-zA-Z_]{2,38}'; + const USERNAME_PATTERN = '[0-9a-zA-Z_]{2,38}|[0-9a-zA-Z]'; public static function checkUsername($username) { return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1; @@ -74,6 +74,10 @@ class FreshRSS_user_Controller extends Minz_ActionController { FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } + $current_token = FreshRSS_Context::$user_conf->token; + $token = Minz_Request::param('token', $current_token); + FreshRSS_Context::$user_conf->token = $token; + $ok &= FreshRSS_Context::$user_conf->save(); if ($ok) { @@ -213,6 +217,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $userDAO = new FreshRSS_UserDAO(); $ok &= $userDAO->deleteUser($username); $ok &= recursive_unlink($user_data); + array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt')); } return $ok; } diff --git a/app/FreshRSS.php b/app/FreshRSS.php index e4caf23d1..90d6fae06 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -41,7 +41,7 @@ class FreshRSS extends Minz_FrontController { $current_user = Minz_Session::param('currentUser', '_'); Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'), - join_path(USERS_PATH, '_', 'config.default.php'), + join_path(FRESHRSS_PATH, 'config-user.default.php'), $configuration_setter); // Finish to initialize the other FreshRSS / Minz components. @@ -80,7 +80,7 @@ class FreshRSS extends Minz_FrontController { public static function loadStylesAndScripts() { $theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme); if ($theme) { - foreach($theme['files'] as $file) { + foreach(array_reverse($theme['files']) as $file) { if ($file[0] === '_') { $theme_id = 'base-theme'; $filename = substr($file, 1); @@ -91,13 +91,13 @@ class FreshRSS extends Minz_FrontController { $filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename); $url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime; header('Link: <' . Minz_Url::display($url, '', 'root') . '>;rel=preload', false); //HTTP2 - Minz_View::appendStyle(Minz_Url::display($url)); + Minz_View::prependStyle(Minz_Url::display($url)); } } - - Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js'))); - Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); - Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); + //Use prepend to insert before extensions. Added in reverse order. + Minz_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); + Minz_View::prependScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); + Minz_View::prependScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js'))); } private static function loadNotifications() { diff --git a/app/Models/Auth.php b/app/Models/Auth.php index 476627e10..4de058999 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -74,6 +74,10 @@ class FreshRSS_Auth { public static function giveAccess() { $current_user = Minz_Session::param('currentUser'); $user_conf = get_user_configuration($current_user); + if ($user_conf == null) { + self::$login_ok = false; + return; + } $system_conf = Minz_Configuration::get('system'); switch ($system_conf->auth_type) { @@ -120,13 +124,28 @@ class FreshRSS_Auth { * Removes all accesses for the current user. */ public static function removeAccess() { - Minz_Session::_param('loginOk'); self::$login_ok = false; - $conf = Minz_Configuration::get('system'); - Minz_Session::_param('currentUser', $conf->default_user); + Minz_Session::_param('loginOk'); Minz_Session::_param('csrf'); + $system_conf = Minz_Configuration::get('system'); - switch ($conf->auth_type) { + $username = ''; + $token_param = Minz_Request::param('token', ''); + if ($token_param != '') { + $username = trim(Minz_Request::param('user', '')); + if ($username != '') { + $conf = get_user_configuration($username); + if ($conf == null) { + $username = ''; + } + } + } + if ($username == '') { + $username = $system_conf->default_user; + } + Minz_Session::_param('currentUser', $username); + + switch ($system_conf->auth_type) { case 'form': Minz_Session::_param('passwordHash'); FreshRSS_FormAuth::deleteCookie(); diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 046f54955..70e1dea2e 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -197,6 +197,10 @@ class FreshRSS_ConfigurationSetter { $data['hide_read_feeds'] = $this->handleBool($value); } + private function _sides_close_article(&$data, $value) { + $data['sides_close_article'] = $this->handleBool($value); + } + private function _lazyload(&$data, $value) { $data['lazyload'] = $this->handleBool($value); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index a562a963a..26cd24797 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -22,7 +22,6 @@ class FreshRSS_Entry extends Minz_Model { public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '', $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') { - $this->_guid($guid); $this->_title($title); $this->_author($author); $this->_content($content); @@ -32,6 +31,7 @@ class FreshRSS_Entry extends Minz_Model { $this->_isFavorite($is_favorite); $this->_feed($feed); $this->_tags(preg_split('/[\s#]/', $tags)); + $this->_guid($guid); } public function id() { @@ -101,6 +101,12 @@ class FreshRSS_Entry extends Minz_Model { $this->id = $value; } public function _guid($value) { + if ($value == '') { + $value = $this->link; + if ($value == '') { + $value = $this->hash(); + } + } $this->guid = $value; } public function _title($value) { diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index afcde3d7f..7e836097a 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -88,6 +88,38 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return false; } + protected function createEntryTempTable() { + $ok = false; + $hadTransaction = $this->bd->inTransaction(); + if ($hadTransaction) { + $this->bd->commit(); + } + try { + $db = FreshRSS_Context::$system_conf->db; + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + Minz_Log::warning('SQL CREATE TABLE entrytmp...'); + if (defined('SQL_CREATE_TABLE_ENTRYTMP')) { + $sql = sprintf(SQL_CREATE_TABLE_ENTRYTMP, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok = $stm && $stm->execute(); + } else { + global $SQL_CREATE_TABLE_ENTRYTMP; + $ok = !empty($SQL_CREATE_TABLE_ENTRYTMP); + foreach ($SQL_CREATE_TABLE_ENTRYTMP as $instruction) { + $sql = sprintf($instruction, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok &= $stm && $stm->execute(); + } + } + } catch (Exception $e) { + Minz_Log::error('FreshRSS_EntryDAO::createEntryTempTable error: ' . $e->getMessage()); + } + if ($hadTransaction) { + $this->bd->beginTransaction(); + } + return $ok; + } + protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { if ($errorInfo[0] === '42S22') { //ER_BAD_FIELD_ERROR @@ -97,6 +129,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $this->addColumn($column); } } + } elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) { //ER_BAD_TABLE_ERROR + return $this->createEntryTempTable(); //v1.7 } } if (isset($errorInfo[1])) { @@ -110,8 +144,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { private $addEntryPrepared = null; public function addEntry($valuesTmp) { - if ($this->addEntryPrepared === null) { - $sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, ' + if ($this->addEntryPrepared == null) { + $sql = 'INSERT INTO `' . $this->prefix . 'entrytmp` (id, guid, title, author, ' . ($this->isCompressed() ? 'content_bin' : 'content') . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' . 'VALUES(:id, :guid, :title, :author, ' @@ -121,43 +155,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . ', :is_read, :is_favorite, :id_feed, :tags)'; $this->addEntryPrepared = $this->bd->prepare($sql); } - $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']); - $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760); - $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']); - $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']); - $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255); - $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']); - $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255); - $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']); - $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']); - $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023); - $valuesTmp['link'] = safe_ascii($valuesTmp['link']); - $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']); - $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT); - $valuesTmp['lastSeen'] = time(); - $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT); - $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0; - $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT); - $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0; - $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT); - $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT); - $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023); - $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']); + if ($this->addEntryPrepared) { + $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']); + $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760); + $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']); + $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']); + $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255); + $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']); + $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255); + $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']); + $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']); + $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023); + $valuesTmp['link'] = safe_ascii($valuesTmp['link']); + $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']); + $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT); + $valuesTmp['lastSeen'] = time(); + $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT); + $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0; + $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT); + $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0; + $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT); + $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT); + $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023); + $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']); - if ($this->hasNativeHex()) { - $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']); - } else { - $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+ - $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']); + if ($this->hasNativeHex()) { + $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']); + } else { + $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+ + $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']); + } } - if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) { - return $this->bd->lastInsertId(); + return true; } else { $info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); if ($this->autoUpdateDb($info)) { + $this->addEntryPrepared = null; return $this->addEntry($valuesTmp); - } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries + } elseif ((int)((int)$info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); } @@ -165,6 +201,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } + public function commitNewEntries() { + $sql = 'SET @rank=(SELECT MAX(id) - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); ' . //MySQL-specific + 'INSERT IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' . + 'SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date; ' . + 'DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= @rank;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + private $updateEntryPrepared = null; public function updateEntry($valuesTmp) { @@ -212,7 +264,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) { - return $this->bd->lastInsertId(); + return true; } else { $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); if ($this->autoUpdateDb($info)) { @@ -578,18 +630,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 '; } if ($filter) { - if ($filter->getIntitle()) { - $search .= 'AND ' . $alias . 'title LIKE ? '; - $values[] = "%{$filter->getIntitle()}%"; - } - if ($filter->getInurl()) { - $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; - $values[] = "%{$filter->getInurl()}%"; - } - if ($filter->getAuthor()) { - $search .= 'AND ' . $alias . 'author LIKE ? '; - $values[] = "%{$filter->getAuthor()}%"; - } if ($filter->getMinDate()) { $search .= 'AND ' . $alias . 'id >= ? '; $values[] = "{$filter->getMinDate()}000000"; @@ -606,20 +646,69 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $search .= 'AND ' . $alias . 'date <= ? '; $values[] = $filter->getMaxPubdate(); } + + if ($filter->getAuthor()) { + foreach ($filter->getAuthor() as $author) { + $search .= 'AND ' . $alias . 'author LIKE ? '; + $values[] = "%{$author}%"; + } + } + if ($filter->getIntitle()) { + foreach ($filter->getIntitle() as $title) { + $search .= 'AND ' . $alias . 'title LIKE ? '; + $values[] = "%{$title}%"; + } + } if ($filter->getTags()) { - $tags = $filter->getTags(); - foreach ($tags as $tag) { + foreach ($filter->getTags() as $tag) { $search .= 'AND ' . $alias . 'tags LIKE ? '; $values[] = "%{$tag}%"; } } + if ($filter->getInurl()) { + foreach ($filter->getInurl() as $url) { + $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; + $values[] = "%{$url}%"; + } + } + + if ($filter->getNotAuthor()) { + foreach ($filter->getNotAuthor() as $author) { + $search .= 'AND (NOT ' . $alias . 'author LIKE ?) '; + $values[] = "%{$author}%"; + } + } + if ($filter->getNotIntitle()) { + foreach ($filter->getNotIntitle() as $title) { + $search .= 'AND (NOT ' . $alias . 'title LIKE ?) '; + $values[] = "%{$title}%"; + } + } + if ($filter->getNotTags()) { + foreach ($filter->getNotTags() as $tag) { + $search .= 'AND (NOT ' . $alias . 'tags LIKE ?) '; + $values[] = "%{$tag}%"; + } + } + if ($filter->getNotInurl()) { + foreach ($filter->getNotInurl() as $url) { + $search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) '; + $values[] = "%{$url}%"; + } + } + if ($filter->getSearch()) { - $search_values = $filter->getSearch(); - foreach ($search_values as $search_value) { + foreach ($filter->getSearch() as $search_value) { $search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? '; $values[] = "%{$search_value}%"; } } + if ($filter->getNotSearch()) { + foreach ($filter->getNotSearch() as $search_value) { + $search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) '; + $values[] = "%{$search_value}%"; + } + } } return array($values, $search); } diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index b96a62ebc..b25993c47 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -11,6 +11,11 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { } protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) { //undefined_table + return $this->createEntryTempTable(); + } + } return false; } @@ -18,6 +23,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { return false; } + public function commitNewEntries() { + $sql = 'DO $$ +DECLARE +maxrank bigint := (SELECT MAX(id) FROM `' . $this->prefix . 'entrytmp`); +rank bigint := (SELECT maxrank - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); +BEGIN + INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + (SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date); + DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank; +END $$;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + public function size($all = true) { $db = FreshRSS_Context::$system_conf->db; $sql = 'SELECT pg_size_pretty(pg_database_size(?))'; diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 34e854608..ad7bcd865 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -7,21 +7,42 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } protected function autoUpdateDb($errorInfo) { - if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR - //autoAddColumn - if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { - $showCreate = $tableInfo->fetchColumn(); - Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoUpdateDb: ' . $showCreate); - foreach (array('lastSeen', 'hash') as $column) { - if (stripos($showCreate, $column) === false) { - return $this->addColumn($column); - } + Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true)); + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) { + $showCreate = $tableInfo->fetchColumn(); + if (stripos($showCreate, 'entrytmp') === false) { + return $this->createEntryTempTable(); + } + } + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { + $showCreate = $tableInfo->fetchColumn(); + foreach (array('lastSeen', 'hash') as $column) { + if (stripos($showCreate, $column) === false) { + return $this->addColumn($column); } } } return false; } + public function commitNewEntries() { + $sql = ' +CREATE TEMP TABLE `tmp` AS SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date; +INSERT OR IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `tmp` ORDER BY date; +DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`); +DROP TABLE `tmp`;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + protected function sqlConcat($s1, $s2) { return $s1 . '||' . $s2; } diff --git a/app/Models/Factory.php b/app/Models/Factory.php index 6502c38b7..dfccc883e 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -3,14 +3,7 @@ class FreshRSS_Factory { public static function createFeedDao($username = null) { - $conf = Minz_Configuration::get('system'); - switch ($conf->db['type']) { - case 'sqlite': - case 'pgsql': - return new FreshRSS_FeedDAOSQLite($username); - default: - return new FreshRSS_FeedDAO($username); - } + return new FreshRSS_FeedDAO($username); } public static function createEntryDao($username = null) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 7a9cf8612..52d49db6e 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -318,7 +318,7 @@ class FreshRSS_Feed extends Minz_Model { $elinks = array(); foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if (empty($elinks[$elink])) { + if ($elink != '' && empty($elinks[$elink])) { $elinks[$elink] = '1'; $mime = strtolower($enclosure->get_type()); if (strpos($mime, 'image/') === 0) { @@ -327,6 +327,8 @@ class FreshRSS_Feed extends Minz_Model { $content .= '
'; } elseif (strpos($mime, 'video/') === 0) { $content .= ''; + } elseif (strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) { + $content .= ''; } else { unset($elinks[$elink]); } @@ -335,7 +337,7 @@ class FreshRSS_Feed extends Minz_Model { $entry = new FreshRSS_Entry( $this->id(), - $item->get_id(), + $item->get_id(false, false), $title === null ? '' : $title, $author === null ? '' : html_only_entity_decode($author->name), $content === null ? '' : $content, @@ -429,7 +431,7 @@ class FreshRSS_Feed extends Minz_Model { } } else { @mkdir($path, 0777, true); - $key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); + $key = sha1($path . FreshRSS_Context::$system_conf->salt); $hubJson = array( 'hub' => $this->hubUrl, 'key' => $key, @@ -451,15 +453,16 @@ class FreshRSS_Feed extends Minz_Model { //Parameter true to subscribe, false to unsubscribe. function pubSubHubbubSubscribe($state) { - if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { - $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json'; + $url = $this->selfUrl ? $this->selfUrl : $this->url; + if (FreshRSS_Context::$system_conf->base_url && $url) { + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; $hubFile = @file_get_contents($hubFilename); if ($hubFile === false) { Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url); return false; } $hubJson = json_decode($hubFile, true); - if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) { Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url); return false; } @@ -474,13 +477,13 @@ class FreshRSS_Feed extends Minz_Model { } $ch = curl_init(); curl_setopt_array($ch, array( - CURLOPT_URL => $this->hubUrl, + CURLOPT_URL => $hubJson['hub'], CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', CURLOPT_POSTFIELDS => 'hub.verify=sync' . '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe') - . '&hub.topic=' . urlencode($this->selfUrl) + . '&hub.topic=' . urlencode($url) . '&hub.callback=' . urlencode($callbackUrl) ) ); @@ -488,7 +491,7 @@ class FreshRSS_Feed extends Minz_Model { $info = curl_getinfo($ch); file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . - 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl . + 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $url . ' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND); if (substr($info['http_code'], 0, 1) == '2') { diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 0168aebd9..d278122e3 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -92,29 +92,15 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } - public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) { - if ($updateCache) { - $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE - . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' - . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),' - . '`lastUpdate`=?, error=? ' - . 'WHERE id=?'; - } else { - $sql = 'UPDATE `' . $this->prefix . 'feed` ' - . 'SET `lastUpdate`=?, error=? ' - . 'WHERE id=?'; - } - - if ($mtime <= 0) { - $mtime = time(); - } - + public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue() + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET `lastUpdate`=?, error=? ' + . 'WHERE id=?'; $values = array( - $mtime, + $mtime <= 0 ? time() : $mtime, $inError ? 1 : 0, $id, ); - $stm = $this->bd->prepare($sql); if ($stm && $stm->execute($values)) { @@ -294,18 +280,28 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $res[0]['count']; } - public function updateCachedValues() { //For one single feed, call updateLastUpdate($id) - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'INNER JOIN (' - . 'SELECT e.id_feed, ' - . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, ' - . 'COUNT(e.id) AS nbEntries ' - . 'FROM `' . $this->prefix . 'entry` e ' - . 'GROUP BY e.id_feed' - . ') x ON x.id_feed=f.id ' - . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads'; + public function updateCachedValue($id) { //For multiple feeds, call updateCachedValues() + $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE + . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' + . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0) ' + . 'WHERE id=?'; + $values = array($id); $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error updateCachedValue: ' . $info[2]); + return false; + } + } + + public function updateCachedValues() { //For one single feed, call updateCachedValue($id) + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' + . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)'; + $stm = $this->bd->prepare($sql); if ($stm && $stm->execute()) { return $stm->rowCount(); } else { @@ -343,7 +339,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $affected; } - public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after + public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateCachedValue($id) or updateCachedValues() just after $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' . 'WHERE id_feed=:id_feed AND id<=:id_max ' . 'AND is_favorite=0 ' //Do not remove favourites diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php deleted file mode 100644 index 440ae74da..000000000 --- a/app/Models/FeedDAOSQLite.php +++ /dev/null @@ -1,19 +0,0 @@ -prefix . 'feed` ' - . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' - . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)'; - $stm = $this->bd->prepare($sql); - if ($stm && $stm->execute()) { - return $stm->rowCount(); - } else { - $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error updateCachedValues: ' . $info[2]); - return false; - } - } - -} diff --git a/app/Models/Search.php b/app/Models/Search.php index 575a9a2cb..5cc7f8e8d 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -23,18 +23,35 @@ class FreshRSS_Search { private $tags; private $search; + private $not_intitle; + private $not_inurl; + private $not_author; + private $not_tags; + private $not_search; + public function __construct($input) { - if (strcmp($input, '') == 0) { + if ($input == '') { return; } $this->raw_input = $input; + + $input = preg_replace('/:"(.*?)"/', ':"\1"', $input); + + $input = $this->parseNotIntitleSearch($input); + $input = $this->parseNotAuthorSearch($input); + $input = $this->parseNotInurlSearch($input); + $input = $this->parseNotTagsSeach($input); + + $input = $this->parsePubdateSearch($input); + $input = $this->parseDateSearch($input); + $input = $this->parseIntitleSearch($input); $input = $this->parseAuthorSearch($input); $input = $this->parseInurlSearch($input); - $input = $this->parsePubdateSearch($input); - $input = $this->parseDateSearch($input); $input = $this->parseTagsSeach($input); - $this->parseSearch($input); + + $input = $this->parseNotSearch($input); + $input = $this->parseSearch($input); } public function __toString() { @@ -48,6 +65,9 @@ class FreshRSS_Search { public function getIntitle() { return $this->intitle; } + public function getNotIntitle() { + return $this->not_intitle; + } public function getMinDate() { return $this->min_date; @@ -68,18 +88,34 @@ class FreshRSS_Search { public function getInurl() { return $this->inurl; } + public function getNotInurl() { + return $this->not_inurl; + } public function getAuthor() { return $this->author; } + public function getNotAuthor() { + return $this->not_author; + } public function getTags() { return $this->tags; } + public function getNotTags() { + return $this->not_tags; + } public function getSearch() { return $this->search; } + public function getNotSearch() { + return $this->not_search; + } + + private static function removeEmptyValues($anArray) { + return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array(); + } /** * Parse the search string to find intitle keyword and the search related @@ -90,14 +126,28 @@ class FreshRSS_Search { * @return string */ private function parseIntitleSearch($input) { - if (preg_match('/intitle:(?Pshift to mark all articles as read',
'title' => 'Shortcuts',
'user_filter' => 'Access user filters',
- 'user_filter_help' => 'If there is only one user filter, it is used. Else filters are accessible by their number.',
+ 'user_filter_help' => 'If there is only one user filter, it is used. Otherwise, filters are accessible by their number.',
),
'user' => array(
'articles_and_size' => '%s articles (%s)',
diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php
index e7f6b9f85..334d9a8f5 100644
--- a/app/i18n/en/feedback.php
+++ b/app/i18n/en/feedback.php
@@ -2,7 +2,7 @@
return array(
'admin' => array(
- 'optimization_complete' => 'Optimisation complete',
+ 'optimization_complete' => 'Optimization complete',
),
'access' => array(
'denied' => 'You don’t have permission to access this page',
@@ -39,26 +39,26 @@ return array(
'ok' => '%s is now enabled',
),
'no_access' => 'You have no access on %s',
- 'not_enabled' => '%s is not enabled yet',
+ 'not_enabled' => '%s is not enabled',
'not_found' => '%s does not exist',
),
'import_export' => array(
'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.',
'feeds_imported' => 'Your feeds have been imported and will now be updated',
- 'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred',
+ 'feeds_imported_with_errors' => 'Your feeds have been imported, but some errors occurred',
'file_cannot_be_uploaded' => 'File cannot be uploaded!',
'no_zip_extension' => 'ZIP extension is not present on your server.',
'zip_error' => 'An error occured during ZIP import.',
),
'sub' => array(
- 'actualize' => 'Actualise',
+ 'actualize' => 'Updating',
'category' => array(
'created' => 'Category %s has been created.',
'deleted' => 'Category has been deleted.',
'emptied' => 'Category has been emptied',
'error' => 'Category cannot be updated',
'name_exists' => 'Category name already exists.',
- 'no_id' => 'You must precise the id of the category.',
+ 'no_id' => 'You must specify the id of the category.',
'no_name' => 'Category name cannot be empty.',
'not_delete_default' => 'You cannot delete the default category!',
'not_exist' => 'The category does not exist!',
@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS will now be updated to the version %s.',
'error' => 'The update process has encountered an error: %s',
- 'file_is_nok' => 'Check permissions on %s directory. HTTP server must have rights to write into',
+ 'file_is_nok' => 'New version %s available, but check permissions on %s directory. HTTP server must have rights to write into',
'finished' => 'Update completed!',
'none' => 'No update to apply',
'server_not_found' => 'Update server cannot be found. [%s]',
diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php
index 1ee5336bd..05281769f 100644
--- a/app/i18n/en/gen.php
+++ b/app/i18n/en/gen.php
@@ -103,7 +103,7 @@ return array(
'js' => array(
'category_empty' => 'Empty category',
'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
- 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!',
+ 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favourites and user queries. It cannot be cancelled!',
'feedback' => array(
'body_new_articles' => 'There are %%d new articles to read on FreshRSS.',
'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.',
@@ -121,6 +121,7 @@ return array(
'nl' => 'Nederlands',
'ru' => 'Русский',
'tr' => 'Türkçe',
+ 'zh-cn' => '简体中文'
),
'menu' => array(
'about' => 'About',
@@ -173,7 +174,7 @@ return array(
'blank_to_disable' => 'Leave blank to disable',
'by_author' => 'By %s',
'by_default' => 'By default',
- 'damn' => 'Damn!',
+ 'damn' => 'Blast!',
'default_category' => 'Uncategorized',
'no' => 'No',
'not_applicable' => 'Not available',
diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php
index eb6413e3c..a4686de4e 100644
--- a/app/i18n/en/index.php
+++ b/app/i18n/en/index.php
@@ -41,7 +41,7 @@ return array(
'mark_cat_read' => 'Mark category as read',
'mark_feed_read' => 'Mark feed as read',
'newer_first' => 'Newer first',
- 'non-starred' => 'Show all but favorites',
+ 'non-starred' => 'Show all but favourites',
'normal_view' => 'Normal view',
'older_first' => 'Oldest first',
'queries' => 'User queries',
@@ -49,7 +49,7 @@ return array(
'reader_view' => 'Reading view',
'rss_view' => 'RSS feed',
'search_short' => 'Search',
- 'starred' => 'Show only favorites',
+ 'starred' => 'Show only favourites',
'stats' => 'Statistics',
'subscription' => 'Subscriptions management',
'unread' => 'Show only unread',
diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php
index 789433ee6..86600e882 100644
--- a/app/i18n/en/sub.php
+++ b/app/i18n/en/sub.php
@@ -10,10 +10,10 @@ return array(
'feed' => array(
'add' => 'Add a RSS feed',
'advanced' => 'Advanced',
- 'archiving' => 'Archivage',
+ 'archiving' => 'Archiving',
'auth' => array(
'configuration' => 'Login',
- 'help' => 'Connection allows to access HTTP protected RSS feeds',
+ 'help' => 'Allows access to HTTP protected RSS feeds',
'http' => 'HTTP Authentication',
'password' => 'HTTP password',
'username' => 'HTTP username',
@@ -22,7 +22,7 @@ return array(
'css_path' => 'Articles CSS path on original website',
'description' => 'Description',
'empty' => 'This feed is empty. Please verify that it is still maintained.',
- 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
+ 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then update it.',
'in_main_stream' => 'Show in main stream',
'informations' => 'Information',
'keep_history' => 'Minimum number of articles to keep',
diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php
index 7a6d12e17..0c8188623 100644
--- a/app/i18n/fr/conf.php
+++ b/app/i18n/fr/conf.php
@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Afficher les catégories pliées par défaut',
'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)',
'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
+ 'sides_close_article' => 'Cliquer hors de la zone de texte ferme l’article',
'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)',
'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.',
'read' => array(
diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php
index 5966fc3a7..aa19cd02b 100644
--- a/app/i18n/fr/feedback.php
+++ b/app/i18n/fr/feedback.php
@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS va maintenant être mis à jour vers la version %s.',
'error' => 'La mise à jour a rencontré un problème : %s',
- 'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans',
+ 'file_is_nok' => 'Nouvelle version %s disponible, mais veuillez vérifier les droits sur le répertoire %s. Le serveur HTTP doit être capable d’écrire dedans',
'finished' => 'La mise à jour est terminée !',
'none' => 'Aucune mise à jour à appliquer',
'server_not_found' => 'Le serveur de mise à jour n’a pas été trouvé. [%s]',
diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php
index 19b62c9a7..e6ce86ef9 100644
--- a/app/i18n/it/conf.php
+++ b/app/i18n/it/conf.php
@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Mostra categorie aperte di predefinito',
'hide_read_feeds' => 'Nascondi categorie e feeds con articoli già letti (non funziona se “Mostra tutti gli articoli” è selezionato)',
'img_with_lazyload' => 'Usa la modalità "caricamento ritardato" per le immagini',
+ 'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO
'jump_next' => 'Salta al successivo feed o categoria non letto',
'number_divided_when_reader' => 'Diviso 2 nella modalità di lettura.',
'read' => array(
diff --git a/app/i18n/it/feedback.php b/app/i18n/it/feedback.php
index 5851cb2e6..8f3cf3ed6 100644
--- a/app/i18n/it/feedback.php
+++ b/app/i18n/it/feedback.php
@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS verrà aggiornato alla versione %s.',
'error' => 'Il processo di aggiornamento ha riscontrato il seguente errore: %s',
- 'file_is_nok' => 'Verifica i permessi della cartella %s. Il server HTTP deve avere i permessi per la scrittura ',
+ 'file_is_nok' => 'Nuova versione %s, ma verifica i permessi della cartella %s. Il server HTTP deve avere i permessi per la scrittura ',
'finished' => 'Aggiornamento completato con successo!',
'none' => 'Nessun aggiornamento disponibile',
'server_not_found' => 'Server per aggiornamento non disponibile. [%s]',
diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php
index 607fb1892..fdfe6e3bc 100644
--- a/app/i18n/nl/admin.php
+++ b/app/i18n/nl/admin.php
@@ -5,8 +5,8 @@ return array(
'allow_anonymous' => 'Sta bezoekers toe om artikelen te lezen van de standaard gebruiker (%s)',
'allow_anonymous_refresh' => 'Sta bezoekers toe om de artikelen te vernieuwen',
'api_enabled' => 'Sta API toegang toe (nodig voor mobiele apps)',
- 'form' => 'Web formulier (traditioneel, benodigd JavaScript)',
- 'http' => 'HTTP (voor geavanceerde gebruikers met HTTPS)',
+ 'form' => 'Web formulier (traditioneel, JavaScript vereist)',
+ 'http' => 'HTTP (voor gevorderde gebruikers met HTTPS)',
'none' => 'Geen (gevaarlijk)',
'title' => 'Authenticatie',
'title_reset' => 'Authenticatie terugzetten',
@@ -37,8 +37,8 @@ return array(
'ok' => 'U hebt de cURL uitbreiding.',
),
'data' => array(
- 'nok' => 'Controleer de permissies op de ./data map. HTTP server moet rechten hebben om hierin te schrijven',
- 'ok' => 'Permissies op de data map zijn goed.',
+ 'nok' => 'Controleer de permissies op de ./data map. De HTTP server moet rechten hebben om hierin te schrijven',
+ 'ok' => 'Permissies op de data map zijn in orde.',
),
'database' => 'Database installatie',
'dom' => array(
@@ -46,16 +46,16 @@ return array(
'ok' => 'U hebt de benodigde bibliotheek voor het bladeren van DOM.',
),
'entries' => array(
- 'nok' => 'Invoer tabel is slecht geconfigureerd.',
- 'ok' => 'Invoer tabel is ok.',
+ 'nok' => 'Invoertabel is slecht geconfigureerd.',
+ 'ok' => 'Invoertabel is ok.',
),
'favicons' => array(
'nok' => 'Controleer de permissies op de ./data/favicons map. HTTP server moet rechten hebben om hierin te schrijven',
'ok' => 'Permissies op de favicons map zijn goed.',
),
'feeds' => array(
- 'nok' => 'Feed tabel is slecht geconfigureerd.',
- 'ok' => 'Feed tabel is ok.',
+ 'nok' => 'Feedtabel is slecht geconfigureerd.',
+ 'ok' => 'Feedtabel is ok.',
),
'fileinfo' => array(
'nok' => 'U mist de PHP fileinfo (fileinfo package).',
@@ -107,11 +107,11 @@ return array(
'enabled' => 'Ingeschakeld',
'no_configure_view' => 'Deze uitbreiding kan niet worden geconfigureerd.',
'system' => array(
- '_' => 'Systeem uitbreidingen',
- 'no_rights' => 'Systeem uitbreidingen (U hebt hier geen rechten op)',
+ '_' => 'Systeemuitbreidingen',
+ 'no_rights' => 'Systeemuitbreidingen (U hebt hier geen rechten op)',
),
'title' => 'Uitbreidingen',
- 'user' => 'Gebruikers uitbreidingen',
+ 'user' => 'Gebruikersuitbreidingen',
),
'stats' => array(
'_' => 'Statistieken',
@@ -137,7 +137,7 @@ return array(
'no_idle' => 'Er is geen gepauzeerde feed!',
'number_entries' => '%d artikelen',
'percent_of_total' => '%% van totaal',
- 'repartition' => 'Artikelen verdeling',
+ 'repartition' => 'Artikelverdeling',
'status_favorites' => 'Favorieten',
'status_read' => 'Gelezen',
'status_total' => 'Totaal',
@@ -167,20 +167,20 @@ return array(
),
'user' => array(
'articles_and_size' => '%s artikelen (%s)',
- 'create' => 'Creëer nieuwe gebruiker',
+ 'create' => 'Creëer nieuwe gebruiker',
'language' => 'Taal',
'number' => 'Er is %d accounts gemaakt',
'numbers' => 'Er zijn %d accounts gemaakt',
- 'password_form' => 'Wachtwoordshift 可以将全部文章设为已读',
+ 'title' => '快捷键',
+ 'user_filter' => '显示自定义查询',
+ 'user_filter_help' => '如果有多个自定义过滤器,则会按照它们的编号访问。',
+ ),
+ 'user' => array(
+ '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
new file mode 100644
index 000000000..bee5914e9
--- /dev/null
+++ b/app/i18n/zh-cn/feedback.php
@@ -0,0 +1,109 @@
+ array(
+ 'optimization_complete' => '优化完成',
+ ),
+ 'access' => array(
+ 'denied' => '你无权访问此页面',
+ 'not_found' => '你寻找的页面不存在',
+ ),
+ 'auth' => array(
+ 'form' => array(
+ 'not_set' => '配置认证方式时出错。请稍后重试。',
+ 'set' => 'Form 是你当前默认的认证方式。',
+ ),
+ 'login' => array(
+ 'invalid' => '用户名或密码无效',
+ 'success' => '你已成功登录',
+ ),
+ 'logout' => array(
+ 'success' => '你已登出',
+ ),
+ 'no_password_set' => '管理员密码尚未设置。此特性不可用。',
+ ),
+ 'conf' => array(
+ 'error' => '保存配置时出错',
+ 'query_created' => '查询 "%s" 已创建。',
+ 'shortcuts_updated' => '快捷键已更新',
+ 'updated' => '配置已更新',
+ ),
+ 'extensions' => array(
+ 'already_enabled' => '%s 已启用',
+ 'disable' => array(
+ 'ko' => '%s 禁用失败。检查 FressRSS 日志 查看详情。',
+ 'ok' => '%s 现已禁用',
+ ),
+ 'enable' => array(
+ 'ko' => '%s 启用失败。检查 FressRSS 日志 查看详情。',
+ 'ok' => '%s 现已禁用',
+ ),
+ 'no_access' => '你无权访问 %s',
+ 'not_enabled' => '%s 未启用',
+ 'not_found' => '%s 不存在',
+ ),
+ 'import_export' => array(
+ 'export_no_zip_extension' => '服务器未启用 ZIP 扩展。请尝试一个一个导出文件。',
+ 'feeds_imported' => '你的 RSS 源已导入,即将更新',
+ 'feeds_imported_with_errors' => '你的 RSS 源已导入,但发生错误',
+ 'file_cannot_be_uploaded' => '文件未能上传!',
+ 'no_zip_extension' => '服务器未启用 ZIP 扩展。',
+ 'zip_error' => '导入 ZIP 文件时出错',
+ ),
+ 'sub' => array(
+ 'actualize' => '获取',
+ 'category' => array(
+ 'created' => '分类 %s 已创建。',
+ 'deleted' => '分类已删除。',
+ 'emptied' => '分类已清空。',
+ 'error' => '分类更新失败。',
+ 'name_exists' => '分类名已存在。',
+ 'no_id' => '你必须明确分类 ID',
+ 'no_name' => '分类名不能为空。',
+ 'not_delete_default' => '你不能删除默认分类!',
+ 'not_exist' => '分类不存在!',
+ 'over_max' => '你已达到分类数限制 (%d)',
+ 'updated' => '分类已更新。',
+ ),
+ 'feed' => array(
+ 'actualized' => '%s 已更新',
+ 'actualizeds' => 'RSS 源已更新',
+ 'added' => 'RSS 源 %s 已添加',
+ 'already_subscribed' => '你已订阅 %s',
+ 'deleted' => 'RSS 源已删除',
+ 'error' => 'RSS 源更新失败',
+ 'internal_problem' => 'RSS 源添加失败。检查 FressRSS 日志 查看详情。',
+ 'invalid_url' => 'URL %s 无效',
+ 'marked_read' => 'RSS 源已被设为已读',
+ 'n_actualized' => '%d 个 RSS 源已更新',
+ 'n_entries_deleted' => '%d 篇文章已删除',
+ 'no_refresh' => '没有可刷新的 RSS 源…',
+ 'not_added' => '%s 添加失败',
+ 'over_max' => '你已达到 RSS 源数限制 (%d)',
+ 'updated' => 'RSS 源已更新',
+ ),
+ 'purge_completed' => '清除完成 (%d 篇文章已删除)',
+ ),
+ 'update' => array(
+ 'can_apply' => 'FreshRSS 将更新到 版本 %s.',
+ 'error' => '更新出错:%s',
+ 'file_is_nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。',
+ 'finished' => '更新完成!',
+ 'none' => '没有可用更新',
+ 'server_not_found' => '找不到更新服务器 [%s]',
+ ),
+ 'user' => array(
+ 'created' => array(
+ '_' => '用户 %s 已创建',
+ 'error' => '用户 %s 创建失败',
+ ),
+ 'deleted' => array(
+ '_' => '用户 %s 已删除',
+ 'error' => '用户 %s 删除失败',
+ ),
+ ),
+ 'profile' => array(
+ 'error' => '你的帐户修改失败',
+ 'updated' => '你的帐户已修改',
+ ),
+);
diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php
new file mode 100644
index 000000000..a4ef03bc9
--- /dev/null
+++ b/app/i18n/zh-cn/gen.php
@@ -0,0 +1,185 @@
+ array(
+ 'actualize' => '获取',
+ 'back_to_rss_feeds' => '← 返回',
+ 'cancel' => '取消',
+ 'create' => '创建',
+ 'disable' => '禁用',
+ 'empty' => '清空',
+ 'enable' => '启用',
+ 'export' => '导出',
+ 'filter' => '过滤器',
+ 'import' => '导入',
+ 'manage' => '管理',
+ 'mark_read' => '设为已读',
+ 'mark_favorite' => '加入收藏',
+ 'remove' => '删除',
+ 'see_website' => '查看网站',
+ 'submit' => '提交',
+ 'truncate' => '删除所有文章',
+ ),
+ 'auth' => array(
+ 'email' => 'Email 地址',
+ 'keep_logged_in' => '自动登录(%s 天)',
+ 'login' => '登录',
+ 'logout' => '登出',
+ 'password' => array(
+ '_' => '密码',
+ 'format' => '至少 7 个字符',
+ ),
+ 'registration' => array(
+ '_' => '新账户',
+ 'ask' => '创建新账户?',
+ 'title' => '账户创建',
+ ),
+ 'reset' => '认证重置',
+ 'username' => array(
+ '_' => '用户名',
+ 'admin' => '管理员用户名',
+ 'format' => '最大 16 个数字或字母',
+ ),
+ ),
+ 'date' => array(
+ 'Apr' => '\\A\\p\\r\\i\\l',
+ 'Aug' => '\\A\\u\\g\\u\\s\\t',
+ 'Dec' => '\\D\\e\\c\\e\\m\\b\\e\\r',
+ 'Feb' => '\\F\\e\\b\\r\\u\\a\\r\\y',
+ 'Jan' => '\\J\\a\\n\\u\\a\\r\\y',
+ 'Jul' => '\\J\\u\\l\\y',
+ 'Jun' => '\\J\\u\\n\\e',
+ 'Mar' => '\\M\\a\\r\\c\\h',
+ 'May' => '\\M\\a\\y',
+ 'Nov' => '\\N\\o\\v\\e\\m\\b\\e\\r',
+ 'Oct' => '\\O\\c\\t\\o\\b\\e\\r',
+ 'Sep' => '\\S\\e\\p\\t\\e\\m\\b\\e\\r',
+ 'apr' => '四月',
+ 'april' => '四月',
+ 'aug' => '八月',
+ 'august' => '八月',
+ 'before_yesterday' => '昨天以前',
+ 'dec' => '十二月',
+ 'december' => '十二月',
+ 'feb' => '二月',
+ 'february' => '二月',
+ 'format_date' => 'Y\\年n\\月j\\日',
+ 'format_date_hour' => 'Y\\年n\\月j\\日 H\\:i',
+ 'fri' => '周五',
+ 'jan' => '一月',
+ 'january' => '一月',
+ 'jul' => '七月',
+ 'july' => '七月',
+ 'jun' => '六月',
+ 'june' => '六月',
+ 'last_3_month' => '最近三个月',
+ 'last_6_month' => '最近六个月',
+ 'last_month' => '上月',
+ 'last_week' => '上周',
+ 'last_year' => '去年',
+ 'mar' => '三月',
+ 'march' => '三月',
+ 'may' => '五月',
+ 'mon' => '周一',
+ 'month' => '个月',
+ 'nov' => '十一月',
+ 'november' => '十一月',
+ 'oct' => '十月',
+ 'october' => '十月',
+ 'sat' => '周日',
+ 'sep' => '九月',
+ 'september' => '九月',
+ 'sun' => '周日',
+ 'thu' => '周四',
+ 'today' => '今天',
+ 'tue' => '周二',
+ 'wed' => '周三',
+ 'yesterday' => '昨天',
+ ),
+ 'freshrss' => array(
+ '_' => 'FreshRSS',
+ 'about' => '关于 FreshRSS',
+ ),
+ 'js' => array(
+ 'category_empty' => '清空分类',
+ 'confirm_action' => '你确定要执行此操作吗?这将不可撤销!',
+ 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询。这将不可撤销!',
+ 'feedback' => array(
+ 'body_new_articles' => 'FreshRSS 中有 %%d 篇文章等待阅读。',
+ 'request_failed' => '请求失败,这可能是因为网络连接问题。',
+ 'title_new_articles' => 'FreshRSS: 新文章!',
+ ),
+ 'new_article' => '发现新文章,点击刷新页面。',
+ 'should_be_activated' => 'JavaScript 必须启用',
+ ),
+ 'lang' => array(
+ 'cz' => 'Čeština',
+ 'de' => 'Deutsch',
+ 'en' => 'English1',
+ 'fr' => 'Français',
+ 'it' => 'Italiano1',
+ 'nl' => 'Nederlands',
+ 'ru' => 'Русский',
+ 'tr' => 'Türkçe',
+ 'zh-cn' => '简体中文'
+ ),
+ 'menu' => array(
+ 'about' => '关于',
+ 'admin' => '管理',
+ 'archiving' => '存档',
+ 'authentication' => '认证',
+ 'check_install' => '环境检查',
+ 'configuration' => '配置',
+ 'display' => '显示',
+ 'extensions' => '扩展',
+ 'logs' => '日志',
+ 'queries' => '自定义查询',
+ 'reading' => '阅读',
+ 'search' => '搜索内容或#标签',
+ 'sharing' => '分享',
+ 'shortcuts' => '快捷键',
+ 'stats' => '统计',
+ 'system' => '系统配置',
+ 'update' => '更新',
+ 'user_management' => '用户管理',
+ 'user_profile' => '用户帐户',
+ ),
+ 'pagination' => array(
+ 'first' => '第一页',
+ 'last' => '最后一页',
+ 'load_more' => '载入更多文章',
+ 'mark_all_read' => '全部设为已读',
+ 'next' => '下一页',
+ 'nothing_to_load' => '没有更多文章了',
+ 'previous' => '上一页',
+ ),
+ 'share' => array(
+ 'blogotext' => 'Blogotext',
+ 'diaspora' => 'Diaspora*',
+ 'email' => 'Email',
+ 'facebook' => 'Facebook',
+ 'g+' => 'Google+',
+ 'movim' => 'Movim',
+ 'print' => 'Print',
+ 'shaarli' => 'Shaarli',
+ 'twitter' => 'Twitter',
+ 'wallabag' => 'wallabag v1',
+ 'wallabagv2' => 'wallabag v2',
+ 'jdh' => 'Journal du hacker',
+ 'Known' => 'Known based sites',
+ 'gnusocial' => 'GNU social',
+ ),
+ 'short' => array(
+ 'attention' => '警告!',
+ 'blank_to_disable' => '留空以禁用',
+ 'by_author' => 'By %s',
+ 'by_default' => '默认',
+ 'damn' => '错误!',
+ 'default_category' => '未分类',
+ 'no' => '否',
+ 'not_applicable' => '不可用',
+ 'ok' => '正常!',
+ 'or' => '或',
+ 'yes' => '是',
+ ),
+);
diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php
new file mode 100644
index 000000000..0d6e8e82d
--- /dev/null
+++ b/app/i18n/zh-cn/index.php
@@ -0,0 +1,61 @@
+ array(
+ '_' => '关于',
+ 'agpl3' => 'AGPL 3',
+ 'bugs_reports' => 'Bug 报告',
+ 'credits' => '致谢',
+ 'credits_content' => '某些设计元素来自于 Bootstrap ,尽管 FreshRSS 并没有使用此框架。图标 来自于 GNOME 项目。Open Sans 字体出自 Steve Matteson 之手。FreshRSS 基于 PHP 框架 Minz。',
+ 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 Kriss Feed 或 Leed。 它不仅轻快又易用,而且强大又易于配置。',
+ 'github' => 'Github Issues',
+ 'license' => '授权',
+ 'project_website' => '项目网站',
+ 'title' => '关于',
+ 'version' => '版本',
+ 'website' => '网站',
+ ),
+ 'feed' => array(
+ 'add' => '你可以添加一些 RSS 源。',
+ 'empty' => '暂时没有文章可显示。',
+ 'rss_of' => '%s 的 RSS 源',
+ 'title' => '首页',
+ 'title_global' => '全屏视图',
+ 'title_fav' => '收藏',
+ ),
+ 'log' => array(
+ '_' => '日志',
+ 'clear' => '清除日志',
+ 'empty' => '日志文件为空',
+ 'title' => '日志',
+ ),
+ 'menu' => array(
+ 'about' => '关于 FreshRSS',
+ 'add_query' => '添加查询',
+ 'before_one_day' => '一天前',
+ 'before_one_week' => '一周前',
+ 'favorites' => '收藏 (%s)',
+ 'global_view' => '全屏视图',
+ 'main_stream' => '首页',
+ 'mark_all_read' => '全部设为已读',
+ 'mark_cat_read' => '此分类设为已读',
+ 'mark_feed_read' => '此源设为已读',
+ 'newer_first' => '由新到旧',
+ 'non-starred' => '不显示收藏',
+ 'normal_view' => '普通视图',
+ 'older_first' => '由旧到新',
+ 'queries' => '自定义查询',
+ 'read' => '只显示已读',
+ 'reader_view' => '阅读视图',
+ 'rss_view' => 'RSS 源',
+ 'search_short' => '搜索',
+ 'starred' => '只显示收藏',
+ 'stats' => '统计',
+ 'subscription' => '订阅管理',
+ 'unread' => '只显示未读',
+ ),
+ 'share' => '分享',
+ 'tag' => array(
+ 'related' => '相关标签',
+ ),
+);
diff --git a/app/i18n/zh-cn/install.php b/app/i18n/zh-cn/install.php
new file mode 100644
index 000000000..f247a20fd
--- /dev/null
+++ b/app/i18n/zh-cn/install.php
@@ -0,0 +1,119 @@
+ array(
+ 'finish' => '完成安装',
+ 'fix_errors_before' => '请在继续下一步前修复错误。',
+ 'keep_install' => '保留以前配置',
+ 'next_step' => '下一步',
+ 'reinstall' => '重新安装 FreshRSS',
+ ),
+ 'auth' => array(
+ 'form' => 'Web form (传统方式, 需要 JavaScript)',
+ 'http' => 'HTTP (面向启用 HTTPS 的高级用户)',
+ 'none' => '无 (危险)',
+ 'password_form' => '密码-
+
@@ -629,7 +613,7 @@ function printStep3() { diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index f6d824d55..04ee03cd6 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -149,6 +149,7 @@ token) { + $url_output['params']['user'] = Minz_Session::param('currentUser'); $url_output['params']['token'] = FreshRSS_Context::$user_conf->token; } if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) { diff --git a/app/views/auth/index.phtml b/app/views/auth/index.phtml index 010eae33f..20966f24e 100644 --- a/app/views/auth/index.phtml +++ b/app/views/auth/index.phtml @@ -52,19 +52,6 @@ - -