* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
/**
* @package OStatusPlugin
* @maintainer Brion Vibber <brion@status.net>
hub sends us updates via POST
*/
-
class FeedDBException extends FeedSubException
{
public $obj;
* Higher-level behavior building OStatus stuff on top is handled
* under Ostatus_profile.
*/
-class FeedSub extends Memcached_DataObject
+class FeedSub extends Managed_DataObject
{
public $__table = 'feedsub';
public $created;
public $modified;
- public /*static*/ function staticGet($k, $v=null)
- {
- return parent::staticGet(__CLASS__, $k, $v);
- }
-
- /**
- * return table definition for DB_DataObject
- *
- * DB_DataObject needs to know something about the table to manipulate
- * instances. This method provides all the DB_DataObject needs to know.
- *
- * @return array array of column definitions
- */
-
- function table()
- {
- return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'huburi' => DB_DATAOBJECT_STR,
- 'secret' => DB_DATAOBJECT_STR,
- 'verify_token' => DB_DATAOBJECT_STR,
- 'sub_state' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'last_update' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
- 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
- }
-
- static function schemaDef()
+ public static function schemaDef()
{
- return array(new ColumnDef('id', 'integer',
- /*size*/ null,
- /*nullable*/ false,
- /*key*/ 'PRI',
- /*default*/ null,
- /*extra*/ null,
- /*auto_increment*/ true),
- new ColumnDef('uri', 'varchar',
- 255, false, 'UNI'),
- new ColumnDef('huburi', 'text',
- null, true),
- new ColumnDef('verify_token', 'text',
- null, true),
- new ColumnDef('secret', 'text',
- null, true),
- new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe','inactive')",
- null, false),
- new ColumnDef('sub_start', 'datetime',
- null, true),
- new ColumnDef('sub_end', 'datetime',
- null, true),
- new ColumnDef('last_update', 'datetime',
- null, false),
- new ColumnDef('created', 'datetime',
- null, false),
- new ColumnDef('modified', 'datetime',
- null, false));
- }
-
- /**
- * return key definitions for DB_DataObject
- *
- * DB_DataObject needs to know about keys that the table has; this function
- * defines them.
- *
- * @return array key definitions
- */
-
- function keys()
- {
- return array_keys($this->keyTypes());
- }
-
- /**
- * return key definitions for Memcached_DataObject
- *
- * Our caching system uses the same key definitions, but uses a different
- * method to get them.
- *
- * @return array key definitions
- */
-
- function keyTypes()
- {
- return array('id' => 'K', 'uri' => 'U');
- }
-
- function sequenceKey()
- {
- return array('id', true, false);
+ return array(
+ 'fields' => array(
+ 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'FeedSub local unique id'),
+ 'uri' => array('type' => 'varchar', 'not null' => true, 'length' => 255, 'description' => 'FeedSub uri'),
+ 'huburi' => array('type' => 'text', 'description' => 'FeedSub hub-uri'),
+ 'verify_token' => array('type' => 'text', 'description' => 'FeedSub verify-token'),
+ 'secret' => array('type' => 'text', 'description' => 'FeedSub stored secret'),
+ 'sub_state' => array('type' => 'enum("subscribe","active","unsubscribe","inactive")', 'not null' => true, 'description' => 'subscription state'),
+ 'sub_start' => array('type' => 'datetime', 'description' => 'subscription start'),
+ 'sub_end' => array('type' => 'datetime', 'description' => 'subscription end'),
+ 'last_update' => array('type' => 'datetime', 'not null' => true, 'description' => 'when this record was last updated'),
+ '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'),
+ ),
+ );
}
/**
public function localProfile()
{
if ($this->profile_id) {
- return Profile::staticGet('id', $this->profile_id);
+ return Profile::getKV('id', $this->profile_id);
}
return null;
}
public function localGroup()
{
if ($this->group_id) {
- return User_group::staticGet('id', $this->group_id);
+ return User_group::getKV('id', $this->group_id);
}
return null;
}
*/
public static function ensureFeed($feeduri)
{
- $current = self::staticGet('uri', $feeduri);
+ $current = self::getKV('uri', $feeduri);
if ($current) {
return $current;
}
$discover = new FeedDiscovery();
$discover->discoverFromFeedURL($feeduri);
- $huburi = $discover->getAtomLink('hub');
- if (!$huburi) {
+ $huburi = $discover->getHubLink();
+ if (!$huburi && !common_config('feedsub', 'fallback_hub')) {
throw new FeedSubNoHubException();
}
common_log(LOG_WARNING, "Attempting to (re)start PuSH subscription to $this->uri in unexpected state $this->sub_state");
}
if (empty($this->huburi)) {
- if (common_config('feedsub', 'nohub')) {
+ if (common_config('feedsub', 'fallback_hub')) {
+ // No native hub on this feed?
+ // Use our fallback hub, which handles polling on our behalf.
+ } else if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
+ // We'll never actually get updates in this mode.
return true;
} else {
- throw new ServerException("Attempting to start PuSH subscription for feed with no hub");
+ // TRANS: Server exception.
+ throw new ServerException(_m('Attempting to start PuSH subscription for feed with no hub.'));
}
}
/**
* Send a PuSH unsubscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
+ * 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.
*
* @return bool true on success, false on failure
* @throws ServerException if feed state is not valid
common_log(LOG_WARNING, "Attempting to (re)end PuSH subscription to $this->uri in unexpected state $this->sub_state");
}
if (empty($this->huburi)) {
- if (common_config('feedsub', 'nohub')) {
+ if (common_config('feedsub', 'fallback_hub')) {
+ // No native hub on this feed?
+ // Use our fallback hub, which handles polling on our behalf.
+ } else if (common_config('feedsub', 'nohub')) {
// Fake it! We're just testing remote feeds w/o hubs.
+ // We'll never actually get updates in this mode.
return true;
} else {
- throw new ServerException("Attempting to end PuSH subscription for feed with no hub");
+ // TRANS: Server exception.
+ throw new ServerException(_m('Attempting to end PuSH subscription for feed with no hub.'));
}
}
return $this->doSubscribe('unsubscribe');
}
+ /**
+ * 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.
+ */
+ public function garbageCollect()
+ {
+ if ($this->sub_state == '' || $this->sub_state == 'inactive') {
+ // No active PuSH subscription, we can just leave it be.
+ return true;
+ } else {
+ // PuSH subscription is either active or in an indeterminate state.
+ // Check if we're out of subscribers, and if so send an unsubscribe.
+ $count = 0;
+ Event::handle('FeedSubSubscriberCount', array($this, &$count));
+
+ if ($count) {
+ common_log(LOG_INFO, __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this->uri);
+ return false;
+ } else {
+ common_log(LOG_INFO, __METHOD__ . ': unsubscribing, no users left for ' . $this->uri);
+ return $this->unsubscribe();
+ }
+ }
+ }
+
protected function doSubscribe($mode)
{
$orig = clone($this);
'hub.secret' => $this->secret,
'hub.topic' => $this->uri);
$client = new HTTPClient();
- $response = $client->post($this->huburi, $headers, $post);
+ 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 {
+ throw new FeedSubException('WTF?');
+ }
+ }
+ $response = $client->post($hub, $headers, $post);
$status = $response->getStatus();
if ($status == 202) {
common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
if ($their_hmac === $our_hmac) {
return true;
}
- common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+ if (common_config('feedsub', 'debug')) {
+ $tempfile = tempnam(sys_get_temp_dir(), 'feedsub-receive');
+ if ($tempfile) {
+ file_put_contents($tempfile, $post);
+ }
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac for feed $this->uri on $this->huburi; saved to $tempfile");
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac for feed $this->uri on $this->huburi");
+ }
} else {
common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
}
}
return false;
}
-
}
-