diff options
| author | 2016-10-30 20:15:11 +0100 | |
|---|---|---|
| committer | 2016-10-30 20:15:11 +0100 | |
| commit | 1d3e5bdee069434fd65c2717ae8fcce8c54fe81d (patch) | |
| tree | 39b0ae9ac0b0d1ed7fa11c747a0523cb3faa384b /app | |
| parent | 17c8c039df675b3b0f8d88d14f7316a240eabe76 (diff) | |
| parent | 29e1f048159b7a580bdf1bab184e928f11d104b4 (diff) | |
Merge pull request #1346 from FreshRSS/dev1.6.0
Merge 1.6.0-dev in master
Diffstat (limited to 'app')
81 files changed, 1403 insertions, 985 deletions
diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index e65c146de..922f92844 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -117,7 +117,6 @@ class FreshRSS_category_Controller extends Minz_ActionController { public function deleteAction() { $feedDAO = FreshRSS_Factory::createFeedDao(); $catDAO = new FreshRSS_CategoryDAO(); - $default_category = $catDAO->getDefault(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); if (Minz_Request::isPost()) { @@ -128,11 +127,11 @@ class FreshRSS_category_Controller extends Minz_ActionController { Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); } - if ($id === $default_category->id()) { + if ($id === FreshRSS_CategoryDAO::defaultCategoryId) { Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect); } - if ($feedDAO->changeCategory($id, $default_category->id()) === false) { + if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === false) { Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 147a2fe06..e73f106a6 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -139,7 +139,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function sharingAction() { if (Minz_Request::isPost()) { - $params = Minz_Request::fetchGET(); + $params = Minz_Request::fetchPOST(); FreshRSS_Context::$user_conf->sharing = $params['share']; FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index ffda1450d..c4115584a 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -26,6 +26,62 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } + public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') { + @set_time_limit(300); + + $catDAO = new FreshRSS_CategoryDAO(); + + $cat = null; + if ($cat_id > 0) { + $cat = $catDAO->searchById($cat_id); + } + if ($cat == null && $new_cat_name != '') { + $cat = $catDAO->addCategory(array('name' => $new_cat_name)); + } + if ($cat == null) { + $catDAO->checkDefault(); + } + $cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id(); + + $feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception + $feed->_httpAuth($http_auth); + $feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException + $feed->_category($cat_id); + + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($feedDAO->searchByUrl($feed->url())) { + throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name()); + } + + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if ($feed === null) { + throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name()); + } + + $values = array( + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $title != '' ? $title : $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => time(), + 'httpAuth' => $feed->httpAuth(), + ); + + $id = $feedDAO->addFeed($values); + if (!$id) { + // There was an error in database... we cannot say what here. + throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name()); + } + $feed->_id($id); + + // Ok, feed has been added in database. Now we have to refresh entries. + self::actualizeFeed($id, $url, false, null, true); + + return $feed; + } + /** * This action subscribes to a feed. * @@ -59,7 +115,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->catDAO = new FreshRSS_CategoryDAO(); $url_redirect = array( 'c' => 'subscription', 'a' => 'index', @@ -74,26 +129,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } if (Minz_Request::isPost()) { - @set_time_limit(300); - $cat = Minz_Request::param('category'); + $new_cat_name = ''; if ($cat === 'nc') { // User want to create a new category, new_category parameter // must exist $new_cat = Minz_Request::param('new_category'); - if (empty($new_cat['name'])) { - $cat = false; - } else { - $cat = $this->catDAO->addCategory($new_cat); - } - } - - if ($cat === false) { - // If category was not given or if creating new category failed, - // get the default category - $this->catDAO->checkDefault(); - $def_cat = $this->catDAO->getDefault(); - $cat = $def_cat->id(); + $new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : ''; } // HTTP information are useful if feed is protected behind a @@ -105,103 +147,24 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $http_auth = $user . ':' . $pass; } - $transaction_started = false; try { - $feed = new FreshRSS_Feed($url); + $feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth); } catch (FreshRSS_BadUrl_Exception $e) { // Given url was not a valid url! Minz_Log::warning($e->getMessage()); Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect); - } - - $feed->_httpAuth($http_auth); - - try { - $feed->load(true); } catch (FreshRSS_Feed_Exception $e) { // Something went bad (timeout, server not found, etc.) Minz_Log::warning($e->getMessage()); - Minz_Request::bad( - _t('feedback.sub.feed.internal_problem', _url('index', 'logs')), - $url_redirect - ); + Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect); } catch (Minz_FileNotExistException $e) { // Cache directory doesn't exist! Minz_Log::error($e->getMessage()); - Minz_Request::bad( - _t('feedback.sub.feed.internal_problem', _url('index', 'logs')), - $url_redirect - ); - } - - if ($feedDAO->searchByUrl($feed->url())) { - Minz_Request::bad( - _t('feedback.sub.feed.already_subscribed', $feed->name()), - $url_redirect - ); - } - - $feed->_category($cat); - - // Call the extension hook - $name = $feed->name(); - $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); - if ($feed === null) { - Minz_Request::bad(_t('feedback.sub.feed.not_added', $name), $url_redirect); - } - - $values = array( - 'url' => $feed->url(), - 'category' => $feed->category(), - 'name' => $feed->name(), - 'website' => $feed->website(), - 'description' => $feed->description(), - 'lastUpdate' => time(), - 'httpAuth' => $feed->httpAuth(), - ); - - $id = $feedDAO->addFeed($values); - if (!$id) { - // There was an error in database... we cannot say what here. - Minz_Request::bad(_t('feedback.sub.feed.not_added', $feed->name()), $url_redirect); - } - - // Ok, feed has been added in database. Now we have to refresh entries. - $feed->_id($id); - $feed->faviconPrepare(); - //$feed->pubSubHubbubPrepare(); //TODO: prepare PubSubHubbub already when adding the feed - - $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; - - $entryDAO = FreshRSS_Factory::createEntryDao(); - // We want chronological order and SimplePie uses reverse order. - $entries = array_reverse($feed->entries()); - - // Calculate date of oldest entries we accept in DB. - $nb_month_old = FreshRSS_Context::$user_conf->old_entries; - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - - // Use a shared statement and a transaction to improve a LOT the - // performances. - $feedDAO->beginTransaction(); - foreach ($entries as $entry) { - // Entries are added without any verification. - $entry->_feed($feed->id()); - $entry->_id(min(time(), $entry->date(true)) . uSecString()); - $entry->_isRead($is_read); - - $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); - if ($entry === null) { - // An extension has returned a null value, there is nothing to insert. - continue; - } - - $values = $entry->toArray(); - $entryDAO->addEntry($values); - } - $feedDAO->updateLastUpdate($feed->id()); - if ($feedDAO->inTransaction()) { - $feedDAO->commit(); + Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect); + } catch (FreshRSS_AlreadySubscribed_Exception $e) { + Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect); + } catch (FreshRSS_FeedNotAdded_Exception $e) { + Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect); } // Entries are in DB, we redirect to feed configuration page. @@ -211,6 +174,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // GET request: we must ask confirmation to user before adding feed. Minz_View::prependTitle(_t('sub.feed.title_add') . ' · '); + $this->catDAO = new FreshRSS_CategoryDAO(); $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { @@ -261,38 +225,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - /** - * This action actualizes entries from one or several feeds. - * - * Parameters are: - * - id (default: false): Feed ID - * - url (default: false): Feed URL - * - force (default: false) - * 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. - */ - public function actualizeAction($simplePiePush = null) { + public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); $entryDAO = FreshRSS_Factory::createEntryDao(); - Minz_Session::_param('actualize_feeds', false); - $id = Minz_Request::param('id'); - $url = Minz_Request::param('url'); - $force = Minz_Request::param('force'); - // Create a list of feeds to actualize. - // If id is set and valid, corresponding feed is added to the list but + // If feed_id is set and valid, corresponding feed is added to the list but // alone in order to automatize further process. $feeds = array(); - if ($id || $url) { - $feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url); + if ($feed_id > 0 || $feed_url) { + $feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url); if ($feed) { $feeds[] = $feed; } } else { - $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); + $feeds = $feedDAO->listFeedsOrderUpdate(-1); } // Calculate date of oldest entries we accept in DB. @@ -309,13 +258,29 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $url = $feed->url(); //For detection of HTTP 301 $pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled(); - if ((!$simplePiePush) && (!$id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) { + if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) { //$text = 'Skip pull of feed using PubSubHubbub: ' . $url; //Minz_Log::debug($text); //file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); continue; //When PubSubHubbub is used, do not pull refresh so often } + $mtime = 0; + $ttl = $feed->ttl(); + if ($ttl == -1) { + continue; //Feed refresh is disabled + } + if ((!$simplePiePush) && (!$feed_id) && + ($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) { + //Too early to refresh from source, but check whether the feed was updated by another user + $mtime = $feed->cacheModifiedTime(); + if ($feed->lastUpdate() + 10 >= $mtime) { + continue; //Nothing newer from other users + } + //Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user'); + //Will take advantage of the newer cache + } + if (!$feed->lock()) { Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; @@ -325,7 +290,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if ($simplePiePush) { $feed->loadEntries($simplePiePush); //Used by PubSubHubbub } else { - $feed->load(false); + $feed->load(false, $isNewFeed); } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); @@ -335,7 +300,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $feed_history = $feed->keepHistory(); - if ($feed_history == -2) { + if ($isNewFeed) { + $feed_history = -1; //∞ + } elseif ($feed_history == -2) { // TODO: -2 must be a constant! // -2 means we take the default value from configuration $feed_history = FreshRSS_Context::$user_conf->keep_history_default; @@ -346,7 +313,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if (count($entries) > 0) { $newGuids = array(); foreach ($entries as $entry) { - $newGuids[] = $entry->guid(); + $newGuids[] = safe_ascii($entry->guid()); } // For this feed, check existing GUIDs already in database. $existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids); @@ -375,7 +342,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // This entry should not be added considering configuration and date. $oldGuids[] = $entry->guid(); } else { - if ($entry_date < $date_min) { + if ($isNewFeed) { + $id = min(time(), $entry_date) . uSecString(); + } 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 } else { @@ -404,7 +373,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->addEntry($entry->toArray()); } } - $entryDAO->updateLastSeen($feed->id(), $oldGuids); + $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); } if ($feed_history >= 0 && rand(0, 30) === 1) { @@ -423,7 +392,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - $feedDAO->updateLastUpdate($feed->id(), 0, $entryDAO->inTransaction()); + $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime); if ($entryDAO->inTransaction()) { $entryDAO->commit(); } @@ -464,6 +433,26 @@ class FreshRSS_feed_Controller extends Minz_ActionController { break; } } + return array($updated_feeds, reset($feeds)); + } + + /** + * This action actualizes entries from one or several feeds. + * + * Parameters are: + * - id (default: false): Feed ID + * - url (default: false): Feed URL + * - force (default: false) + * 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. + */ + public function actualizeAction() { + Minz_Session::_param('actualize_feeds', false); + $id = Minz_Request::param('id'); + $url = Minz_Request::param('url'); + $force = Minz_Request::param('force'); + + list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force); if (Minz_Request::param('ajax')) { // Most of the time, ajax request is for only one feed. But since @@ -479,7 +468,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { // Redirect to the main page with correct notification. if ($updated_feeds === 1) { - $feed = reset($feeds); Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( 'params' => array('get' => 'f_' . $feed->id()) )); @@ -492,6 +480,36 @@ class FreshRSS_feed_Controller extends Minz_ActionController { return $updated_feeds; } + public static function renameFeed($feed_id, $feed_name) { + if ($feed_id <= 0 || $feed_name == '') { + return false; + } + $feedDAO = FreshRSS_Factory::createFeedDao(); + return $feedDAO->updateFeed($feed_id, array('name' => $feed_name)); + } + + public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') { + if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) { + return false; + } + + $catDAO = new FreshRSS_CategoryDAO(); + if ($cat_id > 0) { + $cat = $catDAO->searchById($cat_id); + $cat_id = $cat == null ? 0 : $cat->id(); + } + if ($cat_id <= 1 && $new_cat_name != '') { + $cat_id = $catDAO->addCategory(array('name' => $new_cat_name)); + } + if ($cat_id <= 1) { + $catDAO->checkDefault(); + $cat_id = FreshRSS_CategoryDAO::defaultCategoryId; + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + return $feedDAO->updateFeed($feed_id, array('category' => $cat_id)); + } + /** * This action changes the category of a feed. * @@ -512,20 +530,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed_id = Minz_Request::param('f_id'); $cat_id = Minz_Request::param('c_id'); - if ($cat_id === false) { - // If category was not given get the default one. - $catDAO = new FreshRSS_CategoryDAO(); - $catDAO->checkDefault(); - $def_cat = $catDAO->getDefault(); - $cat_id = $def_cat->id(); - } - - $feedDAO = FreshRSS_Factory::createFeedDao(); - $values = array('category' => $cat_id); - - $feed = $feedDAO->searchById($feed_id); - if ($feed && ($feed->category() == $cat_id || - $feedDAO->updateFeed($feed_id, $values))) { + if (self::moveFeed($feed_id, $cat_id)) { // TODO: return something useful } else { Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' . @@ -534,6 +539,21 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } + public static function deleteFeed($feed_id) { + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($feedDAO->deleteFeed($feed_id)) { + // TODO: Delete old favicon + + // Remove related queries + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed_id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); + + return true; + } + return false; + } + /** * This action deletes a feed. * @@ -552,21 +572,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if (!$redirect_url) { $redirect_url = array('c' => 'subscription', 'a' => 'index'); } - if (!Minz_Request::isPost()) { Minz_Request::forward($redirect_url, true); } $id = Minz_Request::param('id'); - $feedDAO = FreshRSS_Factory::createFeedDao(); - if ($feedDAO->deleteFeed($id)) { - // TODO: Delete old favicon - - // Remove related queries - FreshRSS_Context::$user_conf->queries = remove_query_by_get( - 'f_' . $id, FreshRSS_Context::$user_conf->queries); - FreshRSS_Context::$user_conf->save(); + if (self::deleteFeed($id)) { Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); } else { Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 60e467255..3ba91a243 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -29,32 +29,14 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('sub.import_export.title') . ' · '); } - /** - * This action handles import action. - * - * It must be reached by a POST request. - * - * Parameter is: - * - file (default: nothing!) - * Available file types are: zip, json or xml. - */ - public function importAction() { - if (!Minz_Request::isPost()) { - Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); - } - - $file = $_FILES['file']; - $status_file = $file['error']; - - if ($status_file !== 0) { - Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file); - Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), - array('c' => 'importExport', 'a' => 'index')); - } + public function importFile($name, $path, $username = null) { + require_once(LIB_PATH . '/lib_opml.php'); - @set_time_limit(300); + $this->catDAO = new FreshRSS_CategoryDAO($username); + $this->entryDAO = FreshRSS_Factory::createEntryDao($username); + $this->feedDAO = FreshRSS_Factory::createFeedDao($username); - $type_file = $this->guessFileType($file['name']); + $type_file = self::guessFileType($name); $list_files = array( 'opml' => array(), @@ -65,21 +47,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { // We try to list all files according to their type $list = array(); if ($type_file === 'zip' && extension_loaded('zip')) { - $zip = zip_open($file['tmp_name']); - + $zip = zip_open($path); if (!is_resource($zip)) { // zip_open cannot open file: something is wrong - Minz_Log::warning('Zip archive cannot be imported. Error code: ' . $zip); - Minz_Request::bad(_t('feedback.import_export.zip_error'), - array('c' => 'importExport', 'a' => 'index')); + throw new FreshRSS_Zip_Exception($zip); } - while (($zipfile = zip_read($zip)) !== false) { if (!is_resource($zipfile)) { // zip_entry() can also return an error code! - Minz_Log::warning('Zip file cannot be imported. Error code: ' . $zipfile); + throw new FreshRSS_Zip_Exception($zipfile); } else { - $type_zipfile = $this->guessFileType(zip_entry_name($zipfile)); + $type_zipfile = self::guessFileType(zip_entry_name($zipfile)); if ($type_file !== 'unknown') { $list_files[$type_zipfile][] = zip_entry_read( $zipfile, @@ -88,29 +66,88 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } } } - zip_close($zip); } elseif ($type_file === 'zip') { - // Zip extension is not loaded - Minz_Request::bad(_t('feedback.import_export.no_zip_extension'), - array('c' => 'importExport', 'a' => 'index')); + // ZIP extension is not loaded + throw new FreshRSS_ZipMissing_Exception(); } elseif ($type_file !== 'unknown') { - $list_files[$type_file][] = file_get_contents($file['tmp_name']); + $list_files[$type_file][] = file_get_contents($path); } // Import file contents. // OPML first(so categories and feeds are imported) // Starred articles then so the "favourite" status is already set // And finally all other files. - $error = false; + $ok = true; foreach ($list_files['opml'] as $opml_file) { - $error = $this->importOpml($opml_file); + if (!$this->importOpml($opml_file)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML import' . "\n"); + } else { + Minz_Log::warning('Error during OPML import'); + } + } } foreach ($list_files['json_starred'] as $article_file) { - $error = $this->importJson($article_file, true); + if (!$this->importJson($article_file, true)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n"); + } else { + Minz_Log::warning('Error during JSON stars import'); + } + } } foreach ($list_files['json_feed'] as $article_file) { - $error = $this->importJson($article_file); + if (!$this->importJson($article_file)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n"); + } else { + Minz_Log::warning('Error during JSON feeds import'); + } + } + } + + return $ok; + } + + /** + * This action handles import action. + * + * It must be reached by a POST request. + * + * Parameter is: + * - file (default: nothing!) + * Available file types are: zip, json or xml. + */ + public function importAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } + + $file = $_FILES['file']; + $status_file = $file['error']; + + if ($status_file !== 0) { + Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file); + Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), + array('c' => 'importExport', 'a' => 'index')); + } + + @set_time_limit(300); + + $error = false; + try { + $error = !$this->importFile($file['name'], $file['tmp_name']); + } catch (FreshRSS_ZipMissing_Exception $zme) { + Minz_Request::bad(_t('feedback.import_export.no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); + } catch (FreshRSS_Zip_Exception $ze) { + Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode()); + Minz_Request::bad(_t('feedback.import_export.zip_error'), + array('c' => 'importExport', 'a' => 'index')); } // And finally, we get import status and redirect to the home page @@ -126,7 +163,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * Itis a *very* basic guess file type function. Only based on filename. * That's could be improved but should be enough for what we have to do. */ - private function guessFileType($filename) { + private static function guessFileType($filename) { if (substr_compare($filename, '.zip', -4) === 0) { return 'zip'; } elseif (substr_compare($filename, '.opml', -5) === 0 || @@ -146,15 +183,19 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * This method parses and imports an OPML file. * * @param string $opml_file the OPML file content. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function importOpml($opml_file) { $opml_array = array(); try { $opml_array = libopml_parse_string($opml_file, false); } catch (LibOPML_Exception $e) { - Minz_Log::warning($e->getMessage()); - return true; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } + return false; } $this->catDAO->checkDefault(); @@ -167,51 +208,49 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param array $opml_elements an OPML element (body or outline). * @param string $parent_cat the name of the parent category. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function addOpmlElements($opml_elements, $parent_cat = null) { - $error = false; + $ok = true; $nb_feeds = count($this->feedDAO->listFeeds()); $nb_cats = count($this->catDAO->listCategories(false)); $limits = FreshRSS_Context::$system_conf->limits; foreach ($opml_elements as $elt) { - $is_error = false; if (isset($elt['xmlUrl'])) { // If xmlUrl exists, it means it is a feed - if ($nb_feeds >= $limits['max_feeds']) { + if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) { Minz_Log::warning(_t('feedback.sub.feed.over_max', - $limits['max_feeds'])); - $is_error = true; + $limits['max_feeds'])); + $ok = false; continue; } - $is_error = $this->addFeedOpml($elt, $parent_cat); - if (!$is_error) { - $nb_feeds += 1; + if ($this->addFeedOpml($elt, $parent_cat)) { + $nb_feeds++; + } else { + $ok = false; } } else { // No xmlUrl? It should be a category! $limit_reached = ($nb_cats >= $limits['max_categories']); - if ($limit_reached) { + if (!FreshRSS_Context::$isCli && $limit_reached) { Minz_Log::warning(_t('feedback.sub.category.over_max', - $limits['max_categories'])); + $limits['max_categories'])); + $ok = false; + continue; } - $is_error = $this->addCategoryOpml($elt, $parent_cat, $limit_reached); - if (!$is_error) { - $nb_cats += 1; + if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) { + $nb_cats++; + } else { + $ok = false; } } - - if (!$error && $is_error) { - // oops: there is at least one error! - $error = $is_error; - } } - return $error; + return $ok; } /** @@ -219,21 +258,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param array $feed_elt an OPML element (must be a feed element). * @param string $parent_cat the name of the parent category. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function addFeedOpml($feed_elt, $parent_cat) { - $default_cat = $this->catDAO->getDefault(); - if (is_null($parent_cat)) { + if ($parent_cat == null) { // This feed has no parent category so we get the default one + $this->catDAO->checkDefault(); + $default_cat = $this->catDAO->getDefault(); $parent_cat = $default_cat->name(); } $cat = $this->catDAO->searchByName($parent_cat); - if (is_null($cat)) { + if ($cat == null) { // If there is not $cat, it means parent category does not exist in // database. // If it happens, take the default category. - $cat = $default_cat; + $this->catDAO->checkDefault(); + $cat = $this->catDAO->getDefault(); } // We get different useful information @@ -259,7 +300,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { // Call the extension hook $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); - if (!is_null($feed)) { + if ($feed != null) { // addFeedObject checks if feed is already in DB so nothing else to // check here $id = $this->feedDAO->addFeedObject($feed); @@ -268,11 +309,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $error = true; } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::warning($e->getMessage()); + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } $error = true; } - return $error; + if ($error) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n"); + } else { + Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id()); + } + } + + return !$error; } /** @@ -282,29 +335,34 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param string $parent_cat the name of the parent category. * @param boolean $cat_limit_reached indicates if category limit has been reached. * if yes, category is not added (but we try for feeds!) - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) { // Create a new Category object - $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text'])); + $catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']); + $cat = new FreshRSS_Category($catName); $error = true; - if (!$cat_limit_reached) { + if (FreshRSS_Context::$isCli || !$cat_limit_reached) { $id = $this->catDAO->addCategoryObject($cat); $error = ($id === false); } + if ($error) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n"); + } else { + Minz_Log::warning('Error during OPML category import from URL: ' . $catName); + } + } if (isset($cat_elt['@outlines'])) { // Our cat_elt contains more categories or more feeds, so we // add them recursively. // Note: FreshRSS does not support yet category arborescence - $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name()); - if (!$error && $res) { - $error = true; - } + $error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName); } - return $error; + return !$error; } /** @@ -312,13 +370,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param string $article_file the JSON file content. * @param boolean $starred true if articles from the file must be starred. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function importJson($article_file, $starred = false) { $article_object = json_decode($article_file, true); - if (is_null($article_object)) { - Minz_Log::warning('Try to import a non-JSON file'); - return true; + if ($article_object == null) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n"); + } else { + Minz_Log::warning('Try to import a non-JSON file'); + } + return false; } $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; @@ -337,29 +399,36 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed = new FreshRSS_Feed($item['origin'][$key]); $feed = $this->feedDAO->searchByUrl($feed->url()); - if (is_null($feed)) { + if ($feed == null) { // Feed does not exist in DB,we should to try to add it. - if ($nb_feeds >= $limits['max_feeds']) { + if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) { // Oops, no more place! Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds'])); } else { $feed = $this->addFeedJson($item['origin'], $google_compliant); } - if (is_null($feed)) { + if ($feed == null) { // Still null? It means something went wrong. $error = true; } else { - // Nice! Increase the counter. - $nb_feeds += 1; + $nb_feeds++; } } - if (!is_null($feed)) { + if ($feed != null) { $article_to_feed[$item['id']] = $feed->id(); } } + $newGuids = array(); + foreach ($article_object['items'] as $item) { + $newGuids[] = safe_ascii($item['id']); + } + // For this feed, check existing GUIDs already in database. + $existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids); + unset($newGuids); + // Then, articles are imported. $this->entryDAO->beginTransaction(); foreach ($article_object['items'] as $item) { @@ -376,7 +445,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($google_compliant) { // Remove tags containing "/state/com.google" which are useless. $tags = array_filter($tags, function($var) { - return strpos($var, '/state/com.google') === false; + return strpos($var, '/state/com.google') !== false; }); } @@ -389,13 +458,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_tags($tags); $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); - if (is_null($entry)) { + if ($entry == null) { // An extension has returned a null value, there is nothing to insert. continue; } $values = $entry->toArray(); - $id = $this->entryDAO->addEntry($values); + if (isset($existingHashForGuids[$entry->guid()])) { + $id = $this->entryDAO->updateEntry($values); + } else { + $id = $this->entryDAO->addEntry($values); + } if (!$error && ($id === false)) { $error = true; @@ -403,7 +476,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $this->entryDAO->commit(); - return $error; + return !$error; } /** @@ -415,8 +488,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * else null. */ private function addFeedJson($origin, $google_compliant) { - $default_cat = $this->catDAO->getDefault(); - $return = null; $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; $url = $origin[$key]; @@ -426,13 +497,13 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { try { // Create a Feed object and add it in database. $feed = new FreshRSS_Feed($url); - $feed->_category($default_cat->id()); + $feed->_category(FreshRSS_CategoryDAO::defaultCategoryId); $feed->_name($name); $feed->_website($website); // Call the extension hook $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); - if (!is_null($feed)) { + if ($feed != null) { // addFeedObject checks if feed is already in DB so nothing else to // check here. $id = $this->feedDAO->addFeedObject($feed); @@ -443,67 +514,98 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::warning($e->getMessage()); + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } } return $return; } - /** - * This action handles export action. - * - * This action must be reached by a POST request. - * - * Parameters are: - * - export_opml (default: false) - * - export_starred (default: false) - * - export_feeds (default: array()) a list of feed ids - */ - public function exportAction() { - if (!Minz_Request::isPost()) { - Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); - } + public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) { + require_once(LIB_PATH . '/lib_opml.php'); - $this->view->_useLayout(false); + $this->catDAO = new FreshRSS_CategoryDAO($username); + $this->entryDAO = FreshRSS_Factory::createEntryDao($username); + $this->feedDAO = FreshRSS_Factory::createFeedDao($username); + + if ($export_feeds === true) { + //All feeds + $export_feeds = $this->feedDAO->listFeedsIds(); + } + if (!is_array($export_feeds)) { + $export_feeds = array(); + } - $export_opml = Minz_Request::param('export_opml', false); - $export_starred = Minz_Request::param('export_starred', false); - $export_feeds = Minz_Request::param('export_feeds', array()); + $day = date('Y-m-d'); $export_files = array(); if ($export_opml) { - $export_files['feeds.opml'] = $this->generateOpml(); + $export_files["feeds_${day}.opml.xml"] = $this->generateOpml(); } if ($export_starred) { - $export_files['starred.json'] = $this->generateEntries('starred'); + $export_files["starred_${day}.json"] = $this->generateEntries('starred'); } foreach ($export_feeds as $feed_id) { $feed = $this->feedDAO->searchById($feed_id); if ($feed) { - $filename = 'feed_' . $feed->category() . '_' + $filename = "feed_${day}_" . $feed->category() . '_' . $feed->id() . '.json'; - $export_files[$filename] = $this->generateEntries('feed', $feed); + $export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries); } } $nb_files = count($export_files); if ($nb_files > 1) { - // If there are more than 1 file to export, we need a zip archive. + // If there are more than 1 file to export, we need a ZIP archive. try { - $this->exportZip($export_files); + $this->sendZip($export_files); } catch (Exception $e) { - # Oops, there is no Zip extension! - Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'), - array('c' => 'importExport', 'a' => 'index')); + throw new FreshRSS_ZipMissing_Exception($e); } } elseif ($nb_files === 1) { // Only one file? Guess its type and export it. $filename = key($export_files); - $type = $this->guessFileType($filename); - $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type); - } else { + $type = self::guessFileType($filename); + $this->sendFile('freshrss_' . $filename, $export_files[$filename], $type); + } + return $nb_files; + } + + /** + * This action handles export action. + * + * This action must be reached by a POST request. + * + * Parameters are: + * - export_opml (default: false) + * - export_starred (default: false) + * - export_feeds (default: array()) a list of feed ids + */ + public function exportAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } + $this->view->_useLayout(false); + + $nb_files = 0; + try { + $nb_files = $this->exportFile( + Minz_Request::param('export_opml', false), + Minz_Request::param('export_starred', false), + Minz_Request::param('export_feeds', array()) + ); + } catch (FreshRSS_ZipMissing_Exception $zme) { + # Oops, there is no ZIP extension! + Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); + } + + if ($nb_files < 1) { // Nothing to do... Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } @@ -532,7 +634,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param FreshRSS_Feed $feed feed of which we want to get entries. * @return string the JSON file content. */ - private function generateEntries($type, $feed = NULL) { + private function generateEntries($type, $feed = NULL, $maxFeedEntries = 50) { $this->view->categories = $this->catDAO->listCategories(); if ($type == 'starred') { @@ -542,12 +644,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->view->entries = $this->entryDAO->listWhere( 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all'] ); - } elseif ($type == 'feed' && !is_null($feed)) { + } elseif ($type === 'feed' && $feed != null) { $this->view->list_title = _t('sub.import_export.feed_list', $feed->name()); $this->view->type = 'feed/' . $feed->id(); $this->view->entries = $this->entryDAO->listWhere( 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', - FreshRSS_Context::$user_conf->posts_per_page + $maxFeedEntries ); $this->view->feed = $feed; } @@ -561,7 +663,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param array $files list of files where key is filename and value the content. * @throws Exception if Zip extension is not loaded. */ - private function exportZip($files) { + private function sendZip($files) { if (!extension_loaded('zip')) { throw new Exception(); } @@ -579,7 +681,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $zip->close(); header('Content-Type: application/zip'); header('Content-Length: ' . filesize($zip_file)); - header('Content-Disposition: attachment; filename="freshrss_export.zip"'); + $day = date('Y-m-d'); + header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"'); readfile($zip_file); unlink($zip_file); } @@ -592,16 +695,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param string $type the file type (opml, json_feed or json_starred). * If equals to unknown, nothing happens. */ - private function exportFile($filename, $content, $type) { + private function sendFile($filename, $content, $type) { if ($type === 'unknown') { return; } $content_type = ''; if ($type === 'opml') { - $content_type = "text/opml"; + $content_type = 'application/xml'; } elseif ($type === 'json_feed' || $type === 'json_starred') { - $content_type = "text/json"; + $content_type = 'application/json'; } header('Content-Type: ' . $content_type . '; charset=utf-8'); diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 2332d225d..5ca147ff3 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -34,7 +34,9 @@ class FreshRSS_index_Controller extends Minz_ActionController { $this->view->callbackBeforeContent = function($view) { try { + FreshRSS_Context::$number++; //+1 for pagination $entries = FreshRSS_index_Controller::listEntriesByContext(); + FreshRSS_Context::$number--; $nb_entries = count($entries); if ($nb_entries > FreshRSS_Context::$number) { @@ -154,8 +156,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { * - order (default: conf->sort_order) * - nb (default: conf->posts_per_page) * - next (default: empty string) + * - hours (default: 0) */ private function updateContext() { + if (empty(FreshRSS_Context::$categories)) { + $catDAO = new FreshRSS_CategoryDAO(); + FreshRSS_Context::$categories = $catDAO->listCategories(); + } + // Update number of read / unread variables. $entryDAO = FreshRSS_Factory::createEntryDao(); FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites(); @@ -180,10 +188,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { FreshRSS_Context::$order = Minz_Request::param( 'order', FreshRSS_Context::$user_conf->sort_order ); - FreshRSS_Context::$number = Minz_Request::param( - 'nb', FreshRSS_Context::$user_conf->posts_per_page - ); + FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page)); + if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) { + FreshRSS_Context::$number = max( + FreshRSS_Context::$user_conf->max_posts_per_rss, + FreshRSS_Context::$user_conf->posts_per_page); + } FreshRSS_Context::$first_id = Minz_Request::param('next', ''); + FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0)); } /** @@ -201,11 +213,31 @@ class FreshRSS_index_Controller extends Minz_ActionController { $id = ''; } - return $entryDAO->listWhere( + $limit = FreshRSS_Context::$number; + + $date_min = 0; + if (FreshRSS_Context::$sinceHours) { + $date_min = time() - (FreshRSS_Context::$sinceHours * 3600); + $limit = FreshRSS_Context::$user_conf->max_posts_per_rss; + } + + $entries = $entryDAO->listWhere( $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, - FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id, - FreshRSS_Context::$search + $limit, FreshRSS_Context::$first_id, + FreshRSS_Context::$search, $date_min ); + + if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) { + $date_min = 0; + $limit = FreshRSS_Context::$user_conf->min_posts_per_rss; + $entries = $entryDAO->listWhere( + $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, + $limit, FreshRSS_Context::$first_id, + FreshRSS_Context::$search, $date_min + ); + } + + return $entries; } /** diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 4a597ae7d..5d1dee72c 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -18,6 +18,27 @@ class FreshRSS_stats_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('admin.stats.title') . ' · '); } + private function convertToSerie($data) { + $serie = array(); + + foreach ($data as $key => $value) { + $serie[] = array($key, $value); + } + + return $serie; + } + + private function convertToPieSerie($data) { + $serie = array(); + + foreach ($data as $value) { + $value['data'] = array(array(0, (int) $value['data'])); + $serie[] = $value; + } + + return $serie; + } + /** * This action handles the statistic main page. * @@ -33,10 +54,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $statsDAO = FreshRSS_Factory::createStatsDAO(); Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js'))); $this->view->repartition = $statsDAO->calculateEntryRepartition(); - $this->view->count = $statsDAO->calculateEntryCount(); - $this->view->average = $statsDAO->calculateEntryAverage(); - $this->view->feedByCategory = $statsDAO->calculateFeedByCategory(); - $this->view->entryByCategory = $statsDAO->calculateEntryByCategory(); + $entryCount = $statsDAO->calculateEntryCount(); + $this->view->count = $this->convertToSerie($entryCount); + $this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2); + $this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory()); + $this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory()); $this->view->topFeed = $statsDAO->calculateTopFeed(); } @@ -118,11 +140,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $this->view->days = $statsDAO->getDays(); $this->view->months = $statsDAO->getMonths(); $this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id); - $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id); + $this->view->repartitionHour = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id)); $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id); - $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id); + $this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id)); $this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id); - $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id); + $this->view->repartitionMonth = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerMonth($id)); $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id); } } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 0521bc008..9d6ae18e6 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -24,6 +24,16 @@ class FreshRSS_user_Controller extends Minz_ActionController { } } + public static function hashPassword($passwordPlain) { + if (!function_exists('password_hash')) { + include_once(LIB_PATH . '/password_compat.php'); + } + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); + $passwordPlain = ''; + $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + return $passwordHash == '' ? '' : $passwordHash; + } + /** * This action displays the user profile page. */ @@ -41,12 +51,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { if ($passwordPlain != '') { Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP $_POST['newPasswordPlain'] = ''; - if (!function_exists('password_hash')) { - include_once(LIB_PATH . '/password_compat.php'); - } - $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); - $passwordPlain = ''; - $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + $passwordHash = self::hashPassword($passwordPlain); $ok &= ($passwordHash != ''); FreshRSS_Context::$user_conf->passwordHash = $passwordHash; } @@ -54,12 +59,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true); if ($passwordPlain != '') { - if (!function_exists('password_hash')) { - include_once(LIB_PATH . '/password_compat.php'); - } - $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); - $passwordPlain = ''; - $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + $passwordHash = self::hashPassword($passwordPlain); $ok &= ($passwordHash != ''); FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } @@ -99,6 +99,50 @@ class FreshRSS_user_Controller extends Minz_ActionController { $this->view->size_user = $entryDAO->size(); } + public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { + if (!is_array($userConfig)) { + $userConfig = array(); + } + + $ok = ($new_user_name != '') && ctype_alnum($new_user_name); + + if ($ok) { + $languages = Minz_Translate::availableLanguages(); + if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) { + $userConfig['language'] = 'en'; + } + + $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive + + $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php'); + $ok &= !file_exists($configPath); + } + if ($ok) { + $passwordHash = ''; + if ($passwordPlain != '') { + $passwordHash = self::hashPassword($passwordPlain); + $ok &= ($passwordHash != ''); + } + + $apiPasswordHash = ''; + if ($apiPasswordPlain != '') { + $apiPasswordHash = self::hashPassword($apiPasswordPlain); + $ok &= ($apiPasswordHash != ''); + } + } + if ($ok) { + mkdir(join_path(DATA_PATH, 'users', $new_user_name)); + $userConfig['passwordHash'] = $passwordHash; + $userConfig['apiPasswordHash'] = $apiPasswordHash; + $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false); + } + if ($ok) { + $userDAO = new FreshRSS_UserDAO(); + $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds); + } + return $ok; + } + /** * This action creates a new user. * @@ -116,57 +160,13 @@ class FreshRSS_user_Controller extends Minz_ActionController { FreshRSS_Auth::hasAccess('admin') || !max_registrations_reached() )) { - $db = FreshRSS_Context::$system_conf->db; - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - - $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); - $languages = Minz_Translate::availableLanguages(); - if (!isset($languages[$new_user_language])) { - $new_user_language = FreshRSS_Context::$user_conf->language; - } - $new_user_name = Minz_Request::param('new_user_name'); - $ok = ($new_user_name != '') && ctype_alnum($new_user_name); - - if ($ok) { - $default_user = FreshRSS_Context::$system_conf->default_user; - $ok &= (strcasecmp($new_user_name, $default_user) !== 0); //It is forbidden to alter the default user - - $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive + $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); + $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); - $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php'); - $ok &= !file_exists($configPath); - } - if ($ok) { - $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); - $passwordHash = ''; - if ($passwordPlain != '') { - Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP - $_POST['new_user_passwordPlain'] = ''; - if (!function_exists('password_hash')) { - include_once(LIB_PATH . '/password_compat.php'); - } - $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); - $passwordPlain = ''; - $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js - $ok &= ($passwordHash != ''); - } - if (empty($passwordHash)) { - $passwordHash = ''; - } - } - if ($ok) { - mkdir(join_path(DATA_PATH, 'users', $new_user_name)); - $config_array = array( - 'language' => $new_user_language, - 'passwordHash' => $passwordHash, - ); - $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';') !== false); - } - if ($ok) { - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->createUser($new_user_name); - } + $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language)); + Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP + $_POST['new_user_passwordPlain'] = ''; invalidateHttpCache(); $notif = array( @@ -183,6 +183,27 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_Request::forward($redirect_url, true); } + public static function deleteUser($username) { + $db = FreshRSS_Context::$system_conf->db; + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + + $ok = ctype_alnum($username); + if ($ok) { + $default_user = FreshRSS_Context::$system_conf->default_user; + $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user + } + $user_data = join_path(DATA_PATH, 'users', $username); + if ($ok) { + $ok &= is_dir($user_data); + } + if ($ok) { + $userDAO = new FreshRSS_UserDAO(); + $ok &= $userDAO->deleteUser($username); + $ok &= recursive_unlink($user_data); + } + return $ok; + } + /** * This action delete an existing user. * @@ -204,16 +225,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { FreshRSS_Auth::hasAccess('admin') || $self_deletion )) { - $db = FreshRSS_Context::$system_conf->db; - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - - $ok = ctype_alnum($username); - $user_data = join_path(DATA_PATH, 'users', $username); - - if ($ok) { - $default_user = FreshRSS_Context::$system_conf->default_user; - $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user - } + $ok = true; if ($ok && $self_deletion) { // We check the password if it's a self-destruction $nonce = Minz_Session::param('nonce'); @@ -225,12 +237,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { ); } if ($ok) { - $ok &= is_dir($user_data); - } - if ($ok) { - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->deleteUser($username); - $ok &= recursive_unlink($user_data); + $ok &= self::deleteUser($username); } if ($ok && $self_deletion) { FreshRSS_Auth::removeAccess(); diff --git a/app/Exceptions/AlreadySubscribedException.php b/app/Exceptions/AlreadySubscribedException.php new file mode 100644 index 000000000..33b9f9555 --- /dev/null +++ b/app/Exceptions/AlreadySubscribedException.php @@ -0,0 +1,14 @@ +<?php + +class FreshRSS_AlreadySubscribed_Exception extends Exception { + private $feedName = ''; + + public function __construct($url, $feedName) { + parent::__construct('Already subscribed! ' . $url, 2135); + $this->feedName = $feedName; + } + + public function feedName() { + return $this->feedName; + } +} diff --git a/app/Exceptions/FeedNotAddedException.php b/app/Exceptions/FeedNotAddedException.php new file mode 100644 index 000000000..350a17c56 --- /dev/null +++ b/app/Exceptions/FeedNotAddedException.php @@ -0,0 +1,14 @@ +<?php + +class FreshRSS_FeedNotAdded_Exception extends Exception { + private $feedName = ''; + + public function __construct($url, $feedName) { + parent::__construct('Feed not added! ' . $url, 2147); + $this->feedName = $feedName; + } + + public function feedName() { + return $this->feedName; + } +} diff --git a/app/Exceptions/ZipException.php b/app/Exceptions/ZipException.php new file mode 100644 index 000000000..8441daedf --- /dev/null +++ b/app/Exceptions/ZipException.php @@ -0,0 +1,14 @@ +<?php + +class FreshRSS_Zip_Exception extends Exception { + private $zipErrorCode = 0; + + public function __construct($zipErrorCode) { + parent::__construct('ZIP error! ' . $url, 2141); + $this->zipErrorCode = $zipErrorCode; + } + + public function zipErrorCode() { + return $this->zipErrorCode; + } +} diff --git a/app/Exceptions/ZipMissingException.php b/app/Exceptions/ZipMissingException.php new file mode 100644 index 000000000..864cc3991 --- /dev/null +++ b/app/Exceptions/ZipMissingException.php @@ -0,0 +1,4 @@ +<?php + +class FreshRSS_ZipMissing_Exception extends Exception { +} diff --git a/app/FreshRSS.php b/app/FreshRSS.php index f9c371d27..e4caf23d1 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -49,7 +49,7 @@ class FreshRSS extends Minz_FrontController { self::initI18n(); self::loadNotifications(); // Enable extensions for the current (logged) user. - if (FreshRSS_Auth::hasAccess()) { + if (FreshRSS_Auth::hasAccess() || $system_conf->allow_anonymous) { $ext_list = FreshRSS_Context::$user_conf->extensions_enabled; Minz_ExtensionManager::enableByList($ext_list); } diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index fc431553e..c2d57c241 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -1,6 +1,9 @@ <?php class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { + + const defaultCategoryId = 1; + public function addCategory($valuesTmp) { $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)'; $stm = $this->bd->prepare($sql); @@ -10,7 +13,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable ); if ($stm && $stm->execute($values)) { - return $this->bd->lastInsertId(); + return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"'); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); Minz_Log::error('SQL error addCategory: ' . $info[2]); @@ -50,6 +53,9 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable } public function deleteCategory($id) { + if ($id <= self::defaultCategoryId) { + return false; + } $sql = 'DELETE FROM `' . $this->prefix . 'category` WHERE id=?'; $stm = $this->bd->prepare($sql); @@ -100,7 +106,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable public function listCategories($prePopulateFeeds = true, $details = false) { if ($prePopulateFeeds) { $sql = 'SELECT c.id AS c_id, c.name AS c_name, ' - . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ') + . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads` ') . 'FROM `' . $this->prefix . 'category` c ' . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id ' . 'GROUP BY f.id, c_id ' @@ -117,7 +123,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable } public function getDefault() { - $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=1'; + $sql = 'SELECT * FROM `' . $this->prefix . 'category` WHERE id=' . self::defaultCategoryId; $stm = $this->bd->prepare($sql); $stm->execute(); @@ -131,11 +137,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable } } public function checkDefault() { - $def_cat = $this->searchById(1); + $def_cat = $this->searchById(self::defaultCategoryId); if ($def_cat == null) { $cat = new FreshRSS_Category(_t('gen.short.default_category')); - $cat->_id(1); + $cat->_id(self::defaultCategoryId); $values = array( 'id' => $cat->id(), @@ -207,12 +213,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable $previousLine = null; $feedsDao = array(); + $feedDao = FreshRSS_Factory::createFeedDAO(); foreach ($listDAO as $line) { if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) { // End of the current category, we add it to the $list $cat = new FreshRSS_Category( $previousLine['c_name'], - FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id']) + $feedDao->daoToFeed($feedsDao, $previousLine['c_id']) ); $cat->_id($previousLine['c_id']); $list[$previousLine['c_id']] = $cat; @@ -228,7 +235,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable if ($previousLine != null) { $cat = new FreshRSS_Category( $previousLine['c_name'], - FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id']) + $feedDao->daoToFeed($feedsDao, $previousLine['c_id']) ); $cat->_id($previousLine['c_id']); $list[$previousLine['c_id']] = $cat; diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 79bd0170b..046f54955 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -282,6 +282,7 @@ class FreshRSS_ConfigurationSetter { switch ($value['type']) { case 'mysql': + case 'pgsql': if (empty($value['host']) || empty($value['user']) || empty($value['base']) || diff --git a/app/Models/Context.php b/app/Models/Context.php index 2a58bd4ba..fd0e79fc1 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -35,6 +35,9 @@ class FreshRSS_Context { public static $first_id = ''; public static $next_id = ''; public static $id_max = ''; + public static $sinceHours = 0; + + public static $isCli = false; /** * Initialize the context. @@ -45,9 +48,6 @@ class FreshRSS_Context { // Init configuration. self::$system_conf = Minz_Configuration::get('system'); self::$user_conf = Minz_Configuration::get('user'); - - $catDAO = new FreshRSS_CategoryDAO(); - self::$categories = $catDAO->listCategories(); } /** @@ -139,15 +139,22 @@ class FreshRSS_Context { $id = substr($get, 2); $nb_unread = 0; + if (empty(self::$categories)) { + $catDAO = new FreshRSS_CategoryDAO(); + self::$categories = $catDAO->listCategories(); + } + switch($type) { case 'a': self::$current_get['all'] = true; self::$name = _t('index.feed.title'); + self::$description = self::$system_conf->meta_description; self::$get_unread = self::$total_unread; break; case 's': self::$current_get['starred'] = true; self::$name = _t('index.feed.title_fav'); + self::$description = self::$system_conf->meta_description; self::$get_unread = self::$total_starred['unread']; // Update state if favorite is not yet enabled. @@ -198,11 +205,16 @@ class FreshRSS_Context { /** * Set the value of $next_get attribute. */ - public static function _nextGet() { + private static function _nextGet() { $get = self::currentGet(); // By default, $next_get == $get self::$next_get = $get; + if (empty(self::$categories)) { + $catDAO = new FreshRSS_CategoryDAO(); + self::$categories = $catDAO->listCategories(); + } + if (self::$user_conf->onread_jump_next && strlen($get) > 2) { $another_unread_id = ''; $found_current_get = false; diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php new file mode 100644 index 000000000..a4edaa448 --- /dev/null +++ b/app/Models/DatabaseDAOPGSQL.php @@ -0,0 +1,43 @@ +<?php + +/** + * This class is used to test database is well-constructed. + */ +class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO { + public function tablesAreCorrect() { + $db = FreshRSS_Context::$system_conf->db; + $dbowner = $db['user']; + $sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?'; + $stm = $this->bd->prepare($sql); + $values = array($dbowner); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + + $tables = array( + $this->prefix . 'category' => false, + $this->prefix . 'feed' => false, + $this->prefix . 'entry' => false, + ); + foreach ($res as $value) { + $tables[array_pop($value)] = true; + } + + return count(array_keys($tables, true, true)) == count($tables); + } + + public function getSchema($table) { + $sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?'; + $stm = $this->bd->prepare($sql); + $stm->execute(array($this->prefix . $table)); + return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC)); + } + + public function daoToSchema($dao) { + return array( + 'name' => $dao['field'], + 'type' => strtolower($dao['type']), + 'notnull' => (bool)$dao['null'], + 'default' => $dao['default'], + ); + } +} diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 8f64098e5..4c6a9ea20 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -3,13 +3,21 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { public function isCompressed() { - return parent::$sharedDbType !== 'sqlite'; + return parent::$sharedDbType === 'mysql'; } public function hasNativeHex() { return parent::$sharedDbType !== 'sqlite'; } + public function sqlHexDecode($x) { + return 'unhex(' . $x . ')'; + } + + public function sqlHexEncode($x) { + return 'hex(' . $x . ')'; + } + protected function addColumn($name) { Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name); $hasTransaction = false; @@ -20,7 +28,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $this->bd->beginTransaction(); $hasTransaction = true; } - $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0'); + $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN `lastSeen` INT(11) DEFAULT 0'); if ($stm && $stm->execute()) { $stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);'); //"IF NOT EXISTS" does not exist in MySQL 5.7 if ($stm && $stm->execute()) { @@ -105,32 +113,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->addEntryPrepared === null) { $sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, ' . ($this->isCompressed() ? 'content_bin' : 'content') - . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) ' - . 'VALUES(?, ?, ?, ?, ' - . ($this->isCompressed() ? 'COMPRESS(?)' : '?') - . ', ?, ?, ?, ' - . ($this->hasNativeHex() ? 'X?' : '?') - . ', ?, ?, ?, ?)'; + . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' + . 'VALUES(:id, :guid, :title, :author, ' + . ($this->isCompressed() ? 'COMPRESS(:content)' : ':content') + . ', :link, :date, :last_seen, ' + . $this->sqlHexDecode(':hash') + . ', :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']); + } - $values = array( - $valuesTmp['id'], - substr($valuesTmp['guid'], 0, 760), - substr($valuesTmp['title'], 0, 255), - substr($valuesTmp['author'], 0, 255), - $valuesTmp['content'], - substr($valuesTmp['link'], 0, 1023), - $valuesTmp['date'], - time(), - $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']), // X'09AF' hexadecimal literals do not work with SQLite/PDO //hex2bin() is PHP5.4+ - $valuesTmp['is_read'] ? 1 : 0, - $valuesTmp['is_favorite'] ? 1 : 0, - $valuesTmp['id_feed'], - substr($valuesTmp['tags'], 0, 1023), - ); - - if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) { + if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) { return $this->bd->lastInsertId(); } else { $info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo(); @@ -153,35 +174,44 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($this->updateEntryPrepared === null) { $sql = 'UPDATE `' . $this->prefix . 'entry` ' - . 'SET title=?, author=?, ' - . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?') - . ', link=?, date=?, lastSeen=?, hash=' - . ($this->hasNativeHex() ? 'X?' : '?') - . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ') - . 'tags=? ' - . 'WHERE id_feed=? AND guid=?'; + . 'SET title=:title, author=:author, ' + . ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content') + . ', link=:link, date=:date, `lastSeen`=:last_seen, ' + . 'hash=' . $this->sqlHexDecode(':hash') + . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ') + . 'tags=:tags ' + . 'WHERE id_feed=:id_feed AND guid=:guid'; $this->updateEntryPrepared = $this->bd->prepare($sql); } - $values = array( - substr($valuesTmp['title'], 0, 255), - substr($valuesTmp['author'], 0, 255), - $valuesTmp['content'], - substr($valuesTmp['link'], 0, 1023), - $valuesTmp['date'], - time(), - $this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']), - ); + $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760); + $this->updateEntryPrepared->bindParam(':guid', $valuesTmp['guid']); + $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255); + $this->updateEntryPrepared->bindParam(':title', $valuesTmp['title']); + $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255); + $this->updateEntryPrepared->bindParam(':author', $valuesTmp['author']); + $this->updateEntryPrepared->bindParam(':content', $valuesTmp['content']); + $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023); + $valuesTmp['link'] = safe_ascii($valuesTmp['link']); + $this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']); + $this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT); + $valuesTmp['lastSeen'] = time(); + $this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT); if ($valuesTmp['is_read'] !== null) { - $values[] = $valuesTmp['is_read'] ? 1 : 0; + $this->updateEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'] ? 1 : 0, PDO::PARAM_INT); + } + $this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT); + $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023); + $this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']); + + if ($this->hasNativeHex()) { + $this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']); + } else { + $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+ + $this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']); } - $values = array_merge($values, array( - substr($valuesTmp['tags'], 0, 1023), - $valuesTmp['id_feed'], - substr($valuesTmp['guid'], 0, 760), - )); - if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) { + if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) { return $this->bd->lastInsertId(); } else { $info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo(); @@ -246,15 +276,19 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'WHERE e.is_read=0 ' . 'GROUP BY e.id_feed' . ') x ON x.id_feed=f.id ' - . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) ' - . 'WHERE 1'; + . 'SET f.`cache_nbUnreads`=COALESCE(x.nbUnreads, 0)'; + $hasWhere = false; $values = array(); if ($feedId !== false) { - $sql .= ' AND f.id=?'; + $sql .= $hasWhere ? ' AND' : ' WHERE'; + $hasWhere = true; + $sql .= ' f.id=?'; $values[] = $id; } if ($catId !== false) { - $sql .= ' AND f.category=?'; + $sql .= $hasWhere ? ' AND' : ' WHERE'; + $hasWhere = true; + $sql .= ' f.category=?'; $values[] = $catId; } $stm = $this->bd->prepare($sql); @@ -309,7 +343,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } else { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' . 'SET e.is_read=?,' - . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + . 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 ' . 'WHERE e.id=? AND e.is_read=?'; $values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1); $stm = $this->bd->prepare($sql); @@ -430,17 +464,17 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } $this->bd->beginTransaction(); - $sql = 'UPDATE `' . $this->prefix . 'entry` e ' - . 'SET e.is_read=1 ' - . 'WHERE e.id_feed=? AND e.is_read=0 AND e.id <= ?'; + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET is_read=1 ' + . 'WHERE id_feed=? AND is_read=0 AND id <= ?'; $values = array($id_feed, $idMax); - list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state); + list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state); $stm = $this->bd->prepare($sql . $search); if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error markReadFeed: ' . $info[2]); + Minz_Log::error('SQL error markReadFeed: ' . $info[2] . ' with SQL: ' . $sql . $search); $this->bd->rollBack(); return false; } @@ -448,13 +482,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($affected > 0) { $sql = 'UPDATE `' . $this->prefix . 'feed` ' - . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected + . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected . ' WHERE id=?'; $values = array($id_feed); $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error markReadFeed: ' . $info[2]); + Minz_Log::error('SQL error markReadFeed cache: ' . $info[2]); $this->bd->rollBack(); return false; } @@ -658,7 +692,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if (count($guids) < 1) { return array(); } - $sql = 'SELECT guid, hex(hash) AS hexHash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)'; + $guids = array_unique($guids); + $sql = 'SELECT guid, ' . $this->sqlHexEncode('hash') . ' AS hex_hash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)'; $stm = $this->bd->prepare($sql); $values = array($id_feed); $values = array_merge($values, $guids); @@ -666,7 +701,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $result = array(); $rows = $stm->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { - $result[$row['guid']] = $row['hexHash']; + $result[$row['guid']] = $row['hex_hash']; } return $result; } else { @@ -680,13 +715,16 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } - public function updateLastSeen($id_feed, $guids) { + public function updateLastSeen($id_feed, $guids, $mtime = 0) { if (count($guids) < 1) { return 0; } - $sql = 'UPDATE `' . $this->prefix . 'entry` SET lastSeen=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)'; + $sql = 'UPDATE `' . $this->prefix . 'entry` SET `lastSeen`=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)'; $stm = $this->bd->prepare($sql); - $values = array(time(), $id_feed); + if ($mtime <= 0) { + $mtime = time(); + } + $values = array($mtime, $id_feed); $values = array_merge($values, $guids); if ($stm && $stm->execute($values)) { return $stm->rowCount(); diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php new file mode 100644 index 000000000..b96a62ebc --- /dev/null +++ b/app/Models/EntryDAOPGSQL.php @@ -0,0 +1,31 @@ +<?php + +class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { + + public function sqlHexDecode($x) { + return 'decode(' . $x . ", 'hex')"; + } + + public function sqlHexEncode($x) { + return 'encode(' . $x . ", 'hex')"; + } + + protected function autoUpdateDb($errorInfo) { + return false; + } + + protected function addColumn($name) { + return false; + } + + public function size($all = true) { + $db = FreshRSS_Context::$system_conf->db; + $sql = 'SELECT pg_size_pretty(pg_database_size(?))'; + $values = array($db['base']); + $stm = $this->bd->prepare($sql); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); + return $res[0]; + } + +} diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index dad34a93d..34e854608 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -2,6 +2,10 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { + public function sqlHexDecode($x) { + return $x; + } + protected function autoUpdateDb($errorInfo) { if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR //autoAddColumn @@ -24,17 +28,21 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { protected function updateCacheUnreads($catId = false, $feedId = false) { $sql = 'UPDATE `' . $this->prefix . 'feed` ' - . 'SET cache_nbUnreads=(' + . 'SET `cache_nbUnreads`=(' . 'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e ' - . 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) ' - . 'WHERE 1'; + . 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0)'; + $hasWhere = false; $values = array(); if ($feedId !== false) { - $sql .= ' AND id=?'; + $sql .= $hasWhere ? ' AND' : ' WHERE'; + $hasWhere = true; + $sql .= ' id=?'; $values[] = $feedId; } if ($catId !== false) { - $sql .= ' AND category=?'; + $sql .= $hasWhere ? ' AND' : ' WHERE'; + $hasWhere = true; + $sql .= ' category=?'; $values[] = $catId; } $stm = $this->bd->prepare($sql); @@ -82,7 +90,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } $affected = $stm->rowCount(); if ($affected > 0) { - $sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + $sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 ' . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)'; $values = array($ids); $stm = $this->bd->prepare($sql); diff --git a/app/Models/Factory.php b/app/Models/Factory.php index db09d155d..764987c46 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -4,37 +4,47 @@ class FreshRSS_Factory { public static function createFeedDao($username = null) { $conf = Minz_Configuration::get('system'); - if ($conf->db['type'] === 'sqlite') { - return new FreshRSS_FeedDAOSQLite($username); - } else { - return new FreshRSS_FeedDAO($username); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_FeedDAOSQLite($username); + default: + return new FreshRSS_FeedDAO($username); } } public static function createEntryDao($username = null) { $conf = Minz_Configuration::get('system'); - if ($conf->db['type'] === 'sqlite') { - return new FreshRSS_EntryDAOSQLite($username); - } else { - return new FreshRSS_EntryDAO($username); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_EntryDAOSQLite($username); + case 'pgsql': + return new FreshRSS_EntryDAOPGSQL($username); + default: + return new FreshRSS_EntryDAO($username); } } public static function createStatsDAO($username = null) { $conf = Minz_Configuration::get('system'); - if ($conf->db['type'] === 'sqlite') { - return new FreshRSS_StatsDAOSQLite($username); - } else { - return new FreshRSS_StatsDAO($username); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_StatsDAOSQLite($username); + case 'pgsql': + return new FreshRSS_StatsDAOPGSQL($username); + default: + return new FreshRSS_StatsDAO($username); } } public static function createDatabaseDAO($username = null) { $conf = Minz_Configuration::get('system'); - if ($conf->db['type'] === 'sqlite') { - return new FreshRSS_DatabaseDAOSQLite($username); - } else { - return new FreshRSS_DatabaseDAO($username); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_DatabaseDAOSQLite($username); + case 'pgsql': + return new FreshRSS_DatabaseDAOPGSQL($username); + default: + return new FreshRSS_DatabaseDAO($username); } } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 6104b1e31..97cb1c47e 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -131,13 +131,26 @@ class FreshRSS_Feed extends Minz_Model { return $this->nbNotRead; } public function faviconPrepare() { - $file = DATA_PATH . '/favicons/' . $this->hash() . '.txt'; - if (!file_exists($file)) { - $t = $this->website; - if ($t == '') { - $t = $this->url; + global $favicons_dir; + require_once(LIB_PATH . '/favicons.php'); + $url = $this->website; + if ($url == '') { + $url = $this->url; + } + $txt = $favicons_dir . $this->hash() . '.txt'; + if (!file_exists($txt)) { + file_put_contents($txt, $url); + } + if (FreshRSS_Context::$isCli) { + $ico = $favicons_dir . $this->hash() . '.ico'; + $ico_mtime = @filemtime($ico); + $txt_mtime = @filemtime($txt); + if ($txt_mtime != false && + ($ico_mtime == false || $ico_mtime < $txt_mtime || ($ico_mtime < time() - (14 * 86400)))) { + // no ico file or we should download a new one. + $url = file_get_contents($txt); + download_favicon($url, $ico) || touch($ico); } - file_put_contents($file, $t); } } public static function faviconDelete($hash) { @@ -216,7 +229,7 @@ class FreshRSS_Feed extends Minz_Model { $this->nbEntries = intval($value); } - public function load($loadDetails = false) { + public function load($loadDetails = false, $noCache = false) { if ($this->url !== null) { if (CACHE_PATH === false) { throw new Minz_FileNotExistException( @@ -268,7 +281,7 @@ class FreshRSS_Feed extends Minz_Model { $this->_url($clean_url); } - if (($mtime === true) || ($mtime > $this->lastUpdate)) { + if (($mtime === true) || ($mtime > $this->lastUpdate) || $noCache) { //Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url); $this->loadEntries($feed); // et on charge les articles du flux } else { @@ -309,11 +322,11 @@ class FreshRSS_Feed extends Minz_Model { $elinks[$elink] = '1'; $mime = strtolower($enclosure->get_type()); if (strpos($mime, 'image/') === 0) { - $content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />'; + $content .= '<p class="enclosure"><img src="' . $elink . '" alt="" /></p>'; } elseif (strpos($mime, 'audio/') === 0) { - $content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />'; + $content .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>'; } elseif (strpos($mime, 'video/') === 0) { - $content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />'; + $content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>'; } else { unset($elinks[$elink]); } @@ -340,6 +353,10 @@ class FreshRSS_Feed extends Minz_Model { $this->entries = $entries; } + function cacheModifiedTime() { + return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc'); + } + function lock() { $this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock'; if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) { @@ -460,7 +477,7 @@ class FreshRSS_Feed extends Minz_Model { CURLOPT_URL => $this->hubUrl, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, - CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', + CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', CURLOPT_POSTFIELDS => 'hub.verify=sync' . '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe') . '&hub.topic=' . urlencode($this->selfUrl) diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 475d39286..68398efd5 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -2,9 +2,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { public function addFeed($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)'; + $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, `lastUpdate`, priority, `httpAuth`, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)'; $stm = $this->bd->prepare($sql); + $valuesTmp['url'] = safe_ascii($valuesTmp['url']); + $valuesTmp['website'] = safe_ascii($valuesTmp['website']); + $values = array( substr($valuesTmp['url'], 0, 511), $valuesTmp['category'], @@ -16,7 +19,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { ); if ($stm && $stm->execute($values)) { - return $this->bd->lastInsertId(); + return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"'); } else { $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); Minz_Log::error('SQL error addFeed: ' . $info[2]); @@ -55,6 +58,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } public function updateFeed($id, $valuesTmp) { + if (isset($valuesTmp['url'])) { + $valuesTmp['url'] = safe_ascii($valuesTmp['url']); + } + if (isset($valuesTmp['website'])) { + $valuesTmp['website'] = safe_ascii($valuesTmp['website']); + } + $set = ''; foreach ($valuesTmp as $key => $v) { $set .= $key . '=?, '; @@ -82,22 +92,26 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } - public function updateLastUpdate($id, $inError = 0, $updateCache = true) { + 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=? ' + . '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=? ' + . 'SET `lastUpdate`=?, error=? ' . 'WHERE id=?'; } + if ($mtime <= 0) { + $mtime = time(); + } + $values = array( - time(), - $inError, + $mtime, + $inError ? 1 : 0, $id, ); @@ -198,6 +212,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } + public function listFeedsIds() { + $sql = 'SELECT id FROM `' . $this->prefix . 'feed`'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + return $stm->fetchAll(PDO::FETCH_COLUMN, 0); + } + public function listFeeds() { $sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name'; $stm = $this->bd->prepare($sql); @@ -222,14 +243,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $feedCategoryNames; } + /** + * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL. + */ public function listFeedsOrderUpdate($defaultCacheDuration = 3600) { - if ($defaultCacheDuration < 0) { - $defaultCacheDuration = 2147483647; - } - $sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl ' + $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl ' . 'FROM `' . $this->prefix . 'feed` ' - . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ' - . 'ORDER BY lastUpdate'; + . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl <> -1 AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ') + . 'ORDER BY `lastUpdate`'; $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute())) { $sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2'; //v0.7.3 @@ -282,7 +303,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . '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'; + . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads'; $stm = $this->bd->prepare($sql); if ($stm && $stm->execute()) { @@ -308,7 +329,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $affected = $stm->rowCount(); $sql = 'UPDATE `' . $this->prefix . 'feed` ' - . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?'; + . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?'; $values = array($id); $stm = $this->bd->prepare($sql); if (!($stm && $stm->execute($values))) { @@ -326,7 +347,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' . 'WHERE id_feed=:id_feed AND id<=:id_max ' . 'AND is_favorite=0 ' //Do not remove favourites - . 'AND lastSeen < (SELECT maxLastSeen FROM (SELECT (MAX(e3.lastSeen)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance + . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' $stm = $this->bd->prepare($sql); diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php index 7599fda53..440ae74da 100644 --- a/app/Models/FeedDAOSQLite.php +++ b/app/Models/FeedDAOSQLite.php @@ -4,8 +4,8 @@ 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)'; + . '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(); diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 4f83ff577..2ce4f2944 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -4,6 +4,10 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { const ENTRY_COUNT_PERIOD = 30; + protected function sqlFloor($s) { + return "FLOOR($s)"; + } + /** * Calculates entry repartition for all feeds and for main stream. * @@ -37,12 +41,12 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { $filter .= "AND e.id_feed = {$feed}"; } $sql = <<<SQL -SELECT COUNT(1) AS `total`, -COUNT(1) - SUM(e.is_read) AS `unread`, -SUM(e.is_read) AS `read`, -SUM(e.is_favorite) AS `favorite` -FROM {$this->prefix}entry AS e -, {$this->prefix}feed AS f +SELECT COUNT(1) AS total, +COUNT(1) - SUM(e.is_read) AS count_unreads, +SUM(e.is_read) AS count_reads, +SUM(e.is_favorite) AS count_favorites +FROM `{$this->prefix}entry` AS e +, `{$this->prefix}feed` AS f WHERE e.id_feed = f.id {$filter} SQL; @@ -61,14 +65,16 @@ SQL; */ public function calculateEntryCount() { $count = $this->initEntryCountArray(); - $period = self::ENTRY_COUNT_PERIOD; + $midnight = mktime(0, 0, 0); + $oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400); // Get stats per day for the last 30 days + $sqlDay = $this->sqlFloor("(date - $midnight) / 86400"); $sql = <<<SQL -SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day, -COUNT(1) AS count -FROM {$this->prefix}entry AS e -WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d') +SELECT {$sqlDay} AS day, +COUNT(*) as count +FROM `{$this->prefix}entry` +WHERE date >= {$oldest} AND date < {$midnight} GROUP BY day ORDER BY day ASC SQL; @@ -80,28 +86,7 @@ SQL; $count[$value['day']] = (int) $value['count']; } - return $this->convertToSerie($count); - } - - /** - * Calculates entry average per day on a 30 days period. - * - * @return integer - */ - public function calculateEntryAverage() { - $period = self::ENTRY_COUNT_PERIOD; - - // Get stats per day for the last 30 days - $sql = <<<SQL -SELECT COUNT(1) / {$period} AS average -FROM {$this->prefix}entry AS e -WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d') -SQL; - $stm = $this->bd->prepare($sql); - $stm->execute(); - $res = $stm->fetch(PDO::FETCH_NAMED); - - return round($res['average'], 2); + return $count; } /** @@ -158,7 +143,7 @@ SQL; $sql = <<<SQL SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period , COUNT(1) AS count -FROM {$this->prefix}entry AS e +FROM `{$this->prefix}entry` AS e {$restrict} GROUP BY period ORDER BY period ASC @@ -173,7 +158,7 @@ SQL; $repartition[(int) $value['period']] = (int) $value['count']; } - return $this->convertToSerie($repartition); + return $repartition; } /** @@ -222,7 +207,7 @@ SQL; SELECT COUNT(1) AS count , MIN(date) AS date_min , MAX(date) AS date_max -FROM {$this->prefix}entry AS e +FROM `{$this->prefix}entry` AS e {$restrict} SQL; $stm = $this->bd->prepare($sql); @@ -266,8 +251,8 @@ SQL; $sql = <<<SQL SELECT c.name AS label , COUNT(f.id) AS data -FROM {$this->prefix}category AS c, -{$this->prefix}feed AS f +FROM `{$this->prefix}category` AS c, +`{$this->prefix}feed` AS f WHERE c.id = f.category GROUP BY label ORDER BY data DESC @@ -276,7 +261,7 @@ SQL; $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $this->convertToPieSerie($res); + return $res; } /** @@ -289,9 +274,9 @@ SQL; $sql = <<<SQL SELECT c.name AS label , COUNT(e.id) AS data -FROM {$this->prefix}category AS c, -{$this->prefix}feed AS f, -{$this->prefix}entry AS e +FROM `{$this->prefix}category` AS c, +`{$this->prefix}feed` AS f, +`{$this->prefix}entry` AS e WHERE c.id = f.category AND f.id = e.id_feed GROUP BY label @@ -301,7 +286,7 @@ SQL; $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $this->convertToPieSerie($res); + return $res; } /** @@ -315,9 +300,9 @@ SELECT f.id AS id , MAX(f.name) AS name , MAX(c.name) AS category , COUNT(e.id) AS count -FROM {$this->prefix}category AS c, -{$this->prefix}feed AS f, -{$this->prefix}entry AS e +FROM `{$this->prefix}category` AS c, +`{$this->prefix}feed` AS f, +`{$this->prefix}entry` AS e WHERE c.id = f.category AND f.id = e.id_feed GROUP BY f.id @@ -340,8 +325,8 @@ SELECT MAX(f.id) as id , MAX(f.name) AS name , MAX(date) AS last_date , COUNT(*) AS nb_articles -FROM {$this->prefix}feed AS f, -{$this->prefix}entry AS e +FROM `{$this->prefix}feed` AS f, +`{$this->prefix}entry` AS e WHERE f.id = e.id_feed GROUP BY f.id ORDER BY name @@ -351,27 +336,6 @@ SQL; return $stm->fetchAll(PDO::FETCH_ASSOC); } - protected function convertToSerie($data) { - $serie = array(); - - foreach ($data as $key => $value) { - $serie[] = array($key, $value); - } - - return $serie; - } - - protected function convertToPieSerie($data) { - $serie = array(); - - foreach ($data as $value) { - $value['data'] = array(array(0, (int) $value['data'])); - $serie[] = $value; - } - - return $serie; - } - /** * Gets days ready for graphs * diff --git a/app/Models/StatsDAOPGSQL.php b/app/Models/StatsDAOPGSQL.php new file mode 100644 index 000000000..1effbb64b --- /dev/null +++ b/app/Models/StatsDAOPGSQL.php @@ -0,0 +1,67 @@ +<?php + +class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO { + + /** + * Calculates the number of article per hour of the day per feed + * + * @param integer $feed id + * @return string + */ + public function calculateEntryRepartitionPerFeedPerHour($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed); + } + + /** + * Calculates the number of article per day of week per feed + * + * @param integer $feed id + * @return string + */ + public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed); + } + + /** + * Calculates the number of article per month per feed + * + * @param integer $feed + * @return string + */ + public function calculateEntryRepartitionPerFeedPerMonth($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed); + } + + /** + * Calculates the number of article per period per feed + * + * @param string $period format string to use for grouping + * @param integer $feed id + * @return string + */ + protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { + $restrict = ''; + if ($feed) { + $restrict = "WHERE e.id_feed = {$feed}"; + } + $sql = <<<SQL +SELECT extract( {$period} from to_timestamp(e.date)) AS period +, COUNT(1) AS count +FROM "{$this->prefix}entry" AS e +{$restrict} +GROUP BY period +ORDER BY period ASC +SQL; + + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_NAMED); + + foreach ($res as $value) { + $repartition[(int) $value['period']] = (int) $value['count']; + } + + return $repartition; + } + +} diff --git a/app/Models/StatsDAOSQLite.php b/app/Models/StatsDAOSQLite.php index 9bfe8b20a..6cfc20463 100644 --- a/app/Models/StatsDAOSQLite.php +++ b/app/Models/StatsDAOSQLite.php @@ -2,59 +2,8 @@ class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO { - /** - * Calculates entry count per day on a 30 days period. - * Returns the result as a JSON object. - * - * @return JSON object - */ - public function calculateEntryCount() { - $count = $this->initEntryCountArray(); - $period = parent::ENTRY_COUNT_PERIOD; - - // Get stats per day for the last 30 days - $sql = <<<SQL -SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day, -COUNT(1) AS count -FROM {$this->prefix}entry AS e -WHERE strftime('%Y%m%d', e.date, 'unixepoch') - BETWEEN strftime('%Y%m%d', 'now', '-{$period} days') - AND strftime('%Y%m%d', 'now', '-1 day') -GROUP BY day -ORDER BY day ASC -SQL; - $stm = $this->bd->prepare($sql); - $stm->execute(); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); - - foreach ($res as $value) { - $count[(int) $value['day']] = (int) $value['count']; - } - - return $this->convertToSerie($count); - } - - /** - * Calculates entry average per day on a 30 days period. - * - * @return integer - */ - public function calculateEntryAverage() { - $period = self::ENTRY_COUNT_PERIOD; - - // Get stats per day for the last 30 days - $sql = <<<SQL -SELECT COUNT(1) / {$period} AS average -FROM {$this->prefix}entry AS e -WHERE strftime('%Y%m%d', e.date, 'unixepoch') - BETWEEN strftime('%Y%m%d', 'now', '-{$period} days') - AND strftime('%Y%m%d', 'now', '-1 day') -SQL; - $stm = $this->bd->prepare($sql); - $stm->execute(); - $res = $stm->fetch(PDO::FETCH_NAMED); - - return round($res['average'], 2); + protected function sqlFloor($s) { + return "CAST(($s) AS INT)"; } protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { @@ -66,7 +15,7 @@ SQL; $sql = <<<SQL SELECT strftime('{$period}', e.date, 'unixepoch') AS period , COUNT(1) AS count -FROM {$this->prefix}entry AS e +FROM `{$this->prefix}entry` AS e {$restrict} GROUP BY period ORDER BY period ASC @@ -81,7 +30,7 @@ SQL; $repartition[(int) $value['period']] = (int) $value['count']; } - return $this->convertToSerie($repartition); + return $repartition; } } diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index b55766ab4..a95ee6bc4 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -1,34 +1,60 @@ <?php class FreshRSS_UserDAO extends Minz_ModelPdo { - public function createUser($username) { + public function createUser($username, $new_user_language, $insertDefaultFeeds = true) { $db = FreshRSS_Context::$system_conf->db; require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); $userPDO = new Minz_ModelPdo($username); - $ok = false; - if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL - $sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', _t('gen.short.default_category')); - $stm = $userPDO->bd->prepare($sql); - $ok = $stm && $stm->execute(); - } else { //E.g. SQLite - global $SQL_CREATE_TABLES; - if (is_array($SQL_CREATE_TABLES)) { - $ok = true; - foreach ($SQL_CREATE_TABLES as $instruction) { - $sql = sprintf($instruction, '', _t('gen.short.default_category')); + $currentLanguage = Minz_Translate::language(); + + try { + Minz_Translate::reset($new_user_language); + $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')); + $stm = $userPDO->bd->prepare($sql); + $ok = $stm && $stm->execute(); + } else { //E.g. SQLite + global $SQL_CREATE_TABLES; + if (is_array($SQL_CREATE_TABLES)) { + $ok = true; + foreach ($SQL_CREATE_TABLES 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 (defined('SQL_INSERT_FEEDS')) { //E.g. MySQL + $sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user); $stm = $userPDO->bd->prepare($sql); - $ok &= ($stm && $stm->execute()); + $ok &= $stm && $stm->execute(); + } else { //E.g. SQLite + global $SQL_INSERT_FEEDS; + if (is_array($SQL_INSERT_FEEDS)) { + foreach ($SQL_INSERT_FEEDS as $instruction) { + $sql = sprintf($instruction, $bd_prefix_user); + $stm = $userPDO->bd->prepare($sql); + $ok &= ($stm && $stm->execute()); + } + } } } + } catch (Exception $e) { + Minz_Log::error('Error while creating user: ' . $e->getMessage()); } + Minz_Translate::reset($currentLanguage); + if ($ok) { return true; } else { $info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo(); - Minz_Log::error('SQL error : ' . $info[2]); + Minz_Log::error('SQL error: ' . $info[2]); return false; } } diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index c78839ef7..a454829d5 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -1,4 +1,6 @@ <?php +define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + define('SQL_CREATE_TABLES', ' CREATE TABLE IF NOT EXISTS `%1$scategory` ( `id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7 @@ -57,11 +59,14 @@ CREATE TABLE IF NOT EXISTS `%1$sentry` ( ENGINE = INNODB; INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s"); +'); + +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 TABLES %1$sentry, %1$sfeed, %1$scategory'); +define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%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 new file mode 100644 index 000000000..9f4240b98 --- /dev/null +++ b/app/SQL/install.sql.pgsql.php @@ -0,0 +1,63 @@ +<?php +define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';'); + +global $SQL_CREATE_TABLES; +$SQL_CREATE_TABLES = array( +'CREATE TABLE IF NOT EXISTS "%1$scategory" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(255) UNIQUE NOT NULL +);', + +'CREATE TABLE IF NOT EXISTS "%1$sfeed" ( + "id" SERIAL PRIMARY KEY, + "url" varchar(511) UNIQUE NOT NULL, + "category" SMALLINT DEFAULT 0, + "name" VARCHAR(255) NOT NULL, + "website" VARCHAR(255), + "description" text, + "lastUpdate" INT DEFAULT 0, + "priority" SMALLINT NOT NULL DEFAULT 10, + "pathEntries" VARCHAR(511) DEFAULT NULL, + "httpAuth" VARCHAR(511) DEFAULT NULL, + "error" smallint DEFAULT 0, + "keep_history" INT NOT NULL DEFAULT -2, + "ttl" INT NOT NULL DEFAULT -2, + "cache_nbEntries" INT DEFAULT 0, + "cache_nbUnreads" INT DEFAULT 0, + FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE +);', +'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");', +'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");', +'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");', + +'CREATE TABLE IF NOT EXISTS "%1$sentry" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "guid" VARCHAR(760) UNIQUE 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$sis_favorite_index ON "%1$sentry" ("is_favorite");', +'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");', +'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");', + +'INSERT INTO "%1$scategory" (name) SELECT \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1);', +); + +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"'); diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 87d5cf286..68d93ba92 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -1,16 +1,16 @@ <?php global $SQL_CREATE_TABLES; $SQL_CREATE_TABLES = array( -'CREATE TABLE IF NOT EXISTS `%1$scategory` ( +'CREATE TABLE IF NOT EXISTS `category` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` varchar(255) NOT NULL, UNIQUE (`name`) );', -'CREATE TABLE IF NOT EXISTS `%1$sfeed` ( +'CREATE TABLE IF NOT EXISTS `feed` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` varchar(511) NOT NULL, - `%1$scategory` SMALLINT DEFAULT 0, + `category` SMALLINT DEFAULT 0, `name` varchar(255) NOT NULL, `website` varchar(255), `description` text, @@ -23,15 +23,15 @@ $SQL_CREATE_TABLES = array( `ttl` INT NOT NULL DEFAULT -2, `cache_nbEntries` int DEFAULT 0, `cache_nbUnreads` int DEFAULT 0, - FOREIGN KEY (`%1$scategory`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, + FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, UNIQUE (`url`) );', -'CREATE INDEX IF NOT EXISTS feed_name_index ON `%1$sfeed`(`name`);', -'CREATE INDEX IF NOT EXISTS feed_priority_index ON `%1$sfeed`(`priority`);', -'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `%1$sfeed`(`keep_history`);', +'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`);', -'CREATE TABLE IF NOT EXISTS `%1$sentry` ( +'CREATE TABLE IF NOT EXISTS `entry` ( `id` bigint NOT NULL, `guid` varchar(760) NOT NULL, `title` varchar(255) NOT NULL, @@ -46,17 +46,21 @@ $SQL_CREATE_TABLES = array( `id_feed` SMALLINT, `tags` varchar(1023), PRIMARY KEY (`id`), - FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + 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 `%1$sentry`(`is_favorite`);', -'CREATE INDEX IF NOT EXISTS entry_is_read_index ON `%1$sentry`(`is_read`);', -'CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `%1$sentry`(`lastSeen`);', //v1.1.1 +'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 -'INSERT OR IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s");', -'INSERT OR 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 OR IGNORE INTO `%1$sfeed` (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);', +'INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "%2$s");', ); -define('SQL_DROP_TABLES', 'DROP TABLES %1$sentry, %1$sfeed, %1$scategory'); +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'); diff --git a/app/actualize_script.php b/app/actualize_script.php index fc4f9bfbb..deaa1bf7c 100755 --- a/app/actualize_script.php +++ b/app/actualize_script.php @@ -28,12 +28,13 @@ $app = new FreshRSS(); $system_conf = Minz_Configuration::get('system'); $system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) +FreshRSS_Context::$isCli = true; // Create the list of users to actualize. // Users are processed in a random order but always start with admin $users = listUsers(); shuffle($users); -if ($system_conf->default_user !== ''){ +if ($system_conf->default_user !== '') { array_unshift($users, $system_conf->default_user); $users = array_unique($users); } diff --git a/app/i18n/cz/admin.php b/app/i18n/cz/admin.php index 881c02fc6..84bc58a17 100644 --- a/app/i18n/cz/admin.php +++ b/app/i18n/cz/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'Máte požadovanou knihovnu pro ověřování znaků (ctype).', ), 'curl' => array( - 'nok' => 'Nemáte cURL (balíček php5-curl).', + 'nok' => 'Nemáte cURL (balíček php-curl).', 'ok' => 'Máte rozšíření cURL.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).', ), 'pdo' => array( - 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', - 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP instalace', @@ -93,7 +93,7 @@ return array( 'ok' => 'Oprávnění adresáře users jsou v pořádku.', ), 'zip' => array( - 'nok' => 'Nemáte rozšíření ZIP (balíček php5-zip).', + 'nok' => 'Nemáte rozšíření ZIP (balíček php-zip).', 'ok' => 'Máte rozšíření ZIP.', ), ), diff --git a/app/i18n/cz/feedback.php b/app/i18n/cz/feedback.php index 81302afca..f2bd87c77 100644 --- a/app/i18n/cz/feedback.php +++ b/app/i18n/cz/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s neexistuje', ), 'import_export' => array( - 'export_no_zip_extension' => 'Na serveru není naistalována podpora zip. Zkuste prosím exportovat soubory jeden po druhém.', + 'export_no_zip_extension' => 'Na serveru není naistalována podpora ZIP. Zkuste prosím exportovat soubory jeden po druhém.', 'feeds_imported' => 'Vaše kanály byly naimportovány a nyní budou aktualizovány', 'feeds_imported_with_errors' => 'Vaše kanály byly naimportovány, došlo ale k nějakým chybám', 'file_cannot_be_uploaded' => 'Soubor nelze nahrát!', - 'no_zip_extension' => 'Na serveru není naistalována podpora zip.', - 'zip_error' => 'Během importu zip souboru došlo k chybě.', + 'no_zip_extension' => 'Na serveru není naistalována podpora ZIP.', + 'zip_error' => 'Během importu ZIP souboru došlo k chybě.', ), 'sub' => array( 'actualize' => 'Aktualizovat', diff --git a/app/i18n/cz/install.php b/app/i18n/cz/install.php index 6b94c0d4b..c40ae46e2 100644 --- a/app/i18n/cz/install.php +++ b/app/i18n/cz/install.php @@ -41,7 +41,7 @@ return array( 'ok' => 'Je nainstalována požadovaná knihovna pro ověřování znaků (ctype).', ), 'curl' => array( - 'nok' => 'Nemáte cURL (balíček php5-curl).', + 'nok' => 'Nemáte cURL (balíček php-curl).', 'ok' => 'Máte rozšíření cURL.', ), 'data' => array( @@ -73,8 +73,8 @@ return array( 'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).', ), 'pdo' => array( - 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', - 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).', + 'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.', diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index cea0541e3..274cf16e1 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Exportovat seznam kanálů (OPML)', 'export_starred' => 'Exportovat oblíbené', 'feed_list' => 'Seznam %s článků', - 'file_to_import' => 'Soubor k importu<br />(OPML, Json nebo Zip)', - 'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo Json)', + 'file_to_import' => 'Soubor k importu<br />(OPML, JSON nebo ZIP)', + 'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo JSON)', 'import' => 'Import', 'starred_list' => 'Seznam oblíbených článků', 'title' => 'Import / export', diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index 7b75fe5f4..c12f32bc8 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).', ), 'curl' => array( - 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).', + 'nok' => 'Ihnen fehlt cURL (Paket php-curl).', 'ok' => 'Sie haben die cURL-Erweiterung.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).', ), 'pdo' => array( - 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).', - 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).', + 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP-Installation', @@ -93,7 +93,7 @@ return array( 'ok' => 'Die Berechtigungen des Verzeichnisses <em>./data/users</em> sind in Ordnung.', ), 'zip' => array( - 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php5-zip).', + 'nok' => 'Ihnen fehlt die ZIP-Erweiterung (Paket php-zip).', 'ok' => 'Sie haben die ZIP-Erweiterung.', ), ), diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php index f93992982..195083b36 100644 --- a/app/i18n/de/feedback.php +++ b/app/i18n/de/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s existiert nicht', ), 'import_export' => array( - 'export_no_zip_extension' => 'Die Zip-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.', + 'export_no_zip_extension' => 'Die ZIP-Erweiterung fehlt auf Ihrem Server. Bitte versuchen Sie die Dateien eine nach der anderen zu exportieren.', 'feeds_imported' => 'Ihre Feeds sind importiert worden und werden jetzt aktualisiert', 'feeds_imported_with_errors' => 'Ihre Feeds sind importiert worden, aber es traten einige Fehler auf', 'file_cannot_be_uploaded' => 'Die Datei kann nicht hochgeladen werden!', - 'no_zip_extension' => 'Die Zip-Erweiterung ist auf Ihrem Server nicht vorhanden.', - 'zip_error' => 'Ein Fehler trat während des Zip-Imports auf.', + 'no_zip_extension' => 'Die ZIP-Erweiterung ist auf Ihrem Server nicht vorhanden.', + 'zip_error' => 'Ein Fehler trat während des ZIP-Imports auf.', ), 'sub' => array( 'actualize' => 'Aktualisieren', diff --git a/app/i18n/de/install.php b/app/i18n/de/install.php index a77822e7b..178e8ea4c 100644 --- a/app/i18n/de/install.php +++ b/app/i18n/de/install.php @@ -4,7 +4,7 @@ return array( 'action' => array( 'finish' => 'Installation fertigstellen', 'fix_errors_before' => 'Bitte Fehler korrigieren, bevor zum nächsten Schritt gesprungen wird.', - 'keep_install' => 'Vorherige Installation beibehalten (Daten)', + 'keep_install' => 'Vorherige Konfiguration beibehalten', 'next_step' => 'Zum nächsten Schritt springen', 'reinstall' => 'Neuinstallation von FreshRSS', ), @@ -41,7 +41,7 @@ return array( 'ok' => 'Sie haben die benötigte Bibliothek für die Überprüfung von Zeichentypen (ctype).', ), 'curl' => array( - 'nok' => 'Ihnen fehlt cURL (Paket php5-curl).', + 'nok' => 'Ihnen fehlt cURL (Paket php-curl).', 'ok' => 'Sie haben die cURL-Erweiterung.', ), 'data' => array( @@ -73,8 +73,8 @@ return array( 'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).', ), 'pdo' => array( - 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).', - 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).', + 'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 0f05a5635..cc98fd25c 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -44,7 +44,7 @@ return array( 'export_opml' => 'Liste der Feeds exportieren (OPML)', 'export_starred' => 'Ihre Favoriten exportieren', 'feed_list' => 'Liste von %s Artikeln', - 'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder Zip)', + 'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder ZIP)', 'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)', 'import' => 'Importieren', 'starred_list' => 'Liste der Lieblingsartikel', diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index a88552087..e5286d948 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -29,12 +29,12 @@ return array( 'ok' => 'Connection to the database is ok.', ), 'ctype' => array( - 'nok' => 'You lack a required library for character type checking (php-ctype).', + 'nok' => 'Cannot find a required library for character type checking (php-ctype).', 'ok' => 'You have the required library for character type checking (ctype).', ), 'curl' => array( - 'nok' => 'You lack cURL (php5-curl package).', - 'ok' => 'You have cURL extension.', + 'nok' => 'Cannot find the cURL library (php-curl package).', + 'ok' => 'You have the cURL library.', ), 'data' => array( 'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into', @@ -42,7 +42,7 @@ return array( ), 'database' => 'Database installation', 'dom' => array( - 'nok' => 'You lack a required library to browse the DOM (php-xml package).', + 'nok' => 'Cannot find a required library to browse the DOM (php-xml package).', 'ok' => 'You have the required library to browse the DOM.', ), 'entries' => array( @@ -59,20 +59,20 @@ return array( ), 'files' => 'File installation', 'json' => array( - 'nok' => 'You lack JSON (php5-json package).', + 'nok' => 'Cannot find JSON (php5-json package).', 'ok' => 'You have JSON extension.', ), 'minz' => array( - 'nok' => 'You lack the Minz framework.', + 'nok' => 'Cannot find the Minz framework.', 'ok' => 'You have the Minz framework.', ), 'pcre' => array( - 'nok' => 'You lack a required library for regular expressions (php-pcre).', + 'nok' => 'Cannot find a required library for regular expressions (php-pcre).', 'ok' => 'You have the required library for regular expressions (PCRE).', ), 'pdo' => array( - 'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', - 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', + 'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP installation', @@ -93,7 +93,7 @@ return array( 'ok' => 'Permissions on users directory are good.', ), 'zip' => array( - 'nok' => 'You lack ZIP extension (php5-zip package).', + 'nok' => 'Cannot find ZIP extension (php-zip package).', 'ok' => 'You have ZIP extension.', ), ), diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php index 7ce2ae9cf..e7f6b9f85 100644 --- a/app/i18n/en/feedback.php +++ b/app/i18n/en/feedback.php @@ -43,12 +43,12 @@ return array( '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.', + '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.', + 'no_zip_extension' => 'ZIP extension is not present on your server.', + 'zip_error' => 'An error occured during ZIP import.', ), 'sub' => array( 'actualize' => 'Actualise', diff --git a/app/i18n/en/install.php b/app/i18n/en/install.php index d1c5f37c8..63d31fe53 100644 --- a/app/i18n/en/install.php +++ b/app/i18n/en/install.php @@ -4,7 +4,7 @@ return array( 'action' => array( 'finish' => 'Complete installation', 'fix_errors_before' => 'Please fix errors before skipping to the next step.', - 'keep_install' => 'Keep previous installation', + 'keep_install' => 'Keep previous configuration', 'next_step' => 'Go to the next step', 'reinstall' => 'Reinstall FreshRSS', ), @@ -25,9 +25,9 @@ return array( ), 'host' => 'Host', 'prefix' => 'Table prefix', - 'password' => 'HTTP password', + 'password' => 'Database password', 'type' => 'Type of database', - 'username' => 'HTTP username', + 'username' => 'Database username', ), 'check' => array( '_' => 'Checks', @@ -37,19 +37,19 @@ return array( 'ok' => 'Permissions on cache directory are good.', ), 'ctype' => array( - 'nok' => 'You lack a required library for character type checking (php-ctype).', + 'nok' => 'Cannot find a required library for character type checking (php-ctype).', 'ok' => 'You have the required library for character type checking (ctype).', ), 'curl' => array( - 'nok' => 'You lack cURL (php5-curl package).', - 'ok' => 'You have cURL extension.', + 'nok' => 'Cannot find the cURL library (php-curl package).', + 'ok' => 'You have the cURL library.', ), 'data' => array( 'nok' => 'Check permissions on <em>./data</em> directory. HTTP server must have rights to write into', 'ok' => 'Permissions on data directory are good.', ), 'dom' => array( - 'nok' => 'You lack a required library to browse the DOM.', + 'nok' => 'Cannot find a required library to browse the DOM.', 'ok' => 'You have the required library to browse the DOM.', ), 'favicons' => array( @@ -61,20 +61,20 @@ return array( 'ok' => 'Your HTTP REFERER is known and corresponds to your server.', ), 'json' => array( - 'nok' => 'You lack a recommended library to parse JSON.', + 'nok' => 'Cannot find a recommended library to parse JSON.', 'ok' => 'You have a recommended library to parse JSON.', ), 'minz' => array( - 'nok' => 'You lack the Minz framework.', + 'nok' => 'Cannot find the Minz framework.', 'ok' => 'You have the Minz framework.', ), 'pcre' => array( - 'nok' => 'You lack a required library for regular expressions (php-pcre).', + 'nok' => 'Cannot find a required library for regular expressions (php-pcre).', 'ok' => 'You have the required library for regular expressions (PCRE).', ), 'pdo' => array( - 'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).', - 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).', + 'nok' => 'Cannot find PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.', @@ -85,7 +85,7 @@ return array( 'ok' => 'Permissions on users directory are good.', ), 'xml' => array( - 'nok' => 'You lack the required library to parse XML.', + 'nok' => 'Cannot find the required library to parse XML.', 'ok' => 'You have the required library to parse XML.', ), ), diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index aaaa02827..789433ee6 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Export list of feeds (OPML)', 'export_starred' => 'Export your favourites', 'feed_list' => 'List of %s articles', - 'file_to_import' => 'File to import<br />(OPML, Json or Zip)', - 'file_to_import_no_zip' => 'File to import<br />(OPML or Json)', + 'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)', + 'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)', 'import' => 'Import', 'starred_list' => 'List of favourite articles', 'title' => 'Import / export', diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index c359e9d24..e9263a5e2 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -29,12 +29,12 @@ return array( 'ok' => 'La connexion à la base de données est bonne.', ), 'ctype' => array( - 'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', - 'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', + 'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).', + 'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).', ), 'curl' => array( - 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', - 'ok' => 'Vous disposez de cURL.', + 'nok' => 'Impossible de trouver la librairie cURL (paquet php-curl).', + 'ok' => 'Vous disposez de la librairie cURL.', ), 'data' => array( 'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data</em>. Le serveur HTTP doit être capable d’écrire dedans', @@ -42,8 +42,8 @@ return array( ), 'database' => 'Installation de la base de données', 'dom' => array( - 'nok' => 'Il manque une librairie pour parcourir le DOM (paquet php-xml).', - 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', + 'nok' => 'Impossible de trouver une librairie pour parcourir le DOM (paquet php-xml).', + 'ok' => 'Vous disposez de la librairie pour parcourir le DOM.', ), 'entries' => array( 'nok' => 'La table entry est mal configurée.', @@ -60,19 +60,19 @@ return array( 'files' => 'Installation des fichiers', 'json' => array( 'nok' => 'Vous ne disposez pas de JSON (paquet php5-json).', - 'ok' => 'Vous disposez de l\'extension JSON.', + 'ok' => 'Vous disposez de l’extension JSON.', ), 'minz' => array( 'nok' => 'Vous ne disposez pas de la librairie Minz.', 'ok' => 'Vous disposez du framework Minz', ), 'pcre' => array( - 'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', - 'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', + 'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).', + 'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).', ), 'pdo' => array( - 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', - 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', + 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'Installation de PHP', @@ -80,7 +80,7 @@ return array( 'ok' => 'Votre version de PHP est la %s, qui est compatible avec FreshRSS.', ), 'tables' => array( - 'nok' => 'Il manque une ou plusieurs tables en base de données.', + 'nok' => 'Impossible de trouver une ou plusieurs tables en base de données.', 'ok' => 'Les tables sont bien présentes en base de données.', ), 'title' => 'Vérification de l’installation', @@ -93,8 +93,8 @@ return array( 'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.', ), 'zip' => array( - 'nok' => 'Vous ne disposez pas de l\'extension ZIP (paquet php5-zip).', - 'ok' => 'Vous disposez de l\'extension ZIP.', + 'nok' => 'Vous ne disposez pas de l’extension ZIP (paquet php-zip).', + 'ok' => 'Vous disposez de l’extension ZIP.', ), ), 'extensions' => array( diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php index 15f3ab859..5966fc3a7 100644 --- a/app/i18n/fr/feedback.php +++ b/app/i18n/fr/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s n’existe pas', ), 'import_export' => array( - 'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', + 'export_no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', 'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.', 'feeds_imported_with_errors' => 'Vos flux ont été importés mais des erreurs sont survenues.', 'file_cannot_be_uploaded' => 'Le fichier ne peut pas être téléchargé !', - 'no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur.', - 'zip_error' => 'Une erreur est survenue durant l’import du fichier Zip.', + 'no_zip_extension' => 'L’extension ZIP n’est pas présente sur votre serveur.', + 'zip_error' => 'Une erreur est survenue durant l’import du fichier ZIP.', ), 'sub' => array( 'actualize' => 'Actualiser', diff --git a/app/i18n/fr/install.php b/app/i18n/fr/install.php index 946a210ee..c50807107 100644 --- a/app/i18n/fr/install.php +++ b/app/i18n/fr/install.php @@ -24,10 +24,10 @@ return array( 'ok' => 'La configuration de la base de données a été enregistrée.', ), 'host' => 'Hôte', - 'password' => 'Mot de passe', + 'password' => 'Mot de passe pour base de données', 'prefix' => 'Préfixe des tables', 'type' => 'Type de base de données', - 'username' => 'Nom d’utilisateur', + 'username' => 'Nom d’utilisateur pour base de données', ), 'check' => array( '_' => 'Vérifications', @@ -37,11 +37,11 @@ return array( 'ok' => 'Les droits sur le répertoire de cache sont bons.', ), 'ctype' => array( - 'nok' => 'Il manque une librairie pour la vérification des types de caractères (php-ctype).', - 'ok' => 'Vous disposez du nécessaire pour la vérification des types de caractères (ctype).', + 'nok' => 'Impossible de trouver une librairie pour la vérification des types de caractères (php-ctype).', + 'ok' => 'Vous disposez de la librairie pour la vérification des types de caractères (ctype).', ), 'curl' => array( - 'nok' => 'Vous ne disposez pas de cURL (paquet php5-curl).', + 'nok' => 'Vous ne disposez pas de cURL (paquet php-curl).', 'ok' => 'Vous disposez de cURL.', ), 'data' => array( @@ -49,8 +49,8 @@ return array( 'ok' => 'Les droits sur le répertoire de data sont bons.', ), 'dom' => array( - 'nok' => 'Il manque une librairie pour parcourir le DOM.', - 'ok' => 'Vous disposez du nécessaire pour parcourir le DOM.', + 'nok' => 'Impossible de trouver une librairie pour parcourir le DOM.', + 'ok' => 'Vous disposez de la librairie pour parcourir le DOM.', ), 'favicons' => array( 'nok' => 'Veuillez vérifier les droits sur le répertoire <em>./data/favicons</em>. Le serveur HTTP doit être capable d’écrire dedans', @@ -61,7 +61,7 @@ return array( 'ok' => 'Le HTTP REFERER est connu et semble correspondre à votre serveur.', ), 'json' => array( - 'nok' => 'Il manque une librairie recommandée pour JSON.', + 'nok' => 'Impossible de trouver une librairie recommandée pour JSON.', 'ok' => 'Vouz disposez de la librairie recommandée pour JSON.', ), 'minz' => array( @@ -69,12 +69,12 @@ return array( 'ok' => 'Vous disposez du framework Minz', ), 'pcre' => array( - 'nok' => 'Il manque une librairie pour les expressions régulières (php-pcre).', - 'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).', + 'nok' => 'Impossible de trouver une librairie pour les expressions régulières (php-pcre).', + 'ok' => 'Vous disposez de la librairie pour les expressions régulières (PCRE).', ), 'pdo' => array( - 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).', - 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).', + 'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.', @@ -85,7 +85,7 @@ return array( 'ok' => 'Les droits sur le répertoire des utilisateurs sont bons.', ), 'xml' => array( - 'nok' => 'Il manque une librairie requise pour XML.', + 'nok' => 'Impossible de trouver une librairie requise pour XML.', 'ok' => 'Vouz disposez de la librairie requise pour XML.', ), ), diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index e3631eb8b..bb3f2fefc 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Exporter la liste des flux (OPML)', 'export_starred' => 'Exporter les favoris', 'feed_list' => 'Liste des articles de %s', - 'file_to_import' => 'Fichier à importer<br />(OPML, Json ou Zip)', - 'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou Json)', + 'file_to_import' => 'Fichier à importer<br />(OPML, JSON ou ZIP)', + 'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou JSON)', 'import' => 'Importer', 'starred_list' => 'Liste des articles favoris', 'title' => 'Importer / exporter', diff --git a/app/i18n/it/admin.php b/app/i18n/it/admin.php index 4eea158f6..c399bc8c8 100644 --- a/app/i18n/it/admin.php +++ b/app/i18n/it/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).', ), 'curl' => array( - 'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).', + 'nok' => 'Manca il supporto per cURL (pacchetto php-curl).', 'ok' => 'Estensione cURL presente.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).', ), 'pdo' => array( - 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).', - 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).', + 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'Installazione PHP', @@ -93,7 +93,7 @@ return array( 'ok' => 'I permessi sulla cartella users sono corretti.', ), 'zip' => array( - 'nok' => 'Manca estensione ZIP (pacchetto php5-zip).', + 'nok' => 'Manca estensione ZIP (pacchetto php-zip).', 'ok' => 'Estensione ZIP presente.', ), ), diff --git a/app/i18n/it/feedback.php b/app/i18n/it/feedback.php index f217586b0..5851cb2e6 100644 --- a/app/i18n/it/feedback.php +++ b/app/i18n/it/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s non disponibile', ), 'import_export' => array( - 'export_no_zip_extension' => 'Estensione Zip non presente sul server. Per favore esporta i files singolarmente.', + 'export_no_zip_extension' => 'Estensione ZIP non presente sul server. Per favore esporta i files singolarmente.', 'feeds_imported' => 'I tuoi feed sono stati importati e saranno aggiornati', 'feeds_imported_with_errors' => 'I tuoi feeds sono stati importati ma si sono verificati alcuni errori', 'file_cannot_be_uploaded' => 'Il file non può essere caricato!', - 'no_zip_extension' => 'Estensione Zip non presente sul server.', - 'zip_error' => 'Si è verificato un errore importando il file Zip', + 'no_zip_extension' => 'Estensione ZIP non presente sul server.', + 'zip_error' => 'Si è verificato un errore importando il file ZIP', ), 'sub' => array( 'actualize' => 'Aggiorna', diff --git a/app/i18n/it/install.php b/app/i18n/it/install.php index a60dd4523..2fa298508 100644 --- a/app/i18n/it/install.php +++ b/app/i18n/it/install.php @@ -4,7 +4,7 @@ return array( 'action' => array( 'finish' => 'Installazione completata', 'fix_errors_before' => 'Per favore correggi gli errori prima di passare al passaggio successivo.', - 'keep_install' => 'Mantieni installazione precedente', + 'keep_install' => 'Mantieni configurazione precedente', 'next_step' => 'Vai al prossimo passaggio', 'reinstall' => 'Reinstalla FreshRSS', ), @@ -25,9 +25,9 @@ return array( ), 'host' => 'Host', 'prefix' => 'Prefisso tabella', - 'password' => 'HTTP password', + 'password' => 'Password del database', 'type' => 'Tipo di database', - 'username' => 'HTTP username', + 'username' => 'Nome utente del database', ), 'check' => array( '_' => 'Controlli', @@ -41,7 +41,7 @@ return array( 'ok' => 'Libreria richiesta per il controllo dei caratteri presente (ctype).', ), 'curl' => array( - 'nok' => 'Manca il supporto per cURL (pacchetto php5-curl).', + 'nok' => 'Manca il supporto per cURL (pacchetto php-curl).', 'ok' => 'Estensione cURL presente.', ), 'data' => array( @@ -73,8 +73,8 @@ return array( 'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).', ), 'pdo' => array( - 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).', - 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).', + 'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'Installazione PHP', diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index dfcee2ce3..758e322c5 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Esporta tutta la lista dei feed (OPML)', 'export_starred' => 'Esporta i tuoi preferiti', 'feed_list' => 'Elenco di %s articoli', - 'file_to_import' => 'File da importare<br />(OPML, Json o Zip)', - 'file_to_import_no_zip' => 'File da importare<br />(OPML o Json)', + 'file_to_import' => 'File da importare<br />(OPML, JSON o ZIP)', + 'file_to_import_no_zip' => 'File da importare<br />(OPML o JSON)', 'import' => 'Importa', 'starred_list' => 'Elenco articoli preferiti', 'title' => 'Importa / esporta', diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php index 9f05d69b1..59637bfef 100644 --- a/app/i18n/nl/admin.php +++ b/app/i18n/nl/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).', ), 'curl' => array( - 'nok' => 'U mist de cURL (php5-curl package).', + 'nok' => 'U mist de cURL (php-curl package).', 'ok' => 'U hebt de cURL uitbreiding.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).', ), 'pdo' => array( - 'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite).', - 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).', + 'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP installatie', @@ -93,7 +93,7 @@ return array( 'ok' => 'Permissies op de users map zijn goed.', ), 'zip' => array( - 'nok' => 'U mist ZIP uitbreiding (php5-zip package).', + 'nok' => 'U mist ZIP uitbreiding (php-zip package).', 'ok' => 'U hebt ZIP uitbreiding.', ), ), diff --git a/app/i18n/nl/feedback.php b/app/i18n/nl/feedback.php index b703c43cf..386b8d415 100644 --- a/app/i18n/nl/feedback.php +++ b/app/i18n/nl/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s bestaat niet', ), 'import_export' => array( - 'export_no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.', + 'export_no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server. Exporteer a.u.b. uw bestanden één voor één.', 'feeds_imported' => 'Uw feeds zijn geimporteerd en worden nu vernieuwd', 'feeds_imported_with_errors' => 'Uw feeds zijn geimporteerd maar er zijn enige fouten opgetreden', 'file_cannot_be_uploaded' => 'Bestand kan niet worden verzonden!', - 'no_zip_extension' => 'Zip uitbreiding is niet aanwezig op uw server.', - 'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het Zip bestand.', + 'no_zip_extension' => 'ZIP uitbreiding is niet aanwezig op uw server.', + 'zip_error' => 'Er is een fout opgetreden tijdens het imporeren van het ZIP bestand.', ), 'sub' => array( 'actualize' => 'Actualiseren', diff --git a/app/i18n/nl/install.php b/app/i18n/nl/install.php index 77783cd48..bd2ba0cce 100644 --- a/app/i18n/nl/install.php +++ b/app/i18n/nl/install.php @@ -25,9 +25,9 @@ return array( ), 'host' => 'Host', 'prefix' => 'Tabel voorvoegsel', - 'password' => 'HTTP wachtwoord', + 'password' => 'Database wachtwoord', 'type' => 'Type database', - 'username' => 'HTTP gebruikersnaam', + 'username' => 'Database gebruikersnaam', ), 'check' => array( '_' => 'Controles', @@ -41,7 +41,7 @@ return array( 'ok' => 'U hebt de benodigde bibliotheek voor character type checking (ctype).', ), 'curl' => array( - 'nok' => 'U mist cURL (php5-curl package).', + 'nok' => 'U mist cURL (php-curl package).', 'ok' => 'U hebt de cURL uitbreiding.', ), 'data' => array( @@ -73,8 +73,8 @@ return array( 'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).', ), 'pdo' => array( - 'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite).', - 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).', + 'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'Uw PHP versie is %s maar FreshRSS benodigd tenminste versie %s.', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 159a58b27..a1ba3f03d 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Exporteer lijst van feeds (OPML)', 'export_starred' => 'Exporteer je fovorieten', 'feed_list' => 'Lijst van %s artikelen', - 'file_to_import' => 'Bestand om te importeren<br />(OPML, Json of Zip)', - 'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of Json)', + 'file_to_import' => 'Bestand om te importeren<br />(OPML, JSON of ZIP)', + 'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of JSON)', 'import' => 'Importeer', 'starred_list' => 'Lijst van favoriete artikelen', 'title' => 'Importeren / exporteren', diff --git a/app/i18n/ru/admin.php b/app/i18n/ru/admin.php index caea627f3..355100689 100644 --- a/app/i18n/ru/admin.php +++ b/app/i18n/ru/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'У вас не установлена библиотека для проверки типов символов (ctype).', ), 'curl' => array( - 'nok' => 'У вас не установлено расширение cURL (пакет php5-curl).', + 'nok' => 'У вас не установлено расширение cURL (пакет php-curl).', 'ok' => 'У вас установлено расширение cURL.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).', ), 'pdo' => array( - 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).', - 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).', + 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP installation', @@ -93,7 +93,7 @@ return array( 'ok' => 'Права на папку users в порядке.', ), 'zip' => array( - 'nok' => 'You lack ZIP extension (php5-zip package).', + 'nok' => 'You lack ZIP extension (php-zip package).', 'ok' => 'You have ZIP extension.', ), ), diff --git a/app/i18n/ru/feedback.php b/app/i18n/ru/feedback.php index 7ce2ae9cf..e7f6b9f85 100644 --- a/app/i18n/ru/feedback.php +++ b/app/i18n/ru/feedback.php @@ -43,12 +43,12 @@ return array( '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.', + '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.', + 'no_zip_extension' => 'ZIP extension is not present on your server.', + 'zip_error' => 'An error occured during ZIP import.', ), 'sub' => array( 'actualize' => 'Actualise', diff --git a/app/i18n/ru/install.php b/app/i18n/ru/install.php index a52e2959b..bad59bbb3 100644 --- a/app/i18n/ru/install.php +++ b/app/i18n/ru/install.php @@ -25,9 +25,9 @@ return array( ), 'host' => 'Хост', 'prefix' => 'Префикс таблицы', - 'password' => 'Пароль HTTP', + 'password' => 'Пароль базы данных', 'type' => 'Тип базы данных', - 'username' => 'Имя пользователя HTTP', + 'username' => 'Имя пользователя базы данных', ), 'check' => array( '_' => 'Проверки', @@ -41,7 +41,7 @@ return array( 'ok' => 'У вас установлена необходимая библиотека для проверки типов символов (ctype).', ), 'curl' => array( - 'nok' => 'У вас нет расширения cURL (пакет php5-curl).', + 'nok' => 'У вас нет расширения cURL (пакет php-curl).', 'ok' => 'У вас установлено расширение cURL.', ), 'data' => array( @@ -69,8 +69,8 @@ return array( 'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).', ), 'pdo' => array( - 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).', - 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).', + 'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'У вас установлен PHP версии %s, но FreshRSS необходима версия не ниже %s.', diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index aaaa02827..789433ee6 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Export list of feeds (OPML)', 'export_starred' => 'Export your favourites', 'feed_list' => 'List of %s articles', - 'file_to_import' => 'File to import<br />(OPML, Json or Zip)', - 'file_to_import_no_zip' => 'File to import<br />(OPML or Json)', + 'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)', + 'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)', 'import' => 'Import', 'starred_list' => 'List of favourite articles', 'title' => 'Import / export', diff --git a/app/i18n/tr/admin.php b/app/i18n/tr/admin.php index 43f8e23c5..e0dbd288d 100644 --- a/app/i18n/tr/admin.php +++ b/app/i18n/tr/admin.php @@ -33,7 +33,7 @@ return array( 'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).', ), 'curl' => array( - 'nok' => 'cURL eksik (php5-curl package).', + 'nok' => 'cURL eksik (php-curl package).', 'ok' => 'cURL eklentisi sorunsuz.', ), 'data' => array( @@ -71,8 +71,8 @@ return array( 'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).', ), 'pdo' => array( - 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).', - 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).', + 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( '_' => 'PHP kurulumu', @@ -93,7 +93,7 @@ return array( 'ok' => 'Kullanıcılar klasörü yetkileri sorunsuz.', ), 'zip' => array( - 'nok' => 'ZIP eklentisi eksik (php5-zip package).', + 'nok' => 'ZIP eklentisi eksik (php-zip package).', 'ok' => 'ZIP eklentisi sorunsuz.', ), ), diff --git a/app/i18n/tr/feedback.php b/app/i18n/tr/feedback.php index a53316206..87361ff51 100644 --- a/app/i18n/tr/feedback.php +++ b/app/i18n/tr/feedback.php @@ -43,12 +43,12 @@ return array( 'not_found' => '%s bulunmamaktadır', ), 'import_export' => array( - 'export_no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.', + 'export_no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor. Lütfen başka dosya formatında dışarı aktarmayı deneyin.', 'feeds_imported' => 'Akışlarınız içe aktarıldı ve şimdi güncellenecek', 'feeds_imported_with_errors' => 'Akışlarınız içeri aktarıldı ama bazı hatalar meydana geldi', 'file_cannot_be_uploaded' => 'Dosya yüklenemedi!', - 'no_zip_extension' => 'Zip eklentisi mevcut sunucunuzda yer almıyor.', - 'zip_error' => 'Zip içe aktarımı sırasında hata meydana geldi.', + 'no_zip_extension' => 'ZIP eklentisi mevcut sunucunuzda yer almıyor.', + 'zip_error' => 'ZIP içe aktarımı sırasında hata meydana geldi.', ), 'sub' => array( 'actualize' => 'Güncelleme', diff --git a/app/i18n/tr/install.php b/app/i18n/tr/install.php index 951a7c5fd..4daa45099 100644 --- a/app/i18n/tr/install.php +++ b/app/i18n/tr/install.php @@ -25,9 +25,9 @@ return array( ), 'host' => 'Sunucu', 'prefix' => 'Tablo ön eki', - 'password' => 'HTTP şifre', + 'password' => 'Veritabanı şifresi', 'type' => 'Veritabanı türü', - 'username' => 'HTTP kullanıcı adı', + 'username' => 'Veritabanı kullanıcı adı', ), 'check' => array( '_' => 'Kontroller', @@ -41,7 +41,7 @@ return array( 'ok' => 'Karakter yazım kontrolü için kütüphane sorunsuz (ctype).', ), 'curl' => array( - 'nok' => 'cURL eksik (php5-curl package).', + 'nok' => 'cURL eksik (php-curl package).', 'ok' => 'cURL eklentisi sorunsuz.', ), 'data' => array( @@ -73,8 +73,8 @@ return array( 'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).', ), 'pdo' => array( - 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).', - 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).', + 'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).', + 'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).', ), 'php' => array( 'nok' => 'PHP versiyonunuz %s fakat FreshRSS için gerekli olan en düşük sürüm %s.', diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index 5ab367ebb..7592096d9 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -44,8 +44,8 @@ return array( 'export_opml' => 'Akış listesini dışarı aktar (OPML)', 'export_starred' => 'Favorileri dışarı aktar', 'feed_list' => '%s makalenin listesi', - 'file_to_import' => 'Dosyadan içe aktar<br />(OPML, Json or Zip)', - 'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or Json)', + 'file_to_import' => 'Dosyadan içe aktar<br />(OPML, JSON or ZIP)', + 'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or JSON)', 'import' => 'İçe aktar', 'starred_list' => 'Favori makaleleirn listesi', 'title' => 'İçe / dışa aktar', diff --git a/app/install.php b/app/install.php index 62695ceb6..fcc901713 100644 --- a/app/install.php +++ b/app/install.php @@ -4,23 +4,18 @@ if (function_exists('opcache_reset')) { } header("Content-Security-Policy: default-src 'self'"); -define('BCRYPT_COST', 9); +require(LIB_PATH . '/lib_install.php'); session_name('FreshRSS'); session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true); session_start(); -Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php')); -Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php')); - if (isset($_GET['step'])) { define('STEP',(int)$_GET['step']); } else { define('STEP', 0); } -define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); - if (STEP === 3 && isset($_POST['type'])) { $_SESSION['bd_type'] = $_POST['type']; } @@ -28,10 +23,13 @@ if (STEP === 3 && isset($_POST['type'])) { if (isset($_SESSION['bd_type'])) { switch ($_SESSION['bd_type']) { case 'mysql': - include(APP_PATH . '/SQL/install.sql.mysql.php'); + include_once(APP_PATH . '/SQL/install.sql.mysql.php'); break; case 'sqlite': - include(APP_PATH . '/SQL/install.sql.sqlite.php'); + include_once(APP_PATH . '/SQL/install.sql.sqlite.php'); + break; + case 'pgsql': + include_once(APP_PATH . '/SQL/install.sql.pgsql.php'); break; } } @@ -130,12 +128,7 @@ function saveStep2() { $password_plain = param('passwordPlain', false); if ($password_plain !== false && cryptAvailable()) { - if (!function_exists('password_hash')) { - include_once(LIB_PATH . '/password_compat.php'); - } - $passwordHash = password_hash($password_plain, PASSWORD_BCRYPT, array('cost' => BCRYPT_COST)); - $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js - $_SESSION['passwordHash'] = $passwordHash; + $_SESSION['passwordHash'] = FreshRSS_user_Controller::hashPassword($password_plain); } if (empty($_SESSION['old_entries']) || @@ -148,7 +141,7 @@ function saveStep2() { return false; } - $_SESSION['salt'] = sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__))); + $_SESSION['salt'] = generateSalt(); if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) { $_SESSION['old_entries'] = $user_default_config->old_entries; } @@ -170,7 +163,7 @@ function saveStep2() { recursive_unlink($user_dir); mkdir($user_dir); - file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ';'); + file_put_contents($user_config_path, "<?php\n return " . var_export($config_array, true) . ";\n"); header('Location: index.php?step=3'); } @@ -199,6 +192,9 @@ function saveStep3() { $_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16); $_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_')); } + if ($_SESSION['bd_type'] === 'pgsql') { + $_SESSION['bd_base'] = strtolower($_SESSION['bd_base']); + } // We use dirname to remove the /i part $base_url = dirname(Minz_Request::guessBaseUrl()); @@ -221,55 +217,30 @@ function saveStep3() { ); @unlink(join_path(DATA_PATH, 'config.php')); //To avoid access-rights problems - file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ';'); + file_put_contents(join_path(DATA_PATH, 'config.php'), "<?php\n return " . var_export($config_array, true) . ";\n"); - $res = checkBD(); + $config_array['db']['default_user'] = $config_array['default_user']; + $config_array['db']['prefix_user'] = $_SESSION['bd_prefix_user']; + $ok = checkDb($config_array['db']) && checkDbUser($config_array['db']); + if (!$ok) { + @unlink(join_path(DATA_PATH, 'config.php')); + } - if ($res) { + if ($ok) { $_SESSION['bd_error'] = ''; header('Location: index.php?step=4'); - } elseif (empty($_SESSION['bd_error'])) { - $_SESSION['bd_error'] = 'Unknown error!'; + } else { + $_SESSION['bd_error'] = empty($config_array['db']['bd_error']) ? 'Unknown error!' : $config_array['db']['bd_error']; } } invalidateHttpCache(); } -function newPdo() { - switch ($_SESSION['bd_type']) { - case 'mysql': - $str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base']; - $driver_options = array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - ); - break; - case 'sqlite': - $str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite'); - $driver_options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - break; - default: - return false; - } - return new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options); -} - -function deleteInstall() { - $res = unlink(join_path(DATA_PATH, 'do-install.txt')); - - if (!$res) { - return false; - } - - header('Location: index.php'); -} - /*** VÉRIFICATIONS ***/ function checkStep() { $s0 = checkStep0(); - $s1 = checkStep1(); + $s1 = checkRequirements(); $s2 = checkStep2(); $s3 = checkStep3(); if (STEP > 0 && $s0['all'] != 'ok') { @@ -295,47 +266,6 @@ function checkStep0() { ); } -function checkStep1() { - $php = version_compare(PHP_VERSION, '5.3.3') >= 0; - $minz = file_exists(join_path(LIB_PATH, 'Minz')); - $curl = extension_loaded('curl'); - $pdo_mysql = extension_loaded('pdo_mysql'); - $pdo_sqlite = extension_loaded('pdo_sqlite'); - $pdo = $pdo_mysql || $pdo_sqlite; - $pcre = extension_loaded('pcre'); - $ctype = extension_loaded('ctype'); - $dom = class_exists('DOMDocument'); - $xml = function_exists('xml_parser_create'); - $json = function_exists('json_encode'); - $data = DATA_PATH && is_writable(DATA_PATH); - $cache = CACHE_PATH && is_writable(CACHE_PATH); - $users = USERS_PATH && is_writable(USERS_PATH); - $favicons = is_writable(join_path(DATA_PATH, 'favicons')); - $http_referer = is_referer_from_same_domain(); - - return array( - 'php' => $php ? 'ok' : 'ko', - 'minz' => $minz ? 'ok' : 'ko', - 'curl' => $curl ? 'ok' : 'ko', - 'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko', - 'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko', - 'pdo' => $pdo ? 'ok' : 'ko', - 'pcre' => $pcre ? 'ok' : 'ko', - 'ctype' => $ctype ? 'ok' : 'ko', - 'dom' => $dom ? 'ok' : 'ko', - 'xml' => $xml ? 'ok' : 'ko', - 'json' => $json ? 'ok' : 'ko', - 'data' => $data ? 'ok' : 'ko', - 'cache' => $cache ? 'ok' : 'ko', - 'users' => $users ? 'ok' : 'ko', - 'favicons' => $favicons ? 'ok' : 'ko', - 'http_referer' => $http_referer ? 'ok' : 'ko', - 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $dom && $xml && - $data && $cache && $users && $favicons && $http_referer ? - 'ok' : 'ko' - ); -} - function freshrss_already_installed() { $conf_path = join_path(DATA_PATH, 'config.php'); if (!file_exists($conf_path)) { @@ -406,43 +336,15 @@ function checkStep3() { ); } -function checkBD() { +function checkDbUser(&$dbOptions) { $ok = false; - + $str = $dbOptions['dsn']; + $driver_options = $dbOptions['options']; try { - $str = ''; - $driver_options = null; - switch ($_SESSION['bd_type']) { - case 'mysql': - $driver_options = array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' - ); - - try { // on ouvre une connexion juste pour créer la base si elle n'existe pas - $str = 'mysql:host=' . $_SESSION['bd_host'] . ';'; - $c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options); - $sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']); - $res = $c->query($sql); - } catch (PDOException $e) { - } - - // on écrase la précédente connexion en sélectionnant la nouvelle BDD - $str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base']; - break; - case 'sqlite': - $str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite'); - $driver_options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - break; - default: - return false; - } - - $c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options); + $c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options); if (defined('SQL_CREATE_TABLES')) { - $sql = sprintf(SQL_CREATE_TABLES, $_SESSION['bd_prefix_user'], _t('gen.short.default_category')); + $sql = sprintf(SQL_CREATE_TABLES, $dbOptions['prefix_user'], _t('gen.short.default_category')); $stm = $c->prepare($sql); $ok = $stm->execute(); } else { @@ -450,7 +352,22 @@ function checkBD() { if (is_array($SQL_CREATE_TABLES)) { $ok = true; foreach ($SQL_CREATE_TABLES as $instruction) { - $sql = sprintf($instruction, $_SESSION['bd_prefix_user'], _t('gen.short.default_category')); + $sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category')); + $stm = $c->prepare($sql); + $ok &= $stm->execute(); + } + } + } + + if (defined('SQL_INSERT_FEEDS')) { + $sql = sprintf(SQL_INSERT_FEEDS, $dbOptions['prefix_user']); + $stm = $c->prepare($sql); + $ok &= $stm->execute(); + } else { + global $SQL_INSERT_FEEDS; + if (is_array($SQL_INSERT_FEEDS)) { + foreach ($SQL_INSERT_FEEDS as $instruction) { + $sql = sprintf($instruction, $dbOptions['prefix_user']); $stm = $c->prepare($sql); $ok &= $stm->execute(); } @@ -458,13 +375,8 @@ function checkBD() { } } catch (PDOException $e) { $ok = false; - $_SESSION['bd_error'] = $e->getMessage(); + $dbOptions['bd_error'] = $e->getMessage(); } - - if (!$ok) { - @unlink(join_path(DATA_PATH, 'config.php')); - } - return $ok; } @@ -507,7 +419,7 @@ function printStep0() { // @todo refactor this view with the check_install action function printStep1() { - $res = checkStep1(); + $res = checkRequirements(); ?> <noscript><p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('install.javascript_is_better'); ?></p></noscript> @@ -690,7 +602,7 @@ function printStep3() { <p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.bdd.conf.ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p> <?php } ?> - <form action="index.php?step=3" method="post"> + <form action="index.php?step=3" method="post" autocomplete="off"> <legend><?php echo _t('install.bdd.conf'); ?></legend> <div class="form-group"> <label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label> @@ -708,6 +620,12 @@ function printStep3() { SQLite </option> <?php }?> + <?php if (extension_loaded('pdo_pgsql')) {?> + <option value="pgsql" + <?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>> + PostgreSQL (⚠️ experimental) + </option> + <?php }?> </select> </div> </div> @@ -716,7 +634,7 @@ function printStep3() { <div class="form-group"> <label class="group-name" for="host"><?php echo _t('install.bdd.host'); ?></label> <div class="group-controls"> - <input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" /> + <input type="text" id="host" name="host" pattern="[0-9A-Za-z_.-]{1,64}(:[0-9]{2,5})?" value="<?php echo isset($_SESSION['bd_host']) ? $_SESSION['bd_host'] : $system_default_config->db['host']; ?>" tabindex="2" /> </div> </div> @@ -730,7 +648,7 @@ function printStep3() { <div class="form-group"> <label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label> <div class="group-controls"> - <input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" /> + <input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" autocomplete="off" /> </div> </div> @@ -796,7 +714,9 @@ case 3: case 4: break; case 5: - deleteInstall(); + if (deleteInstall()) { + header('Location: index.php'); + } break; } ?> diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index e8fdbf842..3e1ee44dd 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -79,13 +79,13 @@ <?php if (FreshRSS_Auth::hasAccess()) { ?> <li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '------'); ?>"><?php echo _t('index.menu.stats'); ?></a></li> <?php } ?> - <li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li> + <li class="item"><a target="_blank" rel="noreferrer" href="http://example.net/"><?php echo _t('gen.action.see_website'); ?></a></li> <?php if (FreshRSS_Auth::hasAccess()) { ?> <li class="separator"></li> <li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '------'); ?>"><?php echo _t('gen.action.manage'); ?></a></li> <li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '------'); ?>"><?php echo _t('gen.action.actualize'); ?></a></li> <li class="item"> - <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?> + <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?> <button class="read_all as-link <?php echo $confirm; ?>" form="mark-read-aside" formaction="<?php echo _url('entry', 'read', 'get', 'f_------'); ?>" diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml index fa10d63e8..e14afe2a7 100644 --- a/app/layout/aside_subscription.phtml +++ b/app/layout/aside_subscription.phtml @@ -10,7 +10,7 @@ </li> <li class="item"> - <a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&url_rss='+encodeURIComponent(url), '_blank');})();"> + <a class="bookmarkClick" href="javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&url_rss='+encodeURIComponent(url);})();"> <?php echo _t('sub.menu.bookmark'); ?> </a> </li> diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml index 4d9ad5458..1f11e0af1 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -42,6 +42,9 @@ } if (isset($this->rss_title)) { $url_rss = $url_base; $url_rss['a'] = 'rss'; + if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) { + $url_rss['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss; + } ?> <link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($url_rss); ?>" /> <?php } if (FreshRSS_Context::$system_conf->allow_robots) { ?> diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index 23255f04f..f6d824d55 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -83,7 +83,7 @@ <div class="stick" id="nav_menu_read_all"> <form id="mark-read-menu" method="post"> - <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?> + <?php $confirm = FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?> <button class="read_all btn <?php echo $confirm; ?>" form="mark-read-menu" formaction="<?php echo Minz_Url::display($mark_read_url); ?>" @@ -151,8 +151,11 @@ if (FreshRSS_Context::$user_conf->token) { $url_output['params']['token'] = FreshRSS_Context::$user_conf->token; } + if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) { + $url_output['params']['hours'] = FreshRSS_Context::$user_conf->since_hours_posts_per_rss; + } ?> - <a class="view_rss btn" target="_blank" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> + <a class="view_rss btn" target="_blank" rel="noreferrer" title="<?php echo _t('index.menu.rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> <?php echo _i('rss'); ?> </a> </div> diff --git a/app/views/auth/index.phtml b/app/views/auth/index.phtml index 74e692ec5..010eae33f 100644 --- a/app/views/auth/index.phtml +++ b/app/views/auth/index.phtml @@ -60,7 +60,7 @@ <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('params' => array('output' => 'rss', 'token' => $token)), 'html', true); ?></kbd> + <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 } ?> diff --git a/app/views/configure/sharing.phtml b/app/views/configure/sharing.phtml index 0dad5bf6d..ffcfb8b29 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -13,7 +13,7 @@ <input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" /> <input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" /> <a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo _i('close'); ?></a></div> - <a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a> + <a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a> </div></div>'> <input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" /> <legend><?php echo _t('conf.sharing'); ?></legend> @@ -38,7 +38,7 @@ <a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo _i('close'); ?></a> </div> <?php if ($share->formType() === 'advanced') { ?> - <a target="_blank" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a> + <a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a> <?php } ?> </div> </div> diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml index caf685d79..5cd59d298 100644 --- a/app/views/feed/add.phtml +++ b/app/views/feed/add.phtml @@ -30,7 +30,7 @@ <label class="group-name"><?php echo _t('sub.feed.website'); ?></label> <div class="group-controls"> <?php echo $this->feed->website(); ?> - <a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a> </div> </div> <?php } ?> @@ -40,9 +40,9 @@ <div class="group-controls"> <div class="stick"> <input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" /> - <a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a> </div> - <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo _t('sub.feed.validator'); ?></a> </div> </div> <div class="form-group"> diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index b7e8f68cd..bf87a255a 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -37,7 +37,7 @@ <div class="group-controls"> <div class="stick"> <input type="text" name="website" id="website" class="extend" value="<?php echo $this->feed->website(); ?>" /> - <a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->website(); ?>"><?php echo _i('link'); ?></a> </div> </div> </div> @@ -46,10 +46,10 @@ <div class="group-controls"> <div class="stick"> <input type="text" name="url" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" /> - <a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="<?php echo $this->feed->url(); ?>"><?php echo _i('link'); ?></a> </div> - <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a> + <a class="btn" target="_blank" rel="noreferrer" href="http://validator.w3.org/feed/check.cgi?url=<?php echo rawurlencode(htmlspecialchars_decode($this->feed->url(), ENT_QUOTES)); ?>"><?php echo _t('sub.feed.validator'); ?></a> </div> </div> <div class="form-group"> diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml index 3af7436c3..a9d5a80ca 100644 --- a/app/views/helpers/index/normal/entry_bottom.phtml +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -52,7 +52,7 @@ $share_options['title'] = $title; $share->update($share_options); ?><li class="item share"> - <a target="_blank" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a> + <a target="_blank" rel="noreferrer" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a> </li><?php } ?></ul> @@ -81,6 +81,6 @@ ?><li class="item date"><?php echo $this->entry->date(); ?></li><?php } if ($bottomline_link) { - ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php + ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?> </ul> diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml index dc544298f..86298e59f 100644 --- a/app/views/helpers/index/normal/entry_header.phtml +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -27,7 +27,7 @@ } } ?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $this->feed->id()); ?>"><img class="favicon" src="<?php echo $this->feed->favicon(); ?>" alt="✇" /> <span><?php echo $this->feed->name(); ?></span></a></li> - <li class="item title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li> + <li class="item title"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li> <?php if ($topline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?> </li><?php } ?> - <?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?> + <?php if ($topline_link) { ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?> </ul> diff --git a/app/views/helpers/pagination.phtml b/app/views/helpers/pagination.phtml index 20957fc67..893451af9 100755 --- a/app/views/helpers/pagination.phtml +++ b/app/views/helpers/pagination.phtml @@ -1,6 +1,7 @@ <?php $url_next = Minz_Request::currentRequest(); $url_next['params']['next'] = FreshRSS_Context::$next_id; + $url_next['params']['state'] = FreshRSS_Context::$state; $url_next['params']['ajax'] = 1; $url_mark_read = array( @@ -26,7 +27,7 @@ </a> <?php } elseif ($url_mark_read) { ?> <button id="bigMarkAsRead" - class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm' : ''; ?>" + class="as-link <?php echo FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>" form="mark-read-pagination" formaction="<?php echo Minz_Url::display($url_mark_read); ?>" type="submit"> diff --git a/app/views/importExport/index.phtml b/app/views/importExport/index.phtml index c0bc412c3..c5049e3ea 100644 --- a/app/views/importExport/index.phtml +++ b/app/views/importExport/index.phtml @@ -44,7 +44,7 @@ $select_args = ' size="' . min(10, count($this->feeds)) .'" multiple="multiple"'; } ?> - <select name="export_feeds[]"<?php echo $select_args; ?>> + <select name="export_feeds[]"<?php echo $select_args; ?> size="10"> <?php echo extension_loaded('zip') ? '' : '<option></option>'; ?> <?php foreach ($this->feeds as $feed) { ?> <option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option> diff --git a/app/views/index/global.phtml b/app/views/index/global.phtml index 1e53e4f8c..f35732c8f 100644 --- a/app/views/index/global.phtml +++ b/app/views/index/global.phtml @@ -11,10 +11,13 @@ <div id="stream" class="global<?php echo $class; ?>"> <?php + $params = Minz_Request::fetchGET(); + unset($params['c']); + unset($params['a']); $url_base = array( 'c' => 'index', 'a' => 'normal', - 'params' => Minz_Request::fetchGET(), + 'params' => $params, ); foreach ($this->categories as $cat) { diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 91ebcebd3..6fda11ed9 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" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1> + <h1 class="title"><a target="_blank" rel="noreferrer" 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/stats/index.phtml b/app/views/stats/index.phtml index 0a2fbdb10..a36f812a8 100644 --- a/app/views/stats/index.phtml +++ b/app/views/stats/index.phtml @@ -23,18 +23,18 @@ </tr> <tr> <th><?php echo _t('admin.stats.status_read'); ?></th> - <td class="numeric"><?php echo format_number($this->repartition['main_stream']['read']); ?></td> - <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['read']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_reads']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_reads']); ?></td> </tr> <tr> <th><?php echo _t('admin.stats.status_unread'); ?></th> - <td class="numeric"><?php echo format_number($this->repartition['main_stream']['unread']); ?></td> - <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['unread']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_unreads']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_unreads']); ?></td> </tr> <tr> <th><?php echo _t('admin.stats.status_favorites'); ?></th> - <td class="numeric"><?php echo format_number($this->repartition['main_stream']['favorite']); ?></td> - <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['favorite']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_favorites']); ?></td> + <td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_favorites']); ?></td> </tr> </tbody> </table> diff --git a/app/views/stats/repartition.phtml b/app/views/stats/repartition.phtml index ffb2c361e..5ebcdce5a 100644 --- a/app/views/stats/repartition.phtml +++ b/app/views/stats/repartition.phtml @@ -12,7 +12,7 @@ if (!empty($feeds)) { echo '<optgroup label="', $category->name(), '">'; foreach ($feeds as $feed) { - if ($this->feed && $feed->id() == $this->feed->id()){ + if ($this->feed && $feed->id() == $this->feed->id()) { echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>'; } else { echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>'; @@ -39,9 +39,9 @@ </tr> <tr> <td class="numeric"><?php echo $this->repartition['total']; ?></td> - <td class="numeric"><?php echo $this->repartition['read']; ?></td> - <td class="numeric"><?php echo $this->repartition['unread']; ?></td> - <td class="numeric"><?php echo $this->repartition['favorite']; ?></td> + <td class="numeric"><?php echo $this->repartition['count_reads']; ?></td> + <td class="numeric"><?php echo $this->repartition['count_unreads']; ?></td> + <td class="numeric"><?php echo $this->repartition['count_favorites']; ?></td> </tr> </table> </div> diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml index aab3aa4c4..a32247d14 100644 --- a/app/views/user/manage.phtml +++ b/app/views/user/manage.phtml @@ -3,7 +3,7 @@ <div class="post"> <a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a> - <form method="post" action="<?php echo _url('user', 'create'); ?>"> + <form method="post" action="<?php echo _url('user', 'create'); ?>" autocomplete="off"> <input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" /> <legend><?php echo _t('admin.user.create'); ?></legend> @@ -30,7 +30,7 @@ <label class="group-name" for="new_user_passwordPlain"><?php echo _t('admin.user.password_form'); ?></label> <div class="group-controls"> <div class="stick"> - <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" /> + <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="new-password" pattern=".{7,}" /> <a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a> </div> <?php echo _i('help'); ?> <?php echo _t('admin.user.password_format'); ?> |
