diff options
| author | 2017-06-03 15:18:04 +0200 | |
|---|---|---|
| committer | 2017-06-03 15:18:04 +0200 | |
| commit | fd43ee546e419fc9fbb9a3ad974d2234d94cffaa (patch) | |
| tree | 2df9fd1daf5b994d7bbde8dd46f7605e4370c268 | |
| parent | be0bcfef7e38f27284ec7b377b342ba389515964 (diff) | |
| parent | 6f3653d430c86b026f8e007a276fdba2b09b61b3 (diff) | |
Merge pull request #1549 from FreshRSS/dev1.7.0
Version 1.7.0
212 files changed, 5857 insertions, 1354 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index caed250ca..620bccc92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 2017-06-03 FreshRSS 1.7.0 +* Features: + * Deferred insertion of new articles, for better chronological order [#530](https://github.com/FreshRSS/FreshRSS/issues/530) + * Better search: + * Possibility to use multiple `intitle:`, `inurl:`, `author:` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478) + * Negative searches with `!` or `-` [#1381](https://github.com/FreshRSS/FreshRSS/issues/1381) + * Examples: `!intitle:unwanted`, `-intitle:unwanted`, `-inurl:unwanted`, `-author:unwanted`, `-#unwanted`, `-unwanted` + * Allow double-quotes, such as `author:"some name"`, in addition to single-quotes such as `author:'some name'` [#1478](https://github.com/FreshRSS/FreshRSS/pull/1478) + * Multi-user tokens (to access RSS outputs of any user) [#1390](https://github.com/FreshRSS/FreshRSS/issues/1390) +* Compatibility: + * Add support for PHP 7.1 [#1471](https://github.com/FreshRSS/FreshRSS/issues/1471) + * PostgreSQL is not experimental anymore [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476) +* Bug fixing + * Fix PubSubHubbub bugs when deleting users, and improved behaviour when removing feeds [#1495](https://github.com/FreshRSS/FreshRSS/pull/1495) + * Fix SQL uniqueness bug with PostgreSQL [#1476](https://github.com/FreshRSS/FreshRSS/pull/1476) + * (Require manual update for existing installations) + * Do not require PHP extension `fileinfo` for favicons [#1461](https://github.com/FreshRSS/FreshRSS/issues/1461) + * Fix UI lowest subscription popup hidden [#1479](https://github.com/FreshRSS/FreshRSS/issues/1479) + * Fix update system via ZIP archive [#1498](https://github.com/FreshRSS/FreshRSS/pull/1498) + * Work around for IE / Edge bug in username pattern in version 1.6.3 [#1511](https://github.com/FreshRSS/FreshRSS/issues/1511) + * Fix *mark as read* articles when adding a new feed [#1535](https://github.com/FreshRSS/FreshRSS/issues/1535) + * Change load order of CSS and JS to help CustomCSS and CustomJS extensions [Extensions#13](https://github.com/FreshRSS/Extensions/issues/13), [#1547](https://github.com/FreshRSS/FreshRSS/pull/1547) +* UI + * New option for not closing the article when clicking outside its area [#1539](https://github.com/FreshRSS/FreshRSS/pull/1539) + * Add shortcut in reader view to open the original page [#1564](https://github.com/FreshRSS/FreshRSS/pull/1564) + * Download icon 💾 for other MIME types (e.g. `application/*`) [#1522](https://github.com/FreshRSS/FreshRSS/pull/1522) +* I18n + * Simplified Chinese [#1541](https://github.com/FreshRSS/FreshRSS/pull/1541) + * Improve English [#1465](https://github.com/FreshRSS/FreshRSS/pull/1465) + * Improve Dutch [#1559](https://github.com/FreshRSS/FreshRSS/pull/1559) +* Security + * Do not require write access to check availability of new versions [#1450](https://github.com/FreshRSS/FreshRSS/issues/1450) +* Misc. + * Move [documentation](./docs/) into FreshRSS code [#1510](https://github.com/FreshRSS/FreshRSS/pull/1510) + * Moved `./data/force-https.default.txt` to `./force-https.default.txt`, + `./data/config.default.php` to `./config.default.php`, + and `./data/users/_/config.default.php` to `./config-user.default.php` [#1531](https://github.com/FreshRSS/FreshRSS/issues/1531) + * Fall back to article URL when the article GUID is empty [#1482](https://github.com/FreshRSS/FreshRSS/issues/1482) + * Rewritten Favicon library using cURL [#1504](https://github.com/FreshRSS/FreshRSS/pull/1504) + * Fix SimplePie option to disable syslog [#1528](https://github.com/FreshRSS/FreshRSS/pull/1528) + + ## 2017-03-11 FreshRSS 1.6.3 * Features diff --git a/CREDITS.md b/CREDITS.md index 57635669a..97651ab20 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -17,10 +17,14 @@ People are sorted by name so please keep this order. * [dswd](https://github.com/dswd): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:dswd) * [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed) * [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/) +* [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong) * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/) * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/) * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb) +* [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc) * [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=edhelas), [Web](http://edhelas.movim.eu/) +* [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler) +* [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=cez81) * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/) * [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/) * [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/) @@ -31,6 +35,7 @@ People are sorted by name so please keep this order. * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie) * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/) * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop) +* [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu) * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/) * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/) * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi) @@ -39,3 +44,4 @@ People are sorted by name so please keep this order. * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/) * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue) * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo) +* [mszkb](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=mszkb) diff --git a/README.fr.md b/README.fr.md index b0b46bf65..b7a28fd91 100644 --- a/README.fr.md +++ b/README.fr.md @@ -14,7 +14,7 @@ Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de per * Démo : http://demo.freshrss.org/ * Licence : [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) - + # Téléchargement Voir la [liste des versions](../../releases). @@ -35,11 +35,15 @@ Nous sommes une communauté amicale. * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances) * Requis : [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), et [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql) * Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [ZIP](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés) -* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental) +* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL 9.2+ * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari. * Fonctionne aussi sur mobile - + + +# Documentation +* http://doc.freshrss.org/fr/ +* https://github.com/FreshRSS/documentation # Installation 1. Récupérez l’application FreshRSS via la commande git ou [en téléchargeant l’archive](../releases) @@ -48,7 +52,7 @@ Nous sommes une communauté amicale. 4. Accédez à FreshRSS à travers votre navigateur Web et suivez les instructions d’installation * ou utilisez [l’interface en ligne de commande](./cli/README.md) 5. Tout devrait fonctionner :) En cas de problème, n’hésitez pas à [nous contacter](https://github.com/FreshRSS/FreshRSS/issues). -6. Des paramètres de configuration avancée peuvent être accédés depuis [config.php](./data/config.default.php). +6. Des paramètres de configuration avancée peuvent être vues dans [config.default.php](./config.default.php) et modifiées dans `data/config.php`. ## Installation automatisée * [](https://dfabric.github.io/DPlatform-ShellCore) @@ -130,7 +134,8 @@ Créer `/etc/cron.d/FreshRSS` avec : * Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`. * En particulier, les données personnelles se trouvent dans le répertoire `./data/`. * Le fichier `./constants.php` définit les chemins d’accès aux répertoires clés de l’application. Si vous les bougez, tout se passe ici. -* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/log/*.log`. +* En cas de problème, les logs peuvent être utile à lire, soit depuis l’interface de FreshRSS, soit manuellement depuis `./data/users/*/log*.txt`. + * Le répertoire spécial `./data/users/_/` contient la partie des logs partagés par tous les utilisateurs. # Sauvegarde @@ -154,7 +159,6 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio * [MINZ](https://github.com/marienfressinaud/MINZ) * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) * [jQuery](http://jquery.com/) -* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon) * [lib_opml](https://github.com/marienfressinaud/lib_opml) * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/) * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) @@ -14,7 +14,7 @@ Finally, it supports [extensions](#extensions) for further tuning. * Demo: http://demo.freshrss.org/ * License: [GNU AGPL 3](http://www.gnu.org/licenses/agpl-3.0.html) - + # Releases See the [list of releases](../../releases). @@ -35,11 +35,15 @@ We are a friendly community. * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance) * Required extensions: [cURL](http://php.net/curl), [DOM](http://php.net/dom), [XML](http://php.net/xml), and [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql) * Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [ZIP](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds) -* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental) +* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL 9.2+ * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari. * Works on mobile - + + +# Documentation +* http://doc.freshrss.org/en/ +* https://github.com/FreshRSS/documentation # Installation 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip) @@ -48,7 +52,7 @@ We are a friendly community. 4. Access FreshRSS with your browser and follow the installation process * or use the [Command-Line Interface](./cli/README.md) 5. Everything should be working :) If you encounter any problem, feel free [contact us](https://github.com/FreshRSS/FreshRSS/issues). -6. Advanced configuration settings can be seen in [config.php](./data/config.default.php). +6. Advanced configuration settings can be seen in [config.default.php](./config.default.php) and modified in `data/config.php`. ## Automated install * [](https://cloudron.io/button.html?app=org.freshrss.cloudronapp) @@ -101,6 +105,7 @@ cd /usr/share/FreshRSS sudo git pull sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/ ``` +See more commands and git commands in the [Command-Line Interface documentation](./cli/README.md). ## Access control It is needed for the multi-user mode to limit access to FreshRSS. You can: @@ -128,10 +133,11 @@ Create `/etc/cron.d/FreshRSS` with: # Advices -* For a better security, expose only the `./p/` folder on the web. +* For a better security, expose only the `./p/` folder on the Web. * Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it. * The `./constants.php` file defines access to application folder. If you want to customize your installation, every thing happens here. -* If you encounter any problem, logs are accessible from the interface or manually in `./data/log/*.log` files. +* If you encounter any problem, logs are accessible from the interface or manually in `./data/users/*/log*.txt` files. + * The special folder `./data/users/_/` contains the part of the logs that are shared by all users. # Backup @@ -155,7 +161,6 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E * [MINZ](https://github.com/marienfressinaud/MINZ) * [php-http-304](http://alexandre.alapetite.fr/doc-alex/php-http-304/) * [jQuery](http://jquery.com/) -* [ArthurHoaro/favicon](https://github.com/ArthurHoaro/favicon) * [lib_opml](https://github.com/marienfressinaud/lib_opml) * [jQuery Plugin Sticky-Kit](http://leafo.net/sticky-kit/) * [keyboard_shortcuts](http://www.openjs.com/scripts/events/keyboard_shortcuts/) diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 1398e4e49..5ad1a51d9 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -27,11 +27,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController { if (Minz_Request::isPost()) { $ok = true; - $current_token = FreshRSS_Context::$user_conf->token; - $token = Minz_Request::param('token', $current_token); - FreshRSS_Context::$user_conf->token = $token; - $ok &= FreshRSS_Context::$user_conf->save(); - $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); $anon_refresh = Minz_Request::param('anon_refresh', false); @@ -123,7 +118,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $challenge = Minz_Request::param('challenge', ''); $conf = get_user_configuration($username); - if (is_null($conf)) { + if ($conf == null) { Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false); return; } @@ -164,7 +159,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { } $conf = get_user_configuration($username); - if (is_null($conf)) { + if ($conf == null) { return; } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index e73f106a6..155221d19 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -109,6 +109,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false); FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false); FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false); + FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::param('sides_close_article', false); FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false); FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false); FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index f71f26a4e..c9b6deaa7 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -226,7 +226,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) { + public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false, $noCommit = false) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -254,6 +254,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. $updated_feeds = 0; + $nb_new_articles = 0; $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { $url = $feed->url(); //For detection of HTTP 301 @@ -308,6 +309,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // -2 means we take the default value from configuration $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } + $needFeedCacheRefresh = false; // We want chronological order and SimplePie uses reverse order. $entries = array_reverse($feed->entries()); @@ -333,6 +335,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . //', old hash ' . $existingHash . ', new hash ' . $entry->hash()); //TODO: Make an updated/is_read policy by feed, in addition to the global one. + $needFeedCacheRefresh = FreshRSS_Context::$user_conf->mark_updated_article_unread; $entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy. if (!$entryDAO->inTransaction()) { $entryDAO->beginTransaction(); @@ -345,6 +348,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { if ($isNewFeed) { $id = min(time(), $entry_date) . uSecString(); + $entry->_isRead($is_read); } elseif ($entry_date < $date_min) { $id = min(time(), $entry_date) . uSecString(); $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read @@ -372,6 +376,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->beginTransaction(); } $entryDAO->addEntry($entry->toArray()); + $nb_new_articles++; } } $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); @@ -388,12 +393,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $date_min, max($feed_history, count($entries) + 10)); if ($nb > 0) { + $needFeedCacheRefresh = true; Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']'); } } - $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime); + $feedDAO->updateLastUpdate($feed->id(), false, $mtime); + if ($needFeedCacheRefresh) { + $feedDAO->updateCachedValue($feed->id()); + } if ($entryDAO->inTransaction()) { $entryDAO->commit(); } @@ -434,7 +443,17 @@ class FreshRSS_feed_Controller extends Minz_ActionController { break; } } - return array($updated_feeds, reset($feeds)); + if (!$noCommit) { + if (!$entryDAO->inTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->commitNewEntries(); + $feedDAO->updateCachedValues(); + if ($entryDAO->inTransaction()) { + $entryDAO->commit(); + } + } + return array($updated_feeds, reset($feeds), $nb_new_articles); } /** @@ -444,6 +463,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { * - id (default: false): Feed ID * - url (default: false): Feed URL * - force (default: false) + * - noCommit (default: 0): Set to 1 to prevent committing the new articles to the main database * If id and url are not specified, all the feeds are actualized. But if force is * false, process stops at 10 feeds to avoid time execution problem. */ @@ -452,8 +472,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); $url = Minz_Request::param('url'); $force = Minz_Request::param('force'); - - list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force); + $noCommit = Minz_Request::fetchPOST('noCommit', 0) == 1; + + if ($id == -1 && !$noCommit) { //Special request only to commit & refresh DB cache + $updated_feeds = 0; + $entryDAO = FreshRSS_Factory::createEntryDao(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $entryDAO->beginTransaction(); + $entryDAO->commitNewEntries(); + $feedDAO->updateCachedValues(); + $entryDAO->commit(); + } else { + list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit); + } if (Minz_Request::param('ajax')) { // Most of the time, ajax request is for only one feed. But since diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 6ae89defb..2bc68848c 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -464,18 +464,22 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $values = $entry->toArray(); + $ok = false; if (isset($existingHashForGuids[$entry->guid()])) { - $id = $this->entryDAO->updateEntry($values); + $ok = $this->entryDAO->updateEntry($values); } else { - $id = $this->entryDAO->addEntry($values); + $ok = $this->entryDAO->addEntry($values); } + $error |= ($ok === false); - if (!$error && ($id === false)) { - $error = true; - } } $this->entryDAO->commit(); + $this->entryDAO->beginTransaction(); + $this->entryDAO->commitNewEntries(); + $this->feedDAO->updateCachedValues(); + $this->entryDAO->commit(); + return !$error; } diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index b4e8a0bff..7a8a3d6c0 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -59,24 +59,26 @@ class FreshRSS_update_Controller extends Minz_ActionController { public function indexAction() { Minz_View::prependTitle(_t('admin.update.title') . ' · '); - if (!is_writable(FRESHRSS_PATH)) { - $this->view->message = array( - 'status' => 'bad', - 'title' => _t('gen.short.damn'), - 'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH) - ); - } elseif (file_exists(UPDATE_FILENAME)) { + if (file_exists(UPDATE_FILENAME)) { // There is an update file to apply! $version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt')); - if (empty($version)) { + if ($version == '') { $version = 'unknown'; } - $this->view->update_to_apply = true; - $this->view->message = array( - 'status' => 'good', - 'title' => _t('gen.short.ok'), - 'body' => _t('feedback.update.can_apply', $version) - ); + if (is_writable(FRESHRSS_PATH)) { + $this->view->update_to_apply = true; + $this->view->message = array( + 'status' => 'good', + 'title' => _t('gen.short.ok'), + 'body' => _t('feedback.update.can_apply', $version), + ); + } else { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.file_is_nok', $version, FRESHRSS_PATH), + ); + } } } @@ -190,6 +192,7 @@ class FreshRSS_update_Controller extends Minz_ActionController { if (self::isGit()) { $res = self::gitPull(); } else { + require(UPDATE_FILENAME); if (Minz_Request::isPost()) { save_info_update(); } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index f910cecd9..3cbbd8633 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * The username is also used as folder name, file name, and part of SQL table name. * '_' is a reserved internal username. */ - const USERNAME_PATTERN = '[0-9a-zA-Z]|[0-9a-zA-Z_]{2,38}'; + const USERNAME_PATTERN = '[0-9a-zA-Z_]{2,38}|[0-9a-zA-Z]'; public static function checkUsername($username) { return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1; @@ -74,6 +74,10 @@ class FreshRSS_user_Controller extends Minz_ActionController { FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } + $current_token = FreshRSS_Context::$user_conf->token; + $token = Minz_Request::param('token', $current_token); + FreshRSS_Context::$user_conf->token = $token; + $ok &= FreshRSS_Context::$user_conf->save(); if ($ok) { @@ -213,6 +217,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $userDAO = new FreshRSS_UserDAO(); $ok &= $userDAO->deleteUser($username); $ok &= recursive_unlink($user_data); + array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt')); } return $ok; } diff --git a/app/FreshRSS.php b/app/FreshRSS.php index e4caf23d1..90d6fae06 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -41,7 +41,7 @@ class FreshRSS extends Minz_FrontController { $current_user = Minz_Session::param('currentUser', '_'); Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php'), - join_path(USERS_PATH, '_', 'config.default.php'), + join_path(FRESHRSS_PATH, 'config-user.default.php'), $configuration_setter); // Finish to initialize the other FreshRSS / Minz components. @@ -80,7 +80,7 @@ class FreshRSS extends Minz_FrontController { public static function loadStylesAndScripts() { $theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme); if ($theme) { - foreach($theme['files'] as $file) { + foreach(array_reverse($theme['files']) as $file) { if ($file[0] === '_') { $theme_id = 'base-theme'; $filename = substr($file, 1); @@ -91,13 +91,13 @@ class FreshRSS extends Minz_FrontController { $filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename); $url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime; header('Link: <' . Minz_Url::display($url, '', 'root') . '>;rel=preload', false); //HTTP2 - Minz_View::appendStyle(Minz_Url::display($url)); + Minz_View::prependStyle(Minz_Url::display($url)); } } - - Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js'))); - Minz_View::appendScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); - Minz_View::appendScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); + //Use prepend to insert before extensions. Added in reverse order. + Minz_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js'))); + Minz_View::prependScript(Minz_Url::display('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js'))); + Minz_View::prependScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js'))); } private static function loadNotifications() { diff --git a/app/Models/Auth.php b/app/Models/Auth.php index 476627e10..4de058999 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -74,6 +74,10 @@ class FreshRSS_Auth { public static function giveAccess() { $current_user = Minz_Session::param('currentUser'); $user_conf = get_user_configuration($current_user); + if ($user_conf == null) { + self::$login_ok = false; + return; + } $system_conf = Minz_Configuration::get('system'); switch ($system_conf->auth_type) { @@ -120,13 +124,28 @@ class FreshRSS_Auth { * Removes all accesses for the current user. */ public static function removeAccess() { - Minz_Session::_param('loginOk'); self::$login_ok = false; - $conf = Minz_Configuration::get('system'); - Minz_Session::_param('currentUser', $conf->default_user); + Minz_Session::_param('loginOk'); Minz_Session::_param('csrf'); + $system_conf = Minz_Configuration::get('system'); - switch ($conf->auth_type) { + $username = ''; + $token_param = Minz_Request::param('token', ''); + if ($token_param != '') { + $username = trim(Minz_Request::param('user', '')); + if ($username != '') { + $conf = get_user_configuration($username); + if ($conf == null) { + $username = ''; + } + } + } + if ($username == '') { + $username = $system_conf->default_user; + } + Minz_Session::_param('currentUser', $username); + + switch ($system_conf->auth_type) { case 'form': Minz_Session::_param('passwordHash'); FreshRSS_FormAuth::deleteCookie(); diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 046f54955..70e1dea2e 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -197,6 +197,10 @@ class FreshRSS_ConfigurationSetter { $data['hide_read_feeds'] = $this->handleBool($value); } + private function _sides_close_article(&$data, $value) { + $data['sides_close_article'] = $this->handleBool($value); + } + private function _lazyload(&$data, $value) { $data['lazyload'] = $this->handleBool($value); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index a562a963a..26cd24797 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -22,7 +22,6 @@ class FreshRSS_Entry extends Minz_Model { public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '', $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') { - $this->_guid($guid); $this->_title($title); $this->_author($author); $this->_content($content); @@ -32,6 +31,7 @@ class FreshRSS_Entry extends Minz_Model { $this->_isFavorite($is_favorite); $this->_feed($feed); $this->_tags(preg_split('/[\s#]/', $tags)); + $this->_guid($guid); } public function id() { @@ -101,6 +101,12 @@ class FreshRSS_Entry extends Minz_Model { $this->id = $value; } public function _guid($value) { + if ($value == '') { + $value = $this->link; + if ($value == '') { + $value = $this->hash(); + } + } $this->guid = $value; } public function _title($value) { diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index afcde3d7f..7e836097a 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -88,6 +88,38 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return false; } + protected function createEntryTempTable() { + $ok = false; + $hadTransaction = $this->bd->inTransaction(); + if ($hadTransaction) { + $this->bd->commit(); + } + try { + $db = FreshRSS_Context::$system_conf->db; + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + Minz_Log::warning('SQL CREATE TABLE entrytmp...'); + if (defined('SQL_CREATE_TABLE_ENTRYTMP')) { + $sql = sprintf(SQL_CREATE_TABLE_ENTRYTMP, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok = $stm && $stm->execute(); + } else { + global $SQL_CREATE_TABLE_ENTRYTMP; + $ok = !empty($SQL_CREATE_TABLE_ENTRYTMP); + foreach ($SQL_CREATE_TABLE_ENTRYTMP as $instruction) { + $sql = sprintf($instruction, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok &= $stm && $stm->execute(); + } + } + } catch (Exception $e) { + Minz_Log::error('FreshRSS_EntryDAO::createEntryTempTable error: ' . $e->getMessage()); + } + if ($hadTransaction) { + $this->bd->beginTransaction(); + } + return $ok; + } + protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { if ($errorInfo[0] === '42S22') { //ER_BAD_FIELD_ERROR @@ -97,6 +129,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $this->addColumn($column); } } + } elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) { //ER_BAD_TABLE_ERROR + return $this->createEntryTempTable(); //v1.7 } } if (isset($errorInfo[1])) { @@ -110,8 +144,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { private $addEntryPrepared = null; public function addEntry($valuesTmp) { - if ($this->addEntryPrepared === null) { - $sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, ' + if ($this->addEntryPrepared == null) { + $sql = 'INSERT INTO `' . $this->prefix . 'entrytmp` (id, guid, title, author, ' . ($this->isCompressed() ? 'content_bin' : 'content') . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' . 'VALUES(:id, :guid, :title, :author, ' @@ -121,43 +155,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . ', :is_read, :is_favorite, :id_feed, :tags)'; $this->addEntryPrepared = $this->bd->prepare($sql); } - $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']); - $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760); - $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']); - $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']); - $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255); - $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']); - $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255); - $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']); - $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']); - $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023); - $valuesTmp['link'] = safe_ascii($valuesTmp['link']); - $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']); - $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT); - $valuesTmp['lastSeen'] = time(); - $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT); - $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0; - $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT); - $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0; - $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT); - $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT); - $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023); - $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']); - - if ($this->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->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->addEntryPrepared && $this->addEntryPrepared->execute()) { - return $this->bd->lastInsertId(); + return true; } else { $info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); if ($this->autoUpdateDb($info)) { + $this->addEntryPrepared = null; return $this->addEntry($valuesTmp); - } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries + } elseif ((int)((int)$info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']); } @@ -165,6 +201,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } + public function commitNewEntries() { + $sql = 'SET @rank=(SELECT MAX(id) - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); ' . //MySQL-specific + 'INSERT IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' . + 'SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date; ' . + 'DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= @rank;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + private $updateEntryPrepared = null; public function updateEntry($valuesTmp) { @@ -212,7 +264,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) { - return $this->bd->lastInsertId(); + return true; } else { $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); if ($this->autoUpdateDb($info)) { @@ -578,18 +630,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 '; } if ($filter) { - if ($filter->getIntitle()) { - $search .= 'AND ' . $alias . 'title LIKE ? '; - $values[] = "%{$filter->getIntitle()}%"; - } - if ($filter->getInurl()) { - $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; - $values[] = "%{$filter->getInurl()}%"; - } - if ($filter->getAuthor()) { - $search .= 'AND ' . $alias . 'author LIKE ? '; - $values[] = "%{$filter->getAuthor()}%"; - } if ($filter->getMinDate()) { $search .= 'AND ' . $alias . 'id >= ? '; $values[] = "{$filter->getMinDate()}000000"; @@ -606,20 +646,69 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $search .= 'AND ' . $alias . 'date <= ? '; $values[] = $filter->getMaxPubdate(); } + + if ($filter->getAuthor()) { + foreach ($filter->getAuthor() as $author) { + $search .= 'AND ' . $alias . 'author LIKE ? '; + $values[] = "%{$author}%"; + } + } + if ($filter->getIntitle()) { + foreach ($filter->getIntitle() as $title) { + $search .= 'AND ' . $alias . 'title LIKE ? '; + $values[] = "%{$title}%"; + } + } if ($filter->getTags()) { - $tags = $filter->getTags(); - foreach ($tags as $tag) { + foreach ($filter->getTags() as $tag) { $search .= 'AND ' . $alias . 'tags LIKE ? '; $values[] = "%{$tag}%"; } } + if ($filter->getInurl()) { + foreach ($filter->getInurl() as $url) { + $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? '; + $values[] = "%{$url}%"; + } + } + + if ($filter->getNotAuthor()) { + foreach ($filter->getNotAuthor() as $author) { + $search .= 'AND (NOT ' . $alias . 'author LIKE ?) '; + $values[] = "%{$author}%"; + } + } + if ($filter->getNotIntitle()) { + foreach ($filter->getNotIntitle() as $title) { + $search .= 'AND (NOT ' . $alias . 'title LIKE ?) '; + $values[] = "%{$title}%"; + } + } + if ($filter->getNotTags()) { + foreach ($filter->getNotTags() as $tag) { + $search .= 'AND (NOT ' . $alias . 'tags LIKE ?) '; + $values[] = "%{$tag}%"; + } + } + if ($filter->getNotInurl()) { + foreach ($filter->getNotInurl() as $url) { + $search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) '; + $values[] = "%{$url}%"; + } + } + if ($filter->getSearch()) { - $search_values = $filter->getSearch(); - foreach ($search_values as $search_value) { + foreach ($filter->getSearch() as $search_value) { $search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? '; $values[] = "%{$search_value}%"; } } + if ($filter->getNotSearch()) { + foreach ($filter->getNotSearch() as $search_value) { + $search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) '; + $values[] = "%{$search_value}%"; + } + } } return array($values, $search); } diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index b96a62ebc..b25993c47 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -11,6 +11,11 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { } protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) { //undefined_table + return $this->createEntryTempTable(); + } + } return false; } @@ -18,6 +23,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { return false; } + public function commitNewEntries() { + $sql = 'DO $$ +DECLARE +maxrank bigint := (SELECT MAX(id) FROM `' . $this->prefix . 'entrytmp`); +rank bigint := (SELECT maxrank - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); +BEGIN + INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + (SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date); + DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank; +END $$;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + public function size($all = true) { $db = FreshRSS_Context::$system_conf->db; $sql = 'SELECT pg_size_pretty(pg_database_size(?))'; diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 34e854608..ad7bcd865 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -7,21 +7,42 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } protected function autoUpdateDb($errorInfo) { - if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR - //autoAddColumn - if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { - $showCreate = $tableInfo->fetchColumn(); - Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoUpdateDb: ' . $showCreate); - foreach (array('lastSeen', 'hash') as $column) { - if (stripos($showCreate, $column) === false) { - return $this->addColumn($column); - } + Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true)); + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) { + $showCreate = $tableInfo->fetchColumn(); + if (stripos($showCreate, 'entrytmp') === false) { + return $this->createEntryTempTable(); + } + } + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { + $showCreate = $tableInfo->fetchColumn(); + foreach (array('lastSeen', 'hash') as $column) { + if (stripos($showCreate, $column) === false) { + return $this->addColumn($column); } } } return false; } + public function commitNewEntries() { + $sql = ' +CREATE TEMP TABLE `tmp` AS SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date; +INSERT OR IGNORE INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `tmp` ORDER BY date; +DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`); +DROP TABLE `tmp`;'; + $hadTransaction = $this->bd->inTransaction(); + if (!$hadTransaction) { + $this->bd->beginTransaction(); + } + $result = $this->bd->exec($sql) !== false; + if (!$hadTransaction) { + $this->bd->commit(); + } + return $result; + } + protected function sqlConcat($s1, $s2) { return $s1 . '||' . $s2; } diff --git a/app/Models/Factory.php b/app/Models/Factory.php index 6502c38b7..dfccc883e 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -3,14 +3,7 @@ class FreshRSS_Factory { public static function createFeedDao($username = null) { - $conf = Minz_Configuration::get('system'); - switch ($conf->db['type']) { - case 'sqlite': - case 'pgsql': - return new FreshRSS_FeedDAOSQLite($username); - default: - return new FreshRSS_FeedDAO($username); - } + return new FreshRSS_FeedDAO($username); } public static function createEntryDao($username = null) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 7a9cf8612..52d49db6e 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -318,7 +318,7 @@ class FreshRSS_Feed extends Minz_Model { $elinks = array(); foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if (empty($elinks[$elink])) { + if ($elink != '' && empty($elinks[$elink])) { $elinks[$elink] = '1'; $mime = strtolower($enclosure->get_type()); if (strpos($mime, 'image/') === 0) { @@ -327,6 +327,8 @@ class FreshRSS_Feed extends Minz_Model { $content .= '<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') { diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 0168aebd9..d278122e3 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -92,29 +92,15 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } - public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) { - if ($updateCache) { - $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE - . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' - . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),' - . '`lastUpdate`=?, error=? ' - . 'WHERE id=?'; - } else { - $sql = 'UPDATE `' . $this->prefix . 'feed` ' - . 'SET `lastUpdate`=?, error=? ' - . 'WHERE id=?'; - } - - if ($mtime <= 0) { - $mtime = time(); - } - + public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue() + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET `lastUpdate`=?, error=? ' + . 'WHERE id=?'; $values = array( - $mtime, + $mtime <= 0 ? time() : $mtime, $inError ? 1 : 0, $id, ); - $stm = $this->bd->prepare($sql); if ($stm && $stm->execute($values)) { @@ -294,18 +280,28 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $res[0]['count']; } - public function updateCachedValues() { //For one single feed, call updateLastUpdate($id) - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'INNER JOIN (' - . 'SELECT e.id_feed, ' - . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, ' - . 'COUNT(e.id) AS nbEntries ' - . 'FROM `' . $this->prefix . 'entry` e ' - . 'GROUP BY e.id_feed' - . ') x ON x.id_feed=f.id ' - . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads'; + public function updateCachedValue($id) { //For multiple feeds, call updateCachedValues() + $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE + . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' + . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0) ' + . 'WHERE id=?'; + $values = array($id); $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error updateCachedValue: ' . $info[2]); + return false; + } + } + + public function updateCachedValues() { //For one single feed, call updateCachedValue($id) + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),' + . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)'; + $stm = $this->bd->prepare($sql); if ($stm && $stm->execute()) { return $stm->rowCount(); } else { @@ -343,7 +339,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $affected; } - public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after + public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateCachedValue($id) or updateCachedValues() just after $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' . 'WHERE id_feed=:id_feed AND id<=:id_max ' . 'AND is_favorite=0 ' //Do not remove favourites diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php deleted file mode 100644 index 440ae74da..000000000 --- a/app/Models/FeedDAOSQLite.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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; - } - } - -} diff --git a/app/Models/Search.php b/app/Models/Search.php index 575a9a2cb..5cc7f8e8d 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -23,18 +23,35 @@ class FreshRSS_Search { private $tags; private $search; + private $not_intitle; + private $not_inurl; + private $not_author; + private $not_tags; + private $not_search; + public function __construct($input) { - if (strcmp($input, '') == 0) { + if ($input == '') { return; } $this->raw_input = $input; + + $input = preg_replace('/:"(.*?)"/', ':"\1"', $input); + + $input = $this->parseNotIntitleSearch($input); + $input = $this->parseNotAuthorSearch($input); + $input = $this->parseNotInurlSearch($input); + $input = $this->parseNotTagsSeach($input); + + $input = $this->parsePubdateSearch($input); + $input = $this->parseDateSearch($input); + $input = $this->parseIntitleSearch($input); $input = $this->parseAuthorSearch($input); $input = $this->parseInurlSearch($input); - $input = $this->parsePubdateSearch($input); - $input = $this->parseDateSearch($input); $input = $this->parseTagsSeach($input); - $this->parseSearch($input); + + $input = $this->parseNotSearch($input); + $input = $this->parseSearch($input); } public function __toString() { @@ -48,6 +65,9 @@ class FreshRSS_Search { public function getIntitle() { return $this->intitle; } + public function getNotIntitle() { + return $this->not_intitle; + } public function getMinDate() { return $this->min_date; @@ -68,18 +88,34 @@ class FreshRSS_Search { public function getInurl() { return $this->inurl; } + public function getNotInurl() { + return $this->not_inurl; + } public function getAuthor() { return $this->author; } + public function getNotAuthor() { + return $this->not_author; + } public function getTags() { return $this->tags; } + public function getNotTags() { + return $this->not_tags; + } public function getSearch() { return $this->search; } + public function getNotSearch() { + return $this->not_search; + } + + private static function removeEmptyValues($anArray) { + return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array(); + } /** * Parse the search string to find intitle keyword and the search related @@ -90,14 +126,28 @@ class FreshRSS_Search { * @return string */ private function parseIntitleSearch($input) { - if (preg_match('/intitle:(?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); } diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index a60caf395..310c7c096 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -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); diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index a454829d5..09defd452 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -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; diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index 9f4240b98..4cfeb2517 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -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"'); diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 68d93ba92..c4e4af006 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -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`'); diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index ec25f988c..fd414e246 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -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( diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php index f2bd87c77..f7b8d8c73 100644 --- a/app/i18n/cz/feedback.php +++ b/app/i18n/cz/feedback.php @@ -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]', diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 7c57d5655..bfc24d2d2 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -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( diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php index 195083b36..e2e9a71ba 100644 --- a/app/i18n/de/feedback.php +++ b/app/i18n/de/feedback.php @@ -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]', diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index e94d9fa80..707627782 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -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', diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index b5ab73510..f4618a1ff 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -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 haven’t created any user query yet.', + 'none' => 'You haven’t 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)', diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php index e7f6b9f85..334d9a8f5 100644 --- a/app/i18n/en/feedback.php +++ b/app/i18n/en/feedback.php @@ -2,7 +2,7 @@ return array( 'admin' => array( - 'optimization_complete' => 'Optimisation complete', + 'optimization_complete' => 'Optimization complete', ), 'access' => array( 'denied' => 'You don’t have permission to access this page', @@ -39,26 +39,26 @@ return array( 'ok' => '%s is now enabled', ), 'no_access' => 'You have no access on %s', - 'not_enabled' => '%s is not enabled yet', + 'not_enabled' => '%s is not enabled', 'not_found' => '%s does not exist', ), 'import_export' => array( 'export_no_zip_extension' => 'ZIP extension is not present on your server. Please try to export files one by one.', 'feeds_imported' => 'Your feeds have been imported and will now be updated', - 'feeds_imported_with_errors' => 'Your feeds have been imported but some errors occurred', + 'feeds_imported_with_errors' => 'Your feeds have been imported, but some errors occurred', 'file_cannot_be_uploaded' => 'File cannot be uploaded!', 'no_zip_extension' => 'ZIP extension is not present on your server.', 'zip_error' => 'An error occured during ZIP import.', ), 'sub' => array( - 'actualize' => 'Actualise', + 'actualize' => 'Updating', 'category' => array( 'created' => 'Category %s has been created.', 'deleted' => 'Category has been deleted.', 'emptied' => 'Category has been emptied', 'error' => 'Category cannot be updated', 'name_exists' => 'Category name already exists.', - 'no_id' => 'You must precise the id of the category.', + 'no_id' => 'You must specify the id of the category.', 'no_name' => 'Category name cannot be empty.', 'not_delete_default' => 'You cannot delete the default category!', 'not_exist' => 'The category does not exist!', @@ -87,7 +87,7 @@ return array( 'update' => array( 'can_apply' => 'FreshRSS will now be updated to the <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]', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 1ee5336bd..05281769f 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -103,7 +103,7 @@ return array( 'js' => array( 'category_empty' => 'Empty category', 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', - 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favorites and user queries. It cannot be cancelled!', + 'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favourites and user queries. It cannot be cancelled!', 'feedback' => array( 'body_new_articles' => 'There are %%d new articles to read on FreshRSS.', 'request_failed' => 'A request has failed, it may have been caused by Internet connection problems.', @@ -121,6 +121,7 @@ return array( 'nl' => 'Nederlands', 'ru' => 'Русский', 'tr' => 'Türkçe', + 'zh-cn' => '简体中文' ), 'menu' => array( 'about' => 'About', @@ -173,7 +174,7 @@ return array( 'blank_to_disable' => 'Leave blank to disable', 'by_author' => 'By <em>%s</em>', 'by_default' => 'By default', - 'damn' => 'Damn!', + 'damn' => 'Blast!', 'default_category' => 'Uncategorized', 'no' => 'No', 'not_applicable' => 'Not available', diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php index eb6413e3c..a4686de4e 100644 --- a/app/i18n/en/index.php +++ b/app/i18n/en/index.php @@ -41,7 +41,7 @@ return array( 'mark_cat_read' => 'Mark category as read', 'mark_feed_read' => 'Mark feed as read', 'newer_first' => 'Newer first', - 'non-starred' => 'Show all but favorites', + 'non-starred' => 'Show all but favourites', 'normal_view' => 'Normal view', 'older_first' => 'Oldest first', 'queries' => 'User queries', @@ -49,7 +49,7 @@ return array( 'reader_view' => 'Reading view', 'rss_view' => 'RSS feed', 'search_short' => 'Search', - 'starred' => 'Show only favorites', + 'starred' => 'Show only favourites', 'stats' => 'Statistics', 'subscription' => 'Subscriptions management', 'unread' => 'Show only unread', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index 789433ee6..86600e882 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -10,10 +10,10 @@ return array( 'feed' => array( 'add' => 'Add a RSS feed', 'advanced' => 'Advanced', - 'archiving' => 'Archivage', + 'archiving' => 'Archiving', 'auth' => array( 'configuration' => 'Login', - 'help' => 'Connection allows to access HTTP protected RSS feeds', + 'help' => 'Allows access to HTTP protected RSS feeds', 'http' => 'HTTP Authentication', 'password' => 'HTTP password', 'username' => 'HTTP username', @@ -22,7 +22,7 @@ return array( 'css_path' => 'Articles CSS path on original website', 'description' => 'Description', 'empty' => 'This feed is empty. Please verify that it is still maintained.', - 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', + 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then update it.', 'in_main_stream' => 'Show in main stream', 'informations' => 'Information', 'keep_history' => 'Minimum number of articles to keep', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index 7a6d12e17..0c8188623 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -93,6 +93,7 @@ return array( 'display_categories_unfolded' => 'Afficher les catégories pliées par défaut', 'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)', 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', + 'sides_close_article' => 'Cliquer hors de la zone de texte ferme l’article', 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', 'number_divided_when_reader' => 'Divisé par 2 dans la vue de lecture.', 'read' => array( diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php index 5966fc3a7..aa19cd02b 100644 --- a/app/i18n/fr/feedback.php +++ b/app/i18n/fr/feedback.php @@ -87,7 +87,7 @@ return array( 'update' => array( 'can_apply' => 'FreshRSS va maintenant être mis à jour vers la <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 n’a pas été trouvé. [%s]', diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php index 19b62c9a7..e6ce86ef9 100644 --- a/app/i18n/it/conf.php +++ b/app/i18n/it/conf.php @@ -93,6 +93,7 @@ return array( 'display_categories_unfolded' => 'Mostra categorie aperte di predefinito', 'hide_read_feeds' => 'Nascondi categorie e feeds con articoli già letti (non funziona se “Mostra tutti gli articoli” è selezionato)', 'img_with_lazyload' => 'Usa la modalità "caricamento ritardato" per le immagini', + 'sides_close_article' => 'Clicking outside of article text area closes the article', //TODO 'jump_next' => 'Salta al successivo feed o categoria non letto', 'number_divided_when_reader' => 'Diviso 2 nella modalità di lettura.', 'read' => array( diff --git a/app/i18n/it/feedback.php b/app/i18n/it/feedback.php index 5851cb2e6..8f3cf3ed6 100644 --- a/app/i18n/it/feedback.php +++ b/app/i18n/it/feedback.php @@ -87,7 +87,7 @@ return array( 'update' => array( 'can_apply' => 'FreshRSS verrà aggiornato alla <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]', diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php index 607fb1892..fdfe6e3bc 100644 --- a/app/i18n/nl/admin.php +++ b/app/i18n/nl/admin.php @@ -5,8 +5,8 @@ return array( 'allow_anonymous' => 'Sta bezoekers toe om artikelen te lezen van de standaard gebruiker (%s)', 'allow_anonymous_refresh' => 'Sta bezoekers toe om de artikelen te vernieuwen', 'api_enabled' => 'Sta <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', ), ); diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php index 573dabf45..77f6307ed 100644 --- a/app/i18n/nl/conf.php +++ b/app/i18n/nl/conf.php @@ -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', ), ); diff --git a/app/i18n/nl/feedback.php b/app/i18n/nl/feedback.php index 386b8d415..cf1274767 100644 --- a/app/i18n/nl/feedback.php +++ b/app/i18n/nl/feedback.php @@ -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]', diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index 11e82cb4d..07d444ec3 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -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( diff --git a/app/i18n/nl/index.php b/app/i18n/nl/index.php index 751806bfa..e0184a0d0 100644 --- a/app/i18n/nl/index.php +++ b/app/i18n/nl/index.php @@ -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', diff --git a/app/i18n/nl/install.php b/app/i18n/nl/install.php index 79db9c794..419ee4c9b 100644 --- a/app/i18n/nl/install.php +++ b/app/i18n/nl/install.php @@ -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', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index a1ba3f03d..e0cebbb4e 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -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', ), ); diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php index 557fbe369..3cf0a5ea9 100644 --- a/app/i18n/ru/conf.php +++ b/app/i18n/ru/conf.php @@ -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( diff --git a/app/i18n/ru/feedback.php b/app/i18n/ru/feedback.php index e7f6b9f85..ffebd6dc9 100644 --- a/app/i18n/ru/feedback.php +++ b/app/i18n/ru/feedback.php @@ -2,108 +2,108 @@ return array( 'admin' => array( - 'optimization_complete' => 'Optimisation complete', + 'optimization_complete' => 'Optimisation complete', //TODO ), 'access' => array( - 'denied' => 'You don’t have permission to access this page', - 'not_found' => 'You are looking for a page which doesn’t exist', + 'denied' => 'You don’t have permission to access this page', //TODO + 'not_found' => 'You are looking for a page which doesn’t 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 hasn’t been set. This feature isn’t available.', + 'no_password_set' => 'Administrator password hasn’t been set. This feature isn’t 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 ), ); diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php index 2fdc248e4..9930c3637 100644 --- a/app/i18n/tr/conf.php +++ b/app/i18n/tr/conf.php @@ -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( diff --git a/app/i18n/tr/feedback.php b/app/i18n/tr/feedback.php index 87361ff51..be79630be 100644 --- a/app/i18n/tr/feedback.php +++ b/app/i18n/tr/feedback.php @@ -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]', diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php new file mode 100644 index 000000000..202114c75 --- /dev/null +++ b/app/i18n/zh-cn/admin.php @@ -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' => '用户', + ), +); diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php new file mode 100644 index 000000000..6ac0bb5e7 --- /dev/null +++ b/app/i18n/zh-cn/conf.php @@ -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' => '用户', + ), +); diff --git a/app/i18n/zh-cn/feedback.php b/app/i18n/zh-cn/feedback.php new file mode 100644 index 000000000..bee5914e9 --- /dev/null +++ b/app/i18n/zh-cn/feedback.php @@ -0,0 +1,109 @@ +<?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' => '你的帐户已修改', + ), +); diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php new file mode 100644 index 000000000..a4ef03bc9 --- /dev/null +++ b/app/i18n/zh-cn/gen.php @@ -0,0 +1,185 @@ +<?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' => '是', + ), +); diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php new file mode 100644 index 000000000..0d6e8e82d --- /dev/null +++ b/app/i18n/zh-cn/index.php @@ -0,0 +1,61 @@ +<?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' => '相关标签', + ), +); diff --git a/app/i18n/zh-cn/install.php b/app/i18n/zh-cn/install.php new file mode 100644 index 000000000..f247a20fd --- /dev/null +++ b/app/i18n/zh-cn/install.php @@ -0,0 +1,119 @@ +<?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' => '最后一步', +); diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php new file mode 100644 index 000000000..0c599e986 --- /dev/null +++ b/app/i18n/zh-cn/sub.php @@ -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 源管理', + ), +); diff --git a/app/install.php b/app/install.php index 9a88e0f37..9e474ca73 100644 --- a/app/install.php +++ b/app/install.php @@ -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')); - $stm = $c->prepare($sql); - $ok = $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']); + $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_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> diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index f6d824d55..04ee03cd6 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -149,6 +149,7 @@ <?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) { diff --git a/app/views/auth/index.phtml b/app/views/auth/index.phtml index 010eae33f..20966f24e 100644 --- a/app/views/auth/index.phtml +++ b/app/views/auth/index.phtml @@ -52,19 +52,6 @@ </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"> diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml index 07dabf15f..ebb00c97b 100644 --- a/app/views/configure/reading.phtml +++ b/app/views/configure/reading.phtml @@ -108,6 +108,16 @@ <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"> <input type="checkbox" name="reading_confirm" id="reading_confirm" value="1"<?php echo FreshRSS_Context::$user_conf->reading_confirm ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->reading_confirm; ?>"/> <?php echo _t('conf.reading.confirm_enabled'); ?> diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 059224305..2da53b679 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -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, diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 6fda11ed9..ba48b2501 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -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>' : '', diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml index a19ee322e..f2af75af0 100644 --- a/app/views/index/reader.phtml +++ b/app/views/index/reader.phtml @@ -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> diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml index 429375e44..f09c87765 100644 --- a/app/views/user/profile.phtml +++ b/app/views/user/profile.phtml @@ -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> diff --git a/cli/README.md b/cli/README.md index 0123e8d6e..1ac8c95ce 100644 --- a/cli/README.md +++ b/cli/README.md @@ -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/ +``` diff --git a/cli/_cli.php b/cli/_cli.php index f5e36eabc..1b26ea738 100644 --- a/cli/_cli.php +++ b/cli/_cli.php @@ -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'); diff --git a/cli/actualize-user.php b/cli/actualize-user.php index 29d51753a..932c6975c 100755 --- a/cli/actualize-user.php +++ b/cli/actualize-user.php @@ -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); diff --git a/data/users/_/config.default.php b/config-user.default.php index f28ef9724..40ab49570 100644 --- a/data/users/_/config.default.php +++ b/config-user.default.php @@ -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, diff --git a/data/config.default.php b/config.default.php index 748df1884..748df1884 100644 --- a/data/config.default.php +++ b/config.default.php diff --git a/constants.php b/constants.php index 7dd2238bf..ef60baea0 100644 --- a/constants.php +++ b/constants.php @@ -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'); diff --git a/data/extensions-data/.gitignore b/data/extensions-data/.gitignore new file mode 100644 index 000000000..0a00d7014 --- /dev/null +++ b/data/extensions-data/.gitignore @@ -0,0 +1 @@ +*/
\ No newline at end of file diff --git a/data/extensions-data/index.html b/data/extensions-data/index.html new file mode 100644 index 000000000..85faaa37e --- /dev/null +++ b/data/extensions-data/index.html @@ -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> diff --git a/data/users/_/.gitignore b/data/users/_/.gitignore new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/data/users/_/.gitignore diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..639517aa0 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,6 @@ +theme: jekyll-theme-cayman +title: FreshRSS +description: Documentation center + +logo: /img/FreshRSS-logo.png +show_downloads: true diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 000000000..9e30b6eae --- /dev/null +++ b/docs/_layouts/default.html @@ -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> diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss new file mode 100644 index 000000000..f7fff902b --- /dev/null +++ b/docs/assets/css/style.scss @@ -0,0 +1,13 @@ +--- +--- + +@import "{{ site.theme }}"; + +.page-header .project-name a { + color: #fff; + + &:hover { + text-decoration: none; + opacity: .7; + } +} diff --git a/docs/en/contributing.md b/docs/en/contributing.md new file mode 100644 index 000000000..7f0c3da6c --- /dev/null +++ b/docs/en/contributing.md @@ -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)! diff --git a/docs/en/developers/01_First_steps.md b/docs/en/developers/01_First_steps.md new file mode 100644 index 000000000..e35dbda12 --- /dev/null +++ b/docs/en/developers/01_First_steps.md @@ -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", +); +``` diff --git a/docs/en/developers/02_Github.md b/docs/en/developers/02_Github.md new file mode 100644 index 000000000..c16a6d040 --- /dev/null +++ b/docs/en/developers/02_Github.md @@ -0,0 +1,11 @@ +# Reporting a bug or a suggestion + +**TODO** + +# Branching + +**TODO** + +# Sending a patch + +**TODO** diff --git a/docs/en/developers/03_Backend/01_Database_schema.md b/docs/en/developers/03_Backend/01_Database_schema.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/en/developers/03_Backend/01_Database_schema.md diff --git a/docs/en/developers/03_Backend/02_Minz.md b/docs/en/developers/03_Backend/02_Minz.md new file mode 100644 index 000000000..cfbea15fe --- /dev/null +++ b/docs/en/developers/03_Backend/02_Minz.md @@ -0,0 +1,27 @@ +# Models + +**TODO** + +# Controllers and actions + +**TODO** + +# Views + +**TODO** + +# Routing + +**TODO** + +# Writing URL + +**TODO** + +# Internationalisation + +**TODO** + +# Understanding internals + +**TODO** diff --git a/docs/en/developers/03_Backend/03_External_libraries.md b/docs/en/developers/03_Backend/03_External_libraries.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/en/developers/03_Backend/03_External_libraries.md diff --git a/docs/en/developers/03_Backend/04_Changing_source_code.md b/docs/en/developers/03_Backend/04_Changing_source_code.md new file mode 100644 index 000000000..e8a5958e4 --- /dev/null +++ b/docs/en/developers/03_Backend/04_Changing_source_code.md @@ -0,0 +1,15 @@ +# Accessing the database + +**TODO** + +# Writing an action and its related view + +**TODO** + +# Authentication + +**TODO** + +# Logs + +**TODO**
\ No newline at end of file diff --git a/docs/en/developers/03_Backend/05_Extensions.md b/docs/en/developers/03_Backend/05_Extensions.md new file mode 100644 index 000000000..c5c00ff08 --- /dev/null +++ b/docs/en/developers/03_Backend/05_Extensions.md @@ -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&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&a=world&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) diff --git a/docs/en/developers/04_Frontend/01_View_files.md b/docs/en/developers/04_Frontend/01_View_files.md new file mode 100644 index 000000000..5eb284dde --- /dev/null +++ b/docs/en/developers/04_Frontend/01_View_files.md @@ -0,0 +1,15 @@ +# The .phtml files + +**TODO** + +# Writing a URL + +**TODO** + +# Displaying an icon + +**TODO** + +# Internationalisation + +**TODO** diff --git a/docs/en/developers/04_Frontend/02_Design.md b/docs/en/developers/04_Frontend/02_Design.md new file mode 100644 index 000000000..c2e622a08 --- /dev/null +++ b/docs/en/developers/04_Frontend/02_Design.md @@ -0,0 +1,11 @@ +# Template file + +**TODO** + +# Writing a new theme + +**TODO** + +# Overriding icons + +**TODO** diff --git a/docs/en/developers/05_Release_new_version.md b/docs/en/developers/05_Release_new_version.md new file mode 100644 index 000000000..e1a23c8ba --- /dev/null +++ b/docs/en/developers/05_Release_new_version.md @@ -0,0 +1 @@ +**TODO** diff --git a/docs/en/img/doc.edit.png b/docs/en/img/doc.edit.png Binary files differnew file mode 100644 index 000000000..bc850e514 --- /dev/null +++ b/docs/en/img/doc.edit.png diff --git a/doc/FreshRSS-logo.png b/docs/en/img/logo_freshrss.png Binary files differindex 763b19cb1..763b19cb1 100644 --- a/doc/FreshRSS-logo.png +++ b/docs/en/img/logo_freshrss.png diff --git a/docs/en/img/users/anonymous_access.1.png b/docs/en/img/users/anonymous_access.1.png Binary files differnew file mode 100644 index 000000000..cd4145e3e --- /dev/null +++ b/docs/en/img/users/anonymous_access.1.png diff --git a/docs/en/img/users/feed.add.1.png b/docs/en/img/users/feed.add.1.png Binary files differnew file mode 100644 index 000000000..b6146857f --- /dev/null +++ b/docs/en/img/users/feed.add.1.png diff --git a/docs/en/img/users/feed.filter.1.png b/docs/en/img/users/feed.filter.1.png Binary files differnew file mode 100644 index 000000000..e4738d1a0 --- /dev/null +++ b/docs/en/img/users/feed.filter.1.png diff --git a/docs/en/img/users/feed.filter.2.png b/docs/en/img/users/feed.filter.2.png Binary files differnew file mode 100644 index 000000000..5e8dd2899 --- /dev/null +++ b/docs/en/img/users/feed.filter.2.png diff --git a/docs/en/img/users/refresh.1.png b/docs/en/img/users/refresh.1.png Binary files differnew file mode 100644 index 000000000..a8c5f7ea0 --- /dev/null +++ b/docs/en/img/users/refresh.1.png diff --git a/docs/en/img/users/refresh.2.png b/docs/en/img/users/refresh.2.png Binary files differnew file mode 100644 index 000000000..1b97ab9ae --- /dev/null +++ b/docs/en/img/users/refresh.2.png diff --git a/docs/en/img/users/refresh.3.png b/docs/en/img/users/refresh.3.png Binary files differnew file mode 100644 index 000000000..e80bfc29f --- /dev/null +++ b/docs/en/img/users/refresh.3.png diff --git a/docs/en/img/users/refresh.4.png b/docs/en/img/users/refresh.4.png Binary files differnew file mode 100644 index 000000000..abbeb5cd4 --- /dev/null +++ b/docs/en/img/users/refresh.4.png diff --git a/docs/en/img/users/refresh.5.png b/docs/en/img/users/refresh.5.png Binary files differnew file mode 100644 index 000000000..fb3113300 --- /dev/null +++ b/docs/en/img/users/refresh.5.png diff --git a/docs/en/img/users/refresh.6.png b/docs/en/img/users/refresh.6.png Binary files differnew file mode 100644 index 000000000..0d78e3976 --- /dev/null +++ b/docs/en/img/users/refresh.6.png diff --git a/docs/en/img/users/status.filter.0.7.png b/docs/en/img/users/status.filter.0.7.png Binary files differnew file mode 100644 index 000000000..4516b937f --- /dev/null +++ b/docs/en/img/users/status.filter.0.7.png diff --git a/docs/en/img/users/status.filter.0.8.png b/docs/en/img/users/status.filter.0.8.png Binary files differnew file mode 100644 index 000000000..5a0b9a3a1 --- /dev/null +++ b/docs/en/img/users/status.filter.0.8.png diff --git a/docs/en/img/users/token.1.png b/docs/en/img/users/token.1.png Binary files differnew file mode 100644 index 000000000..73ce65cf3 --- /dev/null +++ b/docs/en/img/users/token.1.png diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..c2f12380b --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,22 @@ + + +FreshRSS is a RSS aggregator and reader. It gives you possibility to read and follow several news website at a glance without the need to go from a website to another. + +FreshRSS has a lot of features including: + +- RSS and Atom aggregation +- Mark article as favorite if you liked it or if you want to read it later +- Filter and search functionalities are working together to find easily articles +- Statistics help you to know the frequency of publishing of all the websites you are following +- Import/export of your feeds into OPML format +- Several themes created by the community +- "Google Reader"-like API to connect Android applications +- The application is "responsive" which means it adapts to small screens so you can bring articles in your pocket +- Self-hosted: code is free (under AGPL3 licence) and so you can host your own instance of FreshRSS +- Multi-users so you can host your friends and your family +- And a lot more! + +This documentation is splitted in two sections: + +- [users documentation](users/02_First_steps.md) so you can discover all the power of FreshRSS +- [developers documentation](developers/01_First_steps.md) to guide you in the source code of FreshRSS and to help you if you want to contribute diff --git a/docs/en/users/01_Installation.md b/docs/en/users/01_Installation.md new file mode 100644 index 000000000..6c979fd42 --- /dev/null +++ b/docs/en/users/01_Installation.md @@ -0,0 +1,123 @@ +# Server requirements + +FreshRSS is a web application. This means you'll need a web server to run it. FreshRSS requirements are really low, so it could run on most shared host servers. + +You need to verify that your server can run FreshRSS before installing it. If your server has the proper requirements and FreshRSS does not work, please contact us to find a solution. + +| Software | Recommended | Works also with | +| ----------- | ---------------- | ----------------------------- | +| Web server | **Apache 2** | Nginx | +| PHP | **PHP 5.3.7+** | PHP 5.2+ | +| PHP modules | Required: libxml, cURL, PDO_MySQL, PCRE and ctype. \\ Required (32-bit only): GMP \\Recommanded: JSON, Zlib, mbstring, iconv, ZipArchive | | +| Database | **MySQL 5.0.3+** | SQLite 3.7.4+ | +| Browser | **Firefox** | Chrome, Opera, Safari or IE9+ | + +## Important notice + +FreshRSS **CAN** work with PHP 5.3.3. To do so, we are using specific functions available in the ''password_compat'' library for the form authentication. This library is compatible with PHP >= 5.3.7 but some older version include the patch. +It all depends on the distribution: + +* CentOS and RHEL 6.5 are supported. +* On the other hand, **Debian with PHP 5.3.3 is not**! ([For more information](https://github.com/ircmaxell/password_compat#requirements)) + +# Getting the appropriate version of FreshRSS + +FreshRSS has three different releases or branches. Each branch has its own release frequency. So it is better if you spend some time to understand the purpose of each release. + +## Stable release + +[Download](https://github.com/FreshRSS/FreshRSS/archive/master.zip) + +This release is done when we consider that our goal concerning the new features and the stability is reached. It could happen that we make two releases in a really short time if we have a really good coding pace. In reality, we are all working on our spare time, so we release every few months. But this version is really stable, tested thoroughly and you should not face any major bugs. + +## Development release + +[Download](https://github.com/FreshRSS/FreshRSS/archive/dev.zip) + +As its name suggests, it is the working release for developers. **This release is unstable!** If you want to keep track of enhancements on a daily basis, you can use it. But keep in mind that you need to follow the branch activity on Github (via [the branch RSS feed](https://github.com/FreshRSS/FreshRSS/commits/dev.atom) for instance). Some say that the main developers use it on a daily basis without problem. They may know what they are doing… + +# Apache installation + +This is an example Apache virtual hosts configuration file. It covers http and https configuration. + +``` +<VirtualHost *:80> + ServerName example.com + DocumentRoot /path/to/FreshRSS + + ErrorLog ${APACHE_LOG_DIR}/freshrss_error.log + CustomLog ${APACHE_LOG_DIR}/freshrss_access.log combined + + RewriteEngine on + RewriteCond %{SERVER_NAME} = example.com + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent] +</VirtualHost> + +<IfModule mod_ssl.c> +<VirtualHost *:443> + ServerName example.com + DocumentRoot /path/to/FreshRSS + + ErrorLog ${APACHE_LOG_DIR}/freshrss_error.log + CustomLog ${APACHE_LOG_DIR}/freshrss_access.log combined + + SSLCertificateFile /path/to/server.crt + SSLCertificateKeyFile /path/to/server.key + + # Optional letsencrypt config (uncomment) line below + #Include /etc/letsencrypt/options-ssl-apache.conf +</VirtualHost> +</IfModule> +``` + +# Nginx installation + +This is an example nginx configuration file. It covers http, https and php-fpm configuration. + +_You can find simpler config file but they may be incompatible with FreshRSS API._ + +``` +server { + listen 80; # http on port 80 + listen 443 ssl; # https on port 443 + + # https configuration + ssl on; + ssl_certificate /etc/nginx/server.crt; + ssl_certificate_key /etc/nginx/server.key; + + # your server's url(s) + server_name example.com rss.example.com; + + # the folder p of your FreshRSS installation + root /srv/FreshRSS/p/; + + index index.php index.html index.htm; + + # nginx log files + access_log /var/log/nginx/rss.access.log; + error_log /var/log/nginx/rss.error.log; + + # php files handling + # this regex is mandatory because of the API + location ~ ^.+?\.php(/.*)?$ { + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + # By default, the variable PATH_INFO is not set under PHP-FPM + # But FreshRSS API greader.php need it. If you have a "Bad Request" error, double check this var ! + fastcgi_param PATH_INFO $fastcgi_path_info; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location / { + try_files $uri $uri/ index.php; + } +} +``` + +A step-by-step tutorial is available [in french](http://www.pihomeserver.fr/2013/05/08/raspberry-pi-home-server-installer-un-agregateur-de-flux-rss-pour-remplacer-google-reader/). + +# Security + +**TODO** diff --git a/docs/en/users/02_First_steps.md b/docs/en/users/02_First_steps.md new file mode 100644 index 000000000..96ee264a6 --- /dev/null +++ b/docs/en/users/02_First_steps.md @@ -0,0 +1,26 @@ +Learning how to handle a new application is not always easy. We build FreshRSS to be intuitive, but you will need some guidance to get your hand on it. + +This section guides you to the pages you need as a new comer. + +[After you installed the application](01_Installation.md), the first step is to add one or more feeds. You have few options: + +1. [Add a feed manually](04_Subscriptions.md#adding-a-feed) +2. [Import an OPML or JSON file](04_Subscriptions.md#import-and-export) +3. [Use the bookmarklet](04_Subscriptions.md#use-bookmarklet) +4. [Firefox integration](04_Subscriptions.md#firefox-subscription-service) + +Once you have added your feeds to FreshRSS, it is time to read them. You have access to three reading modes: + +1. [The normal view](03_Main_view.md#normal-view) which allows you to display and read quickly new articles +2. [The global view](03_Main_view.md#global-view) which allows you to see in one glance the status of your feeds +3. [The reader view](03_Main_view.md#reader-view) which allows you to have a nice reading experience. + +Now that you know the basic usages, it is time to configure FreshRSS to improve your reading experience. It has a lot of options, so play with them to find your perfect configuration. However, here is few resources to help you: + +* [Organize your feeds in categories](04_Subscriptions.md#feed-management) +* [Change the home page](05_Configuration.md#changing-the-view) +* [Choose the reading options](05_Configuration.md#reading-options) +* [Refresh feeds](03_Main_view.md#refreshing-feeds) +* [Filter articles](03_Main_view.md#filtering-articles) for a fast access to a selection +* [Search an article](03_Main_view.md#searching-articles) published some time ago +* [Access your feeds on a mobile device](06_Mobile_access.md) diff --git a/docs/en/users/03_Main_view.md b/docs/en/users/03_Main_view.md new file mode 100644 index 000000000..53b0beaf2 --- /dev/null +++ b/docs/en/users/03_Main_view.md @@ -0,0 +1,183 @@ +# Normal view + +**TODO** + +# Global view + +**TODO** + +# Reader view + +**TODO** + +# Refreshing feeds + +To use FreshRSS at its full potential, it needs to grab subscribed feeds new articles. To do so, you have several methods available. + +## Automatic update + +This is the recommended method since you can forget about it once it is configured. + +### With the actualize_script.php script + +This method is available only if you have access to the installation server scheduled tasks. + +The script is named *actualize_script.php* and is located in the *app* folder. The scheduled task syntax will not be explained here. However, here is [a quick introduction to crontab](http://www.adminschoice.com/crontab-quick-reference/) that might help you. + +Here is an example to trigger article update every hour. + +```cron +0 * * * * php /path/to/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 +``` + + +### Online cron + +If you do not have access to the installation server scheduled task, you can still automate the update process. + +To do so, you need to create a scheduled task, which need to call a specific URL: https://your.server.net/FreshRSS/p/i/?c=feed&a=actualize (it could be different depending on your installation). Depending on your application authentication method, you need to adapt the scheduled task. + +#### No authentication + +This is the most straightforward since you have a public instance; there is nothing special to configure: + +```cron +0 * * * * curl 'https://your.server.net/FreshRSS/p/i/?c=feed&a=actualize' +``` + +### Form or Persona authentication + +In those cases, if you configure the application to allow anonymous reading, you can also allow anonymous user to update feeds (“Allow anonymous refresh of the articles”). + + + +The URL used in the previous section becomes accessible and therefore, you can use the same syntax for the scheduled task. + +You can also configure an authentication token to grant a special right on the server. + + + +The scheduled task syntax to use will be the following: + +```cron +0 * * * * curl 'https://your.server.net/FreshRSS/p/i/?c=feed&a=actualize&token=my-token' +``` + + +### HTTP authentication + +In that case, the syntax in the two previous section are unusable. It means that you need to provide your credentials to the scheduled task. **Note that this method is highly discouraged since it means that your credentials will be in plain sight!** + +```cron +0 * * * * curl -u alice:password123 'https://your.server.net/FreshRSS/p/i/?c=feed&a=actualize' +``` + +## Manual update + +If you cannot or do not want to use the automatic methods, you can make it manually. There is two ways, the partial or the complete update. + +### Complete update + +This update occurs on all feeds. To trigger it, you need to click on the navigation menu update link. + + + +When the update starts, a progress bar appears and changes while feeds are processed. + + + +### Partial update + +This update occurs on the selected feed only. To trigger it, you need to click on the feed menu update link. + + + +# Filtering articles + +While the number of articles stored by FreshRSS increase, it is important to have efficient filters to display only a subset of the articles. There is several methods with different criterion. Most of the time, those methods can be combined. + +##By category + +It is the easiest method. The only thing to do is clicking on the category title in the side panel. There is two special categories on top of that panel: + + * *Main feed* which displays only articles from feeds marked as available in that category + * *Favourites* which displays only articles marked as favourites + +##By feed + +There is several methods to filter articles by feed: + + * by clicking the feed title in the side panel + * by clicking the feed title in the article details + * by filtering in the feed options from the side panel + * by filtering in the feed configuration + + + +##By status + +Each article has two attributes, which can be combined. The first attribute indicates if the article was read or not. The second attribute indicates if the article was marked as favorite or not. + +With version 0.7, attribute filters are available in the article display dropdown list. With this version, it is not possible to combine those filters. For instance, it is not possible to display only read and favourite articles. + + + +Starting with version 0.8, all attribute filters are visible as toggle icons. They can be combined. As any combination is possible, some have the same result. For instance, the result for all filters selected is the same as no filter selected. + + + +By default, this filter displays only unread articles + +##By content + +It is possible to filter articles by their content by inputting a string in the search field. + +##With the search field + +It is possible to use the search field to further refine results: + +* by author: `author:name` or `author:'composed name'` +* by title: `intitle:keyword` or `intitle:'composed keyword'` +* by URL: `inurl:keyword` or `inurl:'composed keyword'` +* by tag: `#tag` +* by free-text: `keyword` or `'composed keyword'` +* by date of discovery, using the [ISO 8601 time interval format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `date:<date-interval>` + * From a specific day, or month, or year: + * `date:2014-03-30` + * `date:2014-03` or `date:201403` + * `date:2014` + * From a specific time of a given day: + * `date:2014-05-30T13` + * `date:2014-05-30T13:30` + * Between two given dates: + * `date:2014-02/2014-04` + * `date:2014-02--2014-04` + * `date:2014-02/04` + * `date:2014-02-03/05` + * `date:2014-02-03T22:00/22:15` + * `date:2014-02-03T22:00/15` + * After a given date: + * `date:2014-03/` + * Before a given date: + * `date:/2014-03` + * For a specific duration after a given date: + * `date:2014-03/P1W` + * For a specific duration before a given date: + * `date:P1W/2014-05-25T23:59:59` + * For the past duration before now (the trailing slash is optional): + * `date:P1Y/` or `date:P1Y` (past year) + * `date:P2M/` (past two months) + * `date:P3W/` (past three weeks) + * `date:P4D/` (past four days) + * `date:PT5H/` (past five hours) + * `date:PT30M/` (past thirty minutes) + * `date:PT90S/` (past ninety seconds) + * `date:P1DT1H/` (past one day and one hour) +* by date of publication, using the same format: `pubdate:<date-interval>` + +Beware that there is no space between the operator and the value. + +Some operators can be used negatively, to exclude articles, with the same syntax as above, but prefixed by a `!` or `-`: +`-author:name`, `-intitle:keyword`, `-inurl:keyword`, `-#tag`, `!keyword`. + +It is also possible to combine operators to have a very sharp filter, and it is allowed to have multiple instances of `author:`, `intitle:`, `inurl:`, `#`, and free-text. diff --git a/docs/en/users/04_Subscriptions.md b/docs/en/users/04_Subscriptions.md new file mode 100644 index 000000000..0772be4cc --- /dev/null +++ b/docs/en/users/04_Subscriptions.md @@ -0,0 +1,33 @@ +# Adding a feed + +**TODO** + +# Import and export + +**TODO** + +# Use bookmarklet + +**TODO** + +# Feed management + +**TODO** + +# Firefox subscription service + +You can manually add your FreshRSS app to the list of Firefox subscription services which will enable you to subscribe to sites which provide a feed link using the Firefox built-in "Subscribe" button. An in-depth process is described in the [official documentation](https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox) but you can use the following steps: + + 1. Open about:config in Firefox + + 2. Search for "browser.contentHandlers.types." and note the highest number following the returned strings (ie if yo see browser.contentHandlers.types.1.something up to browser.contentHandlers.types.5.somethingelse etc. the highest number is 5). Your contentHandler will have to have a free number so just pick one higher than currently registered (you would chose six in above example). + + 3. You will have to add three new strings to your about config (replace %NUMBER% with the number from previous step and example.com with your installation address): + + | Preference name | Value | Note | + | -------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- | + | browser.contentHandlers.types.%NUMBER%.title | **FreshRSS** | Use any name you would like (ie. "My feeds") | + | browser.contentHandlers.types.%NUMBER%.type | **application/vnd.mozilla.maybe.feed** | Do not change this value! | + | browser.contentHandlers.types.%NUMBER%.uri | **http://EXAMPLE.COM/FreshRss/i?c=feed&a=add&url_rss=%s** | Replace base url with yours and switch to https (if used) | + + 4. Restart Firefox and you can subscribe to sites with the Firefox built-in "Subscribe" button. Just select the name you set under the first Preference name from the dropdown (you can also make it default with the checbox) and you will be redirected to FreshRSS subscription settings (you must be logged in). diff --git a/docs/en/users/05_Configuration.md b/docs/en/users/05_Configuration.md new file mode 100644 index 000000000..f8ac56cd7 --- /dev/null +++ b/docs/en/users/05_Configuration.md @@ -0,0 +1,121 @@ + +# Display + +## Language + +At the moment, FreshRSS is available in French and English. After you confirm your choice, the whole interface will be displayed in the chosen language. + +There are parts of FreshRSS that are not translated and are not intended to be translated. For now, the logs visible in the application as well as the one generated by the script of automatic update are part of it. + +## Theme + +In matters of taste and color, there can be no disputes. This is why FreshRSS offers six official themes: + + * *Blue Lagoon* by **Mister aiR** + * *Dark* by **AD** + * *Flat design* by **Marien Fressinaud** + * *Origine* by **Marien Fressinaud** + * *Pafat* by **Plopoyop** + * *Screwdriver* by **Mister aiR** + +If none of these are suitable for you, it is always possible to create your own. + +To select a theme, simply scroll through the themes and select a theme that appears. After confirmation, the theme will be applied to the interface. + +## Content width + +There are some who prefer short lines of text while others prefer to maximize the available screen space. To satisfy the maximum number of people it is possible to choose the width of the displayed content. There are four settings available: + + * **Fine** which displays content up to 550 pixels + * **Medium** which displays content up to 800 pixels + * **Large** which displays content up to 1000 pixels + * **No limit** which displays the content on 100% of the available space + +## Article icons + +**TODO** + +## HTML5 notification timout + +After the automatic updates of the feeds, FreshRSS uses the HTML5 notification API to notify of the arrival of new articles. + +The duration of this notification can be set. By default, the value is 0. + +# Reading + +**TODO** + +# Archiving + +**TODO** + +# Sharing + +**TODO** + +# Shortcuts + +**TODO** + +# User queries + +**TODO** + +# Users + +**TODO** + +## Authentication methods + +### HTTP Authentication (Apache) + + 1. User control is based on the .htaccess file + 2. It is best practice to place the .htaccess file in ./i/ subdirecotry so API and other third party services can work. + 3. If you want to limit all access to registered users only, place the file in the directory of FreshRSS or in a parent directory. Note that PubsubHubbub and API will not work! + 4. Example .htaccess file for a user "marie": + +``` +AuthUserFile /home/marie/repertoire/.htpasswd +AuthGroupFile /dev/null +AuthName "Chez Marie" +AuthType Basic +Require user marie +``` + +More information can be found in [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html#gettingitworking). + +# Subscription management + +## Information + +**TODO** + +## Archivage + +**TODO** + +## Login + +**TODO** + +## Advanced + +### Retrieve a truncated stream + +The question comes up regularly, so we will try to clarify here how one can retrieve a truncated RSS feed with FreshRSS. Please note that the process is absolutely not "user friendly", but it works :) + +Also know that this way you are generating much more traffic to the originating sites and that they can block you accordingly. The performance of FreshRSS is also imapcted because you have to fetch the contents of the articles one by one. So it's a feature to use sparingly! + +What is meant by "CSS path of articles on the original site" actually corresponds to the "path" consisting of IDs and classes (which in html, matches the id and class attributes) to retrieve only the interesting part that corresponds to the article. Ideally, this path starts with an id (which is unique to the page). + +#### Example 1: Rue89 + +To find this path, you must go to the address of one of the truncated articles (for example http://www.rue89.com/2013/10/15/prof-maths-jai-atteint-lextase-dihn-pedagogie-inversee-246635). You must then look for the "block" of HTML corresponding to the content of the article (in the source code!). + +We find here that the block that encompasses only the content of the article is ```<div class="content clearfix">```. We will only use the ".content" class here. Nevertheless, as said above, it is best to start the path with an id. If we go back to the parent block, this is the block ```<div id="article">``` and that's perfect! The path will be ```#article .content```. + +#### Add the corresponding classes to the articles CSS path on the feed configuration page. Examples: + +* Rue89: ```#article .content``` +* PCINpact: ```#actu_content``` +* Lesnumériques: ```article#body div.text.clearfix```
\ No newline at end of file diff --git a/docs/en/users/06_Mobile_access.md b/docs/en/users/06_Mobile_access.md new file mode 100644 index 000000000..e1a23c8ba --- /dev/null +++ b/docs/en/users/06_Mobile_access.md @@ -0,0 +1 @@ +**TODO** diff --git a/docs/en/users/07_Frequently_Asked_Questions.md b/docs/en/users/07_Frequently_Asked_Questions.md new file mode 100644 index 000000000..6391c55f5 --- /dev/null +++ b/docs/en/users/07_Frequently_Asked_Questions.md @@ -0,0 +1,35 @@ +We may not have answered all of your questions in the previous sections. The FAQ contains some questions that have not been answered elsewhere. + +## What is /i at the end of the application URL? + +Of course, ```/i``` has a purpose! We used it for performance and usability: + +* it allows to serve icons, images, styles and scripts without cookies. Whitout that trick, those files will be downloaded more often, specially when the form or the Personna authentications are used. Also, HTTP requests will be heavier. +* ```./p/``` public root can be served without any HTTP access restrictions. Whereas it could be implemented in ```./p/i/```. +* It spares from having problems while serving public resources like ```favicon.ico```, ```robots.txt```, etc. +* It allows to display the logo instead of a white page while hitting a restriction or a delay during the loading process. + +## Why robots.txt is located in a sub-folder? + +To increase security, FreshRSS is hosted in two sections. The first section is public (```./p``` folder) and the second section is private (everything else). Therefore the ```robots.txt``` file is located in ```./p``` sub-folder. + +As explained in the [security section](/en/User_documentation/Installation/Security), it is highly recommended to make only the public section available at the domain level. With that configuration, ```./p``` is the root folder for http://demo.freshrss.org/, thus making ```robots.txt``` available at the root of the application. + +The same rule applies for ```favicon.ico``` and ```.htaccess```. + +## Why do I have errors while registering a feed? + +There can be different origins for that problem. +The feed syntax can be invalid, it can be unrecognized by the SimplePie library. the hosting server can be the root of the problem, FreshRSS can be buggy. +The first step is to identify what causes the problem. +Here are the steps to follow: + +1. __Verify if the feed syntax is valid__ with the [W3C on-line tool](http://validator.w3.org/feed/ "RSS and Atom feed validator"). If it is not valid, there is nothing we can do. +1. __Verify SimplePie validation__ with the [SimplePie on-line tool](http://simplepie.org/demo/ "SimplePie official demo"). If it is not recognized, there is nothing we can do. +1. __Verify FreshRSS integration__ with the [demo](http://demo.freshrss.org "FreshRSS official demo"). If it is not working, you need to [create an issue on Github](https://github.com/FreshRSS/FreshRSS/issues/new "Create an issue for FreshRSS") so we can have a look at it. If it is working, there is probably something fishy with the hosting server. + +Here is a list of feed which don't work: + +* http://foulab.org/fr/rss/Foulab_News: is not a W3C valid feed (November 2014) +* http://eu.battle.net/hearthstone/fr/feed/news: is not a W3C valid feed (Novembre 2014) +* http://webseriesmag.blogs.liberation.fr/we/atom.xml: is not working for the user but succeed on all the described validations (November 2014)
\ No newline at end of file diff --git a/docs/fr/contributing.md b/docs/fr/contributing.md new file mode 100644 index 000000000..c7ccb4d8a --- /dev/null +++ b/docs/fr/contributing.md @@ -0,0 +1,20 @@ +## Contribuer au code + +Pour cela, vous vous trouvez au bon endroit pour commencer : la documentation est là pour vous mettre le pied à l'étrier afin de découvrir le code. Voici une sélection de pages qui vous aideront à démarrer : + +- [Les premiers pas](developers/01_First_steps.md) +- [Comment l'on fonctionne sur GitHub](developers/02_Github.md) + +S'il vous manque des informations, n'hésitez pas à fouiller un peu la documentation ou venir nous poser directement vos questions sur [la mailing list des développeurs](https://freshrss.org/mailman/listinfo/dev). + +## Contribuer à la documentation + +Il ne vous aura pas échappé que la documentation est encore un peu vide… il y a énormément de choses à faire ! Si vous souhaitez aider à écrire quelques pages, prenez le temps de lire [les informations données sur Github](https://github.com/FreshRSS/documentation/blob/master/README.fr.md). + +Vous pouvez notamment regarder [les tickets ouverts avec le tag "Documentation"](https://github.com/FreshRSS/FreshRSS/issues?labels=Documentation&state=open). Il s'agit de la liste des choses assez spécifiques à ajouter à la documentation. + +## Contribuer au blog + +Vous souhaitez écrire un article à propos des technologies RSS/Atom/PubSubHubbub ou tout simplement nous donner un coup de main à la rédaction d'un billet ? Vous pouvez nous aider ! + +Pour cela, il suffit de vous rendre sur le dépôt GitHub [FreshRSS/freshrss.org](https://github.com/FreshRSS/freshrss.org) et de nous proposer une « Pull Request ». Les articles de blog doivent se trouver dans le répertoire `./blog` et être écrits en Markdown. diff --git a/docs/fr/developers/01_First_steps.md b/docs/fr/developers/01_First_steps.md new file mode 100644 index 000000000..400523e23 --- /dev/null +++ b/docs/fr/developers/01_First_steps.md @@ -0,0 +1,202 @@ +# Configurer son environnement + +**TODO** + +## Docker + +L'image Docker contenant l'environnement de développement de FreshRSS est accessible via : + +`$ docker pull marienfressinaud/freshrss` + +Vous pouvez lire [le fichier README du dépôt dédié](https://github.com/FreshRSS/docker-freshrss). + +# Architecture du projet + +**TODO** + +# Style de codage + +Si vous désirez contribuer au code, il est important de respecter le style de codage suivant. Le code actuel ne le respecte pas entièrement mais il est de notre devoir à tous de le changer dès que l'occasion se présente. + +Aucune nouvelle contribution ne respectant pas ces règles ne sera acceptée tant que les corrections nécessaires ne sont pas appliquées. + +## Espaces, tabulations et autres caractères blancs + +### Indentation +L'indentation du code doit être faite impérativement avec des tabulations. + +### Alignement + +Une fois l'indentation faite, il peut être nécessaire de faire un alignement pour simplifier la lecture. Dans ce cas, il faut utiliser les espaces. + +```php +$resultat = une_fonction_avec_un_nom_long($param1, $param2, + $param3, $param4); +``` + +### Fin de ligne + +Le caractère de fin de ligne doit être un saut de ligne (LF) qui est le caractère de fin de ligne des systèmes *NIX. Ce caractère ne doit pas être précédé par des caractères blanc. + +Il est possible de vérifier la présence de caractères blancs en fin de ligne grâce à Git avec la commande suivante : + +```bash +# commande à lancer avant l'ajout des fichiers dans l'index +git diff --check +# commande à lancer après l'ajout des fichiers dans l'index mais avant le commit +git diff --check --cached +``` + +### Fin de fichier + +Chaque fichier doit se terminer par une ligne vide. + +### Le cas de la virgule, du point et du point-virgule + +Il n'y a pas d'espace avant ces caractères, il y en a un après. + +### Le cas des opérateurs + +Chaque opérateur est entouré d'espaces. + +```php +if ($a == 10) { + // faire quelque chose +} + +echo $a ? 1 : 0; +``` + +### Le cas des parenthèses + +Il n'y a pas d'espaces entre des parenthèses. Il n'y a pas d'espaces avant une parenthèse ouvrante sauf si elle est précédée d'un mot-clé. Il n'y a pas d'espaces après une parenthèse fermante sauf si elle est suivie d'une accolade ouvrante. + +```php +if ($a == 10) { + // faire quelque chose +} + +if ((int)$a == 10) { + // faire quelque chose +} +``` + +### Le cas des fonctions chainées + +Ce cas se présente le plus souvent en Javascript. Quand on a des fonctions chainées, des fonctions anonymes ainsi que des fonctions de rappels, il est très facile de se perdre. Dans ce cas là, on ajoute une indentation supplémentaire pour toute l'instruction et on revient au même niveau pour une instruction de même niveau. + +```javascript +// Première instruction +shortcut.add(shortcuts.mark_read, function () { + //... + }, { + 'disable_in_input': true + }); +// Deuxième instruction +shortcut.add("shift+" + shortcuts.mark_read, function () { + //... + }, { + 'disable_in_input': true + }); +``` + +## Longueur des lignes + +Les lignes ne doivent pas dépasser 80 caractères. Il est cependant autorisé exceptionnellement de dépasser cette limite s'il n'est pas possible de la respecter mais en aucun cas, les lignes ne doivent dépasser les 100 caractères. + +Dans le cas des fonctions, les paramètres peuvent être déclarés sur plusieurs lignes. + +```php +function ma_fonction($param_1, $param_2, + $param_3, $param_4) { + // faire quelque chose +} +``` + +## Nommage + +L'ensemble des éléments du code (fonctions, classes, méthodes et variables) doivent être nommés de manière à décrire leur usage de façon concise. + +### Fonctions et variables + +Les fonctions et les variables doivent suivre la convention "snake case". + +```php +// une fontion +function nom_de_la_fontion() { + // faire quelque chose +} +// une variable +$nom_de_la_variable; +``` + +### Méthodes + +Les méthodes doivent suivre la convention "lower camel case". + +```php +private function nomDeLaMethode() { + // faire quelque chose +} +``` + +### Classes + +Les classes doivent suivre la convention "upper camel case". + +```php +abstract class NomDeLaClasse {} +``` + +## Encodage + +Les fichiers doivent être encodés en UTF-8. + +## Compatibilité avec PHP 5.3 + +Il ne faut pas demander l'indice d'un tableau qui est retourné par une fonction ou une méthode. Il faut passer par une variable intermédiaire. + +```php +// code compatible avec PHP 5.3 +$ma_variable = fonction_qui_retourne_un_tableau(); +echo $ma_variable[0]; +// code incompatible avec PHP 5.3 +echo fonction_qui_retourne_un_tableau()[0]; +``` + +Il ne faut pas utiliser la déclaration raccourcie des tableaux. + +```php +// code compatible avec PHP 5.3 +$variable = array(); +// code incompatible avec PHP 5.3 +$variable = []; +``` + +## Divers + +### Opérateurs +Les opérateurs doivent être en fin de ligne dans le cas de conditions sur plusieurs lignes. + +```php +if ($a == 10 || + $a == 20) { + // faire quelque chose +} +``` + +### Fin des fichiers + +Si le fichier ne contient que du PHP, il ne doit pas comporter de balise fermante + +### Tableaux + +Lors de l'écriture de tableaux sur plusieurs lignes, tous les éléments doivent être suivis d'une virgule (même le dernier). + +```php +$variable = array( + "valeur 1", + "valeur 2", + "valeur 3", +); +``` diff --git a/docs/fr/developers/02_Github.md b/docs/fr/developers/02_Github.md new file mode 100644 index 000000000..3d4bf9639 --- /dev/null +++ b/docs/fr/developers/02_Github.md @@ -0,0 +1,77 @@ +# Remonter un problème ou une suggestion + +Malgré le soin apporté à FreshRSS, il se peut que des bugs apparaissent encore. Le projet est jeune et le développement dynamique, aussi celui-ci pourra être corrigé rapidement. Il se peut aussi que vous ayez en tête une fonctionnalité qui n'existe pas encore. Que celle-ci vous paraisse idiote, farfelue, inutile ou trop spécifique, il ne faut surtout pas hésiter à nous la proposer ! Très souvent des "idées en l'air" ont trouvé une oreille attentive. Ce sont les regards externes qui font le plus évoluer le projet. + +Si vous êtes convaincus qu'il faut vous faire entendre, voici la marche à suivre. + +## Sur GitHub + +GitHub est la plate-forme à privilégier pour vos demandes. En effet, cela nous permet de pouvoir discuter à plusieurs sur un problème ou une suggestion et de faire émerger, souvent, des idées nouvelles. Ne négligeons pas cet aspect "social" ! + + 1. [Rendez-vous sur le gestionnaire de tickets de bugs](https://github.com/FreshRSS/FreshRSS/issues) + 2. Commencez par rechercher si une demande similaire n'a pas déjà été faite. Si oui, n'hésitez pas à ajouter votre voix à la demande. + 3. Si votre demande est nouvelle, [ouvrez un nouveau ticket de bug](https://github.com/FreshRSS/FreshRSS/issues/new) + 4. Rédigez enfin votre demande. Si vous maitrisez l'anglais, c'est la langue à privilégier car cela permet d'ouvrir la discussion à un plus grand nombre de personnes. Sinon, ce n'est pas grave, continuez en français :) + 5. Merci de bien vouloir suivre les quelques conseils donnés plus bas pour faciliter la prise en compte de votre ticket. + +## De façon informelle + +Tout le monde n'aime pas ou n'utilise pas GitHub pour des raisons aussi diverses que légitimes. C'est pourquoi vous pouvez aussi nous contacter de façon plus informelle. + +* Sur [les listes de diffusion](http://freshrss.org/announce-of-the-mailing-lists.html) +* À des évènements / rencontres autour du Logiciel Libre +* Autour d'une bière dans un bar +* Etc. + +## Conseils + +Voici quelques conseils pour bien présenter votre remontée de bug ou votre suggestion : + + +* **Faites attention à l'orthographe.** même si ce n'est pas toujours facile, faites votre maximum ;) +* **Donnez un titre explicite à votre demande**, quitte à ce qu'il soit un peu long. Cela nous aide non seulement à comprendre votre demande, mais aussi à retrouver votre ticket plus tard. +* **Une demande = un ticket.** Vous pouvez avoir des tas d'idées mais vous avez peur de spammer le gestionnaire de bugs : ça ne fait rien. Il vaut mieux avoir un peu trop de tickets que trop de demandes dans un seul. On s'occupera de fermer et regrouper les demandes qui le peuvent. +* Si vous remontez un bug, pensez à nous **fournir les logs de FreshRSS** (accessibles dans les dossier ''data/log/'' de FreshRSS) **et PHP** (l'emplacement peut varier selon les distributions, mais pensez à chercher dans ''/var/log/httpd'' ou ''/var/log/apache''). +* Si vous ne trouvez pas les fichiers de logs, précisez-le dans votre ticket afin que nous sachions que vous avez déjà cherché. +* Tous les bugs ne nécessitent pas les logs, mais si vous doutez, mieux vaut nous les fournir. Les logs sont importants et très utiles pour débugguer ! +* Il se peut que les logs puissent révéler des informations plus ou moins confidentielles, **faites attention à ne rien divulguer de sensible.** + +De plus, face à un bug, je ne peux que vous encourager à suivre le format de message suivant (tiré du [site de Max & Sam](http://sametmax.com/template-de-demande-daide-en-informatique/)) : + +---- + +**Quel est mon objectif ?** + +Donnez le contexte général de ce que vous essayiez de faire. + +**Qu’est-ce que j’ai essayé de faire ?** + +Expliquez pas à pas ce que vous avez fait afin que nous puissions reproduire le bug. + +**Quels résultats ai-je obtenus ?** + +Le bug : ce que vous voyez qui n'aurez pas dû se passer. Ici vous pouvez fournir les logs. + +**Quel était le résultat attendu ?** + +Afin que nous comprenions bien où est le problème... au moins selon vous :p + +**Quelle est ma situation ?** + +Pensez à donner les informations suivantes si vous les connaissez : + + 1. Quel navigateur ? Quelle version ? + 2. Quel serveur : Apache, Nginx ? Quelle version ? + 3. Quelle version de PHP ? + 4. MySQL ou SQLite ? Quelle version ? + 5. Quelle distribution sur le serveur ? Et… quelle version ? + +---- + +# Système de branches + +**TODO** + +# Proposer un patch + +**TODO**
\ No newline at end of file diff --git a/docs/fr/developers/03_Backend/01_Database_schema.md b/docs/fr/developers/03_Backend/01_Database_schema.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/fr/developers/03_Backend/01_Database_schema.md diff --git a/docs/fr/developers/03_Backend/02_Minz.md b/docs/fr/developers/03_Backend/02_Minz.md new file mode 100644 index 000000000..7699f9390 --- /dev/null +++ b/docs/fr/developers/03_Backend/02_Minz.md @@ -0,0 +1,27 @@ +# Modèles + +**TODO** + +# Contrôleurs et actions + +**TODO** + +# Vues + +**TODO** + +# Routage + +**TODO** + +# Écriture des URL + +**TODO** + +# Internationalisation + +**TODO** + +# Comprendres les mécanismes internes + +**TODO** diff --git a/docs/fr/developers/03_Backend/03_External_libraries.md b/docs/fr/developers/03_Backend/03_External_libraries.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/docs/fr/developers/03_Backend/03_External_libraries.md diff --git a/docs/fr/developers/03_Backend/04_Changing_source_code.md b/docs/fr/developers/03_Backend/04_Changing_source_code.md new file mode 100644 index 000000000..0282dd9d2 --- /dev/null +++ b/docs/fr/developers/03_Backend/04_Changing_source_code.md @@ -0,0 +1,15 @@ +# Accès à la base de données + +**TODO** + +# Écrire une action et sa vue associée + +**TODO** + +# Gestion de l'authentification + +**TODO** + +# Gestion des logs + +**TODO** diff --git a/docs/fr/developers/03_Backend/05_Extensions.md b/docs/fr/developers/03_Backend/05_Extensions.md new file mode 100644 index 000000000..0478d77b8 --- /dev/null +++ b/docs/fr/developers/03_Backend/05_Extensions.md @@ -0,0 +1,335 @@ +# Fiche technique 0001 — Écriture d'extensions pour FreshRSS + +## Présentation de FreshRSS + +FreshRSS est un agrégateur de flux RSS / Atom écrit en PHP depuis octobre 2012. Le site officiel est situé à l'adresse [freshrss.org](http://freshrss.org) et son dépot Git est hébergé par Github : [github.com/FreshRSS/FreshRSS](https://github.com/FreshRSS/FreshRSS). + +## Problème à résoudre + +FreshRSS est limité dans ses possibilités techniques par différents facteurs : + +- La disponibilité des développeurs principaux ; +- La volonté d'intégrer certains changements ; +- Le niveau de « hack » nécessaire pour intégrer des fonctionnalités à la marge. + +Si la première limitation peut, en théorie, être levée par la participation de nouveaux contributeurs au projet, elle est en réalité conditionnée par la volonté des contributeurs à s'intéresser au code source du projet en entier. Afin de lever les deux autres limitations quant à elles, il faudra la plupart du temps passer par un « à-coté » souvent synonyme de « fork ». + +Une autre solution consiste à passer par un système d'extensions. En permettant à des utilisateurs d'écrire leur propre extension sans avoir à s'intéresser au cœur même du logiciel de base, on permet : + +1. De réduire la quantité de code source à assimiler pour un nouveau contributeur ; +2. De permettre d'intégrer des nouveautés de façon non-officielles ; +3. De se passer des développeurs principaux pour d'éventuelles améliorations sans passer par la case « fork ». + +Note : il est tout à fait imaginable que les fonctionnalités d'une extension puissent par la suite être intégrées dans le code initial de FreshRSS de façon officielle. Cela permet de proposer un « proof of concept » assez facilement. + + +## Comprendre les mécaniques de base (Minz et MVC) + +**TODO** : bouger dans 02_Minz.md + +Cette fiche technique devrait renvoyer vers la documentation officielle de FreshRSS et de Minz (le framework PHP sur lequel repose FreshRSS). Malheureusement cette documentation n'existe pas encore. Voici donc en quelques mots les principaux éléments à connaître. Il n'est pas nécessaire de lire l'ensemble des chapitres de cette section si vous n'avez pas à utiliser une fonctionnalité dans votre extension (si vous n'avez pas besoin de traduire votre extension, pas besoin d'en savoir plus sur le module `Minz_Translate` par exemple). + +### Architecture MVC + +Minz repose et impose une architecture MVC pour les projets l'utilisant. On distingue dans cette architecture trois composants principaux : + +- Le Modèle : c'est l'objet de base que l'on va manipuler. Dans FreshRSS, les catégories, les flux et les articles sont des modèles. La partie du code qui permet de les manipuler en base de données fait aussi partie du modèle mais est séparée du modèle de base : on parle de DAO (pour « Data Access Object »). Les modèles sont stockés dans un répertoire `Models`. +- La Vue : c'est ce qui représente ce que verra l'utilisateur. La vue est donc simplement du code HTML que l'on mixe avec du PHP pour afficher les informations dynamiques. Les vues sont stockées dans un répertoire `views`. +- Le Contrôleur : c'est ce qui permet de lier modèles et vues entre eux. Typiquement, un contrôleur va charger des modèles à partir de la base de données (une liste d'articles par exemple) pour les « passer » à une vue afin qu'elle les affiche. Les contrôleurs sont stockés dans un répertoire `Controllers`. + +### Le routage + +Afin de lier une URL à un contrôleur, on doit passer par une phase dite de « routage ». Dans FreshRSS, cela est particulièrement simple car il suffit d'indiquer le nom du contrôleur à charger dans l'URL à l'aide d'un paramètre `c`. Par exemple, l'adresse http://exemple.com?c=hello va exécuter le code contenu dans le contrôleur `hello`. + +Une notion qui n'a pas encore été évoquée est le système d'« actions ». Une action est exécutée *sur* un contrôleur. Concrètement, un contrôleur va être représenté par une classe et ses actions par des méthodes. Pour exécuter une action, il est nécessaire d'indiquer un paramètre `a` dans l'URL. + +Exemple de code : + +```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!'; + } +} + +?> +``` + +Si l'on charge l'adresse http://exemple.com?c=hello&a=world, l'action `world` va donc être exécutée sur le contrôleur `hello`. + +Note : si `c` ou `a` n'est pas précisée, la valeur par défaut de chacune de ces variables est `index`. Ainsi l'adresse http://exemple.com?c=hello va exécuter l'action `index` du contrôleur `hello`. + +Plus loin, sera utilisée la convention `hello/world` pour évoquer un couple contrôleur/action. + +### Gestion des vues + +Chaque vue est associée à un contrôleur et à une action. La vue associée à `hello/world` va être stockée dans un fichier bien spécifique : `views/hello/world.phtml`. Cette convention est imposée par Minz. + +Comme expliqué plus haut, les vues sont du code HTML mixé à du PHP. Exemple de code : + +```html +<p> + Phrase passée en paramètre : <?php echo $this->a_variable; ?> +</p> +``` + +La variable `$this->a_variable` a été passée précédemment par le contrôleur (voir exemple précédent). La différence est que dans le contrôleur il est nécessaire de passer par `$this->view` et que dans la vue `$this` suffit. + +### Accéder aux paramètres GET / POST + +Il est souvent nécessaire de profiter des paramètres passés par GET ou par POST. Dans Minz, ces paramètres sont accessibles de façon indistincts à l'aide de la classe `Minz_Request`. Exemple de code : + +```php +<?php + +$default_value = 'foo'; +$param = Minz_Request::param('bar', $default_value); + +// Affichera la valeur du paramètre `bar` (passé via GET ou POST) +// ou "foo" si le paramètre n'existe pas. +echo $param; + +// Force la valeur du paramètre `bar` +Minz_Request::_param('bar', 'baz'); + +// Affichera forcément "baz" puisque nous venons de forcer sa valeur. +// Notez que le second paramètre (valeur par défaut) est facultatif. +echo Minz_Request::param('bar'); + +?> +``` + +La méthode `Minz_Request::isPost()` peut être utile pour n'exécuter un morceau de code que s'il s'agit d'une requête POST. + +Note : il est préférable de n'utiliser `Minz_Request` que dans les contrôleurs. Il est probable que vous rencontriez cette méthode dans les vues de FreshRSS, voire dans les modèles, mais sachez qu'il ne s'agit **pas** d'une bonne pratique. + +### Accéder aux paramètres de session + +L'accès aux paramètres de session est étrangement similaire aux paramètres GET / POST mais passe par la classe `Minz_Session` cette fois-ci ! Il n'y a pas d'exemple ici car vous pouvez reprendre le précédent en changeant tous les `Minz_Request` par des `Minz_Session`. + +### Gestion des URL + +Pour profiter pleinement du système de routage de Minz, il est fortement déconseillé d'écrire les URL en dur dans votre code. Par exemple, la vue suivante doit être évitée : + +```html +<p> + Accéder à la page <a href="http://exemple.com?c=hello&a=world">Hello world</a>! +</p> +``` + +Si un jour il est décidé d'utiliser un système d'« url rewriting » pour avoir des adresses au format http://exemple.com/controller/action, toutes les adresses précédentes deviendraient ineffectives ! + +Préférez donc l'utilisation de la classe `Minz_Url` et de sa méthode `display()`. `Minz_Url::display()` prend en paramètre un tableau de la forme suivante : + +```php +<?php + +$url_array = array( + 'c' => 'hello', + 'a' => 'world', + 'params' => array( + 'foo' => 'bar', + ) +); + +// Affichera quelque chose comme .?c=hello&a=world&foo=bar +echo Minz_Url::display($url_array); + +?> +``` + +Comme cela peut devenir un peu pénible à utiliser à la longue, surtout dans les vues, il est préférable d'utiliser le raccourci `_url()` : + +```php +<?php + +// Affichera la même chose que précédemment +echo _url('hello', 'world', 'foo', 'bar'); + +?> +``` + +Note : en règle générale, la forme raccourcie (`_url()`) doit être utilisée dans les vues tandis que la forme longue (`Minz_Url::display()`) doit être utilisée dans les contrôleurs. + +### Redirections + +Il est souvent nécessaire de rediriger un utilisateur vers une autre page. Pour cela, la classe `Minz_Request` dispose d'une autre méthode utile : `forward()`. Cette méthode prend en argument le même format d'URL que celui vu juste avant. + +Exemple de code : + +```php +<?php + +$url_array = array( + 'c' => 'hello', + 'a' => 'world' +); + +// Indique à Minz de rediriger l'utilisateur vers la page hello/world. +// Notez qu'il s'agit d'une redirection au sens Minz du terme, pas d'une redirection que le navigateur va avoir à gérer (code HTTP 301 ou 302) +// Le code qui suit forward() va ainsi être exécuté ! +Minz_Request::forward($url_array); + +// Pour effectuer une redirection type 302, ajoutez "true". +// Le code qui suivra ne sera alors jamais exécuté. +Minz_Request::forward($url_array, true); + +?> +``` + +Il est très fréquent de vouloir effectuer une redirection tout en affichant un message à l'utilisateur pour lui indiquer comment s'est déroulée l'action effectuée juste avant (validation d'un formulaire par exemple). Un tel message est passé par une variable de session `notification` (note : nous parlerons plutôt de « feedback » désormais pour éviter la confusion avec une notification qui peut survenir à tout moment). Pour faciliter ce genre d'action très fréquente, il existe deux raccourcis qui effectuent tout deux une redirection type 302 en affectant un message de feedback : + +```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); + +// ou + +Minz_Request::bad($feedback_bad, $url_array); + +?> +``` + +### Gestion de la traduction + +Il est fréquent (et c'est un euphémisme) de vouloir afficher des phrases à l'utilisateur. Dans l'exemple précédent par exemple, nous affichions un feedback à l'utilisateur en fonction du résultat d'une validation de formulaire. Le problème est que FreshRSS possède des utilisateurs de différentes nationalités. Il est donc nécessaire de pouvoir gérer différentes langues pour ne pas rester cantonné à l'Anglais ou au Français. + +La solution consiste à utiliser la classe `Minz_Translate` qui permet de traduire dynamiquement FreshRSS (ou toute application basée sur Minz). Avant d'utiliser ce module, il est nécessaire de savoir où trouver les chaînes de caractères à traduire. Chaque langue possède son propre sous-répertoire dans un répertoire parent nommé `i18n`. Par exemple, les fichiers de langue en Français sont situés dans `i18n/fr/`. Il existe sept fichiers différents : + +- `admin.php` pour tout ce qui est relatif à l'administration de FreshRSS ; +- `conf.php` pour l'aspect configuration ; +- `feedback.php` contient les traductions des messages de feedback ; +- `gen.php` stocke ce qui est global à FreshRSS (gen pour « general ») ; +- `index.php` pour la page principale qui liste les flux et la page « À propos » ; +- `install.php` contient les phrases relatives à l'installation de FreshRSS ; +- `sub.php` pour l'aspect gestion des abonnements (sub pour « subscription »). + +Cette organisation permet de ne pas avoir un unique énorme fichier de traduction. + +Les fichiers de traduction sont assez simples : il s'agit seulement de retourner un tableau PHP contenant les traductions. Extrait du fichier `app/i18n/fr/gen.php` : + +```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', + ), +); + +?> +``` + +Pour accéder à ces traductions, `Minz_Translate` va nous aider à l'aide de sa méthode `Minz_Translate::t()`. Comme cela peut être un peu long à taper, il a été introduit un raccourci qui **doit** être utilisé en toutes circonstances : `_t()`. Exemple de code : + +```html +<p> + <a href="<?php echo _url('index', 'index'); ?>"> + <?php echo _t('gen.action.back_to_rss_feeds'); ?> + </a> +</p> +``` + +La chaîne à passer à la fonction `_t()` consiste en une série d'identifiants séparés par des points. Le premier identifiant indique de quel fichier on veut extraire la traduction (dans notre cas présent, de `gen.php`), tandis que les suivantes indiquent des entrées de tableaux. Ainsi `action` est une entrée du tableau principal et `back_to_rss_feeds` est une entrée du tableau `action`. Cela permet d'organiser encore un peu plus nos fichiers de traduction. + +Il existe un petit cas particulier qui permet parfois de se simplifier la vie : le cas de l'identifiant `_`. Celui-ci doit nécessairement être présent en bout de chaîne et permet de donner une valeur à l'identifiant de niveau supérieur. C'est assez dur à expliquer mais très simple à comprendre. Dans l'exemple donné plus haut, un `_` est associé à la valeur `FreshRSS` : cela signifie qu'il n'y a pas besoin d'écrire `_t('gen.freshrss._')` mais `_t('gen.freshrss')` suffit. + +### Gestion de la configuration + +## Écrire une extension pour FreshRSS + +Nous y voilà ! Nous avons abordé les fonctionnalités les plus utiles de Minz et qui permettent de faire tourner FreshRSS correctement et il est plus que temps d'aborder les extensions en elles-même. + +Une extension permet donc d'ajouter des fonctionnalités facilement à FreshRSS sans avoir à toucher au cœur du projet directement. + +### Les fichiers et répertoires de base + +La première chose à noter est que **toutes** les extensions **doivent** se situer dans le répertoire `extensions`, à la base de l'arborescence de FreshRSS. Une extension est un répertoire contenant un ensemble de fichiers et sous-répertoires obligatoires ou facultatifs. La convention veut que l'on précède le nom du répertoire principal par un « x » pour indiquer qu'il ne s'agit pas d'une extension incluse par défaut dans FreshRSS. + +Le répertoire principal d'une extension doit comporter au moins deux fichiers **obligatoire** : + +- Un fichier `metadata.json` qui contient une description de l'extension. Ce fichier est écrit en JSON ; +- Un fichier `extension.php` contenant le point d'entrée de l'extension. + +Il est possible aussi que vous ayez besoin de fichiers ou sous-répertoires additionnels selon vos besoins : + +- `configure.phtml` est le fichier contenant le formulaire permettant de paramétrer votre extension ; +- Un répertoire `static/` contenant fichiers CSS et JavaScript dont vous aurez besoin pour votre extension. Notez que si vous devez écrire beaucoup de CSS il est peut-être plus intéressant d'écrire un thème complet (mais ce n'est pas le sujet de cette fiche technique) ; +- Un répertoire `controllers` contenant des contrôleurs additionnels ; +- Un répertoire `i18n` contenant des traductions supplémentaires ; +- Des répertoires `layout` et `views` permettant de définir de nouvelles vues ou d'écraser les vues actuelles. + +De plus, il est de bon ton d'avoir un fichier `LICENSE` indiquant la licence sous laquelle est distribuée votre extension et un fichier `README` donnant une description détaillée de celle-ci. + +### Écrire le fichier metadata.json + +Le fichier `metadata.json` définit votre extension à travers un certain nombre d'éléments importants. Il doit contenir un tableau JSON valide contenant les entrées suivantes : + +- `name` : le nom de votre extension ; +- `author` : votre nom, éventuellement votre adresse mail mais il n'y a pas de format spécifique à adopter ; +- `description` : une description de votre extension ; +- `version` : le numéro de version actuel de l'extension ; +- `entrypoint` : indique le point d'entrée de votre extension. Il doit correspondre au nom de la classe contenue dans le fichier `extension.php` sans le suffixe `Extension` (donc si le point d'entrée est `HelloWorld`, votre classe s'appellera `HelloWorldExtension`) ; +- `type` : définit le type de votre extension. Il existe deux types : `system` et `user`. Nous étudierons cette différence juste après. + +Seuls les champs `name` et `entrypoint` sont requis. + +### Choisir entre extension « system » ou « user » + +### Écrire le fichier extension.php + +Ce fichier est le point d'entrée de votre extension. Il doit contenir une classe bien spécifique pour fonctionner. Comme évoqué plus haut, le nom de la classe doit être votre `entrypoint` suffixé par `Extension` (`HelloWorldExtension` par exemple). De plus, cette classe doit héritée de la classe `Minz_Extension` pour bénéficier des méthodes propres aux extensions. + +Votre classe va bénéficier de quatre méthodes à redéfinir : + +- `install()` est appelée lorsqu'un utilisateur va cliquer sur le bouton pour activer votre extension. Elle permet par exemple de mettre à jour la base de données d'un utilisateur afin de la rendre compatible avec l'extension. Elle retourne `true` si tout s'est bien passé ou, dans le cas contraire, une chaîne de caractères expliquant le problème ; +- `uninstall()` est appelée lorsqu'un utilisateur va cliquer sur le bouton pour désactiver votre extension. Ainsi, vous pourrez annuler les changements en base de données que vous avez potentiellement faits dans `install()`. Elle retourne `true` si tout s'est bien passé ou, dans le cas contraire, une chaîne de caractères expliquant le problème ; +- `init()` est appelée à chaque chargement de page *si l'extension est activée*. Elle va donc initialiser le comportement de l'extension. C'est la méthode la plus importante ; +- `handleConfigureAction()` est appelée lorsqu'un utilisateur charge le panneau de gestion de l'extension. Plus précisément, elle est appelée lorsque l'URL `?c=extension&a=configure&e=le-nom-de-votre-extension` est chargée. Vous devriez aussi écrire ici le comportement voulu lors de la validation du formulaire contenu dans votre fichier `configure.phtml`. + +De plus, vous disposerez d'un certain nombre de méthodes directement héritées de `Minz_Extension` que vous ne devriez pas redéfinir : + +- Les « getters » tout d'abord. La plupart sont suffisamment explicites pour ne pas les détailler : `getName()`, `getEntrypoint()`, `getPath()` (permet de récupérer le chemin vers votre extension), `getAuthor()`, `getDescription()`, `getVersion()`, `getType()` ; +- `getFileUrl($filename, $type)` va vous retourner l'URL vers un fichier du répertoire `static`. Le premier paramètre est le nom du fichier (sans `static/`), le deuxième est le type de fichier à servir (`css` ou `js`) ; +- `registerController($base_name)` va indiquer à Minz de prendre en compte le contrôleur donné dans le système de routage. Le contrôleur doit se situer dans votre répertoire `Controllers`, le nom du fichier doit être `<base_name>Controller.php` et le nom de la classe `FreshExtension_<base_name>_Controller`. + +TODO : + +- `registerViews()` +- `registerTranslates()` +- `registerHook($hook_name, $hook_function)` + +### Système de « hooks » + +TODO : + +- `entry_before_display` (`function($entry) -> Entry | null`) +- `entry_before_insert` (`function($entry) -> Entry | null`) +- `feed_before_insert` (`function($feed) -> Feed | null`) +- `post_update` (`function(none) -> none`) + +### Écrire le fichier configure.phtml + +TODO diff --git a/docs/fr/developers/04_Frontend/01_View_files.md b/docs/fr/developers/04_Frontend/01_View_files.md new file mode 100644 index 000000000..45174bf58 --- /dev/null +++ b/docs/fr/developers/04_Frontend/01_View_files.md @@ -0,0 +1,15 @@ +# Les fichiers .phtml + +**TODO** + +# Écrire une URL + +**TODO** + +# Afficher une icône + +**TODO** + +# Internationalisation + +**TODO** diff --git a/docs/fr/developers/04_Frontend/02_Design.md b/docs/fr/developers/04_Frontend/02_Design.md new file mode 100644 index 000000000..d05a4c44c --- /dev/null +++ b/docs/fr/developers/04_Frontend/02_Design.md @@ -0,0 +1,11 @@ +# Fichier modèle + +**TODO** + +# Écrire un nouveau thème + +**TODO** + +# Surcharger les icônes + +**TODO** diff --git a/docs/fr/developers/05_Release_new_version.md b/docs/fr/developers/05_Release_new_version.md new file mode 100644 index 000000000..731dc0c76 --- /dev/null +++ b/docs/fr/developers/05_Release_new_version.md @@ -0,0 +1,112 @@ +# Préparer la sortie + +Afin d'avoir le plus de retour possible avant une sortie, il est préférable de l'annoncer sur GitHub en créant un ticket dédié ([voir les exemples](https://github.com/FreshRSS/FreshRSS/search?utf8=%E2%9C%93&q=Call+for+testing&type=Issues)). Ceci est à faire **au moins une semaine à l'avance**. + +Il est aussi recommandé de faire l'annonce sur mailing@freshrss.org. + +# S'assurer de l'état de dev + +Avant de sortir une nouvelle version de FreshRSS, il faut vous assurer que le code est stable et ne présente pas de bugs majeurs. Idéalement, il faudrait que nos tests soient automatisés et exécutés avant toute publication. + +Il faut aussi **vous assurer que le fichier CHANGELOG est à jour** dans la branche de dev avec les mises à jour de la ou les version(s) à sortir. + +# Processus Git + +```bash +$ git checkout master +$ git pull +$ git merge --ff dev +$ vim constants.php +# Mettre à jour le numéro de version x.y.z de FRESHRSS_VERSION +$ git commit -a +Version x.y.z +$ git tag -a x.y.z +Version x.y.z +$ git push && git push --tags +``` + +# Mise à jour de update.freshrss.org + +Il est important de mettre à jour update.freshrss.org puisqu'il s'agit du service par défaut gérant les mises à jour automatiques de FreshRSS. + +Le dépot gérant le code se trouve sur GitHub : [FreshRSS/update.freshrss.org](https://github.com/FreshRSS/update.freshrss.org/). + +## Écriture du script de mise à jour + +Les scripts se trouvent dans le répertoire `./scripts/` et doivent être de la forme `update_to_x.y.z.php`. On trouve aussi dans ce répertoire `update_to_dev.php` destiné aux mises à jour de la branche de dev (ce script ne doit pas inclure de code spécifique à une version particulière !) et `update_util.php` contenant une liste de fonctions utiles à tous les scripts. + +Afin d'écrire un nouveau script, il est préférable de copier / coller celui de la dernière version ou de partir de `update_to_dev.php`. La première chose à faire est de définir l'URL à partir de laquelle sera téléchargée le package FreshRSS (`PACKAGE_URL`). L'URL est de la forme `https://codeload.github.com/FreshRSS/FreshRSS/zip/x.y.z`. + +Il existe ensuite 5 fonctions à remplir : + +- `apply_update()` qui se charge de sauvegarder le répertoire contenant les données, de vérifier sa structure, de télécharger le package FreshRSS, de le déployer et de tout nettoyer. Cette fonction est pré-remplie mais des ajustements peuvent être faits si besoin est (ex. réorganisation de la structure de `./data`). Elle retourne `true` si aucun problème n'est survenu ou une chaîne de caractères indiquant un soucis ; +- `need_info_update()` retourne `true` si l'utilisateur doit intervenir durant la mise à jour ou `false` sinon ; +- `ask_info_update()` affiche un formulaire à l'utilisateur si `need_info_update()` a retourné `true` ; +- `save_info_update()` est chargée de sauvegarder les informations renseignées par l'utilisateur (issues du formulaire de `ask_info_update()`) ; +- `do_post_update()` est exécutée à la fin de la mise à jour et prend en compte le code de la nouvelle version (ex. si la nouvelle version modifie l'objet `Minz_Configuration`, vous bénéficierez de ces améliorations). + +## Mise à jour du fichier de versions + +Lorsque le script a été écrit et versionné, il est nécessaire de mettre à jour le fichier `./versions.php` qui contient une table de correspondances indiquant quelles versions sont mises à jour vers quelles autres versions. + +Voici un exemple de fichier `versions.php` : + +```php +<?php + +return array( + // STABLE + '0.8.0' => '1.0.0', + '0.8.1' => '1.0.0', + '1.0.0' => '1.0.1', // doesn't exist (yet) + // DEV + '1.1.2-dev' => 'dev', + '1.1.3-dev' => 'dev', + '1.1.4-dev' => 'dev', +); +``` + +Et voici comment fonctionne cette table : + +- à gauche se trouve la version N, à droite la version N+1 ; +- les versions `x.y.z-dev` sont **toutes** mises à jour vers `dev` ; +- les versions stables sont mises à jour vers des versions stables ; +- il est possible de sauter plusieurs versions d'un coup à condition que les scripts de mise à jour le prennent en charge ; +- il est conseillé d'indiquer la correspondance de la version courante vers sa potentielle future version en précisant que cette version n'existe pas encore. Tant que le script correspondant n'existera pas, rien ne se passera. + +Il est **très fortement** indiqué de garder ce fichier rangé selon les numéros de versions en séparant les versions stables et de dev. + +## Déploiement + +Avant de mettre à jour update.freshrss.org, il est préférable de tester avec dev.update.freshrss.org qui correspond à la pré-production. Mettez donc à jour dev.update.freshrss.org et changez l'URL `FRESHRSS_UPDATE_WEBSITE` de votre instance FreshRSS. Lancez la mise à jour et vérifiez que celle-ci se déroule correctement. + +Lorsque vous serez satisfait, mettez à jour update.freshrss.org avec le nouveau script et en testant de nouveau puis passez à la suite. + +# Mise à jour des services FreshRSS + +Deux services sont à mettre à jour immédiatement après la mise à jour de update.freshrss.org : + +- rss.freshrss.org ; +- demo.freshrss.org (identifiants publics : `demo` / `demodemo`). + +# Annoncer publiquement la sortie + +Lorsque tout fonctionne, il est temps d'annoncer la sortie au monde entier ! + +- sur GitHub en créant [une nouvelle release](https://github.com/FreshRSS/FreshRSS/releases/new) ; +- sur le blog de freshrss.org au minimum pour les versions stables (écrire l'article sur [FreshRSS/freshrss.org](https://github.com/FreshRSS/freshrss.org)). +- sur Twitter (compte [@FreshRSS](https://twitter.com/FreshRSS)) ; +- et sur mailing@freshrss.org ; + +# Lancer la prochaine version de développement + +```bash +$ git checkout dev +$ vim constants.php +# Mettre à jour le numéro de version de FRESHRSS_VERSION +$ vim CHANGELOG.md +# Préparer la section pour la prochaine version +$ git add CHANGELOG.md && git commit && git push +``` + +Pensez aussi à mettre à jour update.freshrss.org pour qu'il prenne en compte la version de développement actuelle. diff --git a/docs/fr/img/doc.edit.png b/docs/fr/img/doc.edit.png Binary files differnew file mode 100644 index 000000000..bc850e514 --- /dev/null +++ b/docs/fr/img/doc.edit.png diff --git a/docs/fr/img/logo_freshrss.png b/docs/fr/img/logo_freshrss.png Binary files differnew file mode 100644 index 000000000..763b19cb1 --- /dev/null +++ b/docs/fr/img/logo_freshrss.png diff --git a/docs/fr/img/users/anonymous_access.1.png b/docs/fr/img/users/anonymous_access.1.png Binary files differnew file mode 100644 index 000000000..65f985705 --- /dev/null +++ b/docs/fr/img/users/anonymous_access.1.png diff --git a/docs/fr/img/users/feed.add.1.png b/docs/fr/img/users/feed.add.1.png Binary files differnew file mode 100644 index 000000000..b6146857f --- /dev/null +++ b/docs/fr/img/users/feed.add.1.png diff --git a/docs/fr/img/users/feed.filter.1.png b/docs/fr/img/users/feed.filter.1.png Binary files differnew file mode 100644 index 000000000..519ae41bf --- /dev/null +++ b/docs/fr/img/users/feed.filter.1.png diff --git a/docs/fr/img/users/feed.filter.2.png b/docs/fr/img/users/feed.filter.2.png Binary files differnew file mode 100644 index 000000000..5e8dd2899 --- /dev/null +++ b/docs/fr/img/users/feed.filter.2.png diff --git a/docs/fr/img/users/refresh.1.png b/docs/fr/img/users/refresh.1.png Binary files differnew file mode 100644 index 000000000..44b7cb156 --- /dev/null +++ b/docs/fr/img/users/refresh.1.png diff --git a/docs/fr/img/users/refresh.2.png b/docs/fr/img/users/refresh.2.png Binary files differnew file mode 100644 index 000000000..78e577704 --- /dev/null +++ b/docs/fr/img/users/refresh.2.png diff --git a/docs/fr/img/users/refresh.3.png b/docs/fr/img/users/refresh.3.png Binary files differnew file mode 100644 index 000000000..e80bfc29f --- /dev/null +++ b/docs/fr/img/users/refresh.3.png diff --git a/docs/fr/img/users/refresh.4.png b/docs/fr/img/users/refresh.4.png Binary files differnew file mode 100644 index 000000000..abbeb5cd4 --- /dev/null +++ b/docs/fr/img/users/refresh.4.png diff --git a/docs/fr/img/users/refresh.5.png b/docs/fr/img/users/refresh.5.png Binary files differnew file mode 100644 index 000000000..13c885cb1 --- /dev/null +++ b/docs/fr/img/users/refresh.5.png diff --git a/docs/fr/img/users/refresh.6.png b/docs/fr/img/users/refresh.6.png Binary files differnew file mode 100644 index 000000000..0d78e3976 --- /dev/null +++ b/docs/fr/img/users/refresh.6.png diff --git a/docs/fr/img/users/status.filter.0.7.png b/docs/fr/img/users/status.filter.0.7.png Binary files differnew file mode 100644 index 000000000..4516b937f --- /dev/null +++ b/docs/fr/img/users/status.filter.0.7.png diff --git a/docs/fr/img/users/status.filter.0.8.png b/docs/fr/img/users/status.filter.0.8.png Binary files differnew file mode 100644 index 000000000..5a0b9a3a1 --- /dev/null +++ b/docs/fr/img/users/status.filter.0.8.png diff --git a/docs/fr/img/users/token.1.png b/docs/fr/img/users/token.1.png Binary files differnew file mode 100644 index 000000000..9685e5b43 --- /dev/null +++ b/docs/fr/img/users/token.1.png diff --git a/docs/fr/index.md b/docs/fr/index.md new file mode 100644 index 000000000..d43eaec4e --- /dev/null +++ b/docs/fr/index.md @@ -0,0 +1,22 @@ + + +FreshRSS est un agrégateur et lecteur de flux RSS. Il permet de regrouper l'actualité de plusieurs sites différents dans un endroit unique pour que vous puissiez la lire sans devoir aller de site en site. + +FreshRSS a été conçu comme un agrégateur puissant et propose des tas de fonctionnalités : + +- Agrégation des flux RSS et Atom. +- Utilisez les favoris pour marquer les articles qui vous ont plu ou que vous souhaitez lire plus tard. +- Le système de filtrage et de recherche permettent de cibler exactement les articles que vous souhaitez lire. +- Les statistiques permettent de savoir en un coup d'œil quels sont les sites qui publient le plus, ou à l'inverse, le moins. +- Importation / exportation des flux au format OPML. +- Multi-thèmes pour changer l'habillage de FreshRSS. +- « *Responsive design* » : l'application s'adapte aux petits écrans pour emporter FreshRSS dans votre poche. +- Multi-utilisateurs pour héberger plusieurs personnes sur une même installation. +- API Google Reader pour pouvoir y brancher des applications Android. +- Auto-hébergeable : le code source est libre (AGPL3) et vous pouvez donc l'héberger sur votre propre serveur. +- Et bien d'autres ! + +Cette documentation est partagée en deux parties : + +- La [documentation utilisateurs](users/02_First_steps.md) pour découvrir plus en profondeur les fonctionnalités de FreshRSS. +- La [documentation développeurs](developers/01_First_steps.md) pour savoir comment contribuer et mieux comprendre le code source de FreshRSS. diff --git a/docs/fr/users/01_Installation.md b/docs/fr/users/01_Installation.md new file mode 100644 index 000000000..86daa636a --- /dev/null +++ b/docs/fr/users/01_Installation.md @@ -0,0 +1,94 @@ +# Les pré-requis sur le serveur + +FreshRSS est un logiciel développé en PHP reposant sur le modèle client - serveur. C'est-à-dire qu'il vous faudra un serveur web pour en profiter. Ensuite, FreshRSS ne demande pas une configuration très fournie et peut donc, en théorie, tourner sur la plupart des serveurs mutualisés. + +Il est toutefois de votre responsabilité de vérifier que votre hébergement permettra de faire tourner FreshRSS avant de nous taper dessus. Dans le cas où les informations listées ci-dessous ne seraient pas à jour, vous pourrez. + + | Logiciel | Recommandé | Fonctionne aussi avec | + | -------- | ----------- | --------------------- | + | Serveur web | **Apache 2** | Nginx | + | PHP | **PHP 5.3.7+** | PHP 5.2+ | + | Modules PHP | Requis : libxml, cURL, PDO_MySQL, PCRE et ctype \\ Requis (32 bits seulement) : GMP \\ Recommandé : JSON, Zlib, mbstring et iconv, ZipArchive | | + | Base de données | **MySQL 5.0.3+** | SQLite 3.7.4+ | + | Navigateur | **Firefox** | Chrome, Opera, Safari or IE 9+ | + +## Note importante + +FreshRSS **PEUT** fonctionner sur la version de PHP 5.3.3. En effet, nous utilisons des fonctions spécifiques pour la connexion par formulaire et notamment la bibliothèque ''password_compat''. Celle-ci est compatible avec PHP >= 5.3.7 ou certaines versions plus anciennes incluant un patch spécifique. Cela dépend de la distribution : + +* CentOS et la Red Hat Enterprise Linux 6.5 sont supportés. +* En revanche, **Debian avec PHP 5.3.3 n'est pas supporté !** ([Plus d'informations](https://github.com/ircmaxell/password_compat#requirements)) + +# Choisir la bonne version de FreshRSS + +FreshRSS possède trois versions différentes (nous parlons de branches) qui sortent à des fréquences plus ou moins rapides. Aussi prenez le temps de comprendre à quoi correspond chacune de ces versions. + +## La version stable + +[Téléchargement](https://github.com/FreshRSS/FreshRSS/archive/master.zip) + +Cette version sort lorsqu'on considère qu'on a répondu à nos objectifs en terme de nouvelles fonctionnalités. Deux versions peuvent ainsi sortir de façon très rapprochée si les développeurs travaillent bien. En pratique, comme nous nous fixons de nombreux objectifs et que nous travaillons sur notre temps libre, les versions sont souvent assez espacées (plusieurs mois). Son avantage est que le code est particulièrement stable et vous ne devriez pas faire face à de méchants bugs. + +## La version de développement + +[Téléchargement](https://github.com/FreshRSS/FreshRSS/archive/dev.zip) + +Comme son nom l'indique, il s'agit de la version sur laquelle les développeurs travaillent. **Elle est donc totalement instable !** Si vous souhaitez recevoir les améliorations au jour le jour, vous pouvez l'utiliser, mais attention à bien suivre les évolutions sur Github (via [le flux RSS de la branche](https://github.com/FreshRSS/FreshRSS/commits/dev.atom) par exemple). On raconte que les développeurs principaux l'utilisent quotidiennement sans avoir de soucis. Sans doute savent-ils ce qu'ils font… + +# Installation sur Apache + +**TODO** + +Cette partie n'a pas encore été écrite. Néanmoins, comme il s'agit d'une bête application PHP, cela ne pose généralement pas de soucis à installer :) + +# Installation sur Nginx + +Voici un fichier de configuration pour nginx. Il couvre la configuration pour http, https et php. + +_Vous pourrez trouver d'autres fichiers de configuration plus simples mais ces derniers ne seront peut-être pas compatibles avec l'API FreshRSS._ + +``` +server { + listen 80; # http sur le port 80 + listen 443 ssl; # https sur le port 443 + + # configuration https + ssl on; + ssl_certificate /etc/nginx/server.crt; + ssl_certificate_key /etc/nginx/server.key; + + # l'url ou les urls de votre serveur + server_name example.com rss.example.com; + + # le répertoire où se trouve le dossier p de FreshRSS + root /srv/FreshRSS/p/; + + index index.php index.html index.htm; + + # les fichiers de log nginx + access_log /var/log/nginx/rss.access.log; + error_log /var/log/nginx/rss.error.log; + + # gestion des fichiers php + # il est nécessaire d'utiliser cette expression régulière pour le bon fonctionnement de l'API + location ~ ^.+?\.php(/.*)?$ { + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + # Par défaut la variable PATH_INFO n'est pas définie sous PHP-FPM + # or l'API FreshRSS greader.php en a besoin. Si vous avez un "Bad Request", vérifiez bien cette dernière ! + fastcgi_param PATH_INFO $fastcgi_path_info; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location / { + try_files $uri $uri/ index.php; + } +} +``` + +Pour un tutoriel pas à pas, vous pouvez suivre [cet article dédié](http://www.pihomeserver.fr/2013/05/08/raspberry-pi-home-server-installer-un-agregateur-de-flux-rss-pour-remplacer-google-reader/). + +# Conseils de sécurité + +**TODO** diff --git a/docs/fr/users/02_First_steps.md b/docs/fr/users/02_First_steps.md new file mode 100644 index 000000000..589453f28 --- /dev/null +++ b/docs/fr/users/02_First_steps.md @@ -0,0 +1,25 @@ +Découvrir un nouveau logiciel n'est pas toujours facile. Si nous avons voulu FreshRSS le plus intuitif possible, vous aurez peut-être besoin d'un coup de main pour le maîtriser. + +Cette section se propose de vous aider dans la prise en main de l'outil. Il ne s'agit que de liens menant vers les autres pages de la documentation mais ordonnées dans un ordre spécifique aux nouveaux arrivants. + +[Après l'installation](01_Installation.md), la première chose à faire est d'ajouter un ou plusieurs sites à suivre. Pour cela plusieurs choix s'offrent à vous : + + 1. [Ajouter un flux manuellement](04_Subscriptions.md#ajouter-un-flux) + 2. [Importer un fichier OPML ou JSON](04_Subscriptions.md#import-et-export) + 3. [Utiliser le bookmark dédié](04_Subscriptions.md#utiliser-le-bookmark) + +Une fois que vous avez ajouté vos flux à FreshRSS, il est temps de les lire. Pour cela, trois modes de lecture s'offrent à vous : + + 1. [La vue normale](03_Main_view.md#la-vue-normale) qui permet de voir et de lire rapidement les nouveaux articles + 2. [La vue globale](03_Main_view.md#la-vue-globale) est destinée à vous offrir un panorama de l'état de vos flux + 3. [La vue lecture](03_Main_view.md#la-vue-lecture) est pensée pour vous offrir un meilleur confort de lecture + +Bien, vous maitrisez maintenant la vue que vous préférez ? Il est temps de vous offrir un peu plus de confort de lecture. FreshRSS est grandement configurable et c'est à vous de trouver la configuration qui vous conviendra le plus. Voici tout de même quelques pistes pour améliorer votre quotidien sur FreshRSS : + +* [Rangez vos flux dans des catégories](04_Subscriptions.md#organisation_des_flux) +* [Configurez votre page d'accueil](05_Configuration.md#personnaliser-la-vue) +* [Configurez vos options de lecture](05_Configuration.md#options-de-lecture) +* [Mettez à jour vos flux](03_Main_view.md#rafraichir-les-flux) +* [Filtrez les articles](03_Main_view.md#filtrer-les-articles) pour accéder rapidement à ceux que vous voulez lire en priorité +* [Retrouvez un article](03_Main_view.md#rechercher-des-articles) qui a été publié il y a quelques jours ou mois +* [Accédez à vos flux même sur mobile](06_Mobile_access.md) diff --git a/docs/fr/users/03_Main_view.md b/docs/fr/users/03_Main_view.md new file mode 100644 index 000000000..744141b7d --- /dev/null +++ b/docs/fr/users/03_Main_view.md @@ -0,0 +1,183 @@ +# La vue normale + +**TODO** + +# La vue globale + +**TODO** + +# La vue lecture + +**TODO** + +# Rafraîchir les flux + +Pour profiter pleinement de FreshRSS, il faut qu’il récupère les nouveaux articles disponibles des flux auxquels vous avez souscrit. Pour cela, il existe plusieurs méthodes. + +## Mise à jour automatique + +C’est la méthode recommandée car il n’y a pas besoin d’y penser, elle se fait toute seule, à la fréquence que vous avez choisi. + +### Par le script actualize_script.php + +Cette méthode n’est possible que si vous avez accès aux tâches planifiées de la machine sur laquelle est installée votre instance de FreshRSS. + +Le script qui permet de mettre à jour les articles s’appelle *actualize_script.php* et se trouve dans le répertoire *app* de votre instance de FreshRSS. La syntaxe des tâches planifiées ne sera pas expliqué ici, cependant voici [une introduction rapide à crontab](http://www.adminschoice.com/crontab-quick-reference/) qui peut vous aider. + +Ci-dessous vous trouverez un exemple permettant la mise à jour des articles toutes les heures. + +```cron +0 * * * * php /chemin/vers/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 +``` + + +### Cron en-ligne + +Il se peut que vous n’ayez pas accès aux tâches planifiées du serveur hébergeant votre instance de FreshRSS. Il reste une possibilité pour mettre les flux à jour automatiquement. + +Pour cela vous devez paramétrer une tâche cron qui devra charger régulièrement une url spécifique : https://votre.serveur.net/FreshRSS/p/i/?c=feed&a=actualize (à adapter selon votre installation). Différents cas de figure peuvent se présenter à vous désormais. + +##### Aucune authentification + +C’est le cas le plus simple, puisque votre instance est publique, vous n’avez rien de particulier à préciser : + +```cron +0 * * * * curl 'https://votre.serveur.net/FreshRSS/p/i/?c=feed&a=actualize' +``` + +##### Authentification par formulaire ou Persona + +Dans ces cas-là, si vous avez autorisé la lecture anonyme des articles, vous pouvez aussi permettre à n’importe qui de rafraîchir vos flux (« Autoriser le rafraîchissement anonyme des flux »). + + + +L’url précédente devient donc accessible à n’importe qui et vous pouvez utiliser la tâche cron de la partie précédente. + +Vous pouvez aussi configurer un jeton d’authentification pour accorder un droit spécial sur votre serveur. + + + +La tâche cron à utiliser sera de la forme suivante : + +```cron +0 * * * * curl 'https://votre.serveur.net/FreshRSS/p/i/?c=feed&a=actualize&token=mon-token' +``` + + +##### Authentification HTTP + +Dans ce cas-là, le token et les permissions “anonymes” sont inutilisables et il vous sera nécessaire d’indiquer vos identifiants dans la tâche cron. **Notez que cette solution est grandement déconseillée puisqu’elle implique que vos identifiants seront visibles en clair !** + +```cron +0 * * * * curl -u alice:password123 'https://votre.serveur.net/FreshRSS/p/i/?c=feed&a=actualize' +``` + +## Mise à jour manuelle + +Si vous ne pouvez pas ou ne voulez pas utiliser la méthode automatique, vous pouvez le faire de façon manuelle. Il existe deux méthodes qui permettent de mettre à jour tout ou partie des flux. + +### Mise à jour complète + +Cette mise à jour se fait pour l’ensemble des flux de l’instance. Pour initier cette mise à jour, il suffit de cliquer sur le lien de mise à jour disponible dans le menu de navigation. + + + +Lorsque la mise à jour démarre, une barre de progression apparait et s’actualise au fur et à mesure de la récupération des articles. + + + +### Mise à jour partielle + +Cette mise à jour se fait pour le flux sélectionné uniquement. Pour initier cette mise à jour, il suffit de cliquer sur le lien de mise à jour disponible dans le menu du flux. + + + +# Filtrer les articles + +Avec le nombre croissant d’articles stockés par FreshRSS, il devient important d’avoir des filtres efficaces pour n’afficher qu’une partie des articles. Il existe plusieurs méthodes qui filtrent selon des critères différents. Ces méthodes peuvent être combinées dans la plus part des cas. + +##Par catégorie + +C’est la méthode la plus simple. Il suffit de cliquer sur le titre d’une catégorie dans le panneau latéral. Il existe deux catégories spéciales qui sont placées en haut dudit panneau : + + * *Flux principal* qui affiche uniquement les articles des flux marqués comme visible dans cette catégorie + * *Favoris* qui affiche uniquement les articles, tous flux confondus, marqués comme favoris + +##Par flux + +Il existe plusieurs méthodes pour filtrer les articles par flux : + + * en cliquant sur le titre du flux dans le panneau latéral + * en cliquant sur le titre du flux dans le détail de l’article + * en filtrant dans les options du flux dans le panneau latéral + * en filtrant dans la configuration du flux + + + +##Par statut + +Chaque article possède deux attributs qui peuvent être combinés. Le premier attribut indique si l’article a été lu ou non. Le second attribut indique si l’article a été noté comme favori ou non. + +Dans la version 0.7.x, les filtres sur les attributs sont accessibles depuis la liste déroulante qui gère l’affichage des articles. Dans cette version, il n’est pas possible de combiner les filtres. Par exemple, on ne peut pas afficher les articles lus qui ont été notés comme favori. + + + +À partir de la version 0.8, les filtres sur les attributs sont directement accessibles. Il est maintenant possible de les combiner. Comme il est possible de faire toutes les combinaisons, il y en a certaines qui retournent le même résultat. Par exemple, si les quatre filtres sont activés ou désactivés, le résultat sera le même. + + + +Par défaut, le filtre n’affiche que les articles qui n’ont pas été lus. + +##Par contenu + +Il est possible de filtrer les articles par leur contenu en entrant une chaine de caractères dans le champ de recherche prévu à cet effet. + +##Grâce au champ de recherche + +Il est possible d’utiliser le champ de recherche pour raffiner les résultats : + +* par auteur : `author:nom` or `author:'nom composé'` +* par titre : `intitle:mot` or `intitle:'mot composé'` +* par URL: `inurl:mot` or `inurl:'mot composé'` +* par tag: `#tag` +* par texte libre : `mot` or `'mot composé'` +* par date de découverte, en utilisant le [format d’intervalle de dates ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601#Les_dur.C3.A9es_et_intervalles) : `date:<intervalle>` + * D’un jour, mois, ou année : + * `date:2014-03-30` + * `date:2014-03` or `date:201403` + * `date:2014` + * D’une heure précise d’un jour donné : + * `date:2014-05-30T13` + * `date:2014-05-30T13:30` + * Entre deux dates : + * `date:2014-02/2014-04` + * `date:2014-02--2014-04` + * `date:2014-02/04` + * `date:2014-02-03/05` + * `date:2014-02-03T22:00/22:15` + * `date:2014-02-03T22:00/15` + * Après une date donnée : + * `date:2014-03/` + * Avant une date donnée : + * `date:/2014-03` + * Pour une certaine durée après une date donnée : + * `date:2014-03/P1W` + * Pour une certaine durée avant une date donnée : + * `date:P1W/2014-05-25T23:59:59` + * Pour une certaine durée avant l’instant présent (la barre oblique est optionnelle) : + * `date:P1Y/` ou `date:P1Y` (depuis un an) + * `date:P2M/` (depuis deux mois) + * `date:P3W/` (depuis trois semaines) + * `date:P4D/` (depuis quatre jours) + * `date:PT5H/` (depuis cinq heures) + * `date:PT30M/` (depuis trente minutes) + * `date:PT90S/` (depuis 90 seconds) + * `date:P1DT1H/` (depuis un jour et une heure) +* par date de publication, en utilisant le même format : `pubdate:<intervalle>` + +Attention à ne pas introduire d’espace entre l’opérateur et la valeur recherchée. + +Certains opérateurs peuvent être utilisé négativement, pour exclure des articles, avec la même syntaxe que ci-dessus, mais préfixé par `!` ou `-` : +`-author:nom`, `-intitle:mot`, `-inurl:mot`, `-#tag`, `!mot`. + +Il est également possible de combiner les mots-clefs pour faire un filtrage encore plus précis, and et il est autorisé d’avoir plusieurs instances de : `author:`, `intitle:`, `inurl:`, `#`, et texte libre. diff --git a/docs/fr/users/04_Subscriptions.md b/docs/fr/users/04_Subscriptions.md new file mode 100644 index 000000000..ae4541c29 --- /dev/null +++ b/docs/fr/users/04_Subscriptions.md @@ -0,0 +1,15 @@ +# Ajouter un flux + +**TODO** + +# Import et export + +**TODO** + +# Utiliser le « bookmark » + +**TODO** + +# Organisation des flux + +**TODO** diff --git a/docs/fr/users/05_Configuration.md b/docs/fr/users/05_Configuration.md new file mode 100644 index 000000000..75deff462 --- /dev/null +++ b/docs/fr/users/05_Configuration.md @@ -0,0 +1,119 @@ +# Personnaliser la vue + +##Langue +À l'heure actuelle, FreshRSS est disponible en français et en anglais. Après validation de ce choix, la totalité de l'interface sera affichée dans la langue choisie. + +Il y a des parties de FreshRSS qui ne sont pas traduites et qui n'ont pas vocation à l'être. Pour le moment, les logs visibles dans l'application ainsi que celle générées par le script de mise à jour automatique en font partie. + +##Thème +Les goûts et les couleurs, ça ne se discute pas. C'est pourquoi FreshRSS propose six thèmes officiels : + + * *Blue Lagoon* par **Mister aiR** + * *Dark* par **AD** + * *Flat design* par **Marien Fressinaud** + * *Origine* par **Marien Fressinaud** + * *Pafat* par **Plopoyop** + * *Screwdriver* par **Mister aiR** + +Si aucun de ceux proposés ne convient, il est toujours possible de créer son propre thème. + +Pour sélectionner un thème, il suffit de faire défiler les thèmes jusqu'à l'apparition du thème choisi. Après validation, le thème sera appliqué à l'interface. + +##Largeur du contenu +Il y en a qui préfère des lignes de texte courtes, d'autres qui préfèrent maximiser l'espace disponible sur l'écran. Pour satisfaire le maximum de personne, il est possible de choisir la largeur du contenu affiché. Il y a quatre réglages disponibles : + + * **Fine** qui affiche le contenu jusqu'à 550 pixels + * **Moyenne** qui affiche le contenu jusqu'à 800 pixels + * **Large** qui affiche le contenu jusqu'à 1000 pixels + * **Pas de limite** qui affiche le contenu sur 100% de la place disponible + +##Icônes d'article + +**TODO** + +##Temps d'affichage de la notification HTML5 +Après la mise à jour automatique des flux, FreshRSS utilise l'API de notification de HTML5 pour avertir de l'arrivée de nouveaux articles. + +Il est possible de régler la durée d'affichage de cette notification. Par défaut, la valeur est 0. + +# Options de lecture + +**TODO** + +# Archivage + +**TODO** + +# Partage + +**TODO** + +# Raccourcis + +**TODO** + +# Filtres + +**TODO** + +# Utilisateurs + +**TODO** + +## Méthodes d'authentification + +**Brouillon** + +### Authentification HTTP + + 1. Ne laisse rien de visible + 2. Pour Apache, basé sur un fichier .htaccess + - Exemple de .htaccess pour un utilisateur "marie" à placer dans le répertoire de FreshRSS ou dans un répertoire parent : + +``` +AuthUserFile /home/marie/repertoire/.htpasswd +AuthGroupFile /dev/null +AuthName "Chez Marie" +AuthType Basic +Require user marie +``` + +Plus d'informations dans [la documentation d'Apache.](http://httpd.apache.org/docs/trunk/howto/auth.html#gettingitworking) + + +# Gestion des flux + +## Informations + +**TODO** + +## Archivage + +**TODO** + +## Identification + +**TODO** + +## Avancé + +### Récupérer un flux tronqué + +La question revient régulièrement, je vais essayer de clarifier ici comment on peut récupérer un flux RSS tronqué avec FreshRSS. Sachez avant tout que la manière de s'y prendre n'est absolument pas "user friendly", mais elle fonctionne :) + +Sachez aussi que par cette manière vous générez beaucoup plus de trafic vers les sites d'origines et qu'ils peuvent vous bloquer par conséquent. Les performances de FreshRSS sont aussi moins bonnes car vous devez alors aller chercher le contenu des articles un par un. C'est donc une fonctionnalité à utiliser avec parcimonie ! + +Ce que j'entends par "Chemin CSS des articles sur le site d’origine" correspond en fait au "chemin" constitué par les IDs et les classes (en html, correspond aux attributs id et class) pour récupérer uniquement la partie intéressante qui correspond à l'article. L'idéal est que ce chemin commence par un id (qui est unique pour la page) + +#### Exemple 1 : Rue89 + +Pour trouver ce chemin, il faut se rendre à l'adresse d'un des articles tronqués (par exemple http://www.rue89.com/2013/10/15/prof-maths-jai-atteint-lextase-dihn-pedagogie-inversee-246635). Il faut alors chercher le "bloc" HTML correspondant au contenu de l'article (dans le code source !) + +On trouve ici que le bloc qui englobe uniquement le contenu de l'article est ```<div class="content clearfix">```. On ne va garder que la classe .content ici. Néanmoins, comme je le disais plus haut, il est préférable de commencer le chemin avec un id. Si on remonte au bloc parent, il s'agit du bloc ```<div id="article">``` et c'est parfait ! Le chemin sera donc ```#article .content``` + +#### Liste de correspondances site -> chemin css + +* Rue89 : ```#article .content``` +* PCINpact : ```#actu_content``` +* Lesnumériques : ```article#body div.text.clearfix``` + diff --git a/docs/fr/users/06_Mobile_access.md b/docs/fr/users/06_Mobile_access.md new file mode 100644 index 000000000..e1a23c8ba --- /dev/null +++ b/docs/fr/users/06_Mobile_access.md @@ -0,0 +1 @@ +**TODO** diff --git a/docs/fr/users/07_Frequently_Asked_Questions.md b/docs/fr/users/07_Frequently_Asked_Questions.md new file mode 100644 index 000000000..9dc80b2e4 --- /dev/null +++ b/docs/fr/users/07_Frequently_Asked_Questions.md @@ -0,0 +1,35 @@ +Il est possible que nous n'ayons pas répondu à toutes vos questions dans les parties précédentes. La FAQ regroupe certaines interrogations qui n'ont pas trouvé leur réponse ailleurs. + +## C'est quoi ce /i à la fin de l'URL ? + +Bien entendu, le ```/i``` n'est pas là pour faire joli ! Il s'agit d'une question de performances et de praticité : + +* Cela permet de servir les icônes, images, styles, scripts sans cookie. Sans cela, ces fichiers seraient souvent re-téléchargés, en particulier lorsque Persona ou le formulaire de connexion sont utilisés. De plus, les requêtes vers ces ressources seraient plus lourdes. +* La racine publique ```./p/``` peut être servie sans restriction d'accès HTTP (qui peut avantageusement être mise en place dans ```./p/i/```). +* Cela permet d'éviter des problèmes pour des fichiers qui doivent être publics pour bien fonctionner, comme ```favicon.ico```, ```robots.txt```, etc. +* Cela permet aussi d'avoir un logo FreshRSS plutôt qu'une page blanche pour accueillir l'utilisateur par exemple dans le cas de la restriction d'accès HTTP ou lors de l'attente du chargement plus lourd du reste de l'interface. + +## Pourquoi le ```robots.txt``` se trouve dans un sous-répertoire ? + +Afin d'améliorer la sécurité, FreshRSS est découpé en deux parties : une partie publique (le répertoire ```./p```) et une partie privée (tout le reste !). Le ```robots.txt``` se trouve donc dans le sous-répertoire ```./p```. + +Comme expliqué dans les [conseils de sécurité](01_Installation.md#conseils-de-securite), il est recommandé de faire pointer un nom de domaine vers ce sous-répertoire afin que seule la partie publique ne soit accessible par un navigateur web. De cette manière http://demo.freshrss.org/ pointe vers le répertoire ```./p``` et le ```robots.txt``` se trouve bien à la racine du site : http://demo.freshrss.org/robots.txt. + +L'explication est la même pour les fichiers ```favicon.ico``` et ```.htaccess```. + +## Pourquoi j'ai des erreurs quand j'essaye d'enregistrer un flux ? + +Il peut y avoir différentes origines à ce problème. +Le flux peut avoir une syntaxe invalide, il peut ne pas être reconnu par la bibliothèque SimplePie, l'hébergement peut avoir des problèmes, FreshRSS peut être boggué. +Il faut dans un premier temps déterminer la cause du problème. +Voici la liste des étapes à suivre pour la déterminer : + +1. __Vérifier la validité du flux__ grâce à l'[outil en ligne du W3C](http://validator.w3.org/feed/ "Validateur en ligne de flux RSS et Atom"). Si ça ne fonctionne pas, nous ne pouvons rien faire. +1. __Vérifier la reconnaissance par SimplePie__ grâce à l'[outil en ligne de SimplePie](http://simplepie.org/demo/ "Démo officielle de SimplePie"). Si ça ne fonctionne pas, nous ne pouvons rien faire. +1. __Vérifier l'intégration dans FreshRSS__ grâce à la [démo](http://demo.freshrss.org "Démo officielle de FreshRSS"). Si ça ne fonctionne pas, il faut [créer un ticket sur Github](https://github.com/FreshRSS/FreshRSS/issues/new "Créer un ticket pour FreshRSS") pour que l'on puisse regarder ce qui se passe. Si ça fonctionne, il y a probablement un problème avec l'hébergement. + +Voici une liste des flux qui ne fonctionnent pas : + +* http://foulab.org/fr/rss/Foulab_News : ne passe pas la validation W3C (novembre 2014) +* http://eu.battle.net/hearthstone/fr/feed/news : ne passe pas la validation W3C (novembre 2014) +* http://webseriesmag.blogs.liberation.fr/we/atom.xml : ne fonctionne pas chez l'utilisateur mais passe l'ensemble des validations ci-dessus (novembre 2014) diff --git a/docs/fr/users/08_PubSubHubbub.md b/docs/fr/users/08_PubSubHubbub.md new file mode 100644 index 000000000..0f2f14794 --- /dev/null +++ b/docs/fr/users/08_PubSubHubbub.md @@ -0,0 +1,15 @@ +# Qu'est-ce que PubSubHubbub ? + +Derrière ce nom barbare se cache un protocole qui vient compléter Atom et RSS. En effet, le fonctionnement de base de ces deux derniers implique de vérifier à intervalles réguliers s'il existe de nouveaux articles sur les sites suivis. Cela même si le site concerné n'a rien publié depuis la dernière synchronisation. Le protocole PubSubHubbub permet d'éviter des synchronisations inutiles en notifiant en temps réel l'agrégateur de la présence de nouveaux articles. + +# Fonctionnement de PubSubHubbub + +On va retrouver trois notions dans PubSubHubbub : les éditeurs (les sites qui publient du contenu), les abonnés (les agrégateurs de flux RSS) et les hubs. + +Lorsqu'un agrégateur s'abonne à un site et récupère son flux RSS, il peut y trouver l'adresse d'un hub. Si c'est le cas — car un site peut ne pas en préciser —, l'agrégateur va s'abonner au hub et non pas à l'éditeur directement. Ainsi, lorsqu'un éditeur va publier du contenu, il va notifier le hub qui va lui-même notifier et envoyer le contenu à tous ses abonnés. + +Pour pouvoir être notifié, les abonnés doivent fournir une adresse accessible publiquement sur Internet. + +# PubSubHubbub et FreshRSS + +Depuis la version 1.1.2-beta, FreshRSS supporte officiellement PubSubHubbub. Vous pouvez donc recevoir en temps réel les articles des sites qui affichent dans leur flux RSS un « hub ». diff --git a/docs/img/FreshRSS-logo.png b/docs/img/FreshRSS-logo.png Binary files differnew file mode 100644 index 000000000..763b19cb1 --- /dev/null +++ b/docs/img/FreshRSS-logo.png diff --git a/doc/FreshRSS-screenshot.png b/docs/img/FreshRSS-screenshot.png Binary files differindex 2bcd6e6bf..2bcd6e6bf 100644 --- a/doc/FreshRSS-screenshot.png +++ b/docs/img/FreshRSS-screenshot.png diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..9afdcec38 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +# Welcome on FreshRSS documentation center! + +This documentation is under construction. If you want to contribute, [find us on GitHub](https://github.com/FreshRSS/FreshRSS). + +- [English documentation](./en/index.md) +- [Documentation française](./fr/index.md) diff --git a/data/force-https.default.txt b/force-https.default.txt index 044620098..044620098 100644 --- a/data/force-https.default.txt +++ b/force-https.default.txt diff --git a/lib/Favicon/DataAccess.php b/lib/Favicon/DataAccess.php deleted file mode 100644 index 4c1a29541..000000000 --- a/lib/Favicon/DataAccess.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -namespace Favicon; - -/** - * DataAccess is a wrapper used to read/write data locally or remotly - * Aside from SOLID principles, this wrapper is also useful to mock remote resources in unit tests - * Note: remote access warning are silenced because we don't care if a website is unreachable - **/ -class DataAccess { - public function retrieveUrl($url) { - $this->set_context(); - return @file_get_contents($url); - } - - public function retrieveHeader($url) { - $this->set_context(); - $headers = @get_headers($url, 1); - return is_array($headers) ? array_change_key_case($headers) : array(); - } - - public function saveCache($file, $data) { - file_put_contents($file, $data); - } - - public function readCache($file) { - return file_get_contents($file); - } - - private function set_context() { - stream_context_set_default( - array( - 'http' => array( - 'method' => 'GET', - 'timeout' => 10, - 'header' => "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:20.0; Favicon; +https://github.com/ArthurHoaro/favicon) Gecko/20100101 Firefox/32.0\r\n", - ) - ) - ); - } -} diff --git a/lib/Favicon/Favicon.php b/lib/Favicon/Favicon.php deleted file mode 100644 index 8571a1b95..000000000 --- a/lib/Favicon/Favicon.php +++ /dev/null @@ -1,296 +0,0 @@ -<?php - -namespace Favicon; - -class Favicon -{ - protected $url = ''; - protected $cacheDir; - protected $cacheTimeout; - protected $dataAccess; - - public function __construct($args = array()) - { - if (isset($args['url'])) { - $this->url = $args['url']; - } - - $this->cacheDir = __DIR__ . '/../../resources/cache'; - $this->dataAccess = new DataAccess(); - } - - public function cache($args = array()) { - if (isset($args['dir'])) { - $this->cacheDir = $args['dir']; - } - - if (!empty($args['timeout'])) { - $this->cacheTimeout = $args['timeout']; - } else { - $this->cacheTimeout = 0; - } - } - - public static function baseUrl($url, $path = false) - { - $return = ''; - - if (!$url = parse_url($url)) { - return FALSE; - } - - // Scheme - $scheme = isset($url['scheme']) ? strtolower($url['scheme']) : null; - if ($scheme != 'http' && $scheme != 'https') { - - return FALSE; - } - $return .= "{$scheme}://"; - - // Username and password - if (isset($url['user'])) { - $return .= $url['user']; - if (isset($url['pass'])) { - $return .= ":{$url['pass']}"; - } - $return .= '@'; - } - - // Hostname - if( !isset($url['host']) ) { - return FALSE; - } - - $return .= $url['host']; - - // Port - if (isset($url['port'])) { - $return .= ":{$url['port']}"; - } - - // Path - if( $path && isset($url['path']) ) { - $return .= $url['path']; - } - $return .= '/'; - - return $return; - } - - public function info($url) - { - if(empty($url) || $url === false) { - return false; - } - - $max_loop = 5; - - // Discover real status by following redirects. - $loop = TRUE; - while ($loop && $max_loop-- > 0) { - $headers = $this->dataAccess->retrieveHeader($url); - if (empty($headers)) { - return false; - } - $exploded = explode(' ', $headers[0]); - - if( !isset($exploded[1]) ) { - return false; - } - list(,$status) = $exploded; - - switch ($status) { - case '301': - case '302': - $url = isset($headers['location']) ? $headers['location'] : ''; - break; - default: - $loop = FALSE; - break; - } - } - - return array('status' => $status, 'url' => $url); - } - - public function endRedirect($url) { - $out = $this->info($url); - return !empty($out['url']) ? $out['url'] : false; - } - - /** - * Find remote (or cached) favicon - * @return favicon URL, false if nothing was found - **/ - public function get($url = '') - { - // URLs passed to this method take precedence. - if (!empty($url)) { - $this->url = $url; - } - - // Get the base URL without the path for clearer concatenations. - $original = rtrim($this->baseUrl($this->url, true), '/'); - $url = rtrim($this->endRedirect($this->baseUrl($this->url, false)), '/'); - - if(($favicon = $this->checkCache($url)) || ($favicon = $this->getFavicon($url))) { - $base = true; - } - elseif(($favicon = $this->checkCache($original)) || ($favicon = $this->getFavicon($original, false))) { - $base = false; - } - else - return false; - - // Save cache if necessary - $cache = $this->cacheDir . '/' . md5($base ? $url : $original); - if ($this->cacheTimeout && !file_exists($cache) || (is_writable($cache) && time() - filemtime($cache) > $this->cacheTimeout)) { - $this->dataAccess->saveCache($cache, $favicon); - } - - return $favicon; - } - - private function getFavicon($url, $checkDefault = true) { - $favicon = false; - - if(empty($url)) { - return false; - } - - // Try /favicon.ico first. - if( $checkDefault ) { - $info = $this->info("{$url}/favicon.ico"); - if ($info['status'] == '200') { - $favicon = $info['url']; - } - } - - // See if it's specified in a link tag in domain url. - if (!$favicon) { - $favicon = $this->getInPage($url); - } - - // Make sure the favicon is an absolute URL. - if( $favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false ) { - $favicon = $url . '/' . $favicon; - } - - // Sometimes people lie, so check the status. - // And sometimes, it's not even an image. Sneaky bastards! - // If cacheDir isn't writable, that's not our problem - if ($favicon && is_writable($this->cacheDir) && !$this->checkImageMType($favicon)) { - $favicon = false; - } - - return $favicon; - } - - private function getInPage($url) { - $html = $this->dataAccess->retrieveUrl("{$url}/"); - preg_match('!<head.*?>.*</head>!ims', $html, $match); - - if(empty($match) || count($match) == 0) { - return false; - } - - $head = $match[0]; - - $dom = new \DOMDocument(); - // Use error supression, because the HTML might be too malformed. - if (@$dom->loadHTML($head)) { - $links = $dom->getElementsByTagName('link'); - foreach ($links as $link) { - if ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'shortcut icon') { - return $link->getAttribute('href'); - } elseif ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'icon') { - return $link->getAttribute('href'); - } elseif ($link->hasAttribute('href') && strpos($link->getAttribute('href'), 'favicon') !== FALSE) { - return $link->getAttribute('href'); - } - } - } - return false; - } - - private function checkCache($url) { - if ($this->cacheTimeout) { - $cache = $this->cacheDir . '/' . md5($url); - if (file_exists($cache) && is_readable($cache) && (time() - filemtime($cache) < $this->cacheTimeout)) { - return $this->dataAccess->readCache($cache); - } - } - return false; - } - - private function checkImageMType($url) { - $tmpFile = $this->cacheDir . '/tmp.ico'; - - $fileContent = $this->dataAccess->retrieveUrl($url); - $this->dataAccess->saveCache($tmpFile, $fileContent); - - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $isImage = strpos(finfo_file($finfo, $tmpFile), 'image') !== false; - finfo_close($finfo); - - unlink($tmpFile); - - return $isImage; - } - - /** - * @return mixed - */ - public function getCacheDir() - { - return $this->cacheDir; - } - - /** - * @param mixed $cacheDir - */ - public function setCacheDir($cacheDir) - { - $this->cacheDir = $cacheDir; - } - - /** - * @return mixed - */ - public function getCacheTimeout() - { - return $this->cacheTimeout; - } - - /** - * @param mixed $cacheTimeout - */ - public function setCacheTimeout($cacheTimeout) - { - $this->cacheTimeout = $cacheTimeout; - } - - /** - * @return string - */ - public function getUrl() - { - return $this->url; - } - - /** - * @param string $url - */ - public function setUrl($url) - { - $this->url = $url; - } - - /** - * @param DataAccess $dataAccess - */ - public function setDataAccess($dataAccess) - { - $this->dataAccess = $dataAccess; - } -} diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index f9eff3db6..952d983c9 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -33,7 +33,7 @@ class Minz_FrontController { try { Minz_Configuration::register('system', DATA_PATH . '/config.php', - DATA_PATH . '/config.default.php'); + FRESHRSS_PATH . '/config.default.php'); $this->setReporting(); Minz_Request::init(); diff --git a/lib/SimplePie/SimplePie.php b/lib/SimplePie/SimplePie.php index 0f2fdbb87..5cd445b6d 100644 --- a/lib/SimplePie/SimplePie.php +++ b/lib/SimplePie/SimplePie.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2017, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,8 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev-FreshRSS - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @version 1.5 + * @copyright 2004-2017 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -50,7 +50,7 @@ define('SIMPLEPIE_NAME', 'SimplePie'); /** * SimplePie Version */ -define('SIMPLEPIE_VERSION', '1.4-dev-FreshRSS'); +define('SIMPLEPIE_VERSION', '1.5'); /** * SimplePie Build @@ -510,6 +510,14 @@ class SimplePie public $cache = true; /** + * @var bool Force SimplePie to fallback to expired cache, if enabled, + * when feed is unavailable. + * @see SimplePie::force_cache_fallback() + * @access private + */ + public $force_cache_fallback = false; + + /** * @var int Cache duration (in seconds) * @see SimplePie::set_cache_duration() * @access private @@ -615,6 +623,12 @@ class SimplePie public $item_limit = 0; /** + * @var bool Stores if last-modified and/or etag headers were sent with the + * request when checking a feed. + */ + public $check_modified = false; + + /** * @var array Stores the default attributes to be stripped by strip_attributes(). * @see SimplePie::strip_attributes() * @access private @@ -626,7 +640,7 @@ class SimplePie * @see SimplePie::add_attributes() * @access private */ - public $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); //FreshRSS + public $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); /** * @var array Stores the default tags to be stripped by strip_htmltags(). @@ -636,6 +650,12 @@ class SimplePie public $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); /** + * @var bool Should we throw exceptions, or use the old-style error property? + * @access private + */ + public $enable_exceptions = false; + + /** * Use syslog to report HTTP requests done by SimplePie. * @see SimplePie::set_syslog() */ @@ -657,9 +677,9 @@ class SimplePie */ public function __construct() { - if (version_compare(PHP_VERSION, '5.2', '<')) + if (version_compare(PHP_VERSION, '5.3', '<')) { - trigger_error('PHP 4.x, 5.0 and 5.1 are no longer supported. Please upgrade to PHP 5.2 or newer.'); + trigger_error('Please upgrade to PHP 5.3 or newer.'); die(); } @@ -814,7 +834,7 @@ class SimplePie { $this->timeout = (int) $timeout; } - + /** * Set custom curl options * @@ -854,6 +874,21 @@ class SimplePie } /** + * SimplePie to continue to fall back to expired cache, if enabled, when + * feed is unavailable. + * + * This tells SimplePie to ignore any file errors and fall back to cache + * instead. This only works if caching is enabled and cached content + * still exists. + + * @param bool $enable Force use of cache on fail. + */ + public function force_cache_fallback($enable = false) + { + $this->force_cache_fallback= (bool) $enable; + } + + /** * Set the length of time (in seconds) that the contents of a feed will be * cached * @@ -1169,7 +1204,7 @@ class SimplePie $this->sanitize->strip_attributes($attribs); } - public function add_attributes($attribs = '') //FreshRSS + public function add_attributes($attribs = '') { if ($attribs === '') { @@ -1191,11 +1226,11 @@ class SimplePie * * Allows you to override SimplePie's output to match that of your webpage. * This is useful for times when your webpages are not being served as - * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and + * UTF-8. This setting will be obeyed by {@see handle_content_type()}, and * is similar to {@see set_input_encoding()}. * * It should be noted, however, that not all character encodings can support - * all characters. If your page is being served as ISO-8859-1 and you try + * all characters. If your page is being served as ISO-8859-1 and you try * to display a Japanese feed, you'll likely see garbled characters. * Because of this, it is highly recommended to ensure that your webpages * are served as UTF-8. @@ -1293,7 +1328,7 @@ class SimplePie /** * Initialize the feed object * - * This is what makes everything happen. Period. This is where all of the + * This is what makes everything happen. Period. This is where all of the * configuration options get processed, feeds are fetched, cached, and * parsed, and all of that other good stuff. * @@ -1361,6 +1396,7 @@ class SimplePie $this->error = null; $this->data = array(); + $this->check_modified = false; $this->multifeed_objects = array(); $cache = false; @@ -1390,6 +1426,13 @@ class SimplePie $md5 = $this->data['md5']; } } + + // Empty response check + if(empty($this->raw_data)){ + $this->error = "A feed could not be found at `$this->feed_url`. Empty body."; + $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); + return false; + } // Set up array of possible encodings $encodings = array(); @@ -1432,7 +1475,7 @@ class SimplePie // Text MIME-type default elseif (substr($sniffed, 0, 5) === 'text/') { - $encodings[] = 'US-ASCII'; + $encodings[] = 'UTF-8'; } } @@ -1486,11 +1529,27 @@ class SimplePie if (isset($parser)) { // We have an error, just set SimplePie_Misc::error to it and quit - $this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d, encoding %s, URL: %s', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column(), $encoding, $this->feed_url); + $this->error = $this->feed_url; + $this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column()); } else { - $this->error = 'The data could not be converted to UTF-8. You MUST have either the iconv or mbstring extension installed. Upgrading to PHP 5.x (which includes iconv) is highly recommended.'; + $this->error = 'The data could not be converted to UTF-8.'; + if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) { + $this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.'; + } else { + $missingExtensions = array(); + if (!extension_loaded('iconv')) { + $missingExtensions[] = 'iconv'; + } + if (!extension_loaded('mbstring')) { + $missingExtensions[] = 'mbstring'; + } + if (!class_exists('\UConverter')) { + $missingExtensions[] = 'intl (PHP 5.5+)'; + } + $this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.'; + } } $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); @@ -1563,7 +1622,7 @@ class SimplePie $headers['if-none-match'] = $this->data['headers']['etag']; } - $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); + $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options, $this->syslog_enabled)); if ($file->success) { @@ -1575,6 +1634,7 @@ class SimplePie } else { + $this->check_modified = false; $cache->touch(); $this->error = $file->error; return !empty($this->data); @@ -1616,7 +1676,7 @@ class SimplePie $headers = array( 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', ); - $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); + $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options, $this->syslog_enabled)); } } // If the file connection has an error, set SimplePie::error to that and quit @@ -1669,7 +1729,7 @@ class SimplePie $locate = null; } - $file->body = trim($file->body); + $file->body = trim($file->body); //FreshRSS $this->raw_data = $file->body; $this->permanent_url = $file->permanent_url; $headers = $file->headers; @@ -1870,7 +1930,7 @@ class SimplePie * @todo Support <itunes:new-feed-url> * @todo Also, |atom:link|@rel=self * @param bool $permanent Permanent mode to return only the original URL or the first redirection - * iff it is a 301 redirection + * iff it is a 301 redirection * @return string|null */ public function subscribe_url($permanent = false) @@ -1879,14 +1939,19 @@ class SimplePie { if ($this->permanent_url !== null) { - return $this->sanitize($this->permanent_url, SIMPLEPIE_CONSTRUCT_IRI); + // sanitize encodes ampersands which are required when used in a url. + return str_replace('&', '&', + $this->sanitize($this->permanent_url, + SIMPLEPIE_CONSTRUCT_IRI)); } } else { if ($this->feed_url !== null) { - return $this->sanitize($this->feed_url, SIMPLEPIE_CONSTRUCT_IRI); + return str_replace('&', '&', + $this->sanitize($this->feed_url, + SIMPLEPIE_CONSTRUCT_IRI)); } } return null; @@ -2169,7 +2234,7 @@ class SimplePie * Get a category for the feed * * @since Unknown - * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 + * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 * @return SimplePie_Category|null */ public function get_category($key = 0) @@ -2254,7 +2319,7 @@ class SimplePie * Get an author for the feed * * @since 1.1 - * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 + * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 * @return SimplePie_Author|null */ public function get_author($key = 0) @@ -2352,7 +2417,7 @@ class SimplePie * Get a contributor for the feed * * @since 1.1 - * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 + * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 * @return SimplePie_Author|null */ public function get_contributor($key = 0) @@ -2438,7 +2503,7 @@ class SimplePie * Get a single link for the feed * * @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8) - * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 + * @param int $key The link that you want to return. Remember that arrays begin with 0, not 1 * @param string $rel The relationship of the link to return * @return string|null Link URL */ @@ -2548,6 +2613,12 @@ class SimplePie { return $this->data['links'][$rel]; } + else if (isset($this->data['headers']['link']) && + preg_match('/<([^>]+)>; rel='.preg_quote($rel).'/', + $this->data['headers']['link'], $match)) + { + return array($match[1]); + } else { return null; @@ -2949,7 +3020,7 @@ class SimplePie * * @see get_item_quantity() * @since Beta 2 - * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1 + * @param int $key The item that you want to return. Remember that arrays begin with 0, not 1 * @return SimplePie_Item|null */ public function get_item($key = 0) @@ -2976,7 +3047,7 @@ class SimplePie * @since Beta 2 * @param int $start Index to start at * @param int $end Number of items to return. 0 for all items after `$start` - * @return array|null List of {@see SimplePie_Item} objects + * @return SimplePie_Item[]|null List of {@see SimplePie_Item} objects */ public function get_items($start = 0, $end = 0) { @@ -2985,96 +3056,81 @@ class SimplePie if (!empty($this->multifeed_objects)) { $this->data['items'] = SimplePie::merge_items($this->multifeed_objects, $start, $end, $this->item_limit); + if (empty($this->data['items'])) + { + return array(); + } + return $this->data['items']; } - else + $this->data['items'] = array(); + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'entry')) { - $this->data['items'] = array(); - if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'entry')) + $keys = array_keys($items); + foreach ($keys as $key) { - $keys = array_keys($items); - foreach ($keys as $key) - { - $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); - } + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); } - if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'entry')) + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'entry')) + { + $keys = array_keys($items); + foreach ($keys as $key) { - $keys = array_keys($items); - foreach ($keys as $key) - { - $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); - } + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); } - if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'item')) + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) { - $keys = array_keys($items); - foreach ($keys as $key) - { - $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); - } + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); } - if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'item')) + } + if ($items = $this->get_feed_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) { - $keys = array_keys($items); - foreach ($keys as $key) - { - $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); - } + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); } - if ($items = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'item')) + } + if ($items = $this->get_channel_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'item')) + { + $keys = array_keys($items); + foreach ($keys as $key) { - $keys = array_keys($items); - foreach ($keys as $key) - { - $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); - } + $this->data['items'][] = $this->registry->create('Item', array($this, $items[$key])); } } } - if (!empty($this->data['items'])) + if (empty($this->data['items'])) { - // If we want to order it by date, check if all items have a date, and then sort it - if ($this->order_by_date && empty($this->multifeed_objects)) - { - if (!isset($this->data['ordered_items'])) - { - $do_sort = true; - foreach ($this->data['items'] as $item) - { - if (!$item->get_date('U')) - { - $do_sort = false; - break; - } - } - $item = null; - $this->data['ordered_items'] = $this->data['items']; - if ($do_sort) - { - usort($this->data['ordered_items'], array(get_class($this), 'sort_items')); - } - } - $items = $this->data['ordered_items']; - } - else - { - $items = $this->data['items']; - } + return array(); + } - // Slice the data as desired - if ($end === 0) - { - return array_slice($items, $start); - } - else + if ($this->order_by_date) + { + if (!isset($this->data['ordered_items'])) { - return array_slice($items, $start, $end); - } + $this->data['ordered_items'] = $this->data['items']; + usort($this->data['ordered_items'], array(get_class($this), 'sort_items')); + } + $items = $this->data['ordered_items']; } else { - return array(); + $items = $this->data['items']; + } + // Slice the data as desired + if ($end === 0) + { + return array_slice($items, $start); + } + else + { + return array_slice($items, $start, $end); } } @@ -3147,7 +3203,19 @@ class SimplePie */ public static function sort_items($a, $b) { - return $a->get_date('U') <= $b->get_date('U'); + $a_date = $a->get_date('U'); + $b_date = $b->get_date('U'); + if ($a_date && $b_date) { + return $a_date > $b_date ? -1 : 1; + } + // Sort items without dates to the top. + if ($a_date) { + return 1; + } + if ($b_date) { + return -1; + } + return 0; } /** @@ -3180,20 +3248,7 @@ class SimplePie } } - $do_sort = true; - foreach ($items as $item) - { - if (!$item->get_date('U')) - { - $do_sort = false; - break; - } - } - $item = null; - if ($do_sort) - { - usort($items, array(get_class($urls[0]), 'sort_items')); - } + usort($items, array(get_class($urls[0]), 'sort_items')); if ($end === 0) { @@ -3210,4 +3265,42 @@ class SimplePie return array(); } } + + /** + * Store PubSubHubbub links as headers + * + * There is no way to find PuSH links in the body of a microformats feed, + * so they are added to the headers when found, to be used later by get_links. + * @param SimplePie_File $file + * @param string $hub + * @param string $self + */ + private function store_links(&$file, $hub, $self) { + if (isset($file->headers['link']['hub']) || + (isset($file->headers['link']) && + preg_match('/rel=hub/', $file->headers['link']))) + { + return; + } + + if ($hub) + { + if (isset($file->headers['link'])) + { + if ($file->headers['link'] !== '') + { + $file->headers['link'] = ', '; + } + } + else + { + $file->headers['link'] = ''; + } + $file->headers['link'] .= '<'.$hub.'>; rel=hub'; + if ($self) + { + $file->headers['link'] .= ', <'.$self.'>; rel=self'; + } + } + } } diff --git a/lib/SimplePie/SimplePie/Author.php b/lib/SimplePie/SimplePie/Author.php index 19563c5cc..e6768ff29 100644 --- a/lib/SimplePie/SimplePie/Author.php +++ b/lib/SimplePie/SimplePie/Author.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Cache.php b/lib/SimplePie/SimplePie/Cache.php index 86b618693..d98cc6511 100644 --- a/lib/SimplePie/SimplePie/Cache.php +++ b/lib/SimplePie/SimplePie/Cache.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -62,8 +61,10 @@ class SimplePie_Cache * @var array */ protected static $handlers = array( - 'mysql' => 'SimplePie_Cache_MySQL', - 'memcache' => 'SimplePie_Cache_Memcache', + 'mysql' => 'SimplePie_Cache_MySQL', + 'memcache' => 'SimplePie_Cache_Memcache', + 'memcached' => 'SimplePie_Cache_Memcached', + 'redis' => 'SimplePie_Cache_Redis' ); /** diff --git a/lib/SimplePie/SimplePie/Cache/Base.php b/lib/SimplePie/SimplePie/Cache/Base.php index d3f353961..333fb05cf 100644 --- a/lib/SimplePie/SimplePie/Cache/Base.php +++ b/lib/SimplePie/SimplePie/Cache/Base.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Cache/DB.php b/lib/SimplePie/SimplePie/Cache/DB.php index d728a9a6d..7e8f77532 100644 --- a/lib/SimplePie/SimplePie/Cache/DB.php +++ b/lib/SimplePie/SimplePie/Cache/DB.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Cache/File.php b/lib/SimplePie/SimplePie/Cache/File.php index 72e75a4b6..6ba6c5f6e 100644 --- a/lib/SimplePie/SimplePie/Cache/File.php +++ b/lib/SimplePie/SimplePie/Cache/File.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Cache/Memcache.php b/lib/SimplePie/SimplePie/Cache/Memcache.php index 23b1c9367..5190eef93 100644 --- a/lib/SimplePie/SimplePie/Cache/Memcache.php +++ b/lib/SimplePie/SimplePie/Cache/Memcache.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Cache/Memcached.php b/lib/SimplePie/SimplePie/Cache/Memcached.php new file mode 100644 index 000000000..1f73b3890 --- /dev/null +++ b/lib/SimplePie/SimplePie/Cache/Memcached.php @@ -0,0 +1,166 @@ +<?php +/** + * SimplePie + * + * A PHP-Based RSS and Atom Feed Framework. + * Takes the hard work out of managing a complete RSS/Atom solution. + * + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * * Neither the name of the SimplePie Team nor the names of its contributors may be used + * to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS + * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package SimplePie + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @author Ryan Parman + * @author Geoffrey Sneddon + * @author Ryan McCue + * @link http://simplepie.org/ SimplePie + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +/** + * Caches data to memcached + * + * Registered for URLs with the "memcached" protocol + * + * For example, `memcached://localhost:11211/?timeout=3600&prefix=sp_` will + * connect to memcached on `localhost` on port 11211. All tables will be + * prefixed with `sp_` and data will expire after 3600 seconds + * + * @package SimplePie + * @subpackage Caching + * @author Paul L. McNeely + * @uses Memcached + */ +class SimplePie_Cache_Memcached implements SimplePie_Cache_Base +{ + /** + * Memcached instance + * @var Memcached + */ + protected $cache; + + /** + * Options + * @var array + */ + protected $options; + + /** + * Cache name + * @var string + */ + protected $name; + + /** + * Create a new cache object + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $type) { + $this->options = array( + 'host' => '127.0.0.1', + 'port' => 11211, + 'extras' => array( + 'timeout' => 3600, // one hour + 'prefix' => 'simplepie_', + ), + ); + $this->options = SimplePie_Misc::array_merge_recursive($this->options, SimplePie_Cache::parse_URL($location)); + + $this->name = $this->options['extras']['prefix'] . md5("$name:$type"); + + $this->cache = new Memcached(); + $this->cache->addServer($this->options['host'], (int)$this->options['port']); + } + + /** + * Save data to the cache + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) { + if ($data instanceof SimplePie) { + $data = $data->data; + } + + return $this->setData(serialize($data)); + } + + /** + * Retrieve the data saved to the cache + * @return array Data for SimplePie::$data + */ + public function load() { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * @return int Timestamp + */ + public function mtime() { + $data = $this->cache->get($this->name . '_mtime'); + return (int) $data; + } + + /** + * Set the last modified time to the current time + * @return bool Success status + */ + public function touch() { + $data = $this->cache->get($this->name); + return $this->setData($data); + } + + /** + * Remove the cache + * @return bool Success status + */ + public function unlink() { + return $this->cache->delete($this->name, 0); + } + + /** + * Set the last modified time and data to Memcached + * @return bool Success status + */ + private function setData($data) { + + if ($data !== false) { + $this->cache->set($this->name . '_mtime', time(), (int)$this->options['extras']['timeout']); + return $this->cache->set($this->name, $data, (int)$this->options['extras']['timeout']); + } + + return false; + } +} diff --git a/lib/SimplePie/SimplePie/Cache/MySQL.php b/lib/SimplePie/SimplePie/Cache/MySQL.php index 511ef6d3a..8686b6c67 100644 --- a/lib/SimplePie/SimplePie/Cache/MySQL.php +++ b/lib/SimplePie/SimplePie/Cache/MySQL.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -94,6 +93,7 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB 'path' => '', 'extras' => array( 'prefix' => '', + 'cache_purge_time' => 2592000 ), ); @@ -131,16 +131,20 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'cache_data` (`id` TEXT CHARACTER SET utf8 NOT NULL, `items` SMALLINT NOT NULL DEFAULT 0, `data` BLOB NOT NULL, `mtime` INT UNSIGNED NOT NULL, UNIQUE (`id`(125)))'); if ($query === false) { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "cache_data table, check permissions", E_USER_WARNING); $this->mysql = null; + return; } } if (!in_array($this->options['extras']['prefix'] . 'items', $db)) { - $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB CHARACTER SET utf8 NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))'); + $query = $this->mysql->exec('CREATE TABLE `' . $this->options['extras']['prefix'] . 'items` (`feed_id` TEXT CHARACTER SET utf8 NOT NULL, `id` TEXT CHARACTER SET utf8 NOT NULL, `data` MEDIUMBLOB NOT NULL, `posted` INT UNSIGNED NOT NULL, INDEX `feed_id` (`feed_id`(125)))'); if ($query === false) { + trigger_error("Can't create " . $this->options['extras']['prefix'] . "items table, check permissions", E_USER_WARNING); $this->mysql = null; + return; } } } @@ -158,6 +162,17 @@ class SimplePie_Cache_MySQL extends SimplePie_Cache_DB return false; } + $query = $this->mysql->prepare('DELETE i, cd FROM `' . $this->options['extras']['prefix'] . 'cache_data` cd, ' . + '`' . $this->options['extras']['prefix'] . 'items` i ' . + 'WHERE cd.id = i.feed_id ' . + 'AND cd.mtime < (unix_timestamp() - :purge_time)'); + $query->bindValue(':purge_time', $this->options['extras']['cache_purge_time']); + + if (!$query->execute()) + { + return false; + } + if ($data instanceof SimplePie) { $data = clone $data; diff --git a/lib/SimplePie/SimplePie/Cache/Redis.php b/lib/SimplePie/SimplePie/Cache/Redis.php new file mode 100644 index 000000000..04d72c79a --- /dev/null +++ b/lib/SimplePie/SimplePie/Cache/Redis.php @@ -0,0 +1,166 @@ +<?php + +/** + * SimplePie Redis Cache Extension + * + * @package SimplePie + * @author Jan Kozak <galvani78@gmail.com> + * @link http://galvani.cz/ + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 0.2.9 + */ + + +/** + * Caches data to redis + * + * Registered for URLs with the "redis" protocol + * + * For example, `redis://localhost:6379/?timeout=3600&prefix=sp_&dbIndex=0` will + * connect to redis on `localhost` on port 6379. All tables will be + * prefixed with `simple_primary-` and data will expire after 3600 seconds + * + * @package SimplePie + * @subpackage Caching + * @uses Redis + */ +class SimplePie_Cache_Redis implements SimplePie_Cache_Base { + /** + * Redis instance + * + * @var \Redis + */ + protected $cache; + + /** + * Options + * + * @var array + */ + protected $options; + + /** + * Cache name + * + * @var string + */ + protected $name; + + /** + * Cache Data + * + * @var type + */ + protected $data; + + /** + * Create a new cache object + * + * @param string $location Location string (from SimplePie::$cache_location) + * @param string $name Unique ID for the cache + * @param string $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + */ + public function __construct($location, $name, $options = null) { + //$this->cache = \flow\simple\cache\Redis::getRedisClientInstance(); + $parsed = SimplePie_Cache::parse_URL($location); + $redis = new Redis(); + $redis->connect($parsed['host'], $parsed['port']); + $this->cache = $redis; + + if (!is_null($options) && is_array($options)) { + $this->options = $options; + } else { + $this->options = array ( + 'prefix' => 'rss:simple_primary:', + 'expire' => 0, + ); + } + + $this->name = $this->options['prefix'] . $name; + } + + /** + * @param \Redis $cache + */ + public function setRedisClient(\Redis $cache) { + $this->cache = $cache; + } + + /** + * Save data to the cache + * + * @param array|SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @return bool Successfulness + */ + public function save($data) { + if ($data instanceof SimplePie) { + $data = $data->data; + } + $response = $this->cache->set($this->name, serialize($data)); + if ($this->options['expire']) { + $this->cache->expire($this->name, $this->options['expire']); + } + + return $response; + } + + /** + * Retrieve the data saved to the cache + * + * @return array Data for SimplePie::$data + */ + public function load() { + $data = $this->cache->get($this->name); + + if ($data !== false) { + return unserialize($data); + } + return false; + } + + /** + * Retrieve the last modified time for the cache + * + * @return int Timestamp + */ + public function mtime() { + + $data = $this->cache->get($this->name); + + if ($data !== false) { + return time(); + } + + return false; + } + + /** + * Set the last modified time to the current time + * + * @return bool Success status + */ + public function touch() { + + $data = $this->cache->get($this->name); + + if ($data !== false) { + $return = $this->cache->set($this->name, $data); + if ($this->options['expire']) { + return $this->cache->expire($this->name, $this->ttl); + } + return $return; + } + + return false; + } + + /** + * Remove the cache + * + * @return bool Success status + */ + public function unlink() { + return $this->cache->set($this->name, null); + } + +} diff --git a/lib/SimplePie/SimplePie/Caption.php b/lib/SimplePie/SimplePie/Caption.php index a77b02ef1..abf07de1b 100644 --- a/lib/SimplePie/SimplePie/Caption.php +++ b/lib/SimplePie/SimplePie/Caption.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Category.php b/lib/SimplePie/SimplePie/Category.php index c6a273989..df0f13f9a 100644 --- a/lib/SimplePie/SimplePie/Category.php +++ b/lib/SimplePie/SimplePie/Category.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -57,7 +56,7 @@ class SimplePie_Category /** * Category identifier * - * @var string + * @var string|null * @see get_term */ var $term; @@ -65,7 +64,7 @@ class SimplePie_Category /** * Categorization scheme identifier * - * @var string + * @var string|null * @see get_scheme() */ var $scheme; @@ -73,23 +72,36 @@ class SimplePie_Category /** * Human readable label * - * @var string + * @var string|null * @see get_label() */ var $label; /** + * Category type + * + * category for <category> + * subject for <dc:subject> + * + * @var string|null + * @see get_type() + */ + var $type; + + /** * Constructor, used to input the data * - * @param string $term - * @param string $scheme - * @param string $label + * @param string|null $term + * @param string|null $scheme + * @param string|null $label + * @param string|null $type */ - public function __construct($term = null, $scheme = null, $label = null) + public function __construct($term = null, $scheme = null, $label = null, $type = null) { $this->term = $term; $this->scheme = $scheme; $this->label = $label; + $this->type = $type; } /** @@ -110,14 +122,7 @@ class SimplePie_Category */ public function get_term() { - if ($this->term !== null) - { - return $this->term; - } - else - { - return null; - } + return $this->term; } /** @@ -127,31 +132,32 @@ class SimplePie_Category */ public function get_scheme() { - if ($this->scheme !== null) - { - return $this->scheme; - } - else - { - return null; - } + return $this->scheme; } /** * Get the human readable label * + * @param bool $strict * @return string|null */ - public function get_label() + public function get_label($strict = false) { - if ($this->label !== null) - { - return $this->label; - } - else + if ($this->label === null && $strict !== true) { return $this->get_term(); } + return $this->label; + } + + /** + * Get the category type + * + * @return string|null + */ + public function get_type() + { + return $this->type; } } diff --git a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php index ec0bf0952..6caf80f33 100644 --- a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php +++ b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -125,8 +124,8 @@ class SimplePie_Content_Type_Sniffer } } elseif ($official === 'text/html' - || $official === 'text/xml' - || $official === 'application/xml') + || $official === 'text/xml' //FreshRSS + || $official === 'application/xml') //FreshRSS { return $this->feed_or_html(); } @@ -256,12 +255,7 @@ class SimplePie_Content_Type_Sniffer public function feed_or_html() { $len = strlen($this->file->body); - $pos = 0; - if (isset($this->file->body[2]) && $this->file->body[0] === "\xEF" && - $this->file->body[1] === "\xBB" && $this->file->body[2] === "\xBF") { - $pos += 3; //UTF-8 BOM - } - $pos += strspn($this->file->body, "\x09\x0A\x0D\x20", $pos); + $pos = strspn($this->file->body, "\x09\x0A\x0D\x20\xEF\xBB\xBF"); while ($pos < $len) { diff --git a/lib/SimplePie/SimplePie/Copyright.php b/lib/SimplePie/SimplePie/Copyright.php index 09f22f8df..3f3d07d3b 100644 --- a/lib/SimplePie/SimplePie/Copyright.php +++ b/lib/SimplePie/SimplePie/Copyright.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Core.php b/lib/SimplePie/SimplePie/Core.php index 7cf34876f..c856ba361 100644 --- a/lib/SimplePie/SimplePie/Core.php +++ b/lib/SimplePie/SimplePie/Core.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2009, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Credit.php b/lib/SimplePie/SimplePie/Credit.php index 50aef1c68..9bad9ef34 100644 --- a/lib/SimplePie/SimplePie/Credit.php +++ b/lib/SimplePie/SimplePie/Credit.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Decode/HTML/Entities.php b/lib/SimplePie/SimplePie/Decode/HTML/Entities.php index 46b3a1dff..de3f2cb53 100644 --- a/lib/SimplePie/SimplePie/Decode/HTML/Entities.php +++ b/lib/SimplePie/SimplePie/Decode/HTML/Entities.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Enclosure.php b/lib/SimplePie/SimplePie/Enclosure.php index fa0217800..15060e193 100644 --- a/lib/SimplePie/SimplePie/Enclosure.php +++ b/lib/SimplePie/SimplePie/Enclosure.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -451,7 +450,7 @@ class SimplePie_Enclosure /** * Get the duration of the enclosure * - * @param string $convert Convert seconds into hh:mm:ss + * @param bool $convert Convert seconds into hh:mm:ss * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found) */ public function get_duration($convert = false) diff --git a/lib/SimplePie/SimplePie/Exception.php b/lib/SimplePie/SimplePie/Exception.php index 73e104d69..53c015e77 100644 --- a/lib/SimplePie/SimplePie/Exception.php +++ b/lib/SimplePie/SimplePie/Exception.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/File.php b/lib/SimplePie/SimplePie/File.php index 45994d102..8be38f145 100644 --- a/lib/SimplePie/SimplePie/File.php +++ b/lib/SimplePie/SimplePie/File.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -64,7 +63,7 @@ class SimplePie_File var $redirects = 0; var $error; var $method = SIMPLEPIE_FILE_SOURCE_NONE; - var $permanent_url; //FreshRSS + var $permanent_url; public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = array(), $syslog_enabled = SIMPLEPIE_SYSLOG) { @@ -108,6 +107,7 @@ class SimplePie_File curl_setopt($fp, CURLOPT_URL, $url); curl_setopt($fp, CURLOPT_HEADER, 1); curl_setopt($fp, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($fp, CURLOPT_FAILONERROR, 1); curl_setopt($fp, CURLOPT_TIMEOUT, $timeout); curl_setopt($fp, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($fp, CURLOPT_REFERER, $url); @@ -118,8 +118,7 @@ class SimplePie_File curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($fp, CURLOPT_MAXREDIRS, $redirects); } - foreach ($curl_options as $curl_param => $curl_value) - { + foreach ($curl_options as $curl_param => $curl_value) { curl_setopt($fp, $curl_param, $curl_value); } @@ -136,15 +135,17 @@ class SimplePie_File } else { - $info = curl_getinfo($fp); + // Use the updated url provided by curl_getinfo after any redirects. + if ($info = curl_getinfo($fp)) { + $this->url = $info['url']; + } curl_close($fp); - $this->headers = explode("\r\n\r\n", $this->headers, $info['redirect_count'] + 1); - $this->headers = array_pop($this->headers); + $this->headers = SimplePie_HTTP_Parser::prepareHeaders($this->headers, $info['redirect_count'] + 1); $parser = new SimplePie_HTTP_Parser($this->headers); if ($parser->parse()) { $this->headers = $parser->headers; - $this->body = $parser->body; + $this->body = trim($parser->body); $this->status_code = $parser->status_code; if ((in_array($this->status_code, array(300, 301, 302, 303, 307)) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects) { @@ -237,7 +238,7 @@ class SimplePie_File $location = SimplePie_Misc::absolutize_url($this->headers['location'], $url); $previousStatusCode = $this->status_code; $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen); - $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; //FreshRSS + $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; return; } if (isset($this->headers['content-encoding'])) @@ -255,7 +256,7 @@ class SimplePie_File } else { - $this->body = $decoder->data; + $this->body = trim($decoder->data); } break; @@ -298,7 +299,7 @@ class SimplePie_File else { $this->method = SIMPLEPIE_FILE_SOURCE_LOCAL | SIMPLEPIE_FILE_SOURCE_FILE_GET_CONTENTS; - if (empty($url) || !($this->body = file_get_contents($url))) + if (empty($url) || !($this->body = trim(file_get_contents($url)))) { $this->error = 'file_get_contents could not read the file'; $this->success = false; diff --git a/lib/SimplePie/SimplePie/HTTP/Parser.php b/lib/SimplePie/SimplePie/HTTP/Parser.php index 2fc3a6779..3899c53fa 100644 --- a/lib/SimplePie/SimplePie/HTTP/Parser.php +++ b/lib/SimplePie/SimplePie/HTTP/Parser.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -497,4 +496,22 @@ class SimplePie_HTTP_Parser } } } + + /** + * Prepare headers (take care of proxies headers) + * + * @param string $headers Raw headers + * @param integer $count Redirection count. Default to 1. + * + * @return string + */ + static public function prepareHeaders($headers, $count = 1) + { + $data = explode("\r\n\r\n", $headers, $count); + $data = array_pop($data); + if (false !== stripos($data, "HTTP/1.0 200 Connection established\r\n\r\n")) { + $data = str_ireplace("HTTP/1.0 200 Connection established\r\n\r\n", '', $data); + } + return $data; + } } diff --git a/lib/SimplePie/SimplePie/IRI.php b/lib/SimplePie/SimplePie/IRI.php index ed0574701..2b3fbaf07 100644 --- a/lib/SimplePie/SimplePie/IRI.php +++ b/lib/SimplePie/SimplePie/IRI.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -260,6 +259,15 @@ class SimplePie_IRI } /** + * Clean up + */ + public function __destruct() { + $this->set_iri(null, true); + $this->set_path(null, true); + $this->set_authority(null, true); + } + + /** * Create a new IRI object by resolving a relative IRI * * Returns false if $base is not absolute, otherwise an IRI. @@ -768,24 +776,20 @@ class SimplePie_IRI */ public function is_valid() { - $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; - if ($this->ipath !== '' && - ( - $isauthority && ( - $this->ipath[0] !== '/' || - substr($this->ipath, 0, 2) === '//' - ) || - ( - $this->scheme === null && - !$isauthority && - strpos($this->ipath, ':') !== false && - (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) - ) - ) - ) - { - return false; - } + if ($this->ipath === '') return true; + + $isauthority = $this->iuserinfo !== null || $this->ihost !== null || + $this->port !== null; + if ($isauthority && $this->ipath[0] === '/') return true; + + if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) return false; + + // Relative urls cannot have a colon in the first path segment (and the + // slashes themselves are not included so skip the first character). + if (!$this->scheme && !$isauthority && + strpos($this->ipath, ':') !== false && + strpos($this->ipath, '/', 1) !== false && + strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) return false; return true; } @@ -797,9 +801,14 @@ class SimplePie_IRI * @param string $iri * @return bool */ - public function set_iri($iri) + public function set_iri($iri, $clear_cache = false) { static $cache; + if ($clear_cache) + { + $cache = null; + return; + } if (!$cache) { $cache = array(); @@ -879,9 +888,14 @@ class SimplePie_IRI * @param string $authority * @return bool */ - public function set_authority($authority) + public function set_authority($authority, $clear_cache = false) { static $cache; + if ($clear_cache) + { + $cache = null; + return; + } if (!$cache) $cache = array(); @@ -1049,9 +1063,14 @@ class SimplePie_IRI * @param string $ipath * @return bool */ - public function set_path($ipath) + public function set_path($ipath, $clear_cache = false) { static $cache; + if ($clear_cache) + { + $cache = null; + return; + } if (!$cache) { $cache = array(); diff --git a/lib/SimplePie/SimplePie/Item.php b/lib/SimplePie/SimplePie/Item.php index 19ba7c8f4..425538606 100644 --- a/lib/SimplePie/SimplePie/Item.php +++ b/lib/SimplePie/SimplePie/Item.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -203,14 +202,14 @@ class SimplePie_Item * * Uses `<atom:id>`, `<guid>`, `<dc:identifier>` or the `about` attribute * for RDF. If none of these are supplied (or `$hash` is true), creates an - * MD5 hash based on the permalink and title. If either of those are not - * supplied, creates a hash based on the full feed data. + * MD5 hash based on the permalink, title and content. * * @since Beta 2 * @param boolean $hash Should we force using a hash instead of the supplied ID? - * @return string + * @param string|false $fn User-supplied function to generate an hash + * @return string|null */ - public function get_id($hash = false) + public function get_id($hash = false, $fn = 'md5') { if (!$hash) { @@ -238,23 +237,18 @@ class SimplePie_Item { return $this->sanitize($this->data['attribs'][SIMPLEPIE_NAMESPACE_RDF]['about'], SIMPLEPIE_CONSTRUCT_TEXT); } - elseif (($return = $this->get_permalink()) !== null) - { - return $return; - } - elseif (($return = $this->get_title()) !== null) - { - return $return; - } } - if ($this->get_permalink() !== null || $this->get_title() !== null) + if ($fn === false) { - return md5($this->get_permalink() . $this->get_title()); + return null; } - else + elseif (!is_callable($fn)) { - return md5(serialize($this->data)); + trigger_error('User-supplied function $fn must be callable', E_USER_WARNING); + $fn = 'md5'; } + return call_user_func($fn, + $this->get_permalink().$this->get_title().$this->get_content()); } /** @@ -322,41 +316,50 @@ class SimplePie_Item */ public function get_description($description_only = false) { - if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'summary')) + if (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'summary')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'description')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_MAYBE_HTML, $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'description')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'description')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'description')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'summary')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'summary')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'subtitle')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'subtitle')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'description')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_090, 'description')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML); + return $return; } elseif (!$description_only) @@ -385,17 +388,20 @@ class SimplePie_Item */ public function get_content($content_only = false) { - if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'content')) + if (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_10_content_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_10_content_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'content')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'content')) && + ($return = $this->sanitize($tags[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($tags[0]['attribs'])), $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], $this->registry->call('Misc', 'atom_03_construct_type', array($return[0]['attribs'])), $this->get_base($return[0])); + return $return; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10_MODULES_CONTENT, 'encoded')) + elseif (($tags = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_10_MODULES_CONTENT, 'encoded')) && + ($return = $this->sanitize($tags[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($tags[0])))) { - return $this->sanitize($return[0]['data'], SIMPLEPIE_CONSTRUCT_HTML, $this->get_base($return[0])); + return $return; } elseif (!$content_only) { @@ -457,53 +463,56 @@ class SimplePie_Item * Uses `<atom:category>`, `<category>` or `<dc:subject>` * * @since Beta 3 - * @return array|null List of {@see SimplePie_Category} objects + * @return SimplePie_Category[]|null List of {@see SimplePie_Category} objects */ public function get_categories() { $categories = array(); - foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'category') as $category) + $type = 'category'; + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, $type) as $category) { $term = null; $scheme = null; $label = null; if (isset($category['attribs']['']['term'])) { - $term = $this->sanitize($category['attribs']['']['term'], SIMPLEPIE_CONSTRUCT_TEXT); + $term = $this->sanitize($category['attribs']['']['term'], SIMPLEPIE_CONSTRUCT_HTML); } if (isset($category['attribs']['']['scheme'])) { - $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_TEXT); + $scheme = $this->sanitize($category['attribs']['']['scheme'], SIMPLEPIE_CONSTRUCT_HTML); } if (isset($category['attribs']['']['label'])) { - $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_TEXT); + $label = $this->sanitize($category['attribs']['']['label'], SIMPLEPIE_CONSTRUCT_HTML); } - $categories[] = $this->registry->create('Category', array($term, $scheme, $label)); + $categories[] = $this->registry->create('Category', array($term, $scheme, $label, $type)); } - foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'category') as $category) + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, $type) as $category) { // This is really the label, but keep this as the term also for BC. // Label will also work on retrieving because that falls back to term. - $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT); + $term = $this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML); if (isset($category['attribs']['']['domain'])) { - $scheme = $this->sanitize($category['attribs']['']['domain'], SIMPLEPIE_CONSTRUCT_TEXT); + $scheme = $this->sanitize($category['attribs']['']['domain'], SIMPLEPIE_CONSTRUCT_HTML); } else { $scheme = null; } - $categories[] = $this->registry->create('Category', array($term, $scheme, null)); + $categories[] = $this->registry->create('Category', array($term, $scheme, null, $type)); } - foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'subject') as $category) + + $type = 'subject'; + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, $type) as $category) { - $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null, $type)); } - foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'subject') as $category) + foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, $type) as $category) { - $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + $categories[] = $this->registry->create('Category', array($this->sanitize($category['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null, $type)); } if (!empty($categories)) @@ -640,7 +649,7 @@ class SimplePie_Item $email = null; if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'])) { - $name = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + $name = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); } if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'])) { @@ -648,7 +657,7 @@ class SimplePie_Item } if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'])) { - $email = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + $email = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); } if ($name !== null || $email !== null || $uri !== null) { @@ -662,7 +671,7 @@ class SimplePie_Item $email = null; if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'])) { - $name = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + $name = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['name'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); } if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'])) { @@ -670,7 +679,7 @@ class SimplePie_Item } if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'])) { - $email = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_TEXT); + $email = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data'], SIMPLEPIE_CONSTRUCT_HTML); } if ($name !== null || $email !== null || $url !== null) { @@ -679,19 +688,19 @@ class SimplePie_Item } if ($author = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'author')) { - $authors[] = $this->registry->create('Author', array(null, null, $this->sanitize($author[0]['data'], SIMPLEPIE_CONSTRUCT_TEXT))); + $authors[] = $this->registry->create('Author', array(null, null, $this->sanitize($author[0]['data'], SIMPLEPIE_CONSTRUCT_HTML))); } foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'creator') as $author) { - $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); } foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'creator') as $author) { - $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); } foreach ((array) $this->get_item_tags(SIMPLEPIE_NAMESPACE_ITUNES, 'author') as $author) { - $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_TEXT), null, null)); + $authors[] = $this->registry->create('Author', array($this->sanitize($author['data'], SIMPLEPIE_CONSTRUCT_HTML), null, null)); } if (!empty($authors)) @@ -1105,7 +1114,7 @@ class SimplePie_Item * @since Beta 2 * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4). * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists). - * @return array|null List of SimplePie_Enclosure items + * @return SimplePie_Enclosure[]|null List of SimplePie_Enclosure items */ public function get_enclosures() { @@ -2682,7 +2691,9 @@ class SimplePie_Item // PLAYER if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'])) { - $player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + if (isset($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'])) { + $player = $this->sanitize($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } } else { @@ -2815,9 +2826,17 @@ class SimplePie_Item { $length = ceil($link['attribs']['']['length']); } + if (isset($link['attribs']['']['title'])) + { + $title = $this->sanitize($link['attribs']['']['title'], SIMPLEPIE_CONSTRUCT_TEXT); + } + else + { + $title = $title_parent; + } // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor - $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width)); + $this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title, $width)); } } diff --git a/lib/SimplePie/SimplePie/Locator.php b/lib/SimplePie/SimplePie/Locator.php index ba4a843b0..bc314c2cd 100644 --- a/lib/SimplePie/SimplePie/Locator.php +++ b/lib/SimplePie/SimplePie/Locator.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -121,34 +120,41 @@ class SimplePie_Locator { if ($type & SIMPLEPIE_LOCATOR_LOCAL_EXTENSION && $working = $this->extension($this->local)) { - return $working; + return $working[0]; } if ($type & SIMPLEPIE_LOCATOR_LOCAL_BODY && $working = $this->body($this->local)) { - return $working; + return $working[0]; } if ($type & SIMPLEPIE_LOCATOR_REMOTE_EXTENSION && $working = $this->extension($this->elsewhere)) { - return $working; + return $working[0]; } if ($type & SIMPLEPIE_LOCATOR_REMOTE_BODY && $working = $this->body($this->elsewhere)) { - return $working; + return $working[0]; } } return null; } - public function is_feed($file) + public function is_feed($file, $check_html = false) { if ($file->method & SIMPLEPIE_FILE_SOURCE_REMOTE) { $sniffer = $this->registry->create('Content_Type_Sniffer', array($file)); $sniffed = $sniffer->get_type(); - if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml', 'application/x-rss+xml'))) + $mime_types = array('application/rss+xml', 'application/rdf+xml', + 'text/rdf', 'application/atom+xml', 'text/xml', + 'application/xml', 'application/x-rss+xml'); + if ($check_html) + { + $mime_types[] = 'text/html'; + } + if (in_array($sniffed, $mime_types)) { return true; } @@ -226,7 +232,7 @@ class SimplePie_Locator } if ($link->hasAttribute('href') && $link->hasAttribute('rel')) { - $rel = array_unique($this->registry->call('Misc', 'space_seperated_tokens', array(strtolower($link->getAttribute('rel'))))); + $rel = array_unique($this->registry->call('Misc', 'space_separated_tokens', array(strtolower($link->getAttribute('rel'))))); $line = method_exists($link, 'getLineNo') ? $link->getLineNo() : 1; if ($this->base_location < $line) @@ -242,14 +248,14 @@ class SimplePie_Locator continue; } - if (!in_array($href, $done) && in_array('feed', $rel) || (in_array('alternate', $rel) && !in_array('stylesheet', $rel) && $link->hasAttribute('type') && in_array(strtolower($this->registry->call('Misc', 'parse_mime', array($link->getAttribute('type')))), array('application/rss+xml', 'application/atom+xml'))) && !isset($feeds[$href])) + if (!in_array($href, $done) && in_array('feed', $rel) || (in_array('alternate', $rel) && !in_array('stylesheet', $rel) && $link->hasAttribute('type') && in_array(strtolower($this->registry->call('Misc', 'parse_mime', array($link->getAttribute('type')))), array('text/html', 'application/rss+xml', 'application/atom+xml'))) && !isset($feeds[$href])) { $this->checked_feeds++; $headers = array( 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', ); $feed = $this->registry->create('File', array($href, $this->timeout, 5, $headers, $this->useragent)); - if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) + if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed, true)) { $feeds[$href] = $feed; } @@ -275,7 +281,7 @@ class SimplePie_Locator { $href = trim($link->getAttribute('href')); $parsed = $this->registry->call('Misc', 'parse_url', array($href)); - if ($parsed['scheme'] === '' || preg_match('/^(http(s)|feed)?$/i', $parsed['scheme'])) + if ($parsed['scheme'] === '' || preg_match('/^(https?|feed)?$/i', $parsed['scheme'])) { if (method_exists($link, 'getLineNo') && $this->base_location < $link->getLineNo()) { @@ -312,6 +318,57 @@ class SimplePie_Locator return null; } + public function get_rel_link($rel) + { + if ($this->dom === null) + { + throw new SimplePie_Exception('DOMDocument not found, unable to use '. + 'locator'); + } + if (!class_exists('DOMXpath')) + { + throw new SimplePie_Exception('DOMXpath not found, unable to use '. + 'get_rel_link'); + } + + $xpath = new DOMXpath($this->dom); + $query = '//a[@rel and @href] | //link[@rel and @href]'; + foreach ($xpath->query($query) as $link) + { + $href = trim($link->getAttribute('href')); + $parsed = $this->registry->call('Misc', 'parse_url', array($href)); + if ($parsed['scheme'] === '' || + preg_match('/^https?$/i', $parsed['scheme'])) + { + if (method_exists($link, 'getLineNo') && + $this->base_location < $link->getLineNo()) + { + $href = + $this->registry->call('Misc', 'absolutize_url', + array(trim($link->getAttribute('href')), + $this->base)); + } + else + { + $href = + $this->registry->call('Misc', 'absolutize_url', + array(trim($link->getAttribute('href')), + $this->http_base)); + } + if ($href === false) + { + return null; + } + $rel_values = explode(' ', strtolower($link->getAttribute('rel'))); + if (in_array($rel, $rel_values)) + { + return $href; + } + } + } + return null; + } + public function extension(&$array) { foreach ($array as $key => $value) @@ -330,7 +387,7 @@ class SimplePie_Locator $feed = $this->registry->create('File', array($value, $this->timeout, 5, $headers, $this->useragent)); if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) { - return $feed; + return array($feed); } else { @@ -358,7 +415,7 @@ class SimplePie_Locator $feed = $this->registry->create('File', array($value, $this->timeout, 5, null, $this->useragent)); if ($feed->success && ($feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) { - return $feed; + return array($feed); } else { diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index 2d154cbcb..40477c01e 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -128,7 +127,7 @@ class SimplePie_Misc { $attribs[$j][2] = $attribs[$j][1]; } - $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8'); + $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j])); } } } @@ -338,11 +337,16 @@ class SimplePie_Misc { return $return; } - // This is last, as behaviour of this varies with OS userland and PHP version + // This is third, as behaviour of this varies with OS userland and PHP version elseif (function_exists('iconv') && ($return = SimplePie_Misc::change_encoding_iconv($data, $input, $output))) { return $return; } + // This is last, as behaviour of this varies with OS userland and PHP version + elseif (class_exists('\UConverter') && ($return = SimplePie_Misc::change_encoding_uconverter($data, $input, $output))) + { + return $return; + } // If we can't do anything, just fail else { @@ -394,6 +398,17 @@ class SimplePie_Misc } /** + * @param string $data + * @param string $input + * @param string $output + * @return string|false + */ + protected static function change_encoding_uconverter($data, $input, $output) + { + return @\UConverter::transcode($data, $output, $input); + } + + /** * Normalize an encoding name * * This is automatically generated by create.php @@ -1947,7 +1962,7 @@ class SimplePie_Misc return (bool) preg_match('/^([A-Za-z0-9\-._~\x{A0}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFEF}\x{10000}-\x{1FFFD}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}\x{40000}-\x{4FFFD}\x{50000}-\x{5FFFD}\x{60000}-\x{6FFFD}\x{70000}-\x{7FFFD}\x{80000}-\x{8FFFD}\x{90000}-\x{9FFFD}\x{A0000}-\x{AFFFD}\x{B0000}-\x{BFFFD}\x{C0000}-\x{CFFFD}\x{D0000}-\x{DFFFD}\x{E1000}-\x{EFFFD}!$&\'()*+,;=@]|(%[0-9ABCDEF]{2}))+$/u', $string); } - public static function space_seperated_tokens($string) + public static function space_separated_tokens($string) { $space_characters = "\x20\x09\x0A\x0B\x0C\x0D"; $string_length = strlen($string); @@ -2178,7 +2193,8 @@ function embed_wmedia(width, height, link) { /** * Get the SimplePie build timestamp * - * Return SimplePie.php modification time. + * Uses the git index if it exists, otherwise uses the modification time + * of the newest file. */ public static function get_build() { diff --git a/lib/SimplePie/SimplePie/Net/IPv6.php b/lib/SimplePie/SimplePie/Net/IPv6.php index 2ff1afc90..47658aff2 100644 --- a/lib/SimplePie/SimplePie/Net/IPv6.php +++ b/lib/SimplePie/SimplePie/Net/IPv6.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Parse/Date.php b/lib/SimplePie/SimplePie/Parse/Date.php index 50bb5cffa..1f2156655 100644 --- a/lib/SimplePie/SimplePie/Parse/Date.php +++ b/lib/SimplePie/SimplePie/Parse/Date.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -331,8 +330,8 @@ class SimplePie_Parse_Date 'CCT' => 23400, 'CDT' => -18000, 'CEDT' => 7200, - 'CET' => 3600, 'CEST' => 7200, + 'CET' => 3600, 'CGST' => -7200, 'CGT' => -10800, 'CHADT' => 49500, @@ -631,7 +630,7 @@ class SimplePie_Parse_Date /** * Parse a superset of W3C-DTF (allows hyphens and colons to be omitted, as * well as allowing any of upper or lower case "T", horizontal tabs, or - * spaces to be used as the time seperator (including more than one)) + * spaces to be used as the time separator (including more than one)) * * @access protected * @return int Timestamp @@ -691,7 +690,7 @@ class SimplePie_Parse_Date } // Convert the number of seconds to an integer, taking decimals into account - $second = round($match[6] + $match[7] / pow(10, strlen($match[7]))); + $second = round((int)$match[6] + (int)$match[7] / pow(10, strlen($match[7]))); return gmmktime($match[4], $match[5], $second, $match[2], $match[3], $match[1]) - $timezone; } diff --git a/lib/SimplePie/SimplePie/Parser.php b/lib/SimplePie/SimplePie/Parser.php index 7fb7bd9be..9348382ad 100644 --- a/lib/SimplePie/SimplePie/Parser.php +++ b/lib/SimplePie/SimplePie/Parser.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -435,4 +434,231 @@ class SimplePie_Parser } return $cache[$string]; } -} + + private function parse_hcard($data, $category = false) { + $name = ''; + $link = ''; + // Check if h-card is set and pass that information on in the link. + if (isset($data['type']) && in_array('h-card', $data['type'])) { + if (isset($data['properties']['name'][0])) { + $name = $data['properties']['name'][0]; + } + if (isset($data['properties']['url'][0])) { + $link = $data['properties']['url'][0]; + if ($name === '') { + $name = $link; + } + else { + // can't have commas in categories. + $name = str_replace(',', '', $name); + } + $person_tag = $category ? '<span class="person-tag"></span>' : ''; + return '<a class="h-card" href="'.$link.'">'.$person_tag.$name.'</a>'; + } + } + return isset($data['value']) ? $data['value'] : ''; + } + + private function parse_microformats(&$data, $url) { + $feed_title = ''; + $feed_author = NULL; + $author_cache = array(); + $items = array(); + $entries = array(); + $mf = Mf2\parse($data, $url); + // First look for an h-feed. + $h_feed = array(); + foreach ($mf['items'] as $mf_item) { + if (in_array('h-feed', $mf_item['type'])) { + $h_feed = $mf_item; + break; + } + // Also look for an h-feed in the children of each top level item. + if (!isset($mf_item['children'][0]['type'])) continue; + if (in_array('h-feed', $mf_item['children'][0]['type'])) { + $h_feed = $mf_item['children'][0]; + // In this case the parent of the h-feed may be an h-card, so use it as + // the feed_author. + if (in_array('h-card', $mf_item['type'])) $feed_author = $mf_item; + break; + } + } + if (isset($h_feed['children'])) { + $entries = $h_feed['children']; + // Also set the feed title and store author from the h-feed if available. + if (isset($mf['items'][0]['properties']['name'][0])) { + $feed_title = $mf['items'][0]['properties']['name'][0]; + } + if (isset($mf['items'][0]['properties']['author'][0])) { + $feed_author = $mf['items'][0]['properties']['author'][0]; + } + } + else { + $entries = $mf['items']; + } + for ($i = 0; $i < count($entries); $i++) { + $entry = $entries[$i]; + if (in_array('h-entry', $entry['type'])) { + $item = array(); + $title = ''; + $description = ''; + if (isset($entry['properties']['url'][0])) { + $link = $entry['properties']['url'][0]; + if (isset($link['value'])) $link = $link['value']; + $item['link'] = array(array('data' => $link)); + } + if (isset($entry['properties']['uid'][0])) { + $guid = $entry['properties']['uid'][0]; + if (isset($guid['value'])) $guid = $guid['value']; + $item['guid'] = array(array('data' => $guid)); + } + if (isset($entry['properties']['name'][0])) { + $title = $entry['properties']['name'][0]; + if (isset($title['value'])) $title = $title['value']; + $item['title'] = array(array('data' => $title)); + } + if (isset($entry['properties']['author'][0]) || isset($feed_author)) { + // author is a special case, it can be plain text or an h-card array. + // If it's plain text it can also be a url that should be followed to + // get the actual h-card. + $author = isset($entry['properties']['author'][0]) ? + $entry['properties']['author'][0] : $feed_author; + if (!is_string($author)) { + $author = $this->parse_hcard($author); + } + else if (strpos($author, 'http') === 0) { + if (isset($author_cache[$author])) { + $author = $author_cache[$author]; + } + else { + $mf = Mf2\fetch($author); + foreach ($mf['items'] as $hcard) { + // Only interested in an h-card by itself in this case. + if (!in_array('h-card', $hcard['type'])) { + continue; + } + // It must have a url property matching what we fetched. + if (!isset($hcard['properties']['url']) || + !(in_array($author, $hcard['properties']['url']))) { + continue; + } + // Save parse_hcard the trouble of finding the correct url. + $hcard['properties']['url'][0] = $author; + // Cache this h-card for the next h-entry to check. + $author_cache[$author] = $this->parse_hcard($hcard); + $author = $author_cache[$author]; + break; + } + } + } + $item['author'] = array(array('data' => $author)); + } + if (isset($entry['properties']['photo'][0])) { + // If a photo is also in content, don't need to add it again here. + $content = ''; + if (isset($entry['properties']['content'][0]['html'])) { + $content = $entry['properties']['content'][0]['html']; + } + $photo_list = array(); + for ($j = 0; $j < count($entry['properties']['photo']); $j++) { + $photo = $entry['properties']['photo'][$j]; + if (strpos($content, $photo) === false) { + $photo_list[] = $photo; + } + } + // When there's more than one photo show the first and use a lightbox. + $count = count($photo_list); + if ($count > 1) { + $description = '<p>'; + for ($j = 0; $j < $count; $j++) { + $hidden = $j === 0 ? '' : 'class="hidden" '; + $description .= '<a href="'.$photo_list[$j].'" '.$hidden. + 'data-lightbox="image-set-'.$i.'">'. + '<img src="'.$photo_list[$j].'"></a>'; + } + $description .= '<br><b>'.$count.' photos</b></p>'; + } + else if ($count == 1) { + $description = '<p><img src="'.$photo_list[0].'"></p>'; + } + } + if (isset($entry['properties']['content'][0]['html'])) { + // e-content['value'] is the same as p-name when they are on the same + // element. Use this to replace title with a strip_tags version so + // that alt text from images is not included in the title. + if ($entry['properties']['content'][0]['value'] === $title) { + $title = strip_tags($entry['properties']['content'][0]['html']); + $item['title'] = array(array('data' => $title)); + } + $description .= $entry['properties']['content'][0]['html']; + if (isset($entry['properties']['in-reply-to'][0]['value'])) { + $in_reply_to = $entry['properties']['in-reply-to'][0]['value']; + $description .= '<p><span class="in-reply-to"></span> '. + '<a href="'.$in_reply_to.'">'.$in_reply_to.'</a><p>'; + } + $item['description'] = array(array('data' => $description)); + } + if (isset($entry['properties']['category'])) { + $category_csv = ''; + // Categories can also contain h-cards. + foreach ($entry['properties']['category'] as $category) { + if ($category_csv !== '') $category_csv .= ', '; + if (is_string($category)) { + // Can't have commas in categories. + $category_csv .= str_replace(',', '', $category); + } + else { + $category_csv .= $this->parse_hcard($category, true); + } + } + $item['category'] = array(array('data' => $category_csv)); + } + if (isset($entry['properties']['published'][0])) { + $timestamp = strtotime($entry['properties']['published'][0]); + $pub_date = date('F j Y g:ia', $timestamp).' GMT'; + $item['pubDate'] = array(array('data' => $pub_date)); + } + // The title and description are set to the empty string to represent + // a deleted item (which also makes it an invalid rss item). + if (isset($entry['properties']['deleted'][0])) { + $item['title'] = array(array('data' => '')); + $item['description'] = array(array('data' => '')); + } + $items[] = array('child' => array('' => $item)); + } + } + // Mimic RSS data format when storing microformats. + $link = array(array('data' => $url)); + $image = ''; + if (!is_string($feed_author) && + isset($feed_author['properties']['photo'][0])) { + $image = array(array('child' => array('' => array('url' => + array(array('data' => $feed_author['properties']['photo'][0])))))); + } + // Use the a name given for the h-feed, or get the title from the html. + if ($feed_title !== '') { + $feed_title = array(array('data' => htmlspecialchars($feed_title))); + } + else if ($position = strpos($data, '<title>')) { + $start = $position < 200 ? 0 : $position - 200; + $check = substr($data, $start, 400); + $matches = array(); + if (preg_match('/<title>(.+)<\/title>/', $check, $matches)) { + $feed_title = array(array('data' => htmlspecialchars($matches[1]))); + } + } + $channel = array('channel' => array(array('child' => array('' => + array('link' => $link, 'image' => $image, 'title' => $feed_title, + 'item' => $items))))); + $rss = array(array('attribs' => array('' => array('version' => '2.0')), + 'child' => array('' => $channel))); + $this->data = array('child' => array('' => array('rss' => $rss))); + return true; + } + + private function declare_html_entities() { + // This is required because the RSS specification says that entity-encoded + // html is allowed, but the xml specification says they must be declared. + return '<!DOCTYPE html [ <!ENTITY nbsp " "> <!ENTITY iexcl "¡"> <!ENTITY cent "¢"> <!ENTITY pound "£"> <!ENTITY curren "¤"> <!ENTITY yen "¥"> <!ENTITY brvbar "¦"> <!ENTITY sect "§"> <!ENTITY uml "¨"> <!ENTITY copy "©"> <!ENTITY ordf "ª"> <!ENTITY laquo "«"> <!ENTITY not "¬"> <!ENTITY shy "­"> <!ENTITY reg "®"> <!ENTITY macr "¯"> <!ENTITY deg "°"> <!ENTITY plusmn "±"> <!ENTITY sup2 "²"> <!ENTITY sup3 "³"> <!ENTITY acute "´"> <!ENTITY micro "µ"> <!ENTITY para "¶"> <!ENTITY middot "·"> <!ENTITY cedil "¸"> <!ENTITY sup1 "¹"> <!ENTITY ordm "º"> <!ENTITY raquo "»"> <!ENTITY frac14 "¼"> <!ENTITY frac12 "½"> <!ENTITY frac34 "¾"> <!ENTITY iquest "¿"> <!ENTITY Agrave "À"> <!ENTITY Aacute "Á"> <!ENTITY Acirc "Â"> <!ENTITY Atilde "Ã"> <!ENTITY Auml "Ä"> <!ENTITY Aring "Å"> <!ENTITY AElig "Æ"> <!ENTITY Ccedil "Ç"> <!ENTITY Egrave "È"> <!ENTITY Eacute "É"> <!ENTITY Ecirc "Ê"> <!ENTITY Euml "Ë"> <!ENTITY Igrave "Ì"> <!ENTITY Iacute "Í"> <!ENTITY Icirc "Î"> <!ENTITY Iuml "Ï"> <!ENTITY ETH "Ð"> <!ENTITY Ntilde "Ñ"> <!ENTITY Ograve "Ò"> <!ENTITY Oacute "Ó"> <!ENTITY Ocirc "Ô"> <!ENTITY Otilde "Õ"> <!ENTITY Ouml "Ö"> <!ENTITY times "×"> <!ENTITY Oslash "Ø"> <!ENTITY Ugrave "Ù"> <!ENTITY Uacute "Ú"> <!ENTITY Ucirc "Û"> <!ENTITY Uuml "Ü"> <!ENTITY Yacute "Ý"> <!ENTITY THORN "Þ"> <!ENTITY szlig "ß"> <!ENTITY agrave "à"> <!ENTITY aacute "á"> <!ENTITY acirc "â"> <!ENTITY atilde "ã"> <!ENTITY auml "ä"> <!ENTITY aring "å"> <!ENTITY aelig "æ"> <!ENTITY ccedil "ç"> <!ENTITY egrave "è"> <!ENTITY eacute "é"> <!ENTITY ecirc "ê"> <!ENTITY euml "ë"> <!ENTITY igrave "ì"> <!ENTITY iacute "í"> <!ENTITY icirc "î"> <!ENTITY iuml "ï"> <!ENTITY eth "ð"> <!ENTITY ntilde "ñ"> <!ENTITY ograve "ò"> <!ENTITY oacute "ó"> <!ENTITY ocirc "ô"> <!ENTITY otilde "õ"> <!ENTITY ouml "ö"> <!ENTITY divide "÷"> <!ENTITY oslash "ø"> <!ENTITY ugrave "ù"> <!ENTITY uacute "ú"> <!ENTITY ucirc "û"> <!ENTITY uuml "ü"> <!ENTITY yacute "ý"> <!ENTITY thorn "þ"> <!ENTITY yuml "ÿ"> <!ENTITY OElig "Œ"> <!ENTITY oelig "œ"> <!ENTITY Scaron "Š"> <!ENTITY scaron "š"> <!ENTITY Yuml "Ÿ"> <!ENTITY fnof "ƒ"> <!ENTITY circ "ˆ"> <!ENTITY tilde "˜"> <!ENTITY Alpha "Α"> <!ENTITY Beta "Β"> <!ENTITY Gamma "Γ"> <!ENTITY Epsilon "Ε"> <!ENTITY Zeta "Ζ"> <!ENTITY Eta "Η"> <!ENTITY Theta "Θ"> <!ENTITY Iota "Ι"> <!ENTITY Kappa "Κ"> <!ENTITY Lambda "Λ"> <!ENTITY Mu "Μ"> <!ENTITY Nu "Ν"> <!ENTITY Xi "Ξ"> <!ENTITY Omicron "Ο"> <!ENTITY Pi "Π"> <!ENTITY Rho "Ρ"> <!ENTITY Sigma "Σ"> <!ENTITY Tau "Τ"> <!ENTITY Upsilon "Υ"> <!ENTITY Phi "Φ"> <!ENTITY Chi "Χ"> <!ENTITY Psi "Ψ"> <!ENTITY Omega "Ω"> <!ENTITY alpha "α"> <!ENTITY beta "β"> <!ENTITY gamma "γ"> <!ENTITY delta "δ"> <!ENTITY epsilon "ε"> <!ENTITY zeta "ζ"> <!ENTITY eta "η"> <!ENTITY theta "θ"> <!ENTITY iota "ι"> <!ENTITY kappa "κ"> <!ENTITY lambda "λ"> <!ENTITY mu "μ"> <!ENTITY nu "ν"> <!ENTITY xi "ξ"> <!ENTITY omicron "ο"> <!ENTITY pi "π"> <!ENTITY rho "ρ"> <!ENTITY sigmaf "ς"> <!ENTITY sigma "σ"> <!ENTITY tau "τ"> <!ENTITY upsilon "υ"> <!ENTITY phi "φ"> <!ENTITY chi "χ"> <!ENTITY psi "ψ"> <!ENTITY omega "ω"> <!ENTITY thetasym "ϑ"> <!ENTITY upsih "ϒ"> <!ENTITY piv "ϖ"> <!ENTITY ensp " "> <!ENTITY emsp " "> <!ENTITY thinsp " "> <!ENTITY zwnj "‌"> <!ENTITY zwj "‍"> <!ENTITY lrm "‎"> <!ENTITY rlm "‏"> <!ENTITY ndash "–"> <!ENTITY mdash "—"> <!ENTITY lsquo "‘"> <!ENTITY rsquo "’"> <!ENTITY sbquo "‚"> <!ENTITY ldquo "“"> <!ENTITY rdquo "”"> <!ENTITY bdquo "„"> <!ENTITY dagger "†"> <!ENTITY Dagger "‡"> <!ENTITY bull "•"> <!ENTITY hellip "…"> <!ENTITY permil "‰"> <!ENTITY prime "′"> <!ENTITY Prime "″"> <!ENTITY lsaquo "‹"> <!ENTITY rsaquo "›"> <!ENTITY oline "‾"> <!ENTITY frasl "⁄"> <!ENTITY euro "€"> <!ENTITY image "ℑ"> <!ENTITY weierp "℘"> <!ENTITY real "ℜ"> <!ENTITY trade "™"> <!ENTITY alefsym "ℵ"> <!ENTITY larr "←"> <!ENTITY uarr "↑"> <!ENTITY rarr "→"> <!ENTITY darr "↓"> <!ENTITY harr "↔"> <!ENTITY crarr "↵"> <!ENTITY lArr "⇐"> <!ENTITY uArr "⇑"> <!ENTITY rArr "⇒"> <!ENTITY dArr "⇓"> <!ENTITY hArr "⇔"> <!ENTITY forall "∀"> <!ENTITY part "∂"> <!ENTITY exist "∃"> <!ENTITY empty "∅"> <!ENTITY nabla "∇"> <!ENTITY isin "∈"> <!ENTITY notin "∉"> <!ENTITY ni "∋"> <!ENTITY prod "∏"> <!ENTITY sum "∑"> <!ENTITY minus "−"> <!ENTITY lowast "∗"> <!ENTITY radic "√"> <!ENTITY prop "∝"> <!ENTITY infin "∞"> <!ENTITY ang "∠"> <!ENTITY and "∧"> <!ENTITY or "∨"> <!ENTITY cap "∩"> <!ENTITY cup "∪"> <!ENTITY int "∫"> <!ENTITY there4 "∴"> <!ENTITY sim "∼"> <!ENTITY cong "≅"> <!ENTITY asymp "≈"> <!ENTITY ne "≠"> <!ENTITY equiv "≡"> <!ENTITY le "≤"> <!ENTITY ge "≥"> <!ENTITY sub "⊂"> <!ENTITY sup "⊃"> <!ENTITY nsub "⊄"> <!ENTITY sube "⊆"> <!ENTITY supe "⊇"> <!ENTITY oplus "⊕"> <!ENTITY otimes "⊗"> <!ENTITY perp "⊥"> <!ENTITY sdot "⋅"> <!ENTITY lceil "⌈"> <!ENTITY rceil "⌉"> <!ENTITY lfloor "⌊"> <!ENTITY rfloor "⌋"> <!ENTITY lang "〈"> <!ENTITY rang "〉"> <!ENTITY loz "◊"> <!ENTITY spades "♠"> <!ENTITY clubs "♣"> <!ENTITY hearts "♥"> <!ENTITY diams "♦"> ]>'; + } +}
\ No newline at end of file diff --git a/lib/SimplePie/SimplePie/Rating.php b/lib/SimplePie/SimplePie/Rating.php index b5fe80516..eaf57080c 100644 --- a/lib/SimplePie/SimplePie/Rating.php +++ b/lib/SimplePie/SimplePie/Rating.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Registry.php b/lib/SimplePie/SimplePie/Registry.php index dac55e34e..e0909bb74 100755 --- a/lib/SimplePie/SimplePie/Registry.php +++ b/lib/SimplePie/SimplePie/Registry.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Restriction.php b/lib/SimplePie/SimplePie/Restriction.php index a1d59916d..001a5cd28 100644 --- a/lib/SimplePie/SimplePie/Restriction.php +++ b/lib/SimplePie/SimplePie/Restriction.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/Sanitize.php b/lib/SimplePie/SimplePie/Sanitize.php index bdc601100..c55ee50b7 100644 --- a/lib/SimplePie/SimplePie/Sanitize.php +++ b/lib/SimplePie/SimplePie/Sanitize.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue @@ -61,8 +60,8 @@ class SimplePie_Sanitize var $image_handler = ''; var $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); var $encode_instead_of_strip = false; - var $strip_attributes = array('bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc'); - var $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); //FreshRSS + var $strip_attributes = array('bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc'); + var $add_attributes = array('audio' => array('preload' => 'none'), 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), 'video' => array('preload' => 'none')); var $strip_comments = false; var $output_encoding = 'UTF-8'; var $enable_cache = true; @@ -170,7 +169,7 @@ class SimplePie_Sanitize $this->encode_instead_of_strip = (bool) $encode; } - public function strip_attributes($attribs = array('bgsound', 'class', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc')) + public function strip_attributes($attribs = array('bgsound', 'expr', 'id', 'style', 'onclick', 'onerror', 'onfinish', 'onmouseover', 'onmouseout', 'onfocus', 'onblur', 'lowsrc', 'dynsrc')) { if ($attribs) { @@ -323,7 +322,7 @@ class SimplePie_Sanitize { if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML) { - $data = htmlspecialchars_decode($data, ENT_QUOTES); + $data = htmlspecialchars_decode($data, ENT_QUOTES); //FreshRSS if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data)) { $type |= SIMPLEPIE_CONSTRUCT_HTML; @@ -348,6 +347,7 @@ class SimplePie_Sanitize } $document = new DOMDocument(); $document->encoding = 'UTF-8'; + $data = $this->preprocess($data, $type); set_error_handler(array('SimplePie_Misc', 'silence_errors')); @@ -437,19 +437,17 @@ class SimplePie_Sanitize } } - // Remove the DOCTYPE - // Seems to cause segfaulting if we don't do this - if ($document->firstChild instanceof DOMDocumentType) + // Get content node + $div = $document->getElementsByTagName('body')->item(0)->firstChild; + // Finally, convert to a HTML string + if (version_compare(PHP_VERSION, '5.3.6', '>=')) { - $document->removeChild($document->firstChild); + $data = trim($document->saveHTML($div)); + } + else + { + $data = trim($document->saveXML($div)); } - - // Move everything from the body to the root - $real_body = $document->getElementsByTagName('body')->item(0)->childNodes->item(0); - $document->replaceChild($real_body, $document->firstChild); - - // Finally, convert to a HTML string - $data = trim($document->saveHTML()); if ($this->remove_div) { diff --git a/lib/SimplePie/SimplePie/Source.php b/lib/SimplePie/SimplePie/Source.php index 2613798fd..1a66a392d 100644 --- a/lib/SimplePie/SimplePie/Source.php +++ b/lib/SimplePie/SimplePie/Source.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/XML/Declaration/Parser.php b/lib/SimplePie/SimplePie/XML/Declaration/Parser.php index 589e452a2..99e751672 100644 --- a/lib/SimplePie/SimplePie/XML/Declaration/Parser.php +++ b/lib/SimplePie/SimplePie/XML/Declaration/Parser.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/SimplePie/SimplePie/gzdecode.php b/lib/SimplePie/SimplePie/gzdecode.php index 6e65f0811..0e8bc8fc6 100644 --- a/lib/SimplePie/SimplePie/gzdecode.php +++ b/lib/SimplePie/SimplePie/gzdecode.php @@ -5,7 +5,7 @@ * A PHP-Based RSS and Atom Feed Framework. * Takes the hard work out of managing a complete RSS/Atom solution. * - * Copyright (c) 2004-2012, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors + * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are @@ -33,8 +33,7 @@ * POSSIBILITY OF SUCH DAMAGE. * * @package SimplePie - * @version 1.4-dev - * @copyright 2004-2012 Ryan Parman, Geoffrey Sneddon, Ryan McCue + * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue * @author Ryan Parman * @author Geoffrey Sneddon * @author Ryan McCue diff --git a/lib/favicons.php b/lib/favicons.php index d8c97964e..48f7c9fda 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -1,43 +1,109 @@ <?php - -include(LIB_PATH . '/Favicon/Favicon.php'); -include(LIB_PATH . '/Favicon/DataAccess.php'); - $favicons_dir = DATA_PATH . '/favicons/'; $default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico'; -function download_favicon($website, $dest) { - global $favicons_dir, $default_favicon; - - syslog(LOG_INFO, 'FreshRSS Favicon discovery GET ' . $website); - $favicon_getter = new \Favicon\Favicon(); - $favicon_getter->setCacheDir($favicons_dir); - $favicon_url = $favicon_getter->get($website); - - if ($favicon_url === false) { - return @copy($default_favicon, $dest); +function isImgMime($content) { + //Based on https://github.com/ArthurHoaro/favicon/blob/3a4f93da9bb24915b21771eb7873a21bde26f5d1/src/Favicon/Favicon.php#L311-L319 + if ($content == '') { + return false; } + if (!extension_loaded('fileinfo')) { + return true; + } + $isImage = true; + try { + $fInfo = finfo_open(FILEINFO_MIME_TYPE); + $isImage = strpos(finfo_buffer($fInfo, $content), 'image') !== false; + finfo_close($fInfo); + } catch (Exception $e) { + } + return $isImage; +} - syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $favicon_url); - $c = curl_init($favicon_url); - curl_setopt($c, CURLOPT_HEADER, false); - curl_setopt($c, CURLOPT_RETURNTRANSFER, true); - curl_setopt($c, CURLOPT_BINARYTRANSFER, true); - curl_setopt($c, CURLOPT_USERAGENT, 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')'); - $img_raw = curl_exec($c); - $status_code = curl_getinfo($c, CURLINFO_HTTP_CODE); - curl_close($c); +function downloadHttp(&$url, $curlOptions = array()) { + syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url); + if (substr($url, 0, 2) === '//') { + $url = 'https:' . $favicon; + } + if ($url == '' || filter_var($url, FILTER_VALIDATE_URL) === false) { + return ''; + } + $ch = curl_init($url); + curl_setopt_array($ch, array( + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', + )); + if (defined('CURLOPT_ENCODING')) { + curl_setopt($ch, CURLOPT_ENCODING, ''); //Enable all encodings + } + curl_setopt_array($ch, $curlOptions); + $response = curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + if (!empty($info['url']) && (filter_var($info['url'], FILTER_VALIDATE_URL) !== false)) { + $url = $info['url']; //Possible redirect + } + return $info['http_code'] == 200 ? $response : ''; +} - if ($status_code === 200) { - $file = fopen($dest, 'w'); - if ($file !== false) { - fwrite($file, $img_raw); - fclose($file); - return true; +function searchFavicon(&$url) { + $dom = new DOMDocument(); + $html = downloadHttp($url); + if ($html != '' && @$dom->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) { + $rels = array('shortcut icon', 'icon'); + $links = $dom->getElementsByTagName('link'); + foreach ($rels as $rel) { + foreach ($links as $link) { + if ($link->hasAttribute('rel') && $link->hasAttribute('href') && + strtolower(trim($link->getAttribute('rel'))) === $rel) { + $href = trim($link->getAttribute('href')); + if (substr($href, 0, 2) === '//') { + // Case of protocol-relative URLs + if (preg_match('%^(https?:)//%i', $url, $matches)) { + $href = $matches[1] . $href; + } else { + $href = 'https:' . $href; + } + } + if (filter_var($href, FILTER_VALIDATE_URL) === false) { + $href = SimplePie_IRI::absolutize($url, $href); + } + $favicon = downloadHttp($href, array( + CURLOPT_REFERER => $url, + )); + if (isImgMime($favicon)) { + return $favicon; + } + } + } } - } else { - syslog(LOG_WARNING, 'FreshRSS Favicon GET ' . $favicon_url . ' error ' . $status_code); } + return ''; +} - return false; +function download_favicon($url, $dest) { + global $default_favicon; + $url = trim($url); + $favicon = searchFavicon($url); + if ($favicon == '') { + $rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url); + if ($rootUrl != $url) { + $url = $rootUrl; + $favicon = searchFavicon($url); + } + if ($favicon == '') { + $link = $rootUrl . 'favicon.ico'; + $favicon = downloadHttp($link, array( + CURLOPT_REFERER => $url, + )); + if (!isImgMime($favicon)) { + $favicon = ''; + } + } + } + return ($favicon != '' && file_put_contents($dest, $favicon)) || + @copy($default_favicon, $dest); } diff --git a/lib/lib_install.php b/lib/lib_install.php index 76871c98a..bf81c15b4 100644 --- a/lib/lib_install.php +++ b/lib/lib_install.php @@ -2,8 +2,8 @@ define('BCRYPT_COST', 9); -Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php')); -Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php')); +Minz_Configuration::register('default_system', join_path(FRESHRSS_PATH, 'config.default.php')); +Minz_Configuration::register('default_user', join_path(FRESHRSS_PATH, 'config-user.default.php')); function checkRequirements($dbType = '') { $php = version_compare(PHP_VERSION, '5.3.3') >= 0; @@ -67,7 +67,7 @@ function checkRequirements($dbType = '') { 'favicons' => $favicons ? 'ok' : 'ko', 'http_referer' => $http_referer ? 'ok' : 'ko', 'message' => $message ?: 'ok', - 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $fileinfo && $dom && $xml && + 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml && $data && $cache && $users && $favicons && $http_referer && $message == '' ? 'ok' : 'ko' ); diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 4298e90bf..22136854e 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -183,7 +183,7 @@ function customSimplePie() { 'object', 'param', 'plaintext', 'script', 'style', )); $simplePie->strip_attributes(array_merge($simplePie->strip_attributes, array( - 'autoplay', 'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', + 'autoplay', 'class', 'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur', 'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless', 'sizes', 'srcset'))); $simplePie->add_attributes(array( @@ -214,7 +214,7 @@ function customSimplePie() { ), )); $https_domains = array(); - $force = @file(DATA_PATH . '/force-https.default.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $force = @file(FRESHRSS_PATH . '/force-https.default.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (is_array($force)) { $https_domains = array_merge($https_domains, $force); } @@ -334,11 +334,14 @@ function max_registrations_reached() { * @return a Minz_Configuration object, null if the configuration cannot be loaded. */ function get_user_configuration($username) { + if (!FreshRSS_user_Controller::checkUsername($username)) { + return null; + } $namespace = 'user_' . $username; try { Minz_Configuration::register($namespace, join_path(USERS_PATH, $username, 'config.php'), - join_path(USERS_PATH, '_', 'config.default.php')); + join_path(FRESHRSS_PATH, 'config-user.default.php')); } catch (Minz_ConfigurationNamespaceException $e) { // namespace already exists, do nothing. } catch (Minz_FileNotExistException $e) { diff --git a/p/api/greader.php b/p/api/greader.php index 01eca6d4f..e1f4202a7 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -695,7 +695,7 @@ $pathInfos = explode('/', $pathInfo); 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'); if (!FreshRSS_Context::$system_conf->api_enabled) { serviceUnavailable(); diff --git a/p/api/index.php b/p/api/index.php index 3ab4e02b3..580c90255 100644 --- a/p/api/index.php +++ b/p/api/index.php @@ -16,7 +16,7 @@ <dd><?php require('../../constants.php'); require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader -Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php'); +Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php'); echo Minz_Url::display('/api/greader.php', 'html', true); ?></dd> </dl> diff --git a/p/api/pshb.php b/p/api/pshb.php index e9b66b167..4b546908a 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -23,8 +23,13 @@ if (!ctype_xdigit($key)) { chdir(PSHB_PATH); $canonical64 = @file_get_contents('keys/' . $key . '.txt'); if ($canonical64 === false) { + if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'unsubscribe') { + logMe('Warning: Accept unknown unsubscribe'); + header('Connection: close'); + exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); + } header('HTTP/1.1 404 Not Found'); - logMe('Error: Feed key not found!: ' . $key); + logMe('Warning: Feed key not found!: ' . $key); die('Feed key not found!'); } $canonical64 = trim($canonical64); @@ -36,7 +41,7 @@ if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { $hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json'); if ($hubFile === false) { header('HTTP/1.1 404 Not Found'); - //@unlink('keys/' . $key . '.txt'); + unlink('keys/' . $key . '.txt'); logMe('Error: Feed info not found!: ' . $canonical64); die('Feed info not found!'); } @@ -50,8 +55,19 @@ chdir('feeds/' . $canonical64); $users = glob('*.txt', GLOB_NOSORT); if (empty($users)) { header('HTTP/1.1 410 Gone'); - logMe('Error: Nobody is subscribed to this feed anymore!: ' . $canonical64); - die('Nobody is subscribed to this feed anymore!'); + $url = base64url_decode($canonical64); + logMe('Warning: Nobody subscribes to this feed anymore!: ' . $url); + unlink('../../keys/' . $key . '.txt'); + Minz_Configuration::register('system', + DATA_PATH . '/config.php', + FRESHRSS_PATH . '/config.default.php'); + FreshRSS_Context::$system_conf = Minz_Configuration::get('system'); + $feed = new FreshRSS_Feed($url); + $feed->pubSubHubbubSubscribe(false); + unlink('!hub.json'); + chdir('..'); + recursive_unlink($canonical64); + die('Nobody subscribes to this feed anymore!'); } if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { @@ -85,7 +101,7 @@ if ($ORIGINAL_INPUT == '') { die('Missing XML payload!'); } -Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php'); +Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php'); $system_conf = Minz_Configuration::get('system'); $system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) @@ -108,22 +124,27 @@ $nb = 0; foreach ($users as $userFilename) { $username = basename($userFilename, '.txt'); if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) { - break; + logMe('Warning: Removing broken user link: ' . $username . ' for ' . $self); + unlink($userFilename); + continue; } try { Minz_Session::_param('currentUser', $username); Minz_Configuration::register('user', join_path(USERS_PATH, $username, 'config.php'), - join_path(USERS_PATH, '_', 'config.default.php')); + join_path(FRESHRSS_PATH, 'config-user.default.php')); new Minz_ModelPdo($username); //TODO: FIXME: Quick-fix while waiting for a better FreshRSS() constructor/init FreshRSS_Context::init(); - list($updated_feeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie); - if ($updated_feeds > 0) { + list($updated_feeds, $feed, $nb_new_articles) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie); + if ($updated_feeds > 0 || $feed != false) { $nb++; + } else { + logMe('Warning: User ' . $username . ' does not subscribe anymore to ' . $self); + unlink($userFilename); } } catch (Exception $e) { - logMe('Error: ' . $e->getMessage()); + logMe('Error: ' . $e->getMessage() . ' for user ' . $username . ' and feed ' . $self); } } @@ -132,8 +153,8 @@ unset($simplePie); if ($nb === 0) { header('HTTP/1.1 410 Gone'); - logMe('Error: Nobody is subscribed to this feed anymore after all!: ' . $self); - die('Nobody is subscribed to this feed anymore after all!'); + logMe('Warning: Nobody subscribes to this feed anymore after all!: ' . $self); + die('Nobody subscribes to this feed anymore after all!'); } elseif (!empty($hubJson['error'])) { $hubJson['error'] = false; file_put_contents('./!hub.json', json_encode($hubJson)); @@ -1,6 +1,6 @@ <?php - require('../constants.php'); +require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader require(LIB_PATH . '/favicons.php'); require(LIB_PATH . '/http-conditional.php'); @@ -15,7 +15,6 @@ function show_default_favicon($cacheSeconds = 3600) { } } - $id = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '0'; if (!ctype_xdigit($id)) { $id = '0'; diff --git a/p/scripts/main.js b/p/scripts/main.js index 5dbb95c91..5be7bc36b 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -555,39 +555,42 @@ function init_shortcuts() { window.setTimeout(init_shortcuts, 200); return; } - // Touches de manipulation + // Manipulation shortcuts shortcut.add(shortcuts.mark_read, function () { - // on marque comme lu ou non lu + // Toggle the read state var active = $(".flux.current"); mark_read(active, false); }, { 'disable_in_input': true }); shortcut.add("shift+" + shortcuts.mark_read, function () { - // on marque tout comme lu + // Mark everything as read $(".nav_menu .read_all").click(); }, { 'disable_in_input': true }); shortcut.add(shortcuts.mark_favorite, function () { - // on marque comme favori ou non favori + // Toggle the favorite state var active = $(".flux.current"); mark_favorite(active); }, { 'disable_in_input': true }); shortcut.add(shortcuts.collapse_entry, function () { + // Toggle the collapse state collapse_entry(); }, { 'disable_in_input': true }); shortcut.add(shortcuts.auto_share, function () { + // Display the share options auto_share(); }, { 'disable_in_input': true }); shortcut.add(shortcuts.user_filter, function () { + // Display the user filters user_filter(); }, { 'disable_in_input': true @@ -606,7 +609,7 @@ function init_shortcuts() { }); } - // Touches de navigation pour les articles + // Entry navigation shortcuts shortcut.add(shortcuts.prev_entry, prev_entry, { 'disable_in_input': true }); @@ -633,7 +636,7 @@ function init_shortcuts() { }, { 'disable_in_input': true }); - // Touches de navigation pour les flux + // Feed navigation shortcuts shortcut.add("shift+" + shortcuts.prev_entry, prev_feed, { 'disable_in_input': true }); @@ -646,7 +649,7 @@ function init_shortcuts() { shortcut.add("shift+" + shortcuts.last_entry, last_feed, { 'disable_in_input': true }); - // Touches de navigation pour les categories + // Category navigation shortcuts shortcut.add("alt+" + shortcuts.prev_entry, prev_category, { 'disable_in_input': true }); @@ -661,7 +664,7 @@ function init_shortcuts() { }); shortcut.add(shortcuts.go_website, function () { - var url_website = $('.flux.current > .flux_header > .title > a').attr("href"); + var url_website = $('.flux.current a.go_website').attr("href"); if (context.auto_mark_site) { $(".flux.current").each(function () { @@ -705,6 +708,10 @@ function init_stream(divStream) { if ($(e.target).closest('.content, .item.website, .item.link').length > 0) { return; } + if (!context.sides_close_article && $(e.target).is('div.flux_content')) { + // setting for not-closing after clicking outside article area + return; + } var old_active = $(".flux.current"), new_active = $(this).parent(); isCollapsed = true; @@ -803,12 +810,12 @@ function updateFeed(feeds, feeds_count) { if (!feed) { return; } - $.ajax({ type: 'POST', url: feed.url, data : { _csrf: context.csrf, + noCommit: feeds.length > 0 ? 1 : 0, }, }).always(function (data) { feed_processed++; @@ -830,7 +837,6 @@ function init_actualize() { if (ajax_loading) { return false; } - ajax_loading = true; $.getJSON('./?c=javascript&a=actualize').done(function (data) { @@ -841,7 +847,16 @@ function init_actualize() { } if (data.feeds.length === 0) { openNotification(data.feedback_no_refresh, "good"); - ajax_loading = false; + $.ajax({ //Empty request to force refresh server database cache + type: 'POST', + url: './?c=feed&a=actualize&id=-1', + data : { + _csrf: context.csrf, + noCommit: 0, + }, + }).always(function (data) { + ajax_loading = false; + }); return; } //Progress bar diff --git a/p/themes/BlueLagoon/BlueLagoon.css b/p/themes/BlueLagoon/BlueLagoon.css index 27bae5404..150e27908 100644 --- a/p/themes/BlueLagoon/BlueLagoon.css +++ b/p/themes/BlueLagoon/BlueLagoon.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; font-size: 92%; } @@ -368,16 +368,13 @@ a.btn { .dropdown-header { display:none; } -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; - color: #ccc; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; color: #ccc; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: linear-gradient(180deg, #0090FF 0%, #0062BE 100%) #E4992C; diff --git a/p/themes/BlueLagoon/template.css b/p/themes/BlueLagoon/template.css index 8c1a4ed21..4bc0fb735 100644 --- a/p/themes/BlueLagoon/template.css +++ b/p/themes/BlueLagoon/template.css @@ -2,7 +2,7 @@ /*=== GENERAL */ /*============*/ -html, body { +html, body { margin: 0; padding: 0; font-size: 92%; @@ -79,6 +79,7 @@ textarea { input, select, textarea { display: inline-block; max-width: 100%; + font-size: 0.8rem; } input[type="radio"], input[type="checkbox"] { diff --git a/p/themes/Dark/dark.css b/p/themes/Dark/dark.css index f72ed8b5b..d8415ef25 100644 --- a/p/themes/Dark/dark.css +++ b/p/themes/Dark/dark.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #1c1c1c; color: #888; } @@ -334,14 +334,12 @@ a.btn { text-align: left; color: #888; } -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: #26303F; @@ -810,7 +808,7 @@ a.btn { /*=== "Load more" part */ #bigMarkAsRead { text-align: center; - text-decoration: none; + text-decoration: none; } #bigMarkAsRead:hover { background: #111; diff --git a/p/themes/Flat/flat.css b/p/themes/Flat/flat.css index 0672c6a38..0240fe4b4 100644 --- a/p/themes/Flat/flat.css +++ b/p/themes/Flat/flat.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; } @@ -334,14 +334,12 @@ a.btn { text-align: left; color: #34495e; } -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: #2980b9; diff --git a/p/themes/Origine-compact/origine-compact.css b/p/themes/Origine-compact/origine-compact.css index 87d04e93e..8447e2486 100644 --- a/p/themes/Origine-compact/origine-compact.css +++ b/p/themes/Origine-compact/origine-compact.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; } @@ -360,16 +360,12 @@ a.btn { font-weight: bold; text-align: left; } -.dropdown-menu > .item { -} -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: #0062BE; @@ -870,7 +866,7 @@ a.btn { /*=== "Load more" part */ #bigMarkAsRead { text-align: center; - text-decoration: none; + text-decoration: none; color: #666; background: #fafafa; font-size: 1.2em; diff --git a/p/themes/Origine/origine.css b/p/themes/Origine/origine.css index da461087b..becf3f433 100644 --- a/p/themes/Origine/origine.css +++ b/p/themes/Origine/origine.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; } @@ -358,16 +358,12 @@ a.btn { font-weight: bold; text-align: left; } -.dropdown-menu > .item { -} -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: #0062BE; @@ -853,7 +849,7 @@ a.btn { /*=== "Load more" part */ #bigMarkAsRead { text-align: center; - text-decoration: none; + text-decoration: none; text-shadow: 0 -1px 0 #aaa; color: #666; background: #fafafa; diff --git a/p/themes/Pafat/pafat.css b/p/themes/Pafat/pafat.css index abe808eab..23bc6671d 100644 --- a/p/themes/Pafat/pafat.css +++ b/p/themes/Pafat/pafat.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; color : #666; } @@ -146,8 +146,8 @@ form th { .stick .dropdown + .dropdown > .btn { border-left: none; -} - +} + .stick .btn + .dropdown > .btn { border-left: none; border-radius: 0 3px 3px 0; @@ -338,20 +338,13 @@ a.btn { font-weight: bold; text-align: left; } -.dropdown-menu > .item { -} - -.dropdown-menu > .item > a { - padding: 0 22px; - line-height: 2.5em; - color: #666; - font-size: 0.8rem; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; + color: #666; font-size: 0.8rem; } @@ -550,7 +543,7 @@ a.btn { text-decoration: none; color : #C5C6CA; } - + .header > .item.search input { width: 230px; height : 29px; @@ -861,7 +854,7 @@ a.btn { /*=== "Load more" part */ #bigMarkAsRead { text-align: center; - text-decoration: none; + text-decoration: none; color: #666; background: #fafafa; } diff --git a/p/themes/Screwdriver/screwdriver.css b/p/themes/Screwdriver/screwdriver.css index 9dbe523a1..b2c539b13 100644 --- a/p/themes/Screwdriver/screwdriver.css +++ b/p/themes/Screwdriver/screwdriver.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; background: #fafafa; font-size: 92%; } @@ -368,18 +368,13 @@ a.btn { .dropdown-header { display:none; } -.dropdown-menu > .item { -} -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; - color: #ccc; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; color: #ccc; + font-size: 0.8rem; } .dropdown-menu > .item:hover { background: #171717; diff --git a/p/themes/base-theme/base.css b/p/themes/base-theme/base.css index 192a957c9..1bf73d8b3 100644 --- a/p/themes/base-theme/base.css +++ b/p/themes/base-theme/base.css @@ -10,7 +10,7 @@ /*============*/ html, body { height: 100%; - font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", sans-serif; + font-family: "OpenSans", "Cantarell", "Helvetica", "Arial", "PingFang SC", "Microsoft YaHei", sans-serif; } /*=== Links */ @@ -251,14 +251,11 @@ a.btn { } .dropdown-menu > .item { } -.dropdown-menu > .item > a { - padding: 0 25px; - line-height: 2.5em; -} +.dropdown-menu > .item > a, .dropdown-menu > .item > span, .dropdown-menu > .item > .as-link { padding: 0 22px; - line-height: 2em; + line-height: 2.5em; } .dropdown-menu > .item:hover { } diff --git a/p/themes/base-theme/template.css b/p/themes/base-theme/template.css index cc36c3ffa..277bb61b2 100644 --- a/p/themes/base-theme/template.css +++ b/p/themes/base-theme/template.css @@ -88,6 +88,7 @@ input.extend:focus { input, select, textarea { display: inline-block; max-width: 100%; + font-size: 0.8rem; } input[type="radio"], input[type="checkbox"] { @@ -345,7 +346,7 @@ a.btn { /*=== Tree */ .tree { margin: 0; - padding: 0; + padding: 0 0 15em 0; list-style: none; text-align: left; } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..3dd9602be --- /dev/null +++ b/tests/README.md @@ -0,0 +1,7 @@ +# FreshRSS tests + +```sh +cd ./tests/ +wget https://phar.phpunit.de/phpunit.phar +php phpunit.phar --bootstrap bootstrap.php +``` diff --git a/tests/app/Models/CategoryTest.php b/tests/app/Models/CategoryTest.php index da439b785..2fd153aee 100644 --- a/tests/app/Models/CategoryTest.php +++ b/tests/app/Models/CategoryTest.php @@ -1,6 +1,6 @@ <?php -class FreshRSS_CategoryTest extends \PHPUnit_Framework_TestCase { +class FreshRSS_CategoryTest extends PHPUnit\Framework\TestCase { public function test__construct_whenNoParameters_createsObjectWithDefaultValues() { $category = new FreshRSS_Category(); diff --git a/tests/app/Models/ContextTest.php b/tests/app/Models/ContextTest.php deleted file mode 100644 index 4dc8b7757..000000000 --- a/tests/app/Models/ContextTest.php +++ /dev/null @@ -1,5 +0,0 @@ -<?php - -class ContextTest extends \PHPUnit_Framework_TestCase { - -} diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 73ff56cc6..4a7afc6f9 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -2,7 +2,7 @@ require_once(LIB_PATH . '/lib_date.php'); -class SearchTest extends \PHPUnit_Framework_TestCase { +class SearchTest extends PHPUnit\Framework\TestCase { /** * @dataProvider provideEmptyInput @@ -50,22 +50,22 @@ class SearchTest extends \PHPUnit_Framework_TestCase { */ public function provideIntitleSearch() { return array( - array('intitle:word1', 'word1', null), - array('intitle:word1 word2', 'word1', array('word2')), - array('intitle:"word1 word2"', 'word1 word2', null), - array("intitle:'word1 word2'", 'word1 word2', null), - array('word1 intitle:word2', 'word2', array('word1')), - array('word1 intitle:word2 word3', 'word2', array('word1', 'word3')), - array('word1 intitle:"word2 word3"', 'word2 word3', array('word1')), - array("word1 intitle:'word2 word3'", 'word2 word3', array('word1')), - array('intitle:word1 intitle:word2', 'word1', array('intitle:word2')), - array('intitle: word1 word2', null, array('word1', 'word2')), - array('intitle:123', '123', null), - array('intitle:"word1 word2" word3"', 'word1 word2', array('word3"')), - array("intitle:'word1 word2' word3'", 'word1 word2', array("word3'")), - array('intitle:"word1 word2\' word3"', "word1 word2' word3", null), - array("intitle:'word1 word2\" word3'", 'word1 word2" word3', null), - array("intitle:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), + array('intitle:word1', array('word1'), null), + array('intitle:word1 word2', array('word1'), array('word2')), + array('intitle:"word1 word2"', array('word1 word2'), null), + array("intitle:'word1 word2'", array('word1 word2'), null), + array('word1 intitle:word2', array('word2'), array('word1')), + array('word1 intitle:word2 word3', array('word2'), array('word1', 'word3')), + array('word1 intitle:"word2 word3"', array('word2 word3'), array('word1')), + array("word1 intitle:'word2 word3'", array('word2 word3'), array('word1')), + array('intitle:word1 intitle:word2', array('word1', 'word2'), null), + array('intitle: word1 word2', array(), array('word1', 'word2')), + array('intitle:123', array('123'), null), + array('intitle:"word1 word2" word3"', array('word1 word2'), array('word3"')), + array("intitle:'word1 word2' word3'", array('word1 word2'), array("word3'")), + array('intitle:"word1 word2\' word3"', array("word1 word2' word3"), null), + array("intitle:'word1 word2\" word3'", array('word1 word2" word3'), null), + array("intitle:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')), ); } @@ -86,22 +86,22 @@ class SearchTest extends \PHPUnit_Framework_TestCase { */ public function provideAuthorSearch() { return array( - array('author:word1', 'word1', null), - array('author:word1 word2', 'word1', array('word2')), - array('author:"word1 word2"', 'word1 word2', null), - array("author:'word1 word2'", 'word1 word2', null), - array('word1 author:word2', 'word2', array('word1')), - array('word1 author:word2 word3', 'word2', array('word1', 'word3')), - array('word1 author:"word2 word3"', 'word2 word3', array('word1')), - array("word1 author:'word2 word3'", 'word2 word3', array('word1')), - array('author:word1 author:word2', 'word1', array('author:word2')), - array('author: word1 word2', null, array('word1', 'word2')), - array('author:123', '123', null), - array('author:"word1 word2" word3"', 'word1 word2', array('word3"')), - array("author:'word1 word2' word3'", 'word1 word2', array("word3'")), - array('author:"word1 word2\' word3"', "word1 word2' word3", null), - array("author:'word1 word2\" word3'", 'word1 word2" word3', null), - array("author:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), + array('author:word1', array('word1'), null), + array('author:word1 word2', array('word1'), array('word2')), + array('author:"word1 word2"', array('word1 word2'), null), + array("author:'word1 word2'", array('word1 word2'), null), + array('word1 author:word2', array('word2'), array('word1')), + array('word1 author:word2 word3', array('word2'), array('word1', 'word3')), + array('word1 author:"word2 word3"', array('word2 word3'), array('word1')), + array("word1 author:'word2 word3'", array('word2 word3'), array('word1')), + array('author:word1 author:word2', array('word1', 'word2'), null), + array('author: word1 word2', array(), array('word1', 'word2')), + array('author:123', array('123'), null), + array('author:"word1 word2" word3"', array('word1 word2'), array('word3"')), + array("author:'word1 word2' word3'", array('word1 word2'), array("word3'")), + array('author:"word1 word2\' word3"', array("word1 word2' word3"), null), + array("author:'word1 word2\" word3'", array('word1 word2" word3'), null), + array("author:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')), ); } @@ -122,12 +122,13 @@ class SearchTest extends \PHPUnit_Framework_TestCase { */ public function provideInurlSearch() { return array( - array('inurl:word1', 'word1', null), - array('inurl: word1', null, array('word1')), - array('inurl:123', '123', null), - array('inurl:word1 word2', 'word1', array('word2')), - array('inurl:"word1 word2"', '"word1', array('word2"')), - array("inurl:word1 'word2 word3' word4", 'word1', array('word2 word3', 'word4')), + array('inurl:word1', array('word1'), null), + array('inurl: word1', array(), array('word1')), + array('inurl:123', array('123'), null), + array('inurl:word1 word2', array('word1'), array('word2')), + array('inurl:"word1 word2"', array('"word1'), array('word2"')), + array('inurl:word1 word2 inurl:word3', array('word1', 'word3'), array('word2')), + array("inurl:word1 'word2 word3' word4", array('word1'), array('word2 word3', 'word4')), ); } @@ -151,9 +152,9 @@ class SearchTest extends \PHPUnit_Framework_TestCase { array('date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), array('date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), array('date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), - array('date:2007-03-01/2008-05-11', '1172725200', '1210564799'), - array('date:2007-03-01/', '1172725200', ''), - array('date:/2008-05-11', '', '1210564799'), + array('date:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1), + array('date:2007-03-01/', strtotime('2007-03-01'), ''), + array('date:/2008-05-11', '', strtotime('2008-05-12') - 1), ); } @@ -177,9 +178,9 @@ class SearchTest extends \PHPUnit_Framework_TestCase { array('pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', '1172754000', '1210519800'), array('pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', '1172754000', '1210516199'), array('pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', '1172757601', '1210519800'), - array('pubdate:2007-03-01/2008-05-11', '1172725200', '1210564799'), - array('pubdate:2007-03-01/', '1172725200', ''), - array('pubdate:/2008-05-11', '', '1210564799'), + array('pubdate:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1), + array('pubdate:2007-03-01/', strtotime('2007-03-01'), ''), + array('pubdate:/2008-05-11', '', strtotime('2008-05-12') - 1), ); } @@ -201,7 +202,7 @@ class SearchTest extends \PHPUnit_Framework_TestCase { public function provideTagsSearch() { return array( array('#word1', array('word1'), null), - array('# word1', null, array('#', 'word1')), + array('# word1', array(), array('#', 'word1')), array('#123', array('123'), null), array('#word1 word2', array('word1'), array('word2')), array('#"word1 word2"', array('"word1'), array('word2"')), @@ -241,49 +242,49 @@ class SearchTest extends \PHPUnit_Framework_TestCase { return array( array( 'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5', - 'word1', - '1172725200', - '1210564799', - 'word2', - 'word3', - '1172725200', - '1210564799', + array('word1'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, + array('word2'), + array('word3'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, array('word4', 'word5'), null, ), array( 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 date:2007-03-01/2008-05-11', - 'word1', - '1172725200', - '1210564799', - 'word2', - 'word3', - '1172725200', - '1210564799', + array('word1'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, + array('word2'), + array('word3'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, array('word4', 'word5'), array('word6'), ), array( 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11', - 'word1', - '1172725200', - '1210564799', - 'word2', - 'word3', - '1172725200', - '1210564799', + array('word1'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, + array('word2'), + array('word3'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, array('word4', 'word5'), array('word6', 'word7'), ), array( 'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11', - 'word1', - '1172725200', - '1210564799', - 'word2', - 'word3', - '1172725200', - '1210564799', + array('word1'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, + array('word2'), + array('word3'), + strtotime('2007-03-01'), + strtotime('2008-05-12') - 1, array('word4', 'word5'), array('word7 word8', 'word6'), ), diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php index a0928d5ae..5c12a12fc 100644 --- a/tests/app/Models/UserQueryTest.php +++ b/tests/app/Models/UserQueryTest.php @@ -3,7 +3,7 @@ /** * Description of UserQueryTest */ -class UserQueryTest extends \PHPUnit_Framework_TestCase { +class UserQueryTest extends PHPUnit\Framework\TestCase { public function test__construct_whenAllQuery_storesAllParameters() { $query = array('get' => 'a'); |
