diff --git a/CHANGELOG.md b/CHANGELOG.md index b989b113e..605b4256c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2015-xx-xx FreshRSS 1.1.2 (beta) + +* Features + * Support for PubSubHubbub for instant notifications from compatible Web sites. + + ## 2015-05-31 FreshRSS 1.1.1 (beta) * Features diff --git a/README.fr.md b/README.fr.md index 99281d80a..de7dd1fb2 100644 --- a/README.fr.md +++ b/README.fr.md @@ -6,6 +6,7 @@ FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed] Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. Il permet de gérer plusieurs utilisateurs, et dispose d’un mode de lecture anonyme. +Il supporte [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) pour des notifications instantanées depuis les sites compatibles. * Site officiel : http://freshrss.org * Démo : http://demo.freshrss.org/ diff --git a/README.md b/README.md index e0996508c..b73cd069e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ FreshRSS is a self-hosted RSS feed aggregator such as [Leed](http://projet.idlem It is at the same time lightweight, easy to work with, powerful and customizable. It is a multi-user application with an anonymous reading mode. +It supports [PubSubHubbub](https://code.google.com/p/pubsubhubbub/) for instant notifications from compatible Web sites. * Official website: http://freshrss.org * Demo: http://demo.freshrss.org/ diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 8db273aca..957a809cd 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -168,6 +168,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // 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; @@ -261,12 +262,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { * This action actualizes entries from one or several feeds. * * Parameters are: - * - id (default: false) + * - id (default: false): Feed ID + * - url (default: false): Feed URL * - force (default: false) - * If id is not specified, all the feeds are actualized. But if force is + * 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() { + public function actualizeAction($simplePiePush = null) { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -274,14 +276,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController { 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 // alone in order to automatize further process. $feeds = array(); - if ($id) { - $feed = $feedDAO->searchById($id); + if ($id || $url) { + $feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url); if ($feed) { $feeds[] = $feed; } @@ -292,19 +295,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // Calculate date of oldest entries we accept in DB. $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. $updated_feeds = 0; $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { + $url = $feed->url(); //For detection of HTTP 301 + + $pubSubHubbubEnabled = $feed->pubSubHubbubEnabled(); + if ((!$simplePiePush) && (!$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 + } + if (!$feed->lock()) { Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; } - $url = $feed->url(); //For detection of HTTP 301 try { - // Load entries - $feed->load(false); + if ($simplePiePush) { + $feed->loadEntries($simplePiePush); //Used by PubSubHubbub + } else { + $feed->load(false); + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::notice($e->getMessage()); $feedDAO->updateLastUpdate($feed->id(), true); @@ -368,6 +384,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } + if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull! + $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid(); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + Minz_Log::warning($text); + $pubSubHubbubEnabled = false; + $feed->pubSubHubbubError(true); + } + if (!$entryDAO->hasTransaction()) { $entryDAO->beginTransaction(); } @@ -398,13 +422,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->commit(); } - if ($feed->url() !== $url) { - // HTTP 301 Moved Permanently + if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub + if ($feed->selfUrl() !== $url) { //https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs + $selfUrl = checkUrl($feed->selfUrl()); + if ($selfUrl) { + Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(false)) { //Unsubscribe + Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url()); + } + $feed->_url($selfUrl, false); + Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url()); + $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); + } + } + } + elseif ($feed->url() !== $url) { // HTTP 301 Moved Permanently Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url()); $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } $feed->faviconPrepare(); + if ($feed->pubSubHubbubPrepare()) { + Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe + Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url()); + } + } $feed->unlock(); $updated_feeds++; unset($feed); @@ -427,20 +470,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Session::_param('notification', $notif); // No layout in ajax request. $this->view->_useLayout(false); - return; - } - - // 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()) - )); - } elseif ($updated_feeds > 1) { - Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); } else { - Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + // 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()) + )); + } elseif ($updated_feeds > 1) { + Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); + } else { + Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + } } + return $updated_feeds; } /** diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 85fb173ec..bf7ed3967 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -19,6 +19,8 @@ class FreshRSS_Feed extends Minz_Model { private $ttl = -2; private $hash = null; private $lockPath = ''; + private $hubUrl = ''; + private $selfUrl = ''; public function __construct($url, $validate=true) { if ($validate) { @@ -49,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model { public function url() { return $this->url; } + public function selfUrl() { + return $this->selfUrl; + } + public function hubUrl() { + return $this->hubUrl; + } public function category() { return $this->category; } @@ -96,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model { public function ttl() { return $this->ttl; } + // public function ttlExpire() { + // $ttl = $this->ttl; + // if ($ttl == -2) { //Default + // $ttl = FreshRSS_Context::$user_conf->ttl_default; + // } + // if ($ttl == -1) { //Never + // $ttl = 64000000; //~2 years. Good enough for PubSubHubbub logic + // } + // return $this->lastUpdate + $ttl; + // } public function nbEntries() { if ($this->nbEntries < 0) { $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -226,6 +244,11 @@ class FreshRSS_Feed extends Minz_Model { throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']'); } + $links = $feed->get_links('self'); + $this->selfUrl = isset($links[0]) ? $links[0] : null; + $links = $feed->get_links('hub'); + $this->hubUrl = isset($links[0]) ? $links[0] : null; + if ($loadDetails) { // si on a utilisé l'auto-discover, notre url va avoir changé $subscribe_url = $feed->subscribe_url(false); @@ -259,7 +282,7 @@ class FreshRSS_Feed extends Minz_Model { } } - private function loadEntries($feed) { + public function loadEntries($feed) { $entries = array(); foreach ($feed->get_items() as $item) { @@ -333,4 +356,129 @@ class FreshRSS_Feed extends Minz_Model { function unlock() { @unlink($this->lockPath); } + + // + + function pubSubHubbubEnabled() { + $url = $this->selfUrl ? $this->selfUrl : $this->url; + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; + if ($hubFile = @file_get_contents($hubFilename)) { + $hubJson = json_decode($hubFile, true); + if ($hubJson && empty($hubJson['error']) && + (empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) { + return true; + } + } + return false; + } + + function pubSubHubbubError($error = true) { + $url = $this->selfUrl ? $this->selfUrl : $this->url; + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json'; + $hubFile = @file_get_contents($hubFilename); + $hubJson = $hubFile ? json_decode($hubFile, true) : array(); + if (!isset($hubJson['error']) || $hubJson['error'] !== (bool)$error) { + $hubJson['error'] = (bool)$error; + file_put_contents($hubFilename, json_encode($hubJson)); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" + . 'Set error to ' . ($error ? 1 : 0) . ' for ' . $url . "\n", FILE_APPEND); + } + return false; + } + + function pubSubHubbubPrepare() { + $key = ''; + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) { + $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl); + $hubFilename = $path . '/!hub.json'; + if ($hubFile = @file_get_contents($hubFilename)) { + $hubJson = json_decode($hubFile, true); + if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + $text = 'Invalid JSON for PubSubHubbub: ' . $this->url; + Minz_Log::warning($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + return false; + } + if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) { //TODO: Make a better policy + $text = 'PubSubHubbub lease ends at ' + . date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end']) + . ' and needs renewal: ' . $this->url; + Minz_Log::warning($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + $key = $hubJson['key']; //To renew our lease + } elseif (((!empty($hubJson['error'])) || empty($hubJson['lease_end'])) && + (empty($hubJson['lease_start']) || $hubJson['lease_start'] < time() - (3600 * 23))) { //Do not renew too often + $key = $hubJson['key']; //To renew our lease + } + } else { + @mkdir($path, 0777, true); + $key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); + $hubJson = array( + 'hub' => $this->hubUrl, + 'key' => $key, + ); + file_put_contents($hubFilename, json_encode($hubJson)); + @mkdir(PSHB_PATH . '/keys/'); + file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', base64url_encode($this->selfUrl)); + $text = 'PubSubHubbub prepared for ' . $this->url; + Minz_Log::debug($text); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + } + $currentUser = Minz_Session::param('currentUser'); + if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { + touch($path . '/' . $currentUser . '.txt'); + } + } + return $key; + } + + //Parameter true to subscribe, false to unsubscribe. + function pubSubHubbubSubscribe($state) { + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { + $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json'; + $hubFile = @file_get_contents($hubFilename); + if ($hubFile === false) { + Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url); + return false; + } + $hubJson = json_decode($hubFile, true); + if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url); + return false; + } + $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?k=' . $hubJson['key']); + if ($callbackUrl == '') { + Minz_Log::warning('Invalid callback for PubSubHubbub: ' . $this->url); + return false; + } + $ch = curl_init(); + curl_setopt_array($ch, array( + CURLOPT_URL => $this->hubUrl, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')', + CURLOPT_POSTFIELDS => 'hub.verify=sync' + . '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe') + . '&hub.topic=' . urlencode($this->selfUrl) + . '&hub.callback=' . urlencode($callbackUrl) + ) + ); + $response = curl_exec($ch); + $info = curl_getinfo($ch); + + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . + 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl . + ' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND); + + if (!$state) { //unsubscribe + $hubJson['lease_end'] = time() - 60; + file_put_contents($hubFilename, json_encode($hubJson)); + } + + return substr($info['http_code'], 0, 1) == '2'; + } + return false; + } + + // } diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index 29fb1e4d4..9518df66d 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -84,6 +84,7 @@ return array( 'articles_per_page' => 'Počet článků na stranu', 'auto_load_more' => 'Načítat další články dole na stránce', 'auto_remove_article' => 'Po přečtení články schovat', + 'mark_updated_article_unread' => 'Označte aktualizované položky jako nepřečtené', 'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”', 'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené', 'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené', diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index 78712506c..cea0541e3 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'URL kanálu', 'validator' => 'Zkontrolovat platnost kanálu', 'website' => 'URL webové stránky', + 'pubsubhubbub' => 'Okamžité oznámení s PubSubHubbub', ), 'import_export' => array( 'export' => 'Export', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 0479b8f46..0f05a5635 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'Feed-URL', 'validator' => 'Überprüfen Sie die Gültigkeit des Feeds', 'website' => 'Webseiten-URL', + 'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub', ), 'import_export' => array( 'export' => 'Exportieren', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index 8fe150bb2..aaaa02827 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'Feed URL', 'validator' => 'Check the validity of the feed', 'website' => 'Website URL', + 'pubsubhubbub' => 'Instant notification with PubSubHubbub', ), 'import_export' => array( 'export' => 'Export', diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index a3f7c4d6d..0a1a03e41 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -37,6 +37,7 @@ return array( 'url' => 'URL du flux', 'validator' => 'Vérifier la valididé du flux', 'website' => 'URL du site', + 'pubsubhubbub' => 'Notification instantanée par PubSubHubbub', ), 'import_export' => array( 'export' => 'Exporter', diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 0b08d036c..b2cf9f93c 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -126,6 +126,14 @@ ?> +
+ +
+ +
+
diff --git a/constants.php b/constants.php index 6e7796616..d5f0cc06b 100644 --- a/constants.php +++ b/constants.php @@ -19,6 +19,7 @@ define('FRESHRSS_PATH', dirname(__FILE__)); define('UPDATE_FILENAME', DATA_PATH . '/update.php'); define('USERS_PATH', DATA_PATH . '/users'); define('CACHE_PATH', DATA_PATH . '/cache'); + define('PSHB_PATH', DATA_PATH . '/PubSubHubbub'); define('LIB_PATH', FRESHRSS_PATH . '/lib'); define('APP_PATH', FRESHRSS_PATH . '/app'); diff --git a/data/PubSubHubbub/feeds/.gitignore b/data/PubSubHubbub/feeds/.gitignore new file mode 100644 index 000000000..150f68c80 --- /dev/null +++ b/data/PubSubHubbub/feeds/.gitignore @@ -0,0 +1 @@ +*/* diff --git a/data/PubSubHubbub/feeds/README.md b/data/PubSubHubbub/feeds/README.md new file mode 100644 index 000000000..a01a3197f --- /dev/null +++ b/data/PubSubHubbub/feeds/README.md @@ -0,0 +1,7 @@ +List of canonical URLS of the various feeds users have subscribed to. +Several users can have subscribed to the same feed. + +* ./base64url(canonicalUrl)/ + * ./!hub.json + * ./user1.txt + * ./user2.txt diff --git a/data/PubSubHubbub/keys/.gitignore b/data/PubSubHubbub/keys/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/PubSubHubbub/keys/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/PubSubHubbub/keys/README.md b/data/PubSubHubbub/keys/README.md new file mode 100644 index 000000000..bb1e57cd4 --- /dev/null +++ b/data/PubSubHubbub/keys/README.md @@ -0,0 +1,4 @@ +List of keys given to PubSubHubbub hubs + +* ./sha1(random + salt).txt + * base64url(canonicalUrl) diff --git a/data/config.default.php b/data/config.default.php index 8be203d36..80d331df7 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -11,9 +11,11 @@ return array( # Used to make crypto more unique. Generated during install. 'salt' => '', - # Leave empty for most cases. - # Ability to override the address of the FreshRSS instance, - # used when building absolute URLs. + # Specify address of the FreshRSS instance, + # used when building absolute URLs, e.g. for PubSubHubbub. + # Examples: + # https://example.net/FreshRSS/p/ + # https://freshrss.example.net/ 'base_url' => '', # Natural language of the user interface, e.g. `en`, `fr`. diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php index bf74ca1de..8f8ff528c 100644 --- a/data/users/_/config.default.php +++ b/data/users/_/config.default.php @@ -25,7 +25,7 @@ return array ( # In the case an article has changed (e.g. updated content): # Set to `true` to mark it unread, or `false` to leave it as-is. - 'mark_updated_article_unread' => false, + 'mark_updated_article_unread' => false, //TODO: -1 => ignore, 0 => update, 1 => update and mark as unread 'sort_order' => 'DESC', 'anon_access' => false, diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 6342011c8..191a58f35 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -446,3 +446,12 @@ function array_push_unique(&$array, $value) { function array_remove(&$array, $value) { $array = array_diff($array, array($value)); } + +//RFC 4648 +function base64url_encode($data) { + return strtr(rtrim(base64_encode($data), '='), '+/', '-_'); +} +//RFC 4648 +function base64url_decode($data) { + return base64_decode(strtr($data, '-_', '+/')); +} diff --git a/p/api/pshb.php b/p/api/pshb.php new file mode 100644 index 000000000..4bb4694b3 --- /dev/null +++ b/p/api/pshb.php @@ -0,0 +1,133 @@ + $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); + +$key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : ''; +if (!ctype_xdigit($key)) { + header('HTTP/1.1 422 Unprocessable Entity'); + die('Invalid feed key format!'); +} +chdir(PSHB_PATH); +$canonical64 = @file_get_contents('keys/' . $key . '.txt'); +if ($canonical64 === false) { + header('HTTP/1.1 404 Not Found'); + logMe('Error: Feed key not found!: ' . $key); + die('Feed key not found!'); +} +$canonical64 = trim($canonical64); +if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Error: Invalid key reference!: ' . $canonical64); + die('Invalid key reference!'); +} +$hubFile = @file_get_contents('feeds/' . $canonical64 . '/!hub.json'); +if ($hubFile === false) { + header('HTTP/1.1 404 Not Found'); + //@unlink('keys/' . $key . '.txt'); + logMe('Error: Feed info not found!: ' . $canonical64); + die('Feed info not found!'); +} +$hubJson = json_decode($hubFile, true); +if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Error: Invalid key cross-check!: ' . $key); + die('Invalid key cross-check!'); +} +chdir('feeds/' . $canonical64); +$users = glob('*.txt', GLOB_NOSORT); +if (empty($users)) { + header('HTTP/1.1 410 Gone'); + logMe('Error: Nobody is subscribed to this feed anymore!: ' . $canonical64); + die('Nobody is subscribed to this feed anymore!'); +} + +if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { + $leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : intval($_REQUEST['hub_lease_seconds']); + if ($leaseSeconds > 60) { + $hubJson['lease_end'] = time() + $leaseSeconds; + } else { + unset($hubJson['lease_end']); + } + $hubJson['lease_start'] = time(); + if (!isset($hubJson['error'])) { + $hubJson['error'] = true; //Do not assume that PubSubHubbub works until the first successul push + } + file_put_contents('./!hub.json', json_encode($hubJson)); + exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); +} + +if ($ORIGINAL_INPUT == '') { + header('HTTP/1.1 422 Unprocessable Entity'); + die('Missing XML payload!'); +} + +Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php'); +$system_conf = Minz_Configuration::get('system'); +$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!) +Minz_Translate::init('en'); +Minz_Request::_param('ajax', true); +$feedController = new FreshRSS_feed_Controller(); + +$simplePie = customSimplePie(); +$simplePie->set_raw_data($ORIGINAL_INPUT); +$simplePie->init(); +unset($ORIGINAL_INPUT); + +$links = $simplePie->get_links('self'); +$self = isset($links[0]) ? $links[0] : null; + +if ($self !== base64url_decode($canonical64)) { + //header('HTTP/1.1 422 Unprocessable Entity'); + logMe('Warning: Self URL [' . $self . '] does not match registered canonical URL!: ' . base64url_decode($canonical64)); + //die('Self URL does not match registered canonical URL!'); + $self = base64url_decode($canonical64); +} +Minz_Request::_param('url', $self); + +$nb = 0; +foreach ($users as $userFilename) { + $username = basename($userFilename, '.txt'); + if (!file_exists(USERS_PATH . '/' . $username . '/config.php')) { + break; + } + + try { + Minz_Session::_param('currentUser', $username); + Minz_Configuration::register('user', + join_path(USERS_PATH, $username, 'config.php'), + join_path(USERS_PATH, '_', 'config.default.php')); + FreshRSS_Context::init(); + if ($feedController->actualizeAction($simplePie) > 0) { + $nb++; + } + } catch (Exception $e) { + logMe('Error: ' . $e->getMessage()); + } +} + +$simplePie->__destruct(); +unset($simplePie); + +if ($nb === 0) { + header('HTTP/1.1 410 Gone'); + logMe('Error: Nobody is subscribed to this feed anymore after all!: ' . $self); + die('Nobody is subscribed to this feed anymore after all!'); +} elseif (!empty($hubJson['error'])) { + $hubJson['error'] = false; + file_put_contents('./!hub.json', json_encode($hubJson)); +} + +logMe('PubSubHubbub ' . $self . ' done: ' . $nb); +exit('Done: ' . $nb . "\n");