Merge pull request #1549 from FreshRSS/dev

Version 1.7.0
This commit is contained in:
Alexandre Alapetite
2017-06-03 15:18:04 +02:00
committed by GitHub
212 changed files with 5860 additions and 1357 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -14,7 +14,7 @@ Enfin, il permet lajout 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)
![Logo de FreshRSS](./doc/FreshRSS-logo.png)
![Logo de FreshRSS](./docs/img/FreshRSS-logo.png)
# 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 dencore 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 dencodages), [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
![Capture décran de FreshRSS](./doc/FreshRSS-screenshot.png)
![Capture décran de FreshRSS](./docs/img/FreshRSS-screenshot.png)
# Documentation
* http://doc.freshrss.org/fr/
* https://github.com/FreshRSS/documentation
# Installation
1. Récupérez lapplication FreshRSS via la commande git ou [en téléchargeant larchive](../releases)
@@ -48,7 +52,7 @@ Nous sommes une communauté amicale.
4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions dinstallation
* ou utilisez [linterface en ligne de commande](./cli/README.md)
5. Tout devrait fonctionner :) En cas de problème, nhé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
* [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](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 daccès aux répertoires clés de lapplication. Si vous les bougez, tout se passe ici.
* En cas de problème, les logs peuvent être utile à lire, soit depuis linterface de FreshRSS, soit manuellement depuis `./data/log/*.log`.
* En cas de problème, les logs peuvent être utile à lire, soit depuis linterface 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/)

View File

@@ -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)
![FreshRSS logo](./doc/FreshRSS-logo.png)
![FreshRSS logo](./docs/img/FreshRSS-logo.png)
# 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
![FreshRSS screenshot](./doc/FreshRSS-screenshot.png)
![FreshRSS screenshot](./docs/img/FreshRSS-screenshot.png)
# 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
* [![Install on Cloudron](https://cloudron.io/img/button.svg)](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/)

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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(?))';

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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 .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
} elseif (strpos($mime, 'video/') === 0) {
$content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
} elseif (strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) {
$content .= '<p class="enclosure"><a download="" href="' . $elink . '">💾</a></p>';
} 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') {

View File

@@ -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

View File

@@ -1,19 +0,0 @@
<?php
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
public function updateCachedValues() { //For one single feed, call updateLastUpdate($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 {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
return false;
}
}
}

View File

