3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010, StatusNet, Inc.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 * PuSH feed subscription record
23 * @author Brion Vibber <brion@status.net>
25 class HubSub extends Memcached_DataObject
27 public $__table = 'hubsub';
29 public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8
39 public /*static*/ function staticGet($topic, $callback)
41 return parent::staticGet(__CLASS__, 'hashkey', self::hashkey($topic, $callback));
44 protected static function hashkey($topic, $callback)
46 return sha1($topic . '|' . $callback);
50 * return table definition for DB_DataObject
52 * DB_DataObject needs to know something about the table to manipulate
53 * instances. This method provides all the DB_DataObject needs to know.
55 * @return array array of column definitions
60 return array('hashkey' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
61 'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
62 'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
63 'secret' => DB_DATAOBJECT_STR,
64 'challenge' => DB_DATAOBJECT_STR,
65 'lease' => DB_DATAOBJECT_INT,
66 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
67 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
68 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
71 static function schemaDef()
73 return array(new ColumnDef('hashkey', 'char',
77 new ColumnDef('topic', 'varchar',
81 new ColumnDef('callback', 'varchar',
83 new ColumnDef('secret', 'text',
85 new ColumnDef('challenge', 'varchar',
87 new ColumnDef('lease', 'int',
89 new ColumnDef('sub_start', 'datetime',
91 new ColumnDef('sub_end', 'datetime',
93 new ColumnDef('created', 'datetime',
99 return array_keys($this->keyTypes());
102 function sequenceKeys()
104 return array(false, false, false);
108 * return key definitions for DB_DataObject
110 * DB_DataObject needs to know about keys that the table has; this function
113 * @return array key definitions
118 return array('hashkey' => 'K');
122 * Validates a requested lease length, sets length plus
123 * subscription start & end dates.
125 * Does not save to database -- use before insert() or update().
127 * @param int $length in seconds
129 function setLease($length)
131 assert(is_int($length));
137 // We want to garbage collect dead subscriptions!
139 } elseif( $length < $min) {
141 } else if ($length > $max) {
145 $this->lease = $length;
146 $this->start_sub = common_sql_now();
147 $this->end_sub = common_sql_date(time() + $length);
151 * Send a verification ping to subscriber
152 * @param string $mode 'subscribe' or 'unsubscribe'
153 * @param string $token hub.verify_token value, if provided by client
155 function verify($mode, $token=null)
157 assert($mode == 'subscribe' || $mode == 'unsubscribe');
159 // Is this needed? data object fun...
160 $clone = clone($this);
161 $clone->challenge = common_good_rand(16);
162 $clone->update($this);
163 $this->challenge = $clone->challenge;
166 $params = array('hub.mode' => $mode,
167 'hub.topic' => $this->topic,
168 'hub.challenge' => $this->challenge);
169 if ($mode == 'subscribe') {
170 $params['hub.lease_seconds'] = $this->lease;
172 if ($token !== null) {
173 $params['hub.verify_token'] = $token;
175 $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls
178 $request = new HTTPClient();
179 $response = $request->get($url);
180 $status = $response->getStatus();
182 if ($status >= 200 && $status < 300) {
185 // @fixme how can we schedule a second attempt?
187 $fail = "Returned HTTP $status";
189 } catch (Exception $e) {
190 $fail = $e->getMessage();
193 // @fixme how can we schedule a second attempt?
194 // or save a fail count?
196 common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail");
199 if ($mode == 'subscribe') {
200 // Establish or renew the subscription!
201 // This seems unnecessary... dataobject fun!
202 $clone = clone($this);
203 $clone->challenge = null;
204 $clone->setLease($this->lease);
205 $clone->update($this);
208 $this->challenge = null;
209 $this->setLease($this->lease);
210 common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds");
211 } else if ($mode == 'unsubscribe') {
212 common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic");
220 * Insert wrapper; transparently set the hash key from topic and callback columns.
221 * @return boolean success
225 $this->hashkey = self::hashkey($this->topic, $this->callback);
226 return parent::insert();
230 * Schedule delivery of a 'fat ping' to the subscriber's callback
231 * endpoint. If queues are disabled, this will run immediately.
233 * @param string $atom well-formed Atom feed
234 * @param int $retries optional count of retries if POST fails; defaults to hub_retries from config or 0 if unset
236 function distribute($atom, $retries=null)
238 if ($retries === null) {
239 $retries = intval(common_config('ostatus', 'hub_retries'));
242 $data = array('sub' => clone($this),
244 'retries' => $retries);
245 $qm = QueueManager::get();
246 $qm->enqueue($data, 'hubout');
250 * Send a 'fat ping' to the subscriber's callback endpoint
251 * containing the given Atom feed chunk.
253 * Determination of which items to send should be done at
254 * a higher level; don't just shove in a complete feed!
256 * @param string $atom well-formed Atom feed
257 * @throws Exception (HTTP or general)
261 $headers = array('Content-Type: application/atom+xml');
263 $hmac = hash_hmac('sha1', $atom, $this->secret);
264 $headers[] = "X-Hub-Signature: sha1=$hmac";
268 common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac");
270 $request = new HTTPClient();
271 $request->setBody($atom);
272 $response = $request->post($this->callback, $headers);
274 if ($response->isOk()) {
277 throw new Exception("Callback returned status: " .
278 $response->getStatus() .
280 trim($response->getBody()));