diff options
| -rwxr-xr-x | app/Controllers/feedController.php | 55 | ||||
| -rw-r--r-- | app/Models/Feed.php | 69 | ||||
| -rw-r--r-- | constants.php | 1 | ||||
| -rw-r--r-- | data/PubSubHubbub/feeds/.gitignore | 1 | ||||
| -rw-r--r-- | data/PubSubHubbub/feeds/README.md | 12 | ||||
| -rw-r--r-- | data/PubSubHubbub/secrets/.gitignore | 1 | ||||
| -rw-r--r-- | data/PubSubHubbub/secrets/README.md | 4 | ||||
| -rw-r--r-- | data/config.default.php | 8 | ||||
| -rw-r--r-- | lib/lib_rss.php | 9 | ||||
| -rw-r--r-- | p/api/pshb.php | 116 |
10 files changed, 252 insertions, 24 deletions
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 0443b4159..9117da639 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(); $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($simplePie = 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; } @@ -302,8 +305,11 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } try { - // Load entries - $feed->load(false); + if ($simplePie) { + $feed->loadEntries($simplePie); //Used by PubSubHubbub + } else { + $feed->load(false); + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::notice($e->getMessage()); $feedDAO->updateLastUpdate($feed->id(), true); @@ -404,7 +410,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } - $feed->faviconPrepare(); + if ($simplePie === null) { + $feed->faviconPrepare(); + if ($feed->url() === 'http://push-pub.appspot.com/feed') { + $secret = $feed->pubSubHubbubPrepare(); + if ($secret != '') { + Minz_Log::debug('PubSubHubbub subscribe ' . $feed->url()); + $feed->pubSubHubbubSubscribe(true, $secret); + } + } + } $feed->unlock(); $updated_feeds++; unset($feed); @@ -427,20 +442,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..dcf083ea8 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) { @@ -226,6 +228,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 +266,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 +340,64 @@ class FreshRSS_Feed extends Minz_Model { function unlock() { @unlink($this->lockPath); } + + //<PubSubHubbub> + + function pubSubHubbubPrepare() { + $secret = ''; + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { + $path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl); + if (!file_exists($path . '/hub.txt')) { + @mkdir($path, 0777, true); + file_put_contents($path . '/hub.txt', $this->hubUrl); + $secret = sha1(FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true)); + file_put_contents($path . '/secret.txt', $secret); + @mkdir(PSHB_PATH . '/secrets/'); + file_put_contents(PSHB_PATH . '/secrets/' . $secret . '.txt', base64url_encode($this->selfUrl)); + Minz_Log::notice('PubSubHubbub prepared for ' . $this->url); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . + 'PubSubHubbub prepared for ' . $this->url . "\n", FILE_APPEND); + } + $path .= '/' . base64url_encode($this->url); + $currentUser = Minz_Session::param('currentUser'); + if (ctype_alnum($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { + @mkdir($path, 0777, true); + touch($path . '/' . $currentUser . '.txt'); + } + } + return $secret; + } + + //Parameter true to subscribe, false to unsubscribe. + function pubSubHubbubSubscribe($state, $secret = '') { + if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) { + $callbackUrl = checkUrl(FreshRSS_Context::$system_conf->base_url . 'api/pshb.php?s=' . $secret); + if ($callbackUrl == '') { + 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); + return substr($info['http_code'], 0, 1) == '2'; + } + return false; + } + + //</PubSubHubbub> } diff --git a/constants.php b/constants.php index b20bf0710..5bb410e29 100644 --- a/constants.php +++ b/constants.php @@ -18,6 +18,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..15fa8e521 --- /dev/null +++ b/data/PubSubHubbub/feeds/README.md @@ -0,0 +1,12 @@ +List of canonical URLS of the various feeds users have subscribed to. +Several feeds can share the same canonical URL (rel="self"). +Several users can have subscribed to the same feed. + +* ./base64url(canonicalUrl)/ + * ./secret.txt + * ./base64url(feedUrl1)/ + * ./user1.txt + * ./user2.txt + * ./base64url(feedUrl2)/ + * ./user3.txt + * ./user4.txt diff --git a/data/PubSubHubbub/secrets/.gitignore b/data/PubSubHubbub/secrets/.gitignore new file mode 100644 index 000000000..2211df63d --- /dev/null +++ b/data/PubSubHubbub/secrets/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/data/PubSubHubbub/secrets/README.md b/data/PubSubHubbub/secrets/README.md new file mode 100644 index 000000000..ad8158839 --- /dev/null +++ b/data/PubSubHubbub/secrets/README.md @@ -0,0 +1,4 @@ +List of secrets 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/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..bcb8341b1 --- /dev/null +++ b/p/api/pshb.php @@ -0,0 +1,116 @@ +<?php +require('../../constants.php'); +require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader + +define('MAX_PAYLOAD', 3145728); + +header('Content-Type: text/plain; charset=UTF-8'); + +function logMe($text) { + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); +} + +$ORIGINAL_INPUT = file_get_contents('php://input', false, null, -1, MAX_PAYLOAD); + +logMe(print_r(array('_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true)); + +$secret = isset($_GET['s']) ? substr($_GET['s'], 0, 128) : ''; +if (!ctype_xdigit($secret)) { + header('HTTP/1.1 422 Unprocessable Entity'); + die('Invalid feed secret format!'); +} +chdir(PSHB_PATH); +$canonical64 = @file_get_contents('secrets/' . $secret . '.txt'); +if ($canonical64 === false) { + header('HTTP/1.1 404 Not Found'); + logMe('Feed secret not found!: ' . $secret); + die('Feed secret not found!'); +} +$canonical64 = trim($canonical64); +if (!preg_match('/^[A-Za-z0-9_-]+$/D', $canonical64)) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Invalid secret reference!: ' . $canonical64); + die('Invalid secret reference!'); +} +$secret2 = @file_get_contents('feeds/' . $canonical64 . '/secret.txt'); +if ($secret2 === false) { + header('HTTP/1.1 404 Not Found'); + //@unlink('secrets/' . $secret . '.txt'); + logMe('Feed reverse secret not found!: ' . $canonical64); + die('Feed reverse secret not found!'); +} +if ($secret !== $secret2) { + header('HTTP/1.1 500 Internal Server Error'); + logMe('Invalid secret cross-check!: ' . $secret); + die('Invalid secret cross-check!'); +} +chdir('feeds/' . $canonical64); +$users = glob('*/*.txt', GLOB_NOSORT); +if (empty($users)) { + header('HTTP/1.1 410 Gone'); + logMe('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') { + //TODO: hub_lease_seconds + exit(isset($_REQUEST['hub_challenge']) ? $_REQUEST['hub_challenge'] : ''); +} + +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('Self URL does not match registered canonical URL!: ' . $self); + die('Self URL does not match registered canonical URL!'); +} +Minz_Request::_param('url', $self); + +$nb = 0; +foreach ($users as $userLine) { + $userLine = strtr($userLine, '\\', '/'); + $userInfos = explode('/', $userLine); + $feedUrl = isset($userInfos[0]) ? base64url_decode($userInfos[0]) : ''; + $username = isset($userInfos[1]) ? basename($userInfos[1], '.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($e->getMessage()); + } +} + +$simplePie->__destruct(); +unset($simplePie); + +if ($nb === 0) { + header('HTTP/1.1 410 Gone'); + logMe('Nobody is subscribed to this feed anymore after all!: ' . $self); + die('Nobody is subscribed to this feed anymore after all!'); +} + +logMe($self . ' done: ' . $nb); +exit('Done: ' . $nb . "\n"); |