@@ -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('/:&quot;(.*?)&quot;/', ':"\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:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
if (preg_match_all('/\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->intitle = $matches['search'];
return str_replace($matches[0], '', $input);
$input = str_replace($matches[0], '', $input);
}
if (preg_match('/intitle:(?P<search>\w*)/', $input, $matches)) {
$this->intitle = $matches['search'];
return str_replace($matches[0], '', $input);
if (preg_match_all('/\bintitle:(?P<search>\w*)/', $input, $matches)) {
$this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->intitle = self::removeEmptyValues($this->intitle);
return $input;
}
private function parseNotIntitleSearch($input) {
if (preg_match_all('/[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->not_intitle = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $input, $matches)) {
$this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->not_intitle = self::removeEmptyValues($this->not_intitle);
return $input;
}
@@ -112,30 +162,54 @@ class FreshRSS_Search {
* @return string
*/
private function parseAuthorSearch($input) {
if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
if (preg_match_all('/\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->author = $matches['search'];
return str_replace($matches[0], '', $input);
$input = str_replace($matches[0], '', $input);
}
if (preg_match('/author:(?P<search>\w*)/', $input, $matches)) {
$this->author = $matches['search'];
return str_replace($matches[0], '', $input);
if (preg_match_all('/\bauthor:(?P<search>\w*)/', $input, $matches)) {
$this->author = array_merge($this->author ? $this->author : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->author = self::removeEmptyValues($this->author);
return $input;
}
private function parseNotAuthorSearch($input) {
if (preg_match_all('/[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->not_author = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
if (preg_match_all('/[!-]author:(?P<search>\w*)/', $input, $matches)) {
$this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->not_author = self::removeEmptyValues($this->not_author);
return $input;
}
/**
* Parse the search string to find inurl keyword and the search related
* to it.
* The search is the first word following the keyword except.
* The search is the first word following the keyword.
*
* @param string $input
* @return string
*/
private function parseInurlSearch($input) {
if (preg_match('/inurl:(?P<search>[^\s]*)/', $input, $matches)) {
if (preg_match_all('/\binurl:(?P<search>[^\s]*)/', $input, $matches)) {
$this->inurl = $matches['search'];
return str_replace($matches[0], '', $input);
$input = str_replace($matches[0], '', $input);
}
$this->inurl = self::removeEmptyValues($this->inurl);
return $input;
}
private function parseNotInurlSearch($input) {
if (preg_match_all('/[!-]inurl:(?P<search>[^\s]*)/', $input, $matches)) {
$this->not_inurl = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$this->not_inurl = self::removeEmptyValues($this->not_inurl);
return $input;
}
@@ -148,9 +222,12 @@ class FreshRSS_Search {
* @return string
*/
private function parseDateSearch($input) {
if (preg_match('/date:(?P<search>[^\s]*)/', $input, $matches)) {
list($this->min_date, $this->max_date) = parseDateInterval($matches['search']);
return str_replace($matches[0], '', $input);
if (preg_match_all('/\bdate:(?P<search>[^\s]*)/', $input, $matches)) {
$input = str_replace($matches[0], '', $input);
$dates = self::removeEmptyValues($matches['search']);
if (!empty($dates[0])) {
list($this->min_date, $this->max_date) = parseDateInterval($dates[0]);
}
}
return $input;
}
@@ -164,9 +241,12 @@ class FreshRSS_Search {
* @return string
*/
private function parsePubdateSearch($input) {
if (preg_match('/pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']);
return str_replace($matches[0], '', $input);
if (preg_match_all('/\bpubdate:(?P<search>[^\s]*)/', $input, $matches)) {
$input = str_replace($matches[0], '', $input);
$dates = self::removeEmptyValues($matches['search']);
if (!empty($dates[0])) {
list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($dates[0]);
}
}
return $input;
}
@@ -182,8 +262,18 @@ class FreshRSS_Search {
private function parseTagsSeach($input) {
if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
$this->tags = $matches['search'];
return str_replace($matches[0], '', $input);
$input = str_replace($matches[0], '', $input);
}
$this->tags = self::removeEmptyValues($this->tags);
return $input;
}
private function parseNotTagsSeach($input) {
if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
$this->not_tags = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$this->not_tags = self::removeEmptyValues($this->not_tags);
return $input;
}
@@ -196,16 +286,16 @@ class FreshRSS_Search {
* @return string
*/
private function parseSearch($input) {
$input = $this->cleanSearch($input);
if (strcmp($input, '') == 0) {
$input = self::cleanSearch($input);
if ($input == '') {
return;
}
if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->search = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$input = $this->cleanSearch($input);
if (strcmp($input, '') == 0) {
$input = self::cleanSearch($input);
if ($input == '') {
return;
}
if (is_array($this->search)) {
@@ -215,13 +305,33 @@ class FreshRSS_Search {
}
}
private function parseNotSearch($input) {
$input = self::cleanSearch($input);
if ($input == '') {
return;
}
if (preg_match_all('/[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->not_search = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
if ($input == '') {
return;
}
if (preg_match_all('/[!-](?P<search>[^\s]+)/', $input, $matches)) {
$this->not_search = array_merge(is_array($this->not_search) ? $this->not_search : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->not_search = self::removeEmptyValues($this->not_search);
return $input;
}
/**
* Remove all unnecessary spaces in the search
*
* @param string $input
* @return string
*/
private function cleanSearch($input) {
private static function cleanSearch($input) {
$input = preg_replace('/\s+/', ' ', $input);
return trim($input);
}

View File

@@ -14,21 +14,23 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
$ok = false;
$bd_prefix_user = $db['prefix'] . $username . '_';
if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL
$sql = sprintf(SQL_CREATE_TABLES, $bd_prefix_user, _t('gen.short.default_category'));
$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok = $stm && $stm->execute();
} else { //E.g. SQLite
global $SQL_CREATE_TABLES;
global $SQL_CREATE_TABLE_ENTRYTMP;
if (is_array($SQL_CREATE_TABLES)) {
$ok = true;
foreach ($SQL_CREATE_TABLES as $instruction) {
$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP);
$ok = !empty($instructions);
foreach ($instructions as $instruction) {
$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok &= ($stm && $stm->execute());
}
}
}
if ($insertDefaultFeeds) {
if ($ok && $insertDefaultFeeds) {
if (defined('SQL_INSERT_FEEDS')) { //E.g. MySQL
$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
$stm = $userPDO->bd->prepare($sql);

View File

@@ -55,18 +55,44 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` (
INDEX (`is_favorite`), -- v0.7
INDEX (`is_read`), -- v0.7
INDEX `entry_lastSeen_index` (`lastSeen`) -- v1.1.1
-- INDEX `entry_feed_read_index` (`id_feed`,`is_read`) -- v1.7 Located futher down
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");
');
define('SQL_CREATE_TABLE_ENTRYTMP', '
CREATE TABLE IF NOT EXISTS `%1$sentrytmp` ( -- v1.7
`id` bigint NOT NULL,
`guid` varchar(760) CHARACTER SET latin1 NOT NULL,
`title` varchar(255) NOT NULL,
`author` varchar(255),
`content_bin` blob,
`link` varchar(1023) CHARACTER SET latin1 NOT NULL,
`date` int(11),
`lastSeen` INT(11) DEFAULT 0,
`hash` BINARY(16),
`is_read` boolean NOT NULL DEFAULT 0,
`is_favorite` boolean NOT NULL DEFAULT 0,
`id_feed` SMALLINT,
`tags` varchar(1023),
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE KEY (`id_feed`,`guid`),
INDEX (`date`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = INNODB;
CREATE INDEX `entry_feed_read_index` ON `%1$sentry`(`id_feed`,`is_read`); -- v1.7 Located here to be auto-added
');
define('SQL_INSERT_FEEDS', '
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
');
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentry`, `%1$sfeed`, `%1$scategory`');
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`');
define('SQL_UPDATE_UTF8MB4', '
ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -32,7 +32,7 @@ $SQL_CREATE_TABLES = array(
'CREATE TABLE IF NOT EXISTS "%1$sentry" (
"id" BIGINT NOT NULL PRIMARY KEY,
"guid" VARCHAR(760) UNIQUE NOT NULL,
"guid" VARCHAR(760) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"author" VARCHAR(255),
"content" TEXT,
@@ -54,10 +54,34 @@ $SQL_CREATE_TABLES = array(
'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);',
);
global $SQL_CREATE_TABLE_ENTRYTMP;
$SQL_CREATE_TABLE_ENTRYTMP = array(
'CREATE TABLE IF NOT EXISTS "%1$sentrytmp" ( -- v1.7
"id" BIGINT NOT NULL PRIMARY KEY,
"guid" VARCHAR(760) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"author" VARCHAR(255),
"content" TEXT,
"link" VARCHAR(1023) NOT NULL,
"date" INT,
"lastSeen" INT DEFAULT 0,
"hash" BYTEA,
"is_read" SMALLINT NOT NULL DEFAULT 0,
"is_favorite" SMALLINT NOT NULL DEFAULT 0,
"id_feed" SMALLINT,
"tags" VARCHAR(1023),
FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE ("id_feed","guid")
);',
'CREATE INDEX %1$sentrytmp_date_index ON "%1$sentrytmp" ("date");',
'CREATE INDEX %1$sentry_feed_read_index ON "%1$sentry" ("id_feed","is_read");', //v1.7
);
global $SQL_INSERT_FEEDS;
$SQL_INSERT_FEEDS = array(
'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');',
'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');',
);
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentry", "%1$sfeed", "%1$scategory"');
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"');

View File

@@ -26,7 +26,6 @@ $SQL_CREATE_TABLES = array(
FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
UNIQUE (`url`)
);',
'CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);',
'CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);',
'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);',
@@ -49,7 +48,6 @@ $SQL_CREATE_TABLES = array(
FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`id_feed`,`guid`)
);',
'CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`);',
'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`);',
'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`);', //v1.1.1
@@ -57,10 +55,35 @@ $SQL_CREATE_TABLES = array(
'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");',
);
global $SQL_CREATE_TABLE_ENTRYTMP;
$SQL_CREATE_TABLE_ENTRYTMP = array(
'CREATE TABLE IF NOT EXISTS `entrytmp` ( -- v1.7
`id` bigint NOT NULL,
`guid` varchar(760) NOT NULL,
`title` varchar(255) NOT NULL,
`author` varchar(255),
`content` text,
`link` varchar(1023) NOT NULL,
`date` int(11),
`lastSeen` INT(11) DEFAULT 0,
`hash` BINARY(16),
`is_read` boolean NOT NULL DEFAULT 0,
`is_favorite` boolean NOT NULL DEFAULT 0,
`id_feed` SMALLINT,
`tags` varchar(1023),
PRIMARY KEY (`id`),
FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`id_feed`,`guid`)
);',
'CREATE INDEX IF NOT EXISTS entrytmp_date_index ON `entrytmp`(`date`);',
'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);', //v1.7
);
global $SQL_INSERT_FEEDS;
$SQL_INSERT_FEEDS = array(
'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);',
'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);',
);
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS entry, feed, category');
define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytmp`, `entry`, `feed`, `category`');

View File

@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',
'hide_read_feeds' => 'Schovat kategorie a kanály s nulovým počtem nepřečtených článků (nefunguje s nastavením “Zobrazit všechny články”)',
'img_with_lazyload' => 'Použít "lazy load" mód pro načítaní obrázků',
'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO
'jump_next' => 'skočit na další nepřečtený (kanál nebo kategorii)',
'number_divided_when_reader' => 'V režimu “Čtení” děleno dvěma.',
'read' => array(

View File

@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS bude nyní upgradováno na <strong>verzi %s</strong>.',
'error' => 'Během upgrade došlo k chybě: %s',
'file_is_nok' => 'Zkontrolujte oprávnění adresáře <em>%s</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'file_is_nok' => '<strong>Verzi %s</strong>. Zkontrolujte oprávnění adresáře <em>%s</em>. HTTP server musí mít do tohoto adresáře práva zápisu',
'finished' => 'Upgrade hotov!',
'none' => 'Novější verze není k dispozici',
'server_not_found' => 'Nelze nalézt server s instalačním souborem. [%s]',

View File

@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen',
'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)',
'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO
'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)',
'number_divided_when_reader' => 'Geteilt durch 2 in der Lese-Ansicht.',
'read' => array(

View File

@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS wird nun auf die <strong>Version %s</strong> aktualisiert.',
'error' => 'Der Aktualisierungsvorgang stieß auf einen Fehler: %s',
'file_is_nok' => 'Überprüfen Sie die Berechtigungen des Verzeichnisses <em>%s</em>. Der HTTP-Server muss Schreibrechte besitzen',
'file_is_nok' => '<strong>Version %s</strong>. Überprüfen Sie die Berechtigungen des Verzeichnisses <em>%s</em>. Der HTTP-Server muss Schreibrechte besitzen',
'finished' => 'Aktualisierung abgeschlossen!',
'none' => 'Keine Aktualisierung zum Anwenden',
'server_not_found' => 'Der Aktualisierungs-Server kann nicht gefunden werden. [%s]',

View File

@@ -21,11 +21,11 @@ return array(
'ok' => 'Permissions on cache directory are good.',
),
'categories' => array(
'nok' => 'Category table is bad configured.',
'nok' => 'Category table is improperly configured.',
'ok' => 'Category table is ok.',
),
'connection' => array(
'nok' => 'Connection to the database cannot being established.',
'nok' => 'Connection to the database cannot be established.',
'ok' => 'Connection to the database is ok.',
),
'ctype' => array(
@@ -46,7 +46,7 @@ return array(
'ok' => 'You have the required library to browse the DOM.',
),
'entries' => array(
'nok' => 'Entry table is bad configured.',
'nok' => 'Entry table is improperly configured.',
'ok' => 'Entry table is ok.',
),
'favicons' => array(
@@ -54,7 +54,7 @@ return array(
'ok' => 'Permissions on favicons directory are good.',
),
'feeds' => array(
'nok' => 'Feed table is bad configured.',
'nok' => 'Feed table is improperly configured.',
'ok' => 'Feed table is ok.',
),
'fileinfo' => array(
@@ -84,8 +84,8 @@ return array(
'ok' => 'Your PHP version is %s, which is compatible with FreshRSS.',
),
'tables' => array(
'nok' => 'There is one or more lacking tables in the database.',
'ok' => 'Tables are existing in the database.',
'nok' => 'There are one or more missing tables in the database.',
'ok' => 'The appropriate tables exist in the database.',
),
'title' => 'Installation checking',
'tokens' => array(
@@ -103,7 +103,7 @@ return array(
),
'extensions' => array(
'disabled' => 'Disabled',
'empty_list' => 'There is no installed extension',
'empty_list' => 'There are no installed extensions',
'enabled' => 'Enabled',
'no_configure_view' => 'This extension cannot be configured.',
'system' => array(
@@ -160,7 +160,7 @@ return array(
'_' => 'Update system',
'apply' => 'Apply',
'check' => 'Check for new updates',
'current_version' => 'Your current version of FreshRSS is the %s.',
'current_version' => 'Your current version of FreshRSS is %s.',
'last' => 'Last verification: %s',
'none' => 'No update to apply',
'title' => 'Update system',
@@ -169,8 +169,8 @@ return array(
'articles_and_size' => '%s articles (%s)',
'create' => 'Create new user',
'language' => 'Language',
'number' => 'There is %d account created yet',
'numbers' => 'There are %d accounts created yet',
'number' => 'There is %d account created',
'numbers' => 'There are %d accounts created',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
'password_format' => 'At least 7 characters',
'title' => 'Manage users',

View File

@@ -8,7 +8,7 @@ return array(
'help' => 'More options are available in the individual feed settings',
'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
'optimize' => 'Optimise database',
'optimize_help' => 'To do occasionally to reduce the size of the database',
'optimize_help' => 'Do occasionally to reduce the size of the database',
'purge_now' => 'Purge now',
'title' => 'Archiving',
'ttl' => 'Do not automatically refresh more often than',
@@ -44,10 +44,10 @@ return array(
'filter' => 'Filter applied:',
'get_all' => 'Display all articles',
'get_category' => 'Display "%s" category',
'get_favorite' => 'Display favorite articles',
'get_favorite' => 'Display favourite articles',
'get_feed' => 'Display "%s" feed',
'no_filter' => 'No filter',
'none' => 'You havent created any user query yet.',
'none' => 'You havent created any user queries yet.',
'number' => 'Query n°%d',
'order_asc' => 'Display oldest articles first',
'order_desc' => 'Display newest articles first',
@@ -56,14 +56,14 @@ return array(
'state_1' => 'Display read articles',
'state_2' => 'Display unread articles',
'state_3' => 'Display all articles',
'state_4' => 'Display favorite articles',
'state_5' => 'Display read favorite articles',
'state_6' => 'Display unread favorite articles',
'state_7' => 'Display favorite articles',
'state_8' => 'Display not favorite articles',
'state_9' => 'Display read not favorite articles',
'state_10' => 'Display unread not favorite articles',
'state_11' => 'Display not favorite articles',
'state_4' => 'Display favourite articles',
'state_5' => 'Display read favourite articles',
'state_6' => 'Display unread favourite articles',
'state_7' => 'Display favourite articles',
'state_8' => 'Display not favourite articles',
'state_9' => 'Display read not favourite articles',
'state_10' => 'Display unread not favourite articles',
'state_11' => 'Display not favourite articles',
'state_12' => 'Display all articles',
'state_13' => 'Display read articles',
'state_14' => 'Display unread articles',
@@ -74,7 +74,7 @@ return array(
'_' => 'Profile management',
'delete' => array(
'_' => 'Account deletion',
'warn' => 'Your account and all the related data will be deleted.',
'warn' => 'Your account and all related data will be deleted.',
),
'password_api' => 'API password<br /><small>(e.g., for mobile apps)</small>',
'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
@@ -85,21 +85,22 @@ return array(
'_' => 'Reading',
'after_onread' => 'After “mark all as read”,',
'articles_per_page' => 'Number of articles per page',
'auto_load_more' => 'Load next articles at the page bottom',
'auto_load_more' => 'Load more articles at the page bottom',
'auto_remove_article' => 'Hide articles after reading',
'mark_updated_article_unread' => 'Mark updated articles as unread',
'confirm_enabled' => 'Display a confirmation dialog on “mark all as read” actions',
'display_articles_unfolded' => 'Show articles unfolded by default',
'display_categories_unfolded' => 'Show categories folded by default',
'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)',
'hide_read_feeds' => 'Hide categories & feeds with no unread articles (does not work with “Show all articles” configuration)',
'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
'sides_close_article' => 'Clicking outside of article text area closes the article',
'jump_next' => 'jump to next unread sibling (feed or category)',
'number_divided_when_reader' => 'Divided by 2 in the reading view.',
'read' => array(
'article_open_on_website' => 'when article is opened on its original website',
'article_viewed' => 'when article is viewed',
'scroll' => 'while scrolling',
'upon_reception' => 'upon reception of the article',
'upon_reception' => 'upon receiving the article',
'when' => 'Mark article as read…',
),
'show' => array(
@@ -110,7 +111,7 @@ return array(
),
'sort' => array(
'_' => 'Sort order',
'newer_first' => 'Newer first',
'newer_first' => 'Newest first',
'older_first' => 'Oldest first',
),
'sticky_post' => 'Stick the article to the top when opened',
@@ -142,7 +143,7 @@ return array(
'_' => 'Shortcuts',
'article_action' => 'Article actions',
'auto_share' => 'Share',
'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.',
'auto_share_help' => 'If there is only one sharing mode, it is used. Otherwise, modes are accessible by their number.',
'close_dropdown' => 'Close menus',
'collapse_article' => 'Collapse',
'first_article' => 'Skip to the first article',
@@ -162,7 +163,7 @@ return array(
'shift_for_all_read' => '+ <code>shift</code> 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)',

View File

@@ -2,7 +2,7 @@
return array(
'admin' => array(
'optimization_complete' => 'Optimisation complete',
'optimization_complete' => 'Optimization complete',
),
'access' => array(
'denied' => 'You dont 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 <strong>version %s</strong>.',
'error' => 'The update process has encountered an error: %s',
'file_is_nok' => 'Check permissions on <em>%s</em> directory. HTTP server must have rights to write into',
'file_is_nok' => 'New <strong>version %s</strong> available, but check permissions on <em>%s</em> 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]',

View File

@@ -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 <em>%s</em>',
'by_default' => 'By default',
'damn' => 'Damn!',
'damn' => 'Blast!',
'default_category' => 'Uncategorized',
'no' => 'No',
'not_applicable' => 'Not available',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 larticle',
'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(

View File

@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS va maintenant être mis à jour vers la <strong>version %s</strong>.',
'error' => 'La mise à jour a rencontré un problème : %s',
'file_is_nok' => 'Veuillez vérifier les droits sur le répertoire <em>%s</em>. Le serveur HTTP doit être capable décrire dedans',
'file_is_nok' => 'Nouvelle <strong>version %s</strong> disponible, mais veuillez vérifier les droits sur le répertoire <em>%s</em>. 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 na pas été trouvé. [%s]',

View File

@@ -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(

View File

@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS verrà aggiornato alla <strong>versione %s</strong>.',
'error' => 'Il processo di aggiornamento ha riscontrato il seguente errore: %s',
'file_is_nok' => 'Verifica i permessi della cartella <em>%s</em>. Il server HTTP deve avere i permessi per la scrittura ',
'file_is_nok' => 'Nuova <strong>versione %s</strong>, ma verifica i permessi della cartella <em>%s</em>. 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]',

View File

@@ -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 <abbr>API</abbr> toegang toe <small>(nodig voor mobiele apps)</small>',
'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 <em>./data</em> map. HTTP server moet rechten hebben om hierin te schrijven',
'ok' => 'Permissies op de data map zijn goed.',
'nok' => 'Controleer de permissies op de <em>./data</em> 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 <em>./data/favicons</em> 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' => 'Wachtwoord<br /><small>(voor de Web-formulier log in methode)</small>',
'password_form' => 'Wachtwoord<br /><small>(voor de Web-formulier loginmethode)</small>',
'password_format' => 'Ten minste 7 tekens',
'registration' => array(
'allow' => 'Sta het maken van nieuwe accounts toe',
'help' => '0 betekent dat er geen account limiet is',
'help' => '0 betekent dat er geen accountlimiet is',
'number' => 'Max aantal accounts',
),
'title' => 'Beheer gebruikers',
'user_list' => 'Lijst van gebruikers ',
'username' => 'Gebruikers naam',
'username' => 'Gebruikersnaam',
'users' => 'Gebruikers',
),
);

View File

@@ -92,6 +92,7 @@ return array(
'display_categories_unfolded' => 'Toon categoriën ingeklapt als standaard',
'hide_read_feeds' => 'Verberg categoriën en feeds zonder ongelezen artikelen (werkt niet met “Toon alle artikelen” configuratie)',
'img_with_lazyload' => 'Gebruik "lazy load" methode om afbeeldingen te laden',
'sides_close_article' => 'Sluit het artikel door buiten de artikeltekst te klikken',
'jump_next' => 'Ga naar volgende ongelezen (feed of categorie)',
'mark_updated_article_unread' => 'Markeer vernieuwd artikel als ongelezen',
'number_divided_when_reader' => 'Gedeeld door 2 in de lees modus.',
@@ -167,7 +168,7 @@ return array(
'user' => array(
'articles_and_size' => '%s artikelen (%s)',
'current' => 'Huidige gebruiker',
'is_admin' => 'is administrateur',
'is_admin' => 'is beheerder',
'users' => 'Gebruikers',
),
);

View File

@@ -10,17 +10,17 @@ return array(
),
'auth' => array(
'form' => array(
'not_set' => 'Een probleem is opgetreden tijdens de controle van de systeem configuratie. Probeer het later nog eens.',
'not_set' => 'Er is een probleem opgetreden tijdens de controle van de systeemconfiguratie. Probeer het later nog eens.',
'set' => 'Formulier is nu uw standaard authenticatie systeem.',
),
'login' => array(
'invalid' => 'Log in is ongeldig',
'invalid' => 'Login is ongeldig',
'success' => 'U bent ingelogd',
),
'logout' => array(
'success' => 'U bent uitgelogd',
),
'no_password_set' => 'Administrateur wachtwoord is niet ingesteld. Deze mogelijkheid is niet beschikbaar.',
'no_password_set' => 'Beheerderswachtwoord is niet ingesteld. Deze mogelijkheid is niet beschikbaar.',
),
'conf' => array(
'error' => 'Er is een fout opgetreden tijdens het opslaan van de configuratie',
@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS word nu vernieud naar <strong>versie %s</strong>.',
'error' => 'Het vernieuwingsproces kwam een fout tegen: %s',
'file_is_nok' => 'Controleer permissies op <em>%s</em> map. HTTP server moet rechten hebben om er in te schrijven',
'file_is_nok' => '<strong>Versie %s</strong>. Controleer permissies op <em>%s</em> map. HTTP server moet rechten hebben om er in te schrijven',
'finished' => 'Vernieuwing compleet!',
'none' => 'Geen vernieuwing om toe te passen',
'server_not_found' => 'Vernieuwings server kan niet worden gevonden. [%s]',

View File

@@ -37,8 +37,8 @@ return array(
'reset' => 'Authenticatie reset',
'username' => array(
'_' => 'Gebruikersnaam',
'admin' => 'Administrator gebruikersnaam',
'format' => '<small>maximaal 16 alphanumerieke tekens</small>',
'admin' => 'Beheerdersgebruikersnaam',
'format' => '<small>maximaal 16 alfanumerieke tekens</small>',
),
),
'date' => array(
@@ -109,8 +109,8 @@ return array(
'request_failed' => 'Een opdracht is mislukt, mogelijk door Internet verbindings problemen.',
'title_new_articles' => 'FreshRSS: nieuwe artikelen!',
),
'new_article' => 'Er zijn nieuwe artikelen beschikbaar, klik om de pagina te vernieuwen.',
'should_be_activated' => 'JavaScript moet aan staan',
'new_article' => 'Er zijn nieuwe artikelen beschikbaar. Klik om de pagina te vernieuwen.',
'should_be_activated' => 'JavaScript moet aanstaan',
),
'lang' => array(
'cz' => 'Čeština',
@@ -127,7 +127,7 @@ return array(
'admin' => 'Administratie',
'archiving' => 'Archiveren',
'authentication' => 'Authenticatie',
'check_install' => 'Installatie controle',
'check_install' => 'Installatiecontrole',
'configuration' => 'Configuratie',
'display' => 'Opmaak',
'extensions' => 'Uitbreidingen',
@@ -138,9 +138,9 @@ return array(
'sharing' => 'Delen',
'shortcuts' => 'Snelle toegang',
'stats' => 'Statistieken',
'system' => 'Systeem configuratie',
'update' => 'Versie controle',
'user_management' => 'Beheer gebruikers',
'system' => 'Systeemconfiguratie',
'update' => 'Versiecontrole',
'user_management' => 'Gebruikersbeheer',
'user_profile' => 'Profiel',
),
'pagination' => array(

View File

@@ -32,8 +32,8 @@ return array(
'menu' => array(
'about' => 'Over FreshRSS',
'add_query' => 'Voeg een query toe',
'before_one_day' => 'Ouder als een dag',
'before_one_week' => 'Ouder als een week',
'before_one_day' => 'Ouder dan een dag',
'before_one_week' => 'Ouder dan een week',
'favorites' => 'Favorieten (%s)',
'global_view' => 'Globale weergave',
'main_stream' => 'Overzicht',

View File

@@ -14,7 +14,7 @@ return array(
'none' => 'Geen (gevaarlijk)',
'password_form' => 'Wachtwoord<br /><small>(voor de Web-formulier log in methode)</small>',
'password_format' => 'Tenminste 7 tekens',
'type' => 'Authenticatie methode',
'type' => 'Authenticatiemethode',
),
'bdd' => array(
'_' => 'Database',
@@ -98,12 +98,12 @@ return array(
'ok' => 'Algemene configuratie is opgeslagen.',
),
'congratulations' => 'Gefeliciteerd!',
'default_user' => 'Gebruikersnaam van de standaard gebruiker <small>(maximaal 16 alphanumerieke tekens)</small>',
'default_user' => 'Gebruikersnaam van de standaardgebruiker <small>(maximaal 16 alfanumerieke tekens)</small>',
'delete_articles_after' => 'Verwijder artikelen na',
'fix_errors_before' => 'Repareer fouten alvorens U naar de volgende stap gaat.',
'javascript_is_better' => 'FreshRSS werkt beter JavaScript ingeschakeld',
'js' => array(
'confirm_reinstall' => 'U verliest uw vorige configuratie door FreshRSS opnieuw te installeren. Weet u zeker dat u verder wilt gaan?',
'confirm_reinstall' => 'U zal uw vorige configuratie kwijtraken door FreshRSS opnieuw te installeren. Weet u zeker dat u verder wilt gaan?',
),
'language' => array(
'_' => 'Taal',
@@ -111,7 +111,7 @@ return array(
'defined' => 'Taal is bepaald.',
),
'not_deleted' => 'Er ging iets fout! U moet het bestand <em>%s</em> handmatig verwijderen.',
'ok' => 'De installatie procedure is geslaagd.',
'ok' => 'De installatieprocedure is geslaagd.',
'step' => 'stap %d',
'steps' => 'Stappen',
'title' => 'Installatie · FreshRSS',

View File

@@ -53,10 +53,10 @@ return array(
'menu' => array(
'bookmark' => 'Abonneer (FreshRSS bladwijzer)',
'import_export' => 'Importeer / exporteer',
'subscription_management' => 'Abonnementen beheer',
'subscription_management' => 'Abonnementenbeheer',
),
'title' => array(
'_' => 'Abonnementen beheer',
'feed_management' => 'RSS feed beheer',
'_' => 'Abonnementenbeheer',
'feed_management' => 'RSS-feedbeheer',
),
);

View File

@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Show categories folded by default',
'hide_read_feeds' => 'Hide categories & feeds with no unread article (does not work with “Show all articles” configuration)',
'img_with_lazyload' => 'Use "lazy load" mode to load pictures',
'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO
'jump_next' => 'jump to next unread sibling (feed or category)',
'number_divided_when_reader' => 'Divided by 2 in the reading view.',
'read' => array(

View File

@@ -2,108 +2,108 @@
return array(
'admin' => array(
'optimization_complete' => 'Optimisation complete',
'optimization_complete' => 'Optimisation complete', //TODO
),
'access' => array(
'denied' => 'You dont have permission to access this page',
'not_found' => 'You are looking for a page which doesnt exist',
'denied' => 'You dont have permission to access this page', //TODO
'not_found' => 'You are looking for a page which doesnt exist', //TODO
),
'auth' => array(
'form' => array(
'not_set' => 'A problem occured during authentication system configuration. Please retry later.',
'set' => 'Form is now your default authentication system.',
'not_set' => 'A problem occured during authentication system configuration. Please retry later.', //TODO
'set' => 'Form is now your default authentication system.', //TODO
),
'login' => array(
'invalid' => 'Login is invalid',
'success' => 'You are connected',
'invalid' => 'Login is invalid', //TODO
'success' => 'You are connected', //TODO
),
'logout' => array(
'success' => 'You are disconnected',
'success' => 'You are disconnected', //TODO
),
'no_password_set' => 'Administrator password hasnt been set. This feature isnt available.',
'no_password_set' => 'Administrator password hasnt been set. This feature isnt available.', //TODO
),
'conf' => array(
'error' => 'An error occurred during configuration saving',
'query_created' => 'Query "%s" has been created.',
'shortcuts_updated' => 'Shortcuts have been updated',
'updated' => 'Configuration has been updated',
'error' => 'An error occurred during configuration saving', //TODO
'query_created' => 'Query "%s" has been created.', //TODO
'shortcuts_updated' => 'Shortcuts have been updated', //TODO
'updated' => 'Configuration has been updated', //TODO
),
'extensions' => array(
'already_enabled' => '%s is already enabled',
'already_enabled' => '%s is already enabled', //TODO
'disable' => array(
'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.',
'ok' => '%s is now disabled',
'ko' => '%s cannot be disabled. <a href="%s">Check FressRSS logs</a> for details.', //TODO
'ok' => '%s is now disabled', //TODO
),
'enable' => array(
'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.',
'ok' => '%s is now enabled',
'ko' => '%s cannot be enabled. <a href="%s">Check FressRSS logs</a> for details.', //TODO
'ok' => '%s is now enabled', //TODO
),
'no_access' => 'You have no access on %s',
'not_enabled' => '%s is not enabled yet',
'not_found' => '%s does not exist',
'no_access' => 'You have no access on %s', //TODO
'not_enabled' => '%s is not enabled yet', //TODO
'not_found' => '%s does not exist', //TODO
),
'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',
'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.',
'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.', //TODO
'feeds_imported' => 'Your feeds have been imported and will now be updated', //TODO
'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred', //TODO
'file_cannot_be_uploaded' => 'File cannot be uploaded!', //TODO
'no_zip_extension' => 'ZIP extension is not present on your server.', //TODO
'zip_error' => 'An error occured during ZIP import.', //TODO
),
'sub' => array(
'actualize' => 'Actualise',
'actualize' => 'Actualise', //TODO
'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_name' => 'Category name cannot be empty.',
'not_delete_default' => 'You cannot delete the default category!',
'not_exist' => 'The category does not exist!',
'over_max' => 'You have reached your limit of categories (%d)',
'updated' => 'Category has been updated.',
'created' => 'Category %s has been created.', //TODO
'deleted' => 'Category has been deleted.', //TODO
'emptied' => 'Category has been emptied', //TODO
'error' => 'Category cannot be updated', //TODO
'name_exists' => 'Category name already exists.', //TODO
'no_id' => 'You must precise the id of the category.', //TODO
'no_name' => 'Category name cannot be empty.', //TODO
'not_delete_default' => 'You cannot delete the default category!', //TODO
'not_exist' => 'The category does not exist!', //TODO
'over_max' => 'You have reached your limit of categories (%d)', //TODO
'updated' => 'Category has been updated.', //TODO
),
'feed' => array(
'actualized' => '<em>%s</em> has been updated',
'actualizeds' => 'RSS feeds have been updated',
'added' => 'RSS feed <em>%s</em> has been added',
'already_subscribed' => 'You have already subscribed to <em>%s</em>',
'deleted' => 'Feed has been deleted',
'error' => 'Feed cannot be updated',
'internal_problem' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.',
'invalid_url' => 'URL <em>%s</em> is invalid',
'marked_read' => 'Feeds have been marked as read',
'n_actualized' => '%d feeds have been updated',
'n_entries_deleted' => '%d articles have been deleted',
'no_refresh' => 'There is no feed to refresh…',
'not_added' => '<em>%s</em> could not be added',
'over_max' => 'You have reached your limit of feeds (%d)',
'updated' => 'Feed has been updated',
'actualized' => '<em>%s</em> has been updated', //TODO
'actualizeds' => 'RSS feeds have been updated', //TODO
'added' => 'RSS feed <em>%s</em> has been added', //TODO
'already_subscribed' => 'You have already subscribed to <em>%s</em>', //TODO
'deleted' => 'Feed has been deleted', //TODO
'error' => 'Feed cannot be updated', //TODO
'internal_problem' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.', //TODO
'invalid_url' => 'URL <em>%s</em> is invalid', //TODO
'marked_read' => 'Feeds have been marked as read', //TODO
'n_actualized' => '%d feeds have been updated', //TODO
'n_entries_deleted' => '%d articles have been deleted', //TODO
'no_refresh' => 'There is no feed to refresh…', //TODO
'not_added' => '<em>%s</em> could not be added', //TODO
'over_max' => 'You have reached your limit of feeds (%d)', //TODO
'updated' => 'Feed has been updated', //TODO
),
'purge_completed' => 'Purge completed (%d articles deleted)',
'purge_completed' => 'Purge completed (%d articles deleted)', //TODO
),
'update' => array(
'can_apply' => 'FreshRSS will now be updated to the <strong>version %s</strong>.',
'error' => 'The update process has encountered an error: %s',
'file_is_nok' => 'Check permissions on <em>%s</em> 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]',
'can_apply' => 'FreshRSS will now be updated to the <strong>version %s</strong>.', //TODO
'error' => 'The update process has encountered an error: %s', //TODO
'file_is_nok' => 'New <strong>version %s</strong> available, but check permissions on <em>%s</em> directory. HTTP server must have rights to write into', //TODO
'finished' => 'Update completed!', //TODO
'none' => 'No update to apply', //TODO
'server_not_found' => 'Update server cannot be found. [%s]', //TODO
),
'user' => array(
'created' => array(
'_' => 'User %s has been created',
'error' => 'User %s cannot be created',
'_' => 'User %s has been created', //TODO
'error' => 'User %s cannot be created', //TODO
),
'deleted' => array(
'_' => 'User %s has been deleted',
'error' => 'User %s cannot be deleted',
'_' => 'User %s has been deleted', //TODO
'error' => 'User %s cannot be deleted', //TODO
),
),
'profile' => array(
'error' => 'Your profile cannot be modified',
'updated' => 'Your profile has been modified',
'error' => 'Your profile cannot be modified', //TODO
'updated' => 'Your profile has been modified', //TODO
),
);

View File

@@ -93,6 +93,7 @@ return array(
'display_categories_unfolded' => 'Show categories folded by default',
'hide_read_feeds' => 'Okunmamış makalesi olmayan kategori veya akışı gizle ("Tüm makaleleri göster" komutunda çalışmaz)',
'img_with_lazyload' => 'Resimleri yüklemek için "tembel modu" kullan',
'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO
'jump_next' => 'Bir sonraki benzer okunmamışa geç (akış veya kategori)',
'number_divided_when_reader' => 'Okuma modunda ikiye bölünecek.',
'read' => array(

View File

@@ -87,7 +87,7 @@ return array(
'update' => array(
'can_apply' => 'FreshRSS <strong>%s versiyonuna</strong> güncellenecek.',
'error' => 'Güncelleme işlemi sırasında hata: %s',
'file_is_nok' => '<em>%s</em> klasör yetkisini kontrol edin. HTTP yazma yetkisi olmalı',
'file_is_nok' => '<strong>%s versiyonuna</strong>. <em>%s</em> klasör yetkisini kontrol edin. HTTP yazma yetkisi olmalı',
'finished' => 'Güncelleme tamamlandı!',
'none' => 'Güncelleme yok',
'server_not_found' => 'Güncelleme sunucusu bulunamadı. [%s]',

181
app/i18n/zh-cn/admin.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
return array(
'auth' => array(
'allow_anonymous' => '允许匿名阅读默认用户 (%s) 的文章',
'allow_anonymous_refresh' => '允许匿名刷新文章',
'api_enabled' => '允许 <abbr>API</abbr> 访问 <small>(用于手机 APP)</small>',
'form' => 'Web form (传统方式, 需要 JavaScript)',
'http' => 'HTTP (面向启用 HTTPS 的高级用户)',
'none' => '无 (危险)',
'title' => '认证',
'title_reset' => '认证重置',
'token' => '认证口令',
'token_help' => '允许不经认证访问默认用户的 RSS 输出:',
'type' => '认证方式',
'unsafe_autologin' => '允许不安全的自动登陆方式:',
),
'check_install' => array(
'cache' => array(
'nok' => '请检查 <em>./data/cache</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'cache 目录权限正常。',
),
'categories' => array(
'nok' => '分类表配置错误。',
'ok' => '分类表正常。',
),
'connection' => array(
'nok' => '数据库连接失败。',
'ok' => '数据库连接正常。',
),
'ctype' => array(
'nok' => '找不到字符类型检测库 (php-ctype) 。',
'ok' => '你已有字符类型检测库 (ctype) 。',
),
'curl' => array(
'nok' => '找不到 cURL 库 (php-curl package) 。',
'ok' => '你已有 cURL 库。',
),
'data' => array(
'nok' => '请检查 <em>./data</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'data 目录权限正常。',
),
'database' => '数据库相关',
'dom' => array(
'nok' => '找不到用于浏览 DOM 的库 (php-xml) 。',
'ok' => '你已有用于浏览 DOM 的库。',
),
'entries' => array(
'nok' => '条目表配置错误。',
'ok' => '条目表正常。',
),
'favicons' => array(
'nok' => '请检查 <em>./data/favicons</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'favicons 目录权限正常。',
),
'feeds' => array(
'nok' => 'RSS 源表配置错误。',
'ok' => 'RSS 源表正常。',
),
'fileinfo' => array(
'nok' => '找不到 PHP fileinfo 库 (fileinfo) 。',
'ok' => '你已有 fileinfo 库。',
),
'files' => '文件相关',
'json' => array(
'nok' => '找不到 JSON 扩展 (php5-json ) 。',
'ok' => '你已有 JSON 扩展',
),
'minz' => array(
'nok' => '找不到 Minz 框架。',
'ok' => '你已有 Minz 框架。',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库 (php-pcre) 。',
'ok' => '你已有正则表达式解析库 (PCRE) 。',
),
'pdo' => array(
'nok' => '找不到 PDO 或支持的驱动 (pdo_mysql, pdo_sqlite, pdo_pgsql) 。',
'ok' => '你已有 PDO 和支持的至少一种驱动 (pdo_mysql, pdo_sqlite, pdo_pgsql) 。',
),
'php' => array(
'_' => 'PHP 相关',
'nok' => '你的 PHP 版本为 %s但 FreshRSS 最低需要 %s。',
'ok' => '你的 PHP 版本为 %s与 FreshRSS 兼容。',
),
'tables' => array(
'nok' => '数据库中缺少一个或多个表。',
'ok' => '数据库中相关表存在。',
),
'title' => '环境检查',
'tokens' => array(
'nok' => '请检查 <em>./data/tokens</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'tokens 目录权限正常。',
),
'users' => array(
'nok' => '请检查 <em>./data/users</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'users 目录权限正常。',
),
'zip' => array(
'nok' => '找不到 ZIP 扩展 (php-zip) 。',
'ok' => '你已有 ZIP 扩展。',
),
),
'extensions' => array(
'disabled' => '已禁用',
'empty_list' => '没有已安装的扩展',
'enabled' => '已启用',
'no_configure_view' => '此扩展不能配置。',
'system' => array(
'_' => '系统扩展',
'no_rights' => '系统扩展 (你不能修改它)',
),
'title' => '扩展',
'user' => '用户扩展',
),
'stats' => array(
'_' => '统计',
'all_feeds' => '所有 RSS 源',
'category' => '分类',
'entry_count' => '条目数',
'entry_per_category' => '每分类条目数',
'entry_per_day' => '每天条目数 (最近 30 天)',
'entry_per_day_of_week' => '周内每天 (平均: %.2f 条消息)',
'entry_per_hour' => '每小时 (平均: %.2f 条消息)',
'entry_per_month' => '每月 (平均: %.2f 条消息)',
'entry_repartition' => '条目分布',
'feed' => 'RSS 源',
'feed_per_category' => '每分类 RSS 源',
'idle' => '闲置 RSS 源',
'main' => '主要统计',
'main_stream' => '首页',
'menu' => array(
'idle' => '闲置 RSS 源',
'main' => '主要统计',
'repartition' => '文章分布',
),
'no_idle' => '无闲置 RSS 源!',
'number_entries' => '%d 篇文章',
'percent_of_total' => '%%',
'repartition' => '文章分布',
'status_favorites' => '收藏',
'status_read' => '已读',
'status_total' => '总计',
'status_unread' => '未读',
'title' => '统计',
'top_feed' => '前十 RSS 源',
),
'system' => array(
'_' => '系统配置',
'auto-update-url' => '自动升级服务器 URL',
'instance-name' => '实例名称',
'max-categories' => '每用户分类限制',
'max-feeds' => '每用户 RSS 源限制',
'registration' => array(
'help' => '0 表示无账户数限制',
'number' => '最大账户数',
),
),
'update' => array(
'_' => '更新系统',
'apply' => '应用',
'check' => '检查更新',
'current_version' => '当前 FreshRSS 版本为 %s.',
'last' => '上一次检查: %s',
'none' => '没有可用更新',
'title' => '更新系统',
),
'user' => array(
'articles_and_size' => '%s 篇文章 (%s)',
'create' => '创建新用户',
'language' => '语言',
'number' => '已有 %d 个帐户',
'numbers' => '已有 %d 个帐户',
'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
'password_format' => '至少 7 个字符',
'title' => '用户管理',
'user_list' => '用户列表',
'username' => '用户名',
'users' => '用户',
),
);

174
app/i18n/zh-cn/conf.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
return array(
'archiving' => array(
'_' => '存档',
'advanced' => '高级',
'delete_after' => '文章保留',
'help' => '详细选项位于单独的 RSS 源设置',
'keep_history_by_feed' => '至少保存的文章数',
'optimize' => '优化数据库',
'optimize_help' => '不时地执行优化可以减少数据库大小',
'purge_now' => '立即清除',
'title' => '存档',
'ttl' => '最小自动更新时间',
),
'display' => array(
'_' => '显示',
'icon' => array(
'bottom_line' => '底栏',
'entry' => '文章图标',
'publication_date' => '更新日期',
'related_tags' => '相关标签',
'sharing' => '分享',
'top_line' => '顶栏',
),
'language' => '语言',
'notif_html5' => array(
'seconds' => '秒 (0 表示不超时)',
'timeout' => 'HTML5 通知超时时间',
),
'theme' => '主题',
'title' => '显示',
'width' => array(
'content' => '内容宽度',
'large' => '大',
'medium' => '中',
'no_limit' => '无限制',
'thin' => '小',
),
),
'query' => array(
'_' => '自定义查询',
'deprecated' => '此查询不再有效。相关的分类或 RSS 源已被删除。',
'filter' => '生效的过滤器:',
'get_all' => '显示所有文章',
'get_category' => '显示分类 "%s"',
'get_favorite' => '显示收藏文章',
'get_feed' => '显示RSS 源 "%s"',
'no_filter' => '无过滤器',
'none' => '你未创建任何自定义查询。',
'number' => '查询 n°%d',
'order_asc' => '由旧到新显示文章',
'order_desc' => '由新到旧显示文章',
'search' => '搜索 "%s"',
'state_0' => '显示所有文章',
'state_1' => '显示已读文章',
'state_2' => '显示未读文章',
'state_3' => '显示所有文章',
'state_4' => '显示收藏文章',
'state_5' => '显示已读的收藏文章',
'state_6' => '显示未读的收藏文章',
'state_7' => '显示收藏文章',
'state_8' => '显示未收藏文章',
'state_9' => '显示已读的未收藏文章',
'state_10' => '显示未读的未收藏文章',
'state_11' => '显示未收藏文章',
'state_12' => '显示所有文章',
'state_13' => '显示已读文章',
'state_14' => '显示未读文章',
'state_15' => '显示所有文章',
'title' => '自定义查询',
),
'profile' => array(
'_' => '帐户管理',
'delete' => array(
'_' => '账户删除',
'warn' => '你的帐户和所有相关数据都将被删除。',
),
'password_api' => 'API 密码<br /><small>(例如,用于手机 APP)</small>',
'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
'password_format' => '至少 7 个字符',
'title' => '用户帐户',
),
'reading' => array(
'_' => '阅读',
'after_onread' => '“全部设为已读”后,',
'articles_per_page' => '每页文章数',
'auto_load_more' => '在页面底部载入下一篇文章',
'auto_remove_article' => '阅读后隐藏文章',
'mark_updated_article_unread' => '将有更新的文章设为未读',
'confirm_enabled' => '“全部设为已读”时显示确认对话框',
'display_articles_unfolded' => '默认展开文章',
'display_categories_unfolded' => '默认展开分类',
'hide_read_feeds' => '隐藏没有未读文章的分类或 RSS 源 (启用“显示所有文章”时不生效))',
'img_with_lazyload' => '使用 "lazy load" 模式加载图片',
'sides_close_article' => '点击文章外区域以关闭文章',
'jump_next' => '跳转到下一未读项 (RSS 源或分类)',
'number_divided_when_reader' => '阅读视图除以 2',
'read' => array(
'article_open_on_website' => '在打开原文章后',
'article_viewed' => '在文章被浏览后',
'scroll' => '在滚动浏览后',
'upon_reception' => '在接收文章后',
'when' => '将文章设为已读…',
),
'show' => array(
'_' => '文章显示',
'adaptive' => '智能显示',
'all_articles' => '显示所有文章',
'unread' => '只显示未读',
),
'sort' => array(
'_' => '排列顺序',
'newer_first' => '由新到旧',
'older_first' => '由旧到新',
),
'sticky_post' => '打开文章时将其置顶',
'title' => '阅读',
'view' => array(
'default' => '默认视图',
'global' => '全屏视图',
'normal' => '普通视图',
'reader' => '阅读视图',
),
),
'sharing' => array(
'_' => '分享',
'blogotext' => 'Blogotext',
'diaspora' => 'Diaspora*',
'email' => 'Email',
'facebook' => 'Facebook',
'g+' => 'Google+',
'more_information' => '更多信息',
'print' => '打印',
'shaarli' => 'Shaarli',
'share_name' => '名称',
'share_url' => 'URL',
'title' => '分享',
'twitter' => 'Twitter',
'wallabag' => 'wallabag',
),
'shortcut' => array(
'_' => '快捷键',
'article_action' => '文章操作',
'auto_share' => '分享',
'auto_share_help' => '如果有多种分享模式,则会按照它们的编号访问。',
'close_dropdown' => '关闭菜单',
'collapse_article' => '收起文章',
'first_article' => '跳转到第一篇文章',
'focus_search' => '聚焦到搜索框',
'help' => '显示帮助文档',
'javascript' => '若要使用快捷键,必须启用 JavaScript',
'last_article' => '跳转到最后一篇文章',
'load_more' => '载入更多文章',
'mark_read' => '设为已读',
'mark_favorite' => '加入收藏',
'navigation' => '浏览',
'navigation_help' => '搭配 "Shift" 键,浏览快捷键将生效于 RSS 源。<br/>搭配 "Alt" 键,浏览快捷键将生效于分类。',
'next_article' => '跳转到下一篇文章',
'other_action' => '其他操作',
'previous_article' => '跳转到上一篇文章',
'see_on_website' => '在原网站上查看',
'shift_for_all_read' => '+ <code>shift</code> 可以将全部文章设为已读',
'title' => '快捷键',
'user_filter' => '显示自定义查询',
'user_filter_help' => '如果有多个自定义过滤器,则会按照它们的编号访问。',
),
'user' => array(
'articles_and_size' => '%s 篇文章 (%s)',
'current' => '当前用户',
'is_admin' => '此用户为管理员',
'users' => '用户',
),
);

109
app/i18n/zh-cn/feedback.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
return array(
'admin' => 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 禁用失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
'ok' => '%s 现已禁用',
),
'enable' => array(
'ko' => '%s 启用失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
'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' => '<em>%s</em> 已更新',
'actualizeds' => 'RSS 源已更新',
'added' => 'RSS 源 <em>%s</em> 已添加',
'already_subscribed' => '你已订阅 <em>%s</em>',
'deleted' => 'RSS 源已删除',
'error' => 'RSS 源更新失败',
'internal_problem' => 'RSS 源添加失败。<a href="%s">检查 FressRSS 日志</a> 查看详情。',
'invalid_url' => 'URL <em>%s</em> 无效',
'marked_read' => 'RSS 源已被设为已读',
'n_actualized' => '%d 个 RSS 源已更新',
'n_entries_deleted' => '%d 篇文章已删除',
'no_refresh' => '没有可刷新的 RSS 源…',
'not_added' => '<em>%s</em> 添加失败',
'over_max' => '你已达到 RSS 源数限制 (%d)',
'updated' => 'RSS 源已更新',
),
'purge_completed' => '清除完成 (%d 篇文章已删除)',
),
'update' => array(
'can_apply' => 'FreshRSS 将更新到 <strong>版本 %s</strong>.',
'error' => '更新出错:%s',
'file_is_nok' => '请检查 <em>%s</em> 目录权限。HTTP 服务器必须有其写入权限。',
'finished' => '更新完成!',
'none' => '没有可用更新',
'server_not_found' => '找不到更新服务器 [%s]',
),
'user' => array(
'created' => array(
'_' => '用户 %s 已创建',
'error' => '用户 %s 创建失败',
),
'deleted' => array(
'_' => '用户 %s 已删除',
'error' => '用户 %s 删除失败',
),
),
'profile' => array(
'error' => '你的帐户修改失败',
'updated' => '你的帐户已修改',
),
);

185
app/i18n/zh-cn/gen.php Normal file
View File

@@ -0,0 +1,185 @@
<?php
return array(
'action' => 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' => '自动登录<small>(%s 天)</small>',
'login' => '登录',
'logout' => '登出',
'password' => array(
'_' => '密码',
'format' => '<small>至少 7 个字符</small>',
),
'registration' => array(
'_' => '新账户',
'ask' => '创建新账户?',
'title' => '账户创建',
),
'reset' => '认证重置',
'username' => array(
'_' => '用户名',
'admin' => '管理员用户名',
'format' => '<small>最大 16 个数字或字母</small>',
),
),
'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 <em>%s</em>',
'by_default' => '默认',
'damn' => '错误!',
'default_category' => '未分类',
'no' => '否',
'not_applicable' => '不可用',
'ok' => '正常!',
'or' => '或',
'yes' => '是',
),
);

61
app/i18n/zh-cn/index.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
return array(
'about' => array(
'_' => '关于',
'agpl3' => '<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL 3</a>',
'bugs_reports' => 'Bug 报告',
'credits' => '致谢',
'credits_content' => '某些设计元素来自于 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> ,尽管 FreshRSS 并没有使用此框架。<a href="https://git.gnome.org/browse/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://github.com/marienfressinaud/MINZ">Minz</a>。',
'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="http://projet.idleman.fr/leed/">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。',
'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>',
'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' => '相关标签',
),
);

119
app/i18n/zh-cn/install.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
return array(
'action' => array(
'finish' => '完成安装',
'fix_errors_before' => '请在继续下一步前修复错误。',
'keep_install' => '保留以前配置',
'next_step' => '下一步',
'reinstall' => '重新安装 FreshRSS',
),
'auth' => array(
'form' => 'Web form (传统方式, 需要 JavaScript)',
'http' => 'HTTP (面向启用 HTTPS 的高级用户)',
'none' => '无 (危险)',
'password_form' => '密码<br /><small>(用于 Web-form 登录方式)</small>',
'password_format' => '至少 7 个字符',
'type' => '认证方式',
),
'bdd' => array(
'_' => '数据库',
'conf' => array(
'_' => '数据库配置',
'ko' => '请验证你的数据库信息。',
'ok' => '数据库配置已保存。',
),
'host' => '主机',
'prefix' => '表前缀',
'password' => '密码',
'type' => '数据库类型',
'username' => '用户名',
),
'check' => array(
'_' => '检查',
'already_installed' => '我们检测到 FreshRSS 已经安装!',
'cache' => array(
'nok' => '请检查 <em>./data/cache</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'cache 目录权限正常。',
),
'ctype' => array(
'nok' => '找不到字符类型检测库 (php-ctype) 。',
'ok' => '你已有字符类型检测库 (ctype) 。',
),
'curl' => array(
'nok' => '找不到 cURL 库 (php-curl package) 。',
'ok' => '你已有 cURL 库。',
),
'data' => array(
'nok' => '请检查 <em>./data</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'data 目录权限正常。',
),
'dom' => array(
'nok' => '找不到用于浏览 DOM 的库 (php-xml) 。',
'ok' => '你已有用于浏览 DOM 的库。',
),
'favicons' => array(
'nok' => '请检查 <em>./data/favicons</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'favicons 目录权限正常。',
),
'fileinfo' => array(
'nok' => '找不到 PHP fileinfo 库 (fileinfo) 。',
'ok' => '你已有 fileinfo 库。',
),
'http_referer' => array(
'nok' => '请检查你是否修改了 HTTP REFERER。',
'ok' => '你的 HTTP REFERER 已知且与服务器一致。',
),
'json' => array(
'nok' => '找不到推荐的 JSON 解析库。',
'ok' => '你已有推荐的 JSON 解析库。',
),
'minz' => array(
'nok' => '找不到 Minz 框架。',
'ok' => '你已有 Minz 框架。',
),
'pcre' => array(
'nok' => '找不到正则表达式解析库 (php-pcre) 。',
'ok' => '你已有正则表达式解析库 (PCRE) 。',
),
'pdo' => array(
'nok' => '找不到 PDO 或支持的驱动 (pdo_mysql, pdo_sqlite, pdo_pgsql) 。',
'ok' => '你已有 PDO 和支持的至少一种驱动 (pdo_mysql, pdo_sqlite, pdo_pgsql) 。',
),
'php' => array(
'nok' => '你的 PHP 版本为 %s但 FreshRSS 最低需要 %s。',
'ok' => '你的 PHP 版本为 %s与 FreshRSS 兼容。',
),
'users' => array(
'nok' => '请检查 <em>./data/users</em> 目录权限。HTTP 服务器必须有其写入权限。',
'ok' => 'users 目录权限正常。',
),
'xml' => array(
'nok' => '找不到用于 XML 解析库。',
'ok' => '你已有 XML 解析库。',
),
),
'conf' => array(
'_' => '常规配置',
'ok' => '常规配置已保存。',
),
'congratulations' => '恭喜!',
'default_user' => '默认用户名 <small>(最大 16 个数字或字母)</small>',
'delete_articles_after' => '保留文章',
'fix_errors_before' => '请在继续下一步前修复错误。',
'javascript_is_better' => '启用 JavaScript 会使 FreshRSS 工作得更好',
'js' => array(
'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置。你确定要继续吗?',
),
'language' => array(
'_' => '语言',
'choose' => '为 FreshRSS 选择语言',
'defined' => '语言已指定。',
),
'not_deleted' => '出错!你必须手动删除文件 <em>%s</em>。',
'ok' => '安装成功。',
'step' => '步骤 %d',
'steps' => '步骤',
'title' => '安装 FreshRSS',
'this_is_the_end' => '最后一步',
);

62
app/i18n/zh-cn/sub.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
return array(
'category' => array(
'_' => '分类',
'add' => '添加分类',
'empty' => '空分类',
'new' => '新分类',
),
'feed' => array(
'add' => '添加 RSS 源',
'advanced' => '高级',
'archiving' => '存档',
'auth' => array(
'configuration' => '认证',
'help' => '连接启用 HTTP 认证的 RSS 源',
'http' => 'HTTP 认证',
'password' => 'HTTP 密码',
'username' => 'HTTP 用户名',
),
'css_help' => '获取全文(注意,会耗费更多时间!)',
'css_path' => '原网站中文章的 CSS 路径',
'description' => '描述',
'empty' => '此源为空。请确认它是否正常更新。',
'error' => '此源遇到一些问题。请确认它是否可访问后重试。',
'in_main_stream' => '在首页中显示',
'informations' => '信息',
'keep_history' => '至少保存的文章数',
'moved_category_deleted' => '删除分类时,其中的 RSS 源会自动归类到<em>%s</em>。',
'no_selected' => '未选择 RSS 源。',
'number_entries' => '%d 篇文章',
'stats' => '统计',
'think_to_add' => '你可以添加一些 RSS 源。',
'title' => '标题',
'title_add' => '添加 RSS 源',
'ttl' => '最小自动更新时间',
'url' => '源 URL',
'validator' => '检查 RSS 源有效性',
'website' => '网站 URL',
'pubsubhubbub' => 'PubSubHubbub 即时通知',
),
'import_export' => array(
'export' => '导出',
'export_opml' => '导出 RSS 源列表 (OPML)',
'export_starred' => '导出你的收藏',
'feed_list' => '%s 文章列表',
'file_to_import' => '需要导入的文件<br />(OPML, JSON 或 ZIP)',
'file_to_import_no_zip' => '需要导入的文件<br />(OPML 或 JSON)',
'import' => '导入',
'starred_list' => '收藏文章列表',
'title' => '导入/导出',
),
'menu' => array(
'bookmark' => '订阅 (FreshRSS 书签)',
'import_export' => '导入/导出',
'subscription_management' => '订阅管理',
),
'title' => array(
'_' => '订阅管理',
'feed_management' => 'RSS 源管理',
),
);

View File

@@ -88,13 +88,13 @@ function saveStep1() {
// First, we try to get previous configurations
Minz_Configuration::register('system',
join_path(DATA_PATH, 'config.php'),
join_path(DATA_PATH, 'config.default.php'));
join_path(FRESHRSS_PATH, 'config.default.php'));
$system_conf = Minz_Configuration::get('system');
$current_user = $system_conf->default_user;
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'));
$user_conf = Minz_Configuration::get('user');
// Then, we set $_SESSION vars
@@ -342,35 +342,19 @@ function checkDbUser(&$dbOptions) {
$driver_options = $dbOptions['options'];
try {
$c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options);
if (defined('SQL_CREATE_TABLES')) {
$sql = sprintf(SQL_CREATE_TABLES, $dbOptions['prefix_user'], _t('gen.short.default_category'));
$sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_INSERT_FEEDS,
$dbOptions['prefix_user'], _t('gen.short.default_category'));
$stm = $c->prepare($sql);
$ok = $stm->execute();
$ok = $stm && $stm->execute();
} else {
global $SQL_CREATE_TABLES;
if (is_array($SQL_CREATE_TABLES)) {
$ok = true;
foreach ($SQL_CREATE_TABLES as $instruction) {
$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
$stm = $c->prepare($sql);
$ok &= $stm->execute();
}
}
}
if (defined('SQL_INSERT_FEEDS')) {
$sql = sprintf(SQL_INSERT_FEEDS, $dbOptions['prefix_user']);
$stm = $c->prepare($sql);
$ok &= $stm->execute();
} else {
global $SQL_INSERT_FEEDS;
if (is_array($SQL_INSERT_FEEDS)) {
foreach ($SQL_INSERT_FEEDS as $instruction) {
$sql = sprintf($instruction, $dbOptions['prefix_user']);
$stm = $c->prepare($sql);
$ok &= $stm->execute();
}
global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS;
$instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS);
$ok = !empty($instructions);
foreach ($instructions as $instruction) {
$sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category'));
$stm = $c->prepare($sql);
$ok &= $stm && $stm->execute();
}
}
} catch (PDOException $e) {
@@ -481,7 +465,7 @@ function printStep1() {
<?php if ($res['fileinfo'] == 'ok') { ?>
<p class="alert alert-success"><span class="alert-head"><?php echo _t('gen.short.ok'); ?></span> <?php echo _t('install.check.fileinfo.ok'); ?></p>
<?php } else { ?>
<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.fileinfo.nok'); ?></p>
<p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.check.fileinfo.nok'); ?></p>
<?php } ?>
<?php if ($res['data'] == 'ok') { ?>
@@ -629,7 +613,7 @@ function printStep3() {
<?php if (extension_loaded('pdo_pgsql')) {?>
<option value="pgsql"
<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
PostgreSQL (⚠️ experimental)
PostgreSQL
</option>
<?php }?>
</select>

View File

@@ -149,6 +149,7 @@
<?php
$url_output['a'] = 'rss';
if (FreshRSS_Context::$user_conf->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) {

View File

@@ -52,19 +52,6 @@
</div>
</div>
<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
<div class="form-group">
<label class="group-name" for="token"><?php echo _t('admin.auth.token'); ?></label>
<?php $token = FreshRSS_Context::$user_conf->token; ?>
<div class="group-controls">
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
<kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
</div>
</div>
<?php } ?>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="api_enabled">

View File

@@ -106,6 +106,16 @@
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="sides_close_article">
<input type="checkbox" name="sides_close_article" id="sides_close_article" value="1"<?php echo FreshRSS_Context::$user_conf->sides_close_article ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->sides_close_article; ?>"/>
<?php echo _t('conf.reading.sides_close_article'); ?>
<noscript><strong><?php echo _t('gen.js.should_be_activated'); ?></strong></noscript>
</label>
</div>
</div>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="reading_confirm">

View File

@@ -13,6 +13,7 @@ echo htmlspecialchars(json_encode(array(
'auto_load_more' => !!FreshRSS_Context::$user_conf->auto_load_more,
'auto_actualize_feeds' => !!Minz_Session::param('actualize_feeds', false),
'does_lazyload' => !!FreshRSS_Context::$user_conf->lazyload ,
'sides_close_article' => !!FreshRSS_Context::$user_conf->sides_close_article,
'sticky_post' => !!FreshRSS_Context::isStickyPostEnabled(),
'html5_notif_timeout' => FreshRSS_Context::$user_conf->html5_notif_timeout,
'auth_type' => FreshRSS_Context::$system_conf->auth_type,

View File

@@ -66,7 +66,7 @@ if (!empty($this->entries)) {
?><div class="flux_content">
<div class="content <?php echo $content_width; ?>">
<h1 class="title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1>
<?php
$author = $this->entry->author();
echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '',

View File

@@ -19,7 +19,7 @@ if (!empty($this->entries)) {
$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feed()); //We most likely already have the feed object in cache
if (empty($feed)) $feed = $item->feed(true);
?>
<a href="<?php echo $item->link(); ?>">
<a target="_blank" rel="noreferrer" class="go_website" href="<?php echo $item->link(); ?>">
<img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span>
</a>
<h1 class="title"><?php echo $item->title(); ?></h1>

View File

@@ -43,6 +43,19 @@
</div>
<?php } ?>
<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
<div class="form-group">
<label class="group-name" for="token"><?php echo _t('admin.auth.token'); ?></label>
<?php $token = FreshRSS_Context::$user_conf->token; ?>
<div class="group-controls">
<input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo _t('gen.short.blank_to_disable'); ?>"<?php
echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?php echo $token; ?>"/>
<?php echo _i('help'); ?> <?php echo _t('admin.auth.token_help'); ?>
<kbd><?php echo Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_Session::param('currentUser'), 'token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true); ?></kbd>
</div>
</div>
<?php } ?>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>

View File

@@ -89,3 +89,55 @@ Example to get the number of feeds of a given user:
```sh
./cli/user-info.php --user alex | cut -f6
```
# Install and updates
## Using git
If you manage FreshRSS via command line, then installing and updating FreshRSS can be done via git:
```sh
# If your local user does not have write access, prefix all commands by sudo:
sudo ...
# Install FreshRSS
cd /usr/share/
git clone https://github.com/FreshRSS/FreshRSS.git
# Perform all commands below in your FreshRSS directory:
cd /usr/share/FreshRSS
# Use the development version of FreshRSS
git checkout -b dev origin/dev
# Check out a specific version of FreshRSS
# See release names on https://github.com/FreshRSS/FreshRSS/releases
# You will then need to manually change version
# or checkout master or dev branch to get new versions
git checkout 1.7.0
# Verify what branch is used
git branch
# Check whether there is a new version of FreshRSS,
# assuming you are on the /master or /dev branch
git fetch --all
git status
# Discard manual changes (do a backup before)
git reset --hard
# Then re-delete the file forcing the setup wizard
rm data/do-install.txt
# Delete manual additions (do a backup before)
git clean -f -d
# Update to a newer version of FreshRSS,
# assuming you are on the /master or /dev branch
git pull
# Set the rights so that your Web server can access the files
# (Example for Debian / Ubuntu)
chown -R :www-data . && chmod -R g+r . && chmod -R g+w ./data/
```

View File

@@ -8,7 +8,7 @@ require(LIB_PATH . '/lib_rss.php');
Minz_Configuration::register('system',
DATA_PATH . '/config.php',
DATA_PATH . '/config.default.php');
FRESHRSS_PATH . '/config.default.php');
FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
Minz_Translate::init('en');

View File

@@ -14,9 +14,9 @@ $username = cliInitUser($options['user']);
fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
list($nbUpdatedFeeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username\n";
echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username ($nbNewArticles new articles)\n";
invalidateHttpCache($username);

View File

@@ -22,6 +22,7 @@ return array (
'hide_read_feeds' => true,
'onread_jump_next' => true,
'lazyload' => true,
'sides_close_article' => true,
'sticky_post' => true,
'reading_confirm' => false,
'auto_remove_article' => false,

View File

@@ -1,5 +1,5 @@
<?php
define('FRESHRSS_VERSION', '1.6.3');
define('FRESHRSS_VERSION', '1.7.0');
define('FRESHRSS_WEBSITE', 'http://freshrss.org');
define('FRESHRSS_WIKI', 'http://doc.freshrss.org');

1
data/extensions-data/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*/

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=/" />
<title>Redirection</title>
<meta name="robots" content="noindex" />
</head>
<body>
<p><a href="/">Redirection</a></p>
</body>
</html>

0
data/users/_/.gitignore vendored Normal file
View File

6
docs/_config.yml Normal file
View File

@@ -0,0 +1,6 @@
theme: jekyll-theme-cayman
title: FreshRSS
description: Documentation center
logo: /img/FreshRSS-logo.png
show_downloads: true

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
<title>{{ page.title | default: site.title }}</title>
<meta name="description" content="{{ page.description | default: site.description | default: site.github.project_tagline }}"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#157878">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
</head>
<body>
<section class="page-header">
<h1 class="project-name">
<a href="{{ site.github.url }}">{{ site.title | default: site.github.repository_name }}</a>
</h1>
<h2 class="project-tagline">{{ site.description | default: site.github.project_tagline }}</h2>
{% if site.github.is_project_page %}
<a href="{{ site.github.repository_url }}" class="btn">View on GitHub</a>
{% endif %}
{% if site.show_downloads %}
<a href="{{ site.github.zip_url }}" class="btn">Download .zip</a>
<a href="{{ site.github.tar_url }}" class="btn">Download .tar.gz</a>
{% endif %}
</section>
<section class="main-content">
{{ content }}
<footer class="site-footer">
{% if site.github.is_project_page %}
<span class="site-footer-owner"><a href="{{ site.github.repository_url }}">{{ site.github.repository_name }}</a> is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a>.</span>
{% endif %}
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a>.</span>
</footer>
</section>
{% if site.google_analytics %}
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,13 @@
---
---
@import "{{ site.theme }}";
.page-header .project-name a {
color: #fff;
&:hover {
text-decoration: none;
opacity: .7;
}
}

55
docs/en/contributing.md Normal file
View File

@@ -0,0 +1,55 @@
## Join us on the mailing lists
Do you want to ask us some questions? Do you want to discuss with us? Don't hesitate to subscribe to our mailing lists!
- The first mailing is destined to generic information, it should be adapted to users. [Join mailing@freshrss.org](https://freshrss.org/mailman/listinfo/mailing).
- The second mailing is mainly for developers. [Join dev@freshrss.org](https://freshrss.org/mailman/listinfo/dev)
## Report a bug
You found a bug? Don't panic, here are some steps to report it easily:
1. Search for it on [the bug tracker](https://github.com/FreshRSS/FreshRSS/issues) (don't forget to use the search bar).
2. If you find a similar bug, don't hesitate to post a comment to add more importance to the related ticket.
3. If you didn't find it, [open a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new).
If you have to create a new ticket, try to apply the following advices:
- Give an explicit title to the ticket so it will be easier to find it later.
- Be as exhaustive as possible in the description: what did you do? What is the bug? What are the steps to reproduce the bug?
- We also need some information:
+ Your FreshRSS version (on about page or `constants.php` file)
+ Your server configuration: type of hosting, PHP version
+ Your storage system (MySQL / MariaDB / PostgreSQL or SQLite)
+ If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`)
## Fix a bug
Did you want to fix a bug? To keep a great coordination between collaborators, you will have to follow these indications:
1. Be sure the bug is associated to a ticket and say you work on it.
2. [Fork this project repository](https://help.github.com/articles/fork-a-repo/).
3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783).
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **dev branch**.
If you have to write code, please follow [our coding style recommendations](http://doc2.freshrss.org/en/Developer_documentation/First_steps/Coding_style).
**Tip:** if you are searching for bugs easy to fix, have a look at the « [New comers](https://github.com/FreshRSS/FreshRSS/labels/New%20comers) » ticket label.
## Submit an idea
You have great ideas, yes! Don't be shy and open [a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new) on our bug tracker to ask if we can implement it. The greatest ideas often come from the shyest suggestions!
If your idea is nice, we'll have a look at it.
## Contribute to internationalization (i18n)
If you want to improve internationalization, please open a new ticket first and follow indications from « Fix a bug » section.
Translations are present in the subdirectories of `./app/i18n/`.
We are working on a better way to handle internationalization but don't hesitate to suggest any idea!
## Contribute to documentation
The documentation needs a lot of improvements in order to be more useful to new contributors and we are working on it. If you want to give some help, meet us on [the dedicated repository](https://github.com/FreshRSS/documentation)!

View File

@@ -0,0 +1,194 @@
# Environment configuration
**TODO**
# Project architecture
**TODO**
# Coding style
If you want to contribute to the source code, it is important to follow the project coding style. The actual code does not follow it throughout the project, but every time we have an opportunity, we should fix it.
Contributions which do not follow the coding style will be rejected as long as the coding style is not fixed.
## Spaces, tabs and white spaces
### Indent
Code indent must use tabs.
### Alignment
Once the code is indented, it might be useful to align it to ease the reading. In that case, use spaces.
```php
$result = a_function_with_a_really_long_name($param1, $param2,
$param3, $param4);
```
### End of line
The end of line character must be a line feed (LF) which is a default end of line on *NIX systems. This character must not follow other white spaces.
It is possible to verify if there is white spaces before the end of line, with the following Git command:
```bash
# command to check files before adding them in the Git index
git diff --check
# command to check files after adding them in the Git index
git diff --check --cached
```
### End of file
Every file must end by an empty line.
### With commas, dots and semi-columns
There is no space before those characters but there is one after.
### With operators
There is a space before and after every operator.
```php
if ($a == 10) {
// do something
}
echo $a ? 1 : 0;
```
### With brackets
There is no spaces in the brackets. There is no space before the opening bracket except if it is after a keyword. There is no space after the closing bracket except if it is followed by a curly bracket.
```php
if ($a == 10) {
// do something
}
if ((int)$a == 10) {
// do something
}
```
### With chained functions
It happens most of the time in Javascript files. When there is chained functions, closures and callback functions, it is hard to understand the code if not properly formatted. In those cases, we add a new indent level for the complete instruction and reset the indent for a new instruction on the same level.
```javascript
// First instruction
shortcut.add(shortcuts.mark_read, function () {
//...
}, {
'disable_in_input': true
});
// Second instruction
shortcut.add("shift+" + shortcuts.mark_read, function () {
//...
}, {
'disable_in_input': true
});
```
## Line length
Lines should be shorter than 80 characters. However, in some case, it is possible to extend that limit to 100 characters.
With functions, parameters can be declared on different lines.
```php
function my_function($param_1, $param_2,
$param_3, $param_4) {
// do something
}
```
## Naming
All the code elements (functions, classes, methods and variables) must describe their usage in concise way.
### Functions and variables
They must follow the "snake case" convention.
```php
// a function
function function_name() {
// do something
}
// a variable
$variable_name;
```
### Methods
They must follow the "lower camel case" convention.
```php
private function methodName() {
// do something
}
```
### Classes
They must follow the "upper camel case" convention.
```php
abstract class ClassName {}
```
## Encoding
Files must be encoded with UTF-8 character set.
## PHP 5.3 compatibility
Do not get an array item directly from a function or a method. Use a variable.
```php
// code with PHP 5.3 compatibility
$my_variable = function_returning_an_array();
echo $my_variable[0];
// code without PHP 5.3 compatibility
echo function_returning_an_array()[0];
```
Do not use short array declaration.
```php
// code with PHP 5.3 compatibility
$variable = array();
// code without PHP 5.3 compatibility
$variable = [];
```
## Miscellaneous
### Operators
They must be at the end of the line if a condition runs on more than one line.
```php
if ($a == 10 ||
$a == 20) {
// do something
}
```
### End of file
If the file contains only PHP code, the PHP closing tag must be omitted.
### Arrays
If an array declaration runs on more than one line, each element must be followed by a comma even the last one.
```php
$variable = array(
"value 1",
"value 2",
"value 3",
);
```

View File

@@ -0,0 +1,11 @@
# Reporting a bug or a suggestion
**TODO**
# Branching
**TODO**
# Sending a patch
**TODO**

View File

View File

@@ -0,0 +1,27 @@
# Models
**TODO**
# Controllers and actions
**TODO**
# Views
**TODO**
# Routing
**TODO**
# Writing URL
**TODO**
# Internationalisation
**TODO**
# Understanding internals
**TODO**

View File

View File

@@ -0,0 +1,15 @@
# Accessing the database
**TODO**
# Writing an action and its related view
**TODO**
# Authentication
**TODO**
# Logs
**TODO**

View File

@@ -0,0 +1,334 @@
# Writing extensions for FreshRSS
## About FreshRSS
FreshRSS is an RSS / Atom feeds aggregator written in PHP since October 2012. The official site is located at [freshrss.org](http://freshrss.org) and its repository is hosted by Github: [github.com/FreshRSS/FreshRSS](https://github.com/FreshRSS/FreshRSS).
## Problem to solve
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
## Understanding basic mechanics (Minz and MVC)
**TODO** : move to 02_Minz.md
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### MVC Architecture
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Routing
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
Code example:
```php
<?php
class FreshRSS_hello_Controller extends Minz_ActionController {
public function indexAction() {
$this->view->a_variable = 'FooBar';
}
public function worldAction() {
$this->view->a_variable = 'Hello World!';
}
}
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Views
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
Code example:
```html
<p>
This is a parameter passed from the controller: <?php echo $this->a_variable; ?>
</p>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Working with GET / POST
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
Code example:
```php
<?php
$default_value = 'foo';
$param = Minz_Request::param('bar', $default_value);
// Display the value of the parameter `bar` (passed via GET or POST)
// or "foo" if the parameter does not exist.
echo $param;
// Sets the value of the `bar` parameter
Minz_Request::_param('bar', 'baz');
// Will necessarily display "baz" since we have just forced its value.
// Note that the second parameter (default) is optional.
echo Minz_Request::param('bar');
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Access session settings
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Working with URLs
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
```html
<p>
Go to page <a href="http://example.com?c=hello&amp;a=world">Hello world</a>!
</p>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
```php
<?php
$url_array = array(
'c' => 'hello',
'a' => 'world',
'params' => array(
'foo' => 'bar',
)
);
// Show something like .?c=hello&amp;a=world&amp;foo=bar
echo Minz_Url::display($url_array);
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
```php
<?php
// Displays the same as above
echo _url('hello', 'world', 'foo', 'bar');
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Redirections
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
Code example:
```php
<?php
$url_array = array(
'c' => 'hello',
'a' => 'world'
);
// Tells Minz to redirect the user to the hello / world page.
// Note that this is a redirection in the Minz sense of the term, not a redirection that the browser will have to manage (HTTP code 301 or 302)
// The code that follows forward() will thus be executed!
Minz_Request::forward($url_array);
// To perform a type 302 redirect, add "true".
// The code that follows will never be executed.
Minz_Request::forward($url_array, true);
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
```php
<?php
$url_array = array(
'c' => 'hello',
'a' => 'world'
);
$feedback_good = 'Tout s\'est bien passé !';
$feedback_bad = 'Oups, quelque chose n\'a pas marché.';
Minz_Request::good($feedback_good, $url_array);
// or
Minz_Request::bad($feedback_bad, $url_array);
?>
```
### Translation Management
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
```php
<?php
return array(
'action' => array(
'actualize' => 'Actualiser',
'back_to_rss_feeds' => '← Retour à vos flux RSS',
'cancel' => 'Annuler',
'create' => 'Créer',
'disable' => 'Désactiver',
),
'freshrss' => array(
'_' => 'FreshRSS',
'about' => 'À propos de FreshRSS',
),
);
?>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
Code example:
```html
<p>
<a href="<?php echo _url('index', 'index'); ?>">
<?php echo _t('gen.action.back_to_rss_feeds'); ?>
</a>
</p>
```
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
### Configuration management
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
## Write an extension for FreshRSS
Here we are! We've talked about the most useful features of Minz and how to run FreshRSS correctly and it's about time to address the extensions themselves.
An extension allows you to add functionality easily to FreshRSS without having to touch the core of the project directly.
### Basic files and folders
The first thing to note is that **all** extensions **must** be located in the `extensions` directory, at the base of the FreshRSS tree.
An extension is a directory containing a set of mandatory (and optional) files and subdirectories.
The convention requires that the main directory name be preceded by an "x" to indicate that it is not an extension included by default in FreshRSS.
The main directory of an extension must contain at least two **mandatory** files:
- A `metadata.json` file that contains a description of the extension. This file is written in JSON.
- An `extension.php` file containing the entry point of the extension (which is a class that inherits Minz_Extension).
Please note that there is a not a required link between the directory name of the extension and the name of the class inside `extension.php`,
but you should follow our best practice:
If you want to write a `HelloWorld` extension, the directory name should be `xExtension-HelloWorld` and the base class name `HelloWorldExtension`.
In the file `freshrss/extensions/xExtension-HelloWorld/extension.php` you need the structure:
```html
class HelloWorldExtension extends Minz_Extension {
public function init() {
// your code here
}
}
```
There is an example HelloWorld extension that you can download from [our GitHub repo](https://github.com/FreshRSS/xExtension-HelloWorld).
You may also need additional files or subdirectories depending on your needs:
- `configure.phtml` is the file containing the form to parameterize your extension
- A `static/` directory containing CSS and JavaScript files that you will need for your extension (note that if you need to write a lot of CSS it may be more interesting to write a complete theme)
- A `controllers` directory containing additional controllers
- An `i18n` directory containing additional translations
- `layout` and` views` directories to define new views or to overwrite the current views
In addition, it is good to have a `LICENSE` file indicating the license under which your extension is distributed and a` README` file giving a detailed description of it.
### The metadata.json file
The `metadata.json` file defines your extension through a number of important elements. It must contain a valid JSON array containing the following entries:
- `name` : the name of your extension
- `author` : your name, your e-mail address ... but there is no specific format to adopt
- `description` : a description of your extension
- `version` : the current version number of the extension
- `entrypoint` : Indicates the entry point of your extension. It must match the name of the class contained in the file `extension.php` without the suffix` Extension` (so if the entry point is `HelloWorld`, your class will be called` HelloWorldExtension`)
- `type` : Defines the type of your extension. There are two types: `system` and` user`. We will study this difference right after.
Only the `name` and` entrypoint` fields are required.
### Choose between « system » or « user »
A __user__ extension can be enabled by some users and not by others (typically for user preferences).
A __system__ extension in comparison is enabled for every account.
### Writing your own extension.php
This file is the entry point of your extension. It must contain a specific class to function.
As mentioned above, the name of the class must be your `entrypoint` suffixed by` Extension` (`HelloWorldExtension` for example).
In addition, this class must be inherited from the `Minz_Extension` class to benefit from extensions-specific methods.
Your class will benefit from four methods to redefine:
- `install()` is called when a user clicks the button to activate your extension. It allows, for example, to update the database of a user in order to make it compatible with the extension. It returns `true` if everything went well or, if not, a string explaining the problem.
- `uninstall()` is called when a user clicks the button to disable your extension. This will allow you to undo the database changes you potentially made in `install ()`. It returns `true` if everything went well or, if not, a string explaining the problem.
- `init()` is called for every page load *if the extension is enabled*. It will therefore initialize the behavior of the extension. This is the most important method.
- `handleConfigureAction()` is called when a user loads the extension management panel. Specifically, it is called when the `?c=extension&a=configured&e=name-of-your-extension` URL is loaded. You should also write here the behavior you want when validating the form in your `configure.phtml` file.
In addition, you will have a number of methods directly inherited from `Minz_Extension` that you should not redefine:
- The "getters" first: most are explicit enough not to detail them here - `getName()`, `getEntrypoint()`, `getPath()` (allows you to retrieve the path to your extension), `getAuthor()`, `getDescription()`, `getVersion()`, `getType()`.
- `getFileUrl($filename, $type)` will return the URL to a file in the `static` directory. The first parameter is the name of the file (without `static /`), the second is the type of file to be used (`css` or` js`).
- `registerController($base_name)` will tell Minz to take into account the given controller in the routing system. The controller must be located in your `Controllers` directory, the name of the file must be` <base_name>Controller.php` and the name of the `FreshExtension_<base_name>_Controller` class.
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)
- `registerViews()`
- `registerTranslates()`
- `registerHook($hook_name, $hook_function)`
### The « hooks » system
You can register at the FreshRSS event system in an extensions `init()` method, to manipulate data when some of the core functions are executed.
```html
class HelloWorldExtension extends Minz_Extension
{
public function init() {
$this->registerHook('entry_before_display', array($this, 'renderEntry'));
}
public function renderEntry($entry) {
$entry->_content('<h1>Hello World</h1>' . $entry->content());
return $entry;
}
}
```
The following events are available:
- `entry_before_display` (`function($entry) -> Entry | null`) : will be executed every time an entry is rendered. The entry itself (instance of FreshRSS_Entry) will be passed as parameter.
- `entry_before_insert` (`function($entry) -> Entry | null`) : will be executed when a feed is refreshed and new entries will be imported into the database. The new entry (instance of FreshRSS_Entry) will be passed as parameter.
- `feed_before_insert` (`function($feed) -> Feed | null`) : will be executed when a new feed is imported into the database. The new feed (instance of FreshRSS_Feed) will be passed as parameter.
- `post_update` (`function(none) -> none`) : **TODO** add documentation
### Writing your own configure.phtml
When you want to support user configurations for your extension or simply display some information, you have to create the `configure.phtml` file.
**TODO** translate from [french version](https://github.com/FreshRSS/documentation/blob/master/fr/docs/developers/03_Backend/05_Extensions.md)

View File

@@ -0,0 +1,15 @@
# The .phtml files
**TODO**
# Writing a URL
**TODO**
# Displaying an icon
**TODO**
# Internationalisation
**TODO**

View File

@@ -0,0 +1,11 @@
# Template file
**TODO**
# Writing a new theme
**TODO**
# Overriding icons
**TODO**

View File

@@ -0,0 +1 @@
**TODO**

BIN
docs/en/img/doc.edit.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show More