Merge pull request #831 from Alkarex/PubSubHubbub

PubSubHubbub
pull/864/head^2
Alexandre Alapetite 9 years ago
commit ee55fd13a9
  1. 6
      CHANGELOG.md
  2. 1
      README.fr.md
  3. 1
      README.md
  4. 87
      app/Controllers/feedController.php
  5. 150
      app/Models/Feed.php
  6. 1
      app/i18n/cz/conf.php
  7. 1
      app/i18n/cz/sub.php
  8. 1
      app/i18n/de/sub.php
  9. 1
      app/i18n/en/sub.php
  10. 1
      app/i18n/fr/sub.php
  11. 8
      app/views/helpers/feed/update.phtml
  12. 1
      constants.php
  13. 1
      data/PubSubHubbub/feeds/.gitignore
  14. 7
      data/PubSubHubbub/feeds/README.md
  15. 1
      data/PubSubHubbub/keys/.gitignore
  16. 4
      data/PubSubHubbub/keys/README.md
  17. 8
      data/config.default.php
  18. 2
      data/users/_/config.default.php
  19. 9
      lib/lib_rss.php
  20. 133
      p/api/pshb.php

@ -1,5 +1,11 @@
# Changelog # 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) ## 2015-05-31 FreshRSS 1.1.1 (beta)
* Features * Features

@ -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 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 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 * Site officiel : http://freshrss.org
* Démo : http://demo.freshrss.org/ * Démo : http://demo.freshrss.org/

@ -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 at the same time lightweight, easy to work with, powerful and customizable.
It is a multi-user application with an anonymous reading mode. 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 * Official website: http://freshrss.org
* Demo: http://demo.freshrss.org/ * Demo: http://demo.freshrss.org/

@ -168,6 +168,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
// Ok, feed has been added in database. Now we have to refresh entries. // Ok, feed has been added in database. Now we have to refresh entries.
$feed->_id($id); $feed->_id($id);
$feed->faviconPrepare(); $feed->faviconPrepare();
//$feed->pubSubHubbubPrepare(); //TODO: prepare PubSubHubbub already when adding the feed
$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; $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. * This action actualizes entries from one or several feeds.
* *
* Parameters are: * Parameters are:
* - id (default: false) * - id (default: false): Feed ID
* - url (default: false): Feed URL
* - force (default: false) * - 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. * false, process stops at 10 feeds to avoid time execution problem.
*/ */
public function actualizeAction() { public function actualizeAction($simplePiePush = null) {
@set_time_limit(300); @set_time_limit(300);
$feedDAO = FreshRSS_Factory::createFeedDao(); $feedDAO = FreshRSS_Factory::createFeedDao();
@ -274,14 +276,15 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
Minz_Session::_param('actualize_feeds', false); Minz_Session::_param('actualize_feeds', false);
$id = Minz_Request::param('id'); $id = Minz_Request::param('id');
$url = Minz_Request::param('url');
$force = Minz_Request::param('force'); $force = Minz_Request::param('force');
// Create a list of feeds to actualize. // Create a list of feeds to actualize.
// If id is set and valid, corresponding feed is added to the list but // If id is set and valid, corresponding feed is added to the list but
// alone in order to automatize further process. // alone in order to automatize further process.
$feeds = array(); $feeds = array();
if ($id) { if ($id || $url) {
$feed = $feedDAO->searchById($id); $feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
if ($feed) { if ($feed) {
$feeds[] = $feed; $feeds[] = $feed;
} }
@ -292,19 +295,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
// Calculate date of oldest entries we accept in DB. // Calculate date of oldest entries we accept in DB.
$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
$date_min = time() - (3600 * 24 * 30 * $nb_month_old); $date_min = time() - (3600 * 24 * 30 * $nb_month_old);
$pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration.
$updated_feeds = 0; $updated_feeds = 0;
$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
foreach ($feeds as $feed) { 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()) { if (!$feed->lock()) {
Minz_Log::notice('Feed already being actualized: ' . $feed->url()); Minz_Log::notice('Feed already being actualized: ' . $feed->url());
continue; continue;
} }
$url = $feed->url(); //For detection of HTTP 301
try { try {
// Load entries if ($simplePiePush) {
$feed->load(false); $feed->loadEntries($simplePiePush); //Used by PubSubHubbub
} else {
$feed->load(false);
}
} catch (FreshRSS_Feed_Exception $e) { } catch (FreshRSS_Feed_Exception $e) {
Minz_Log::notice($e->getMessage()); Minz_Log::notice($e->getMessage());
$feedDAO->updateLastUpdate($feed->id(), true); $feedDAO->updateLastUpdate($feed->id(), true);
@ -368,6 +384,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
continue; 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()) { if (!$entryDAO->hasTransaction()) {
$entryDAO->beginTransaction(); $entryDAO->beginTransaction();
} }
@ -398,13 +422,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
$entryDAO->commit(); $entryDAO->commit();
} }
if ($feed->url() !== $url) { if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub
// HTTP 301 Moved Permanently 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()); Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
$feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); $feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
} }
$feed->faviconPrepare(); $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(); $feed->unlock();
$updated_feeds++; $updated_feeds++;
unset($feed); unset($feed);
@ -427,20 +470,20 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
Minz_Session::_param('notification', $notif); Minz_Session::_param('notification', $notif);
// No layout in ajax request. // No layout in ajax request.
$this->view->_useLayout(false); $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 { } 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;
} }
/** /**

@ -19,6 +19,8 @@ class FreshRSS_Feed extends Minz_Model {
private $ttl = -2; private $ttl = -2;
private $hash = null; private $hash = null;
private $lockPath = ''; private $lockPath = '';
private $hubUrl = '';
private $selfUrl = '';
public function __construct($url, $validate=true) { public function __construct($url, $validate=true) {
if ($validate) { if ($validate) {
@ -49,6 +51,12 @@ class FreshRSS_Feed extends Minz_Model {
public function url() { public function url() {
return $this->url; return $this->url;
} }
public function selfUrl() {
return $this->selfUrl;
}
public function hubUrl() {
return $this->hubUrl;
}
public function category() { public function category() {
return $this->category; return $this->category;
} }
@ -96,6 +104,16 @@ class FreshRSS_Feed extends Minz_Model {
public function ttl() { public function ttl() {
return $this->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() { public function nbEntries() {
if ($this->nbEntries < 0) { if ($this->nbEntries < 0) {
$feedDAO = FreshRSS_Factory::createFeedDao(); $feedDAO = FreshRSS_Factory::createFeedDao();
@ -226,6 +244,11 @@ class FreshRSS_Feed extends Minz_Model {
throw new FreshRSS_Feed_Exception(($errorMessage == '' ? 'Feed error' : $errorMessage) . ' [' . $url . ']'); 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) { if ($loadDetails) {
// si on a utilisé l'auto-discover, notre url va avoir changé // si on a utilisé l'auto-discover, notre url va avoir changé
$subscribe_url = $feed->subscribe_url(false); $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(); $entries = array();
foreach ($feed->get_items() as $item) { foreach ($feed->get_items() as $item) {
@ -333,4 +356,129 @@ class FreshRSS_Feed extends Minz_Model {
function unlock() { function unlock() {
@unlink($this->lockPath); @unlink($this->lockPath);
} }
//<PubSubHubbub>
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;
}
//</PubSubHubbub>
} }

@ -84,6 +84,7 @@ return array(
'articles_per_page' => 'Počet článků na stranu', 'articles_per_page' => 'Počet článků na stranu',
'auto_load_more' => 'Načítat další články dole na stránce', 'auto_load_more' => 'Načítat další články dole na stránce',
'auto_remove_article' => 'Po přečtení články schovat', '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é”', '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_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené', 'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',

@ -37,6 +37,7 @@ return array(
'url' => 'URL kanálu', 'url' => 'URL kanálu',
'validator' => 'Zkontrolovat platnost kanálu', 'validator' => 'Zkontrolovat platnost kanálu',
'website' => 'URL webové stránky', 'website' => 'URL webové stránky',
'pubsubhubbub' => 'Okamžité oznámení s PubSubHubbub',
), ),
'import_export' => array( 'import_export' => array(
'export' => 'Export', 'export' => 'Export',

@ -37,6 +37,7 @@ return array(
'url' => 'Feed-URL', 'url' => 'Feed-URL',
'validator' => 'Überprüfen Sie die Gültigkeit des Feeds', 'validator' => 'Überprüfen Sie die Gültigkeit des Feeds',
'website' => 'Webseiten-URL', 'website' => 'Webseiten-URL',
'pubsubhubbub' => 'Sofortbenachrichtigung mit PubSubHubbub',
), ),
'import_export' => array( 'import_export' => array(
'export' => 'Exportieren', 'export' => 'Exportieren',

@ -37,6 +37,7 @@ return array(
'url' => 'Feed URL', 'url' => 'Feed URL',
'validator' => 'Check the validity of the feed', 'validator' => 'Check the validity of the feed',
'website' => 'Website URL', 'website' => 'Website URL',
'pubsubhubbub' => 'Instant notification with PubSubHubbub',
), ),
'import_export' => array( 'import_export' => array(
'export' => 'Export', 'export' => 'Export',

@ -37,6 +37,7 @@ return array(
'url' => 'URL du flux', 'url' => 'URL du flux',
'validator' => 'Vérifier la valididé du flux', 'validator' => 'Vérifier la valididé du flux',
'website' => 'URL du site', 'website' => 'URL du site',
'pubsubhubbub' => 'Notification instantanée par PubSubHubbub',
), ),
'import_export' => array( 'import_export' => array(
'export' => 'Exporter', 'export' => 'Exporter',

@ -126,6 +126,14 @@
?></select> ?></select>
</div> </div>
</div> </div>
<div class="form-group">
<label class="group-name" for="pubsubhubbub"><?php echo _t('sub.feed.pubsubhubbub'); ?></label>
<div class="group-controls">
<label class="checkbox" for="pubsubhubbub">
<input type="checkbox" name="pubsubhubbub" id="pubsubhubbub" disabled="disabled" value="1"<?php echo $this->feed->pubSubHubbubEnabled() ? ' checked="checked"' : ''; ?> />
</label>
</div>
</div>
<div class="form-group form-actions"> <div class="form-group form-actions">
<div class="group-controls"> <div class="group-controls">
<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button> <button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>

@ -19,6 +19,7 @@ define('FRESHRSS_PATH', dirname(__FILE__));
define('UPDATE_FILENAME', DATA_PATH . '/update.php'); define('UPDATE_FILENAME', DATA_PATH . '/update.php');
define('USERS_PATH', DATA_PATH . '/users'); define('USERS_PATH', DATA_PATH . '/users');
define('CACHE_PATH', DATA_PATH . '/cache'); define('CACHE_PATH', DATA_PATH . '/cache');
define('PSHB_PATH', DATA_PATH . '/PubSubHubbub');
define('LIB_PATH', FRESHRSS_PATH . '/lib'); define('LIB_PATH', FRESHRSS_PATH . '/lib');
define('APP_PATH', FRESHRSS_PATH . '/app'); define('APP_PATH', FRESHRSS_PATH . '/app');

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

@ -0,0 +1,4 @@
List of keys given to PubSubHubbub hubs
* ./sha1(random + salt).txt
* base64url(canonicalUrl)

@ -11,9 +11,11 @@ return array(
# Used to make crypto more unique. Generated during install. # Used to make crypto more unique. Generated during install.
'salt' => '', 'salt' => '',
# Leave empty for most cases. # Specify address of the FreshRSS instance,
# Ability to override the address of the FreshRSS instance, # used when building absolute URLs, e.g. for PubSubHubbub.
# used when building absolute URLs. # Examples:
# https://example.net/FreshRSS/p/
# https://freshrss.example.net/
'base_url' => '', 'base_url' => '',
# Natural language of the user interface, e.g. `en`, `fr`. # Natural language of the user interface, e.g. `en`, `fr`.

@ -25,7 +25,7 @@ return array (
# In the case an article has changed (e.g. updated content): # 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. # 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', 'sort_order' => 'DESC',
'anon_access' => false, 'anon_access' => false,

@ -446,3 +446,12 @@ function array_push_unique(&$array, $value) {
function array_remove(&$array, $value) { function array_remove(&$array, $value) {
$array = array_diff($array, 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, '-_', '+/'));
}

@ -0,0 +1,133 @@
<?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('_SERVER' => $_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");
Loading…
Cancel
Save