2010-02-18 21:22:21 +00:00
< ? php
2019-09-11 06:32:19 +01:00
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
2010-10-08 18:42:59 +01:00
2010-02-18 21:22:21 +00:00
/**
2019-09-11 06:32:19 +01:00
* FeedSub handles low - level WebSub ( PubSubHubbub / PuSH ) subscriptions .
* Higher - level behavior building OStatus stuff on top is handled
* under Ostatus_profile .
*
* @ package OStatusPlugin
* @ author Brion Vibber < brion @ status . net >
* @ copyright 2009 - 2010 StatusNet , Inc .
* @ license https :// www . gnu . org / licenses / agpl . html GNU AGPL v3 or later
2010-02-18 21:22:21 +00:00
*/
2019-09-11 06:32:19 +01:00
defined ( 'GNUSOCIAL' ) || die ();
2010-02-18 21:22:21 +00:00
/*
2017-05-01 10:04:27 +01:00
WebSub ( previously PubSubHubbub / PuSH ) subscription flow :
2010-02-18 21:22:21 +00:00
$profile -> subscribe ()
sends a sub request to the hub ...
main / push / callback
hub sends confirmation back to us via GET
We verify the request , then echo back the challenge .
On our end , we save the time we subscribed and the lease expiration
main / push / callback
hub sends us updates via POST
*/
2013-08-18 11:10:44 +01:00
class FeedSub extends Managed_DataObject
2010-02-18 21:22:21 +00:00
{
public $__table = 'feedsub' ;
public $id ;
2015-02-12 17:18:55 +00:00
public $uri ; // varchar(191) not 255 because utf8mb4 takes more space
2010-02-18 21:22:21 +00:00
2017-05-01 10:04:27 +01:00
// WebSub subscription data
2010-02-18 21:22:21 +00:00
public $huburi ;
public $secret ;
2014-03-09 22:02:48 +00:00
public $sub_state ; // subscribe, active, unsubscribe, inactive, nohub
2010-02-18 21:22:21 +00:00
public $sub_start ;
public $sub_end ;
public $last_update ;
public $created ;
public $modified ;
2013-08-21 10:01:31 +01:00
public static function schemaDef ()
2010-02-18 21:22:21 +00:00
{
2013-08-21 10:01:31 +01:00
return array (
'fields' => array (
2013-08-21 13:33:45 +01:00
'id' => array ( 'type' => 'serial' , 'not null' => true , 'description' => 'FeedSub local unique id' ),
2015-02-12 17:18:55 +00:00
'uri' => array ( 'type' => 'varchar' , 'not null' => true , 'length' => 191 , 'description' => 'FeedSub uri' ),
2013-08-21 10:01:31 +01:00
'huburi' => array ( 'type' => 'text' , 'description' => 'FeedSub hub-uri' ),
'secret' => array ( 'type' => 'text' , 'description' => 'FeedSub stored secret' ),
2019-09-11 10:48:28 +01:00
'sub_state' => array ( 'type' => 'enum' , 'enum' => array ( 'subscribe' , 'active' , 'unsubscribe' , 'inactive' , 'nohub' ), 'not null' => true , 'description' => 'subscription state' ),
2013-08-21 10:01:31 +01:00
'sub_start' => array ( 'type' => 'datetime' , 'description' => 'subscription start' ),
'sub_end' => array ( 'type' => 'datetime' , 'description' => 'subscription end' ),
2015-02-07 16:08:03 +00:00
'last_update' => array ( 'type' => 'datetime' , 'description' => 'when this record was last updated' ),
2013-08-21 10:01:31 +01:00
'created' => array ( 'type' => 'datetime' , 'not null' => true , 'description' => 'date this record was created' ),
'modified' => array ( 'type' => 'timestamp' , 'not null' => true , 'description' => 'date this record was modified' ),
),
'primary key' => array ( 'id' ),
'unique keys' => array (
'feedsub_uri_key' => array ( 'uri' ),
),
);
2010-02-18 21:22:21 +00:00
}
2014-03-09 12:29:38 +00:00
/**
* Get the feed uri ( http / https )
*/
public function getUri ()
{
if ( empty ( $this -> uri )) {
2014-05-19 16:13:32 +01:00
throw new NoUriException ( $this );
2014-03-09 12:29:38 +00:00
}
return $this -> uri ;
}
2019-09-11 06:32:19 +01:00
public function getLeaseRemaining ()
2016-01-13 19:01:00 +00:00
{
if ( empty ( $this -> sub_end )) {
return null ;
}
return strtotime ( $this -> sub_end ) - time ();
}
2014-03-09 12:29:38 +00:00
/**
2017-05-01 10:04:27 +01:00
* Do we have a hub ? Then we are a WebSub feed .
* WebSub standard : https :// www . w3 . org / TR / websub /
* old : https :// en . wikipedia . org / wiki / PubSubHubbub
2014-03-09 12:29:38 +00:00
*
2014-03-09 22:02:48 +00:00
* If huburi is empty , then doublecheck that we are not using
* a fallback hub . If there is a fallback hub , it is only if the
2017-05-01 10:04:27 +01:00
* sub_state is " nohub " that we assume it ' s not a WebSub feed .
2014-03-09 12:29:38 +00:00
*/
2017-05-01 10:04:27 +01:00
public function isWebSub ()
2014-03-09 12:29:38 +00:00
{
2014-03-09 22:02:48 +00:00
if ( empty ( $this -> huburi )
&& ( ! common_config ( 'feedsub' , 'fallback_hub' )
|| $this -> sub_state === 'nohub' )) {
2019-09-11 06:32:19 +01:00
// Here we have no huburi set. Also, either there is no
// fallback hub configured or sub_state is "nohub".
2014-03-09 22:02:48 +00:00
return false ;
}
return true ;
2014-03-09 12:29:38 +00:00
}
2010-02-18 21:22:21 +00:00
/**
* Fetch the StatusNet - side profile for this feed
* @ return Profile
*/
public function localProfile ()
{
if ( $this -> profile_id ) {
2013-08-18 12:04:58 +01:00
return Profile :: getKV ( 'id' , $this -> profile_id );
2010-02-18 21:22:21 +00:00
}
return null ;
}
/**
* Fetch the StatusNet - side profile for this feed
* @ return Profile
*/
public function localGroup ()
{
if ( $this -> group_id ) {
2013-08-18 12:04:58 +01:00
return User_group :: getKV ( 'id' , $this -> group_id );
2010-02-18 21:22:21 +00:00
}
return null ;
}
/**
* @ param string $feeduri
* @ return FeedSub
2017-05-01 10:04:27 +01:00
* @ throws FeedSubException if feed is invalid or lacks WebSub setup
2010-02-18 21:22:21 +00:00
*/
public static function ensureFeed ( $feeduri )
{
2017-04-27 08:24:12 +01:00
$feedsub = self :: getKV ( 'uri' , $feeduri );
if ( $feedsub instanceof FeedSub ) {
if ( ! empty ( $feedsub -> huburi )) {
// If there is already a huburi we don't
// rediscover it on ensureFeed, call
// ensureHub to do that (compare ->modified
// to see if it might be time to do it).
return $feedsub ;
}
if ( $feedsub -> sub_state !== 'inactive' ) {
throw new ServerException ( 'Can only ensure WebSub hub for inactive (unsubscribed) feeds.' );
}
// If huburi is empty we continue with ensureHub
} else {
// If we don't have that local feed URI
// stored then we create a new DB object.
$feedsub = new FeedSub ();
$feedsub -> uri = $feeduri ;
$feedsub -> sub_state = 'inactive' ;
}
try {
// discover the hub uri
$feedsub -> ensureHub ();
} catch ( FeedSubNoHubException $e ) {
// Only throw this exception if we can't handle huburi-less feeds
// (i.e. we have a fallback hub or we can do feed polling (nohub)
if ( ! common_config ( 'feedsub' , 'fallback_hub' ) && ! common_config ( 'feedsub' , 'nohub' )) {
throw $e ;
}
}
if ( empty ( $feedsub -> id )) {
// if $feedsub doesn't have an id we'll insert it into the db here
$feedsub -> created = common_sql_now ();
$feedsub -> modified = common_sql_now ();
$result = $feedsub -> insert ();
if ( $result === false ) {
throw new FeedDBException ( $feedsub );
}
}
return $feedsub ;
}
/**
* ensureHub will only do $this -> update if ! empty ( $this -> id )
* because otherwise the object has not been created yet .
2017-04-30 09:29:16 +01:00
*
2017-06-21 23:30:38 +01:00
* @ param bool $rediscovered Whether the hub info is rediscovered ( to avoid endless loop nesting )
2017-04-30 09:29:16 +01:00
*
* @ return null if actively avoiding the database
* int number of rows updated in the database ( 0 means untouched )
*
* @ throws ServerException if something went wrong when updating the database
* FeedSubNoHubException if no hub URL was discovered
2017-04-27 08:24:12 +01:00
*/
2017-06-21 23:30:38 +01:00
public function ensureHub ( $rediscovered = false )
2017-04-27 08:24:12 +01:00
{
2017-06-22 13:37:32 +01:00
common_debug ( 'Now inside ensureHub again, $rediscovered==' . _ve ( $rediscovered ));
2017-04-27 08:24:12 +01:00
if ( $this -> sub_state !== 'inactive' ) {
2017-04-30 08:46:15 +01:00
common_log ( LOG_INFO , sprintf ( __METHOD__ . ': Running hub discovery a possibly active feed in %s state for URI %s' , _ve ( $this -> sub_state ), _ve ( $this -> uri )));
2010-02-18 21:22:21 +00:00
}
$discover = new FeedDiscovery ();
2017-04-27 08:24:12 +01:00
$discover -> discoverFromFeedURL ( $this -> uri );
2010-02-18 21:22:21 +00:00
2010-08-03 00:08:54 +01:00
$huburi = $discover -> getHubLink ();
2017-04-27 08:24:12 +01:00
if ( empty ( $huburi )) {
// Will be caught and treated with if statements in regards to
// fallback hub and feed polling (nohub) configuration.
2010-02-18 21:22:21 +00:00
throw new FeedSubNoHubException ();
}
2017-04-30 09:29:16 +01:00
// if we've already got a DB object stored, we want to UPDATE, not INSERT
2017-04-27 08:24:12 +01:00
$orig = ! empty ( $this -> id ) ? clone ( $this ) : null ;
2010-02-18 21:22:21 +00:00
2017-04-30 09:29:16 +01:00
$old_huburi = $this -> huburi ; // most likely null if we're INSERTing
2017-04-27 08:24:12 +01:00
$this -> huburi = $huburi ;
2010-02-18 21:22:21 +00:00
2017-04-27 08:24:12 +01:00
if ( ! empty ( $this -> id )) {
2017-04-30 09:29:16 +01:00
common_debug ( sprintf ( __METHOD__ . ': Feed uri==%s huburi before=%s after=%s (identical==%s)' , _ve ( $this -> uri ), _ve ( $old_huburi ), _ve ( $this -> huburi ), _ve ( $old_huburi === $this -> huburi )));
2017-04-30 08:20:08 +01:00
$result = $this -> update ( $orig );
if ( $result === false ) {
// TODO: Get a DB exception class going...
common_debug ( 'Database update failed for FeedSub id==' . _ve ( $this -> id ) . ' with new huburi: ' . _ve ( $this -> huburi ));
throw new ServerException ( 'Database update failed for FeedSub.' );
}
2017-06-22 13:37:32 +01:00
if ( ! $rediscovered ) {
2017-04-30 09:29:16 +01:00
$this -> renew ();
}
2017-04-30 08:20:08 +01:00
return $result ;
2010-02-18 21:22:21 +00:00
}
2017-04-30 08:20:08 +01:00
return null ; // we haven't done anything with the database
2010-02-18 21:22:21 +00:00
}
/**
* Send a subscription request to the hub for this feed .
* The hub will later send us a confirmation POST to / main / push / callback .
*
2014-05-06 14:40:57 +01:00
* @ return void
2010-02-18 21:22:21 +00:00
* @ throws ServerException if feed state is not valid
*/
2017-06-21 23:30:38 +01:00
public function subscribe ( $rediscovered = false )
2010-02-18 21:22:21 +00:00
{
2019-09-11 06:32:19 +01:00
if ( $this -> sub_state !== 'inactive' ) {
2017-05-01 10:04:27 +01:00
common_log ( LOG_WARNING , sprintf ( 'Attempting to (re)start WebSub subscription to %s in unexpected state %s' , $this -> getUri (), $this -> sub_state ));
2010-02-18 21:22:21 +00:00
}
2014-03-09 12:27:28 +00:00
if ( ! Event :: handle ( 'FeedSubscribe' , array ( $this ))) {
// A plugin handled it
2014-05-06 14:40:57 +01:00
return ;
2014-03-09 12:27:28 +00:00
}
2010-02-18 21:22:21 +00:00
if ( empty ( $this -> huburi )) {
2010-08-03 00:08:54 +01:00
if ( common_config ( 'feedsub' , 'fallback_hub' )) {
// No native hub on this feed?
// Use our fallback hub, which handles polling on our behalf.
2019-09-11 06:32:19 +01:00
} elseif ( common_config ( 'feedsub' , 'nohub' )) {
2015-01-16 00:10:55 +00:00
// For this to actually work, we'll need some polling mechanism.
// The FeedPoller plugin should take care of it.
2014-05-06 14:40:57 +01:00
return ;
2010-02-18 21:22:21 +00:00
} else {
2011-04-10 23:39:27 +01:00
// TRANS: Server exception.
2017-05-01 10:04:27 +01:00
throw new ServerException ( _m ( 'Attempting to start WebSub subscription for feed with no hub.' ));
2010-02-18 21:22:21 +00:00
}
}
2017-06-21 23:30:38 +01:00
$this -> doSubscribe ( 'subscribe' , $rediscovered );
2010-02-18 21:22:21 +00:00
}
/**
2017-05-01 10:04:27 +01:00
* Send a WebSub unsubscription request to the hub for this feed .
2010-02-18 21:22:21 +00:00
* The hub will later send us a confirmation POST to / main / push / callback .
2010-08-06 18:56:18 +01:00
* Warning : this will cancel the subscription even if someone else in
* the system is using it . Most callers will want garbageCollect () instead ,
* which confirms there ' s no uses left .
2010-02-18 21:22:21 +00:00
*
* @ throws ServerException if feed state is not valid
*/
2019-09-11 06:32:19 +01:00
public function unsubscribe ()
{
2010-02-18 21:22:21 +00:00
if ( $this -> sub_state != 'active' ) {
2017-05-01 10:04:27 +01:00
common_log ( LOG_WARNING , sprintf ( 'Attempting to (re)end WebSub subscription to %s in unexpected state %s' , $this -> getUri (), $this -> sub_state ));
2010-02-18 21:22:21 +00:00
}
2014-03-09 12:27:28 +00:00
if ( ! Event :: handle ( 'FeedUnsubscribe' , array ( $this ))) {
// A plugin handled it
2014-05-06 14:40:57 +01:00
return ;
2014-03-09 12:27:28 +00:00
}
2017-04-27 08:24:12 +01:00
if ( empty ( $this -> huburi ) && ! common_config ( 'feedsub' , 'fallback_hub' )) {
/**
* If the huburi is empty and we don ' t have a fallback hub ,
* there is nowhere we can send an unsubscribe to .
*
* A plugin should handle the FeedSub above and set the proper state
* if there is no hub . ( instead of 'nohub' it should be 'inactive' if
* the instance has enabled feed polling for feeds that don ' t publish
2017-05-01 10:04:27 +01:00
* WebSub / PuSH hubs . FeedPoller is a plugin which enables polling .
2017-04-27 08:24:12 +01:00
*
* Secondly , if we don ' t have the setting " nohub " enabled ( i . e . )
* we ' re ready to poll ourselves , there is something odd with the
* database , such as a polling plugin that has been disabled .
*/
if ( ! common_config ( 'feedsub' , 'nohub' )) {
2011-04-10 23:39:27 +01:00
// TRANS: Server exception.
2017-05-01 10:04:27 +01:00
throw new ServerException ( _m ( 'Attempting to end WebSub subscription for feed with no hub.' ));
2010-02-18 21:22:21 +00:00
}
2017-04-27 08:24:12 +01:00
return ;
2010-02-18 21:22:21 +00:00
}
2014-05-06 14:40:57 +01:00
$this -> doSubscribe ( 'unsubscribe' );
2010-02-18 21:22:21 +00:00
}
2010-08-06 18:56:18 +01:00
/**
* Check if there are any active local uses of this feed , and if not then
* make sure it ' s inactive , unsubscribing if necessary .
*
* @ return boolean true if the subscription is now inactive , false if still active .
2014-05-19 16:58:05 +01:00
* @ throws NoProfileException in FeedSubSubscriberCount for missing Profile entries
2014-05-06 14:40:57 +01:00
* @ throws Exception if something goes wrong in unsubscribe () method
2010-08-06 18:56:18 +01:00
*/
public function garbageCollect ()
{
2019-09-11 06:32:19 +01:00
if ( $this -> sub_state === 'inactive' ) {
2017-05-01 10:04:27 +01:00
// No active WebSub subscription, we can just leave it be.
2010-08-06 18:56:18 +01:00
return true ;
}
2014-05-19 16:58:05 +01:00
2017-05-01 10:04:27 +01:00
// WebSub subscription is either active or in an indeterminate state.
2014-05-19 16:58:05 +01:00
// Check if we're out of subscribers, and if so send an unsubscribe.
$count = 0 ;
Event :: handle ( 'FeedSubSubscriberCount' , array ( $this , & $count ));
if ( $count > 0 ) {
common_log ( LOG_INFO , __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this -> getUri ());
return false ;
}
common_log ( LOG_INFO , __METHOD__ . ': unsubscribing, no users left for ' . $this -> getUri ());
// Unsubscribe throws various Exceptions on failure
$this -> unsubscribe ();
return true ;
2010-08-06 18:56:18 +01:00
}
2019-09-11 06:32:19 +01:00
public static function renewalCheck ()
2013-11-02 19:02:28 +00:00
{
$fs = new FeedSub ();
2019-09-11 07:58:13 +01:00
$fs -> whereAdd ( " sub_end IS NOT NULL AND sub_end < (CURRENT_TIMESTAMP + INTERVAL '1' DAY) " );
2019-09-11 06:32:19 +01:00
// find can be both false and 0, depending on why nothing was found
if ( ! $fs -> find ()) {
2013-11-02 19:02:28 +00:00
throw new NoResultException ( $fs );
}
return $fs ;
}
2017-06-21 23:30:38 +01:00
public function renew ( $rediscovered = false )
2013-11-02 19:02:28 +00:00
{
2017-04-30 08:20:08 +01:00
common_debug ( 'FeedSub is being renewed for uri==' . _ve ( $this -> uri ) . ' on huburi==' . _ve ( $this -> huburi ));
2017-06-21 23:30:38 +01:00
$this -> subscribe ( $rediscovered );
2013-11-02 19:02:28 +00:00
}
/**
2014-03-02 23:01:13 +00:00
* Setting to subscribe means it is _waiting_ to become active . This
* cannot be done in a transaction because there is a chance that the
* remote script we ' re calling ( as in the case of PuSHpress ) performs
* the lookup _while_ we ' re POSTing data , which means the transaction
* never completes ( PushcallbackAction gets an 'inactive' state ) .
*
2014-05-06 14:40:57 +01:00
* @ return boolean true when everything is ok ( throws Exception on fail )
* @ throws Exception on failure , can be HTTPClient ' s or our own .
2013-11-02 19:02:28 +00:00
*/
2017-06-21 23:30:38 +01:00
protected function doSubscribe ( $mode , $rediscovered = false )
2010-02-18 21:22:21 +00:00
{
2017-05-01 06:40:16 +01:00
$msg = null ; // carries descriptive error message to enduser (no remote data strings!)
2010-02-18 21:22:21 +00:00
$orig = clone ( $this );
if ( $mode == 'subscribe' ) {
2013-10-21 12:20:30 +01:00
$this -> secret = common_random_hexstr ( 32 );
2010-02-18 21:22:21 +00:00
}
$this -> sub_state = $mode ;
$this -> update ( $orig );
unset ( $orig );
try {
$callback = common_local_url ( 'pushcallback' , array ( 'feed' => $this -> id ));
$headers = array ( 'Content-Type: application/x-www-form-urlencoded' );
$post = array ( 'hub.mode' => $mode ,
'hub.callback' => $callback ,
2014-01-01 18:43:31 +00:00
'hub.verify' => 'async' , // TODO: deprecated, remove when noone uses PuSH <0.4 (only 'async' method used there)
2013-11-06 11:46:59 +00:00
'hub.verify_token' => 'Deprecated-since-PuSH-0.4' , // TODO: rm!
2016-01-13 18:24:07 +00:00
'hub.lease_seconds' => 2592000 , // 3600*24*30, request approximately month long lease (may be changed by hub)
2010-02-18 21:22:21 +00:00
'hub.secret' => $this -> secret ,
2014-04-28 13:08:42 +01:00
'hub.topic' => $this -> getUri ());
2010-02-18 21:22:21 +00:00
$client = new HTTPClient ();
2010-08-03 00:08:54 +01:00
if ( $this -> huburi ) {
$hub = $this -> huburi ;
} else {
if ( common_config ( 'feedsub' , 'fallback_hub' )) {
$hub = common_config ( 'feedsub' , 'fallback_hub' );
if ( common_config ( 'feedsub' , 'hub_user' )) {
$u = common_config ( 'feedsub' , 'hub_user' );
$p = common_config ( 'feedsub' , 'hub_pass' );
$client -> setAuth ( $u , $p );
}
} else {
2017-05-01 10:04:27 +01:00
throw new FeedSubException ( 'Server could not find a usable WebSub hub.' );
2010-08-03 00:08:54 +01:00
}
}
$response = $client -> post ( $hub , $headers , $post );
2010-02-18 21:22:21 +00:00
$status = $response -> getStatus ();
2017-05-01 10:04:27 +01:00
// WebSub specificed response status code
2015-10-27 03:16:39 +00:00
if ( $status == 202 || $status == 204 ) {
2010-02-18 21:22:21 +00:00
common_log ( LOG_INFO , __METHOD__ . ': sub req ok, awaiting verification callback' );
2014-05-06 14:40:57 +01:00
return ;
2019-09-11 06:32:19 +01:00
} elseif ( $status >= 200 && $status < 300 ) {
2010-02-18 21:22:21 +00:00
common_log ( LOG_ERR , __METHOD__ . " : sub req returned unexpected HTTP $status : " . $response -> getBody ());
2017-05-01 06:40:16 +01:00
$msg = sprintf ( _m ( " Unexpected HTTP status: %d " ), $status );
2019-09-11 06:32:19 +01:00
} elseif ( $status == 422 && ! $rediscovered ) {
2017-04-30 09:29:16 +01:00
// Error code regarding something wrong in the data (it seems
2017-05-01 10:04:27 +01:00
// that we're talking to a WebSub hub at least, so let's check
2017-06-21 23:30:38 +01:00
// our own data to be sure we're not mistaken somehow, which
// means rediscovering hub data (the boolean parameter means
// we avoid running this part over and over and over and over):
2017-04-30 09:29:16 +01:00
2017-06-21 23:30:38 +01:00
common_debug ( 'Running ensureHub again due to 422 status, $rediscovered==' . _ve ( $rediscovered ));
2017-06-22 13:37:32 +01:00
$discoveryResult = $this -> ensureHub ( true );
common_debug ( 'ensureHub is now done and its result was: ' . _ve ( $discoveryResult ));
2010-02-18 21:22:21 +00:00
} else {
common_log ( LOG_ERR , __METHOD__ . " : sub req failed with HTTP $status : " . $response -> getBody ());
}
} catch ( Exception $e ) {
2014-05-06 14:40:57 +01:00
common_log ( LOG_ERR , __METHOD__ . " : error \" { $e -> getMessage () } \" hitting hub { $this -> huburi } subscribing to { $this -> getUri () } " );
2010-02-18 21:22:21 +00:00
2014-05-06 14:40:57 +01:00
// Reset the subscription state.
2010-02-18 21:22:21 +00:00
$orig = clone ( $this );
2010-02-21 22:46:26 +00:00
$this -> sub_state = 'inactive' ;
2010-02-18 21:22:21 +00:00
$this -> update ( $orig );
2014-05-06 14:40:57 +01:00
// Throw the Exception again.
throw $e ;
2010-02-18 21:22:21 +00:00
}
2017-05-01 06:40:16 +01:00
throw new ServerException ( " { $mode } request failed " . ( ! is_null ( $msg ) ? " ( $msg ) " : '.' ));
2010-02-18 21:22:21 +00:00
}
/**
2017-05-01 10:04:27 +01:00
* Save WebSub subscription confirmation .
2010-02-18 21:22:21 +00:00
* Sets approximate lease start and end times and finalizes state .
*
* @ param int $lease_seconds provided hub . lease_seconds parameter , if given
*/
2013-11-02 19:02:28 +00:00
public function confirmSubscribe ( $lease_seconds )
2010-02-18 21:22:21 +00:00
{
$original = clone ( $this );
$this -> sub_state = 'active' ;
$this -> sub_start = common_sql_date ( time ());
if ( $lease_seconds > 0 ) {
$this -> sub_end = common_sql_date ( time () + $lease_seconds );
} else {
2019-09-11 06:32:19 +01:00
// Backwards compatibility to StatusNet (PuSH <0.4 supported permanent subs)
2019-11-02 09:21:43 +00:00
$this -> sub_end = $this -> sqlValue ( 'NULL' );
2010-02-18 21:22:21 +00:00
}
2010-02-21 22:46:26 +00:00
$this -> modified = common_sql_now ();
2010-02-18 21:22:21 +00:00
2017-04-21 08:31:27 +01:00
common_debug ( __METHOD__ . ': Updating sub state and metadata for ' . $this -> getUri ());
2010-02-18 21:22:21 +00:00
return $this -> update ( $original );
}
/**
2017-05-01 10:04:27 +01:00
* Save WebSub unsubscription confirmation .
* Wipes active WebSub sub info and resets state .
2010-02-18 21:22:21 +00:00
*/
public function confirmUnsubscribe ()
{
$original = clone ( $this );
2019-11-02 09:21:43 +00:00
$this -> secret = $this -> sqlValue ( 'NULL' );
2019-09-11 06:32:19 +01:00
$this -> sub_state = 'inactive' ;
2019-11-02 09:21:43 +00:00
$this -> sub_start = $this -> sqlValue ( 'NULL' );
$this -> sub_end = $this -> sqlValue ( 'NULL' );
2010-02-21 22:46:26 +00:00
$this -> modified = common_sql_now ();
2010-02-18 21:22:21 +00:00
return $this -> update ( $original );
}
/**
2017-05-01 10:04:27 +01:00
* Accept updates from a WebSub feed . If validated , this object and the
2010-02-18 21:22:21 +00:00
* feed ( as a DOMDocument ) will be passed to the StartFeedSubHandleFeed
* and EndFeedSubHandleFeed events for processing .
*
2010-02-21 21:40:59 +00:00
* Not guaranteed to be running in an immediate POST context ; may be run
* from a queue handler .
*
* Side effects : the feedsub record ' s lastupdate field will be updated
* to the current time ( not published time ) if we got a legit update .
*
2010-02-18 21:22:21 +00:00
* @ param string $post source of Atom or RSS feed
* @ param string $hmac X - Hub - Signature header , if present
*/
public function receive ( $post , $hmac )
{
2016-06-28 10:51:11 +01:00
common_log ( LOG_INFO , sprintf ( __METHOD__ . ': packet for %s with HMAC %s' , _ve ( $this -> getUri ()), _ve ( $hmac )));
2010-02-18 21:22:21 +00:00
2015-01-14 00:16:28 +00:00
if ( ! in_array ( $this -> sub_state , array ( 'active' , 'nohub' ))) {
2017-05-01 10:04:27 +01:00
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': ignoring WebSub for inactive feed %s (in state %s)' , _ve ( $this -> getUri ()), _ve ( $this -> sub_state )));
2010-02-18 21:22:21 +00:00
return ;
}
if ( $post === '' ) {
common_log ( LOG_ERR , __METHOD__ . " : ignoring empty post " );
return ;
}
2017-04-30 08:20:08 +01:00
try {
if ( ! $this -> validatePushSig ( $post , $hmac )) {
// Per spec we silently drop input with a bad sig,
// while reporting receipt to the server.
return ;
}
2017-04-30 08:31:16 +01:00
$this -> receiveFeed ( $post );
2017-04-30 08:20:08 +01:00
} catch ( FeedSubBadPushSignatureException $e ) {
// We got a signature, so something could be wrong. Let's check to see if
// maybe upstream has switched to another hub. Let's fetch feed and then
2017-04-30 09:29:16 +01:00
// compare rel="hub" with $this->huburi, which is done in $this->ensureHub()
2017-04-30 08:20:08 +01:00
2017-04-30 09:29:16 +01:00
$this -> ensureHub ( true );
2010-02-18 21:22:21 +00:00
}
2017-04-21 07:08:39 +01:00
}
2017-04-21 08:31:27 +01:00
/**
* All our feed URIs should be URLs .
*/
public function importFeed ()
{
$feed_url = $this -> getUri ();
// Fetch the URL
try {
common_log ( LOG_INFO , sprintf ( 'Importing feed backlog from %s' , $feed_url ));
$feed_xml = HTTPClient :: quickGet ( $feed_url , 'application/atom+xml' );
} catch ( Exception $e ) {
throw new FeedSubException ( " Could not fetch feed from URL '%s': %s (%d). \n " , $feed_url , $e -> getMessage (), $e -> getCode ());
}
return $this -> receiveFeed ( $feed_xml );
}
2017-04-21 07:08:39 +01:00
protected function receiveFeed ( $feed_xml )
{
// We're passed the XML for the Atom feed as $feed_xml,
// so read it into a DOMDocument and process.
2010-02-18 21:22:21 +00:00
$feed = new DOMDocument ();
2017-04-21 07:08:39 +01:00
if ( ! $feed -> loadXML ( $feed_xml )) {
2010-02-18 21:22:21 +00:00
// @fixme might help to include the err message
common_log ( LOG_ERR , __METHOD__ . " : ignoring invalid XML " );
return ;
}
2010-02-21 21:40:59 +00:00
$orig = clone ( $this );
$this -> last_update = common_sql_now ();
$this -> update ( $orig );
2010-02-18 21:22:21 +00:00
Event :: handle ( 'StartFeedSubReceive' , array ( $this , $feed ));
Event :: handle ( 'EndFeedSubReceive' , array ( $this , $feed ));
}
/**
* Validate the given Atom chunk and HMAC signature against our
* shared secret that was set up at subscription time .
*
* If we don ' t have a shared secret , there should be no signature .
2016-06-28 10:51:11 +01:00
* If we do , our calculated HMAC should match theirs .
2010-02-18 21:22:21 +00:00
*
* @ param string $post raw XML source as POSTed to us
* @ param string $hmac X - Hub - Signature HTTP header value , or empty
* @ return boolean true for a match
*/
protected function validatePushSig ( $post , $hmac )
{
if ( $this -> secret ) {
2016-06-28 10:51:11 +01:00
// {3,16} because shortest hash algorithm name is 3 characters (md2,md4,md5) and longest
// is currently 11 characters, but we'll leave some margin in the end...
if ( preg_match ( '/^([0-9a-zA-Z\-\,]{3,16})=([0-9a-fA-F]+)$/' , $hmac , $matches )) {
$hash_algo = strtolower ( $matches [ 1 ]);
$their_hmac = strtolower ( $matches [ 2 ]);
2017-05-01 10:04:27 +01:00
common_debug ( sprintf ( __METHOD__ . ': WebSub push from feed %s uses HMAC algorithm %s with value: %s' , _ve ( $this -> getUri ()), _ve ( $hash_algo ), _ve ( $their_hmac )));
2016-06-28 10:51:11 +01:00
if ( ! in_array ( $hash_algo , hash_algos ())) {
// We can't handle this at all, PHP doesn't recognize the algorithm name ('md5', 'sha1', 'sha256' etc: https://secure.php.net/manual/en/function.hash-algos.php)
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': HMAC algorithm %s unsupported, not found in PHP hash_algos()' , _ve ( $hash_algo )));
return false ;
} elseif ( ! is_null ( common_config ( 'security' , 'hash_algos' )) && ! in_array ( $hash_algo , common_config ( 'security' , 'hash_algos' ))) {
// We _won't_ handle this because there is a list of accepted hash algorithms and this one is not in it.
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': Whitelist for HMAC algorithms exist, but %s is not included.' , _ve ( $hash_algo )));
return false ;
}
$our_hmac = hash_hmac ( $hash_algo , $post , $this -> secret );
2017-04-30 08:20:08 +01:00
if ( $their_hmac !== $our_hmac ) {
2017-05-01 10:04:27 +01:00
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': ignoring WebSub push with bad HMAC hash: got %s, expected %s for feed %s from hub %s' , _ve ( $their_hmac ), _ve ( $our_hmac ), _ve ( $this -> getUri ()), _ve ( $this -> huburi )));
throw new FeedSubBadPushSignatureException ( 'Incoming WebSub push signature did not match expected HMAC hash.' );
2010-02-18 21:22:21 +00:00
}
2017-04-30 08:20:08 +01:00
return true ;
2010-02-18 21:22:21 +00:00
} else {
2017-05-01 10:04:27 +01:00
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': ignoring WebSub push with bogus HMAC==' , _ve ( $hmac )));
2010-02-18 21:22:21 +00:00
}
} else {
if ( empty ( $hmac )) {
return true ;
} else {
2017-05-01 10:04:27 +01:00
common_log ( LOG_ERR , sprintf ( __METHOD__ . ': ignoring WebSub push with unexpected HMAC==%s' , _ve ( $hmac )));
2010-02-18 21:22:21 +00:00
}
}
return false ;
}
2014-05-19 16:58:05 +01:00
public function delete ( $useWhere = false )
{
try {
$oprofile = Ostatus_profile :: getKV ( 'feeduri' , $this -> getUri ());
2014-05-19 17:34:44 +01:00
if ( $oprofile instanceof Ostatus_profile ) {
// Check if there's a profile. If not, handle the NoProfileException below
$profile = $oprofile -> localProfile ();
}
2014-05-19 16:58:05 +01:00
} catch ( NoProfileException $e ) {
// If the Ostatus_profile has no local Profile bound to it, let's clean it out at the same time
$oprofile -> delete ();
} catch ( NoUriException $e ) {
// FeedSub->getUri() can throw a NoUriException, let's just go ahead and delete it
}
return parent :: delete ( $useWhere );
}
2010-02-18 21:22:21 +00:00
}