]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/classes/FeedSub.php
Message to end-user on why FeedSub failed.
[quix0rs-gnu-social.git] / plugins / OStatus / classes / FeedSub.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009-2010, StatusNet, Inc.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 if (!defined('GNUSOCIAL')) { exit(1); }
21
22 /**
23  * @package OStatusPlugin
24  * @maintainer Brion Vibber <brion@status.net>
25  */
26
27 /*
28 PuSH subscription flow:
29
30     $profile->subscribe()
31         sends a sub request to the hub...
32
33     main/push/callback
34         hub sends confirmation back to us via GET
35         We verify the request, then echo back the challenge.
36         On our end, we save the time we subscribed and the lease expiration
37
38     main/push/callback
39         hub sends us updates via POST
40
41 */
42
43 /**
44  * FeedSub handles low-level PubHubSubbub (PuSH) subscriptions.
45  * Higher-level behavior building OStatus stuff on top is handled
46  * under Ostatus_profile.
47  */
48 class FeedSub extends Managed_DataObject
49 {
50     public $__table = 'feedsub';
51
52     public $id;
53     public $uri;    // varchar(191)   not 255 because utf8mb4 takes more space
54
55     // PuSH subscription data
56     public $huburi;
57     public $secret;
58     public $sub_state; // subscribe, active, unsubscribe, inactive, nohub
59     public $sub_start;
60     public $sub_end;
61     public $last_update;
62
63     public $created;
64     public $modified;
65
66     public static function schemaDef()
67     {
68         return array(
69             'fields' => array(
70                 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'FeedSub local unique id'),
71                 'uri' => array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'FeedSub uri'),
72                 'huburi' => array('type' => 'text', 'description' => 'FeedSub hub-uri'),
73                 'secret' => array('type' => 'text', 'description' => 'FeedSub stored secret'),
74                 'sub_state' => array('type' => 'enum("subscribe","active","unsubscribe","inactive","nohub")', 'not null' => true, 'description' => 'subscription state'),
75                 'sub_start' => array('type' => 'datetime', 'description' => 'subscription start'),
76                 'sub_end' => array('type' => 'datetime', 'description' => 'subscription end'),
77                 'last_update' => array('type' => 'datetime', 'description' => 'when this record was last updated'),
78                 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
79                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
80             ),
81             'primary key' => array('id'),
82             'unique keys' => array(
83                 'feedsub_uri_key' => array('uri'),
84             ),
85         );
86     }
87
88     /**
89      * Get the feed uri (http/https)
90      */
91     public function getUri()
92     {
93         if (empty($this->uri)) {
94             throw new NoUriException($this);
95         }
96         return $this->uri;
97     }
98
99     function getLeaseRemaining()
100     {
101         if (empty($this->sub_end)) {
102             return null;
103         }
104         return strtotime($this->sub_end) - time();
105     }
106
107     /**
108      * Do we have a hub? Then we are a PuSH feed.
109      * https://en.wikipedia.org/wiki/PubSubHubbub
110      *
111      * If huburi is empty, then doublecheck that we are not using
112      * a fallback hub. If there is a fallback hub, it is only if the
113      * sub_state is "nohub" that we assume it's not a PuSH feed.
114      */
115     public function isPuSH()
116     {
117         if (empty($this->huburi)
118                 && (!common_config('feedsub', 'fallback_hub')
119                     || $this->sub_state === 'nohub')) {
120                 // Here we have no huburi set. Also, either there is no 
121                 // fallback hub configured or sub_state is "nohub".
122             return false;
123         }
124         return true;
125     }
126
127     /**
128      * Fetch the StatusNet-side profile for this feed
129      * @return Profile
130      */
131     public function localProfile()
132     {
133         if ($this->profile_id) {
134             return Profile::getKV('id', $this->profile_id);
135         }
136         return null;
137     }
138
139     /**
140      * Fetch the StatusNet-side profile for this feed
141      * @return Profile
142      */
143     public function localGroup()
144     {
145         if ($this->group_id) {
146             return User_group::getKV('id', $this->group_id);
147         }
148         return null;
149     }
150
151     /**
152      * @param string $feeduri
153      * @return FeedSub
154      * @throws FeedSubException if feed is invalid or lacks PuSH setup
155      */
156     public static function ensureFeed($feeduri)
157     {
158         $feedsub = self::getKV('uri', $feeduri);
159         if ($feedsub instanceof FeedSub) {
160             if (!empty($feedsub->huburi)) {
161                 // If there is already a huburi we don't
162                 // rediscover it on ensureFeed, call
163                 // ensureHub to do that (compare ->modified
164                 // to see if it might be time to do it).
165                 return $feedsub;
166             }
167             if ($feedsub->sub_state !== 'inactive') {
168                 throw new ServerException('Can only ensure WebSub hub for inactive (unsubscribed) feeds.');
169             }
170             // If huburi is empty we continue with ensureHub
171         } else {
172             // If we don't have that local feed URI
173             // stored then we create a new DB object.
174             $feedsub = new FeedSub();
175             $feedsub->uri = $feeduri;
176             $feedsub->sub_state = 'inactive';
177         }
178
179         try {
180             // discover the hub uri
181             $feedsub->ensureHub();
182
183         } catch (FeedSubNoHubException $e) {
184             // Only throw this exception if we can't handle huburi-less feeds
185             // (i.e. we have a fallback hub or we can do feed polling (nohub)
186             if (!common_config('feedsub', 'fallback_hub') && !common_config('feedsub', 'nohub')) {
187                 throw $e;
188             }
189         }
190
191         if (empty($feedsub->id)) {
192             // if $feedsub doesn't have an id we'll insert it into the db here
193             $feedsub->created = common_sql_now();
194             $feedsub->modified = common_sql_now();
195             $result = $feedsub->insert();
196             if ($result === false) {
197                 throw new FeedDBException($feedsub);
198             }
199         }
200
201         return $feedsub;
202     }
203
204     /**
205      * ensureHub will only do $this->update if !empty($this->id)
206      * because otherwise the object has not been created yet.
207      *
208      * @param   bool    $autorenew  Whether to autorenew the feed after ensuring the hub URL
209      *
210      * @return  null    if actively avoiding the database
211      *          int     number of rows updated in the database (0 means untouched)
212      *
213      * @throws  ServerException if something went wrong when updating the database
214      *          FeedSubNoHubException   if no hub URL was discovered
215      */
216     public function ensureHub($autorenew=false)
217     {
218         if ($this->sub_state !== 'inactive') {
219             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)));
220         }
221
222         $discover = new FeedDiscovery();
223         $discover->discoverFromFeedURL($this->uri);
224
225         $huburi = $discover->getHubLink();
226         if (empty($huburi)) {
227             // Will be caught and treated with if statements in regards to
228             // fallback hub and feed polling (nohub) configuration.
229             throw new FeedSubNoHubException();
230         }
231
232         // if we've already got a DB object stored, we want to UPDATE, not INSERT
233         $orig = !empty($this->id) ? clone($this) : null;
234
235         $old_huburi = $this->huburi;    // most likely null if we're INSERTing
236         $this->huburi = $huburi;
237
238         if (!empty($this->id)) {
239             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)));
240             $result = $this->update($orig);
241             if ($result === false) {
242                 // TODO: Get a DB exception class going...
243                 common_debug('Database update failed for FeedSub id=='._ve($this->id).' with new huburi: '._ve($this->huburi));
244                 throw new ServerException('Database update failed for FeedSub.');
245             }
246             if ($autorenew) {
247                 $this->renew();
248             }
249             return $result;
250         }
251
252         return null;    // we haven't done anything with the database
253     }
254
255     /**
256      * Send a subscription request to the hub for this feed.
257      * The hub will later send us a confirmation POST to /main/push/callback.
258      *
259      * @return void
260      * @throws ServerException if feed state is not valid
261      */
262     public function subscribe()
263     {
264         if ($this->sub_state && $this->sub_state != 'inactive') {
265             common_log(LOG_WARNING, sprintf('Attempting to (re)start PuSH subscription to %s in unexpected state %s', $this->getUri(), $this->sub_state));
266         }
267
268         if (!Event::handle('FeedSubscribe', array($this))) {
269             // A plugin handled it
270             return;
271         }
272
273         if (empty($this->huburi)) {
274             if (common_config('feedsub', 'fallback_hub')) {
275                 // No native hub on this feed?
276                 // Use our fallback hub, which handles polling on our behalf.
277             } else if (common_config('feedsub', 'nohub')) {
278                 // For this to actually work, we'll need some polling mechanism.
279                 // The FeedPoller plugin should take care of it.
280                 return;
281             } else {
282                 // TRANS: Server exception.
283                 throw new ServerException(_m('Attempting to start PuSH subscription for feed with no hub.'));
284             }
285         }
286
287         $this->doSubscribe('subscribe');
288     }
289
290     /**
291      * Send a PuSH unsubscription request to the hub for this feed.
292      * The hub will later send us a confirmation POST to /main/push/callback.
293      * Warning: this will cancel the subscription even if someone else in
294      * the system is using it. Most callers will want garbageCollect() instead,
295      * which confirms there's no uses left.
296      *
297      * @throws ServerException if feed state is not valid
298      */
299     public function unsubscribe() {
300         if ($this->sub_state != 'active') {
301             common_log(LOG_WARNING, sprintf('Attempting to (re)end PuSH subscription to %s in unexpected state %s', $this->getUri(), $this->sub_state));
302         }
303
304         if (!Event::handle('FeedUnsubscribe', array($this))) {
305             // A plugin handled it
306             return;
307         }
308
309         if (empty($this->huburi) && !common_config('feedsub', 'fallback_hub')) {
310             /**
311              * If the huburi is empty and we don't have a fallback hub,
312              * there is nowhere we can send an unsubscribe to.
313              *
314              * A plugin should handle the FeedSub above and set the proper state
315              * if there is no hub. (instead of 'nohub' it should be 'inactive' if
316              * the instance has enabled feed polling for feeds that don't publish
317              * PuSH/WebSub hubs. FeedPoller is a plugin which enables polling.
318              *
319              * Secondly, if we don't have the setting "nohub" enabled (i.e.)
320              * we're ready to poll ourselves, there is something odd with the
321              * database, such as a polling plugin that has been disabled.
322              */
323
324             if (!common_config('feedsub', 'nohub')) {
325                 // TRANS: Server exception.
326                 throw new ServerException(_m('Attempting to end PuSH subscription for feed with no hub.'));
327             }
328
329             return;
330         }
331
332         $this->doSubscribe('unsubscribe');
333     }
334
335     /**
336      * Check if there are any active local uses of this feed, and if not then
337      * make sure it's inactive, unsubscribing if necessary.
338      *
339      * @return boolean true if the subscription is now inactive, false if still active.
340      * @throws NoProfileException in FeedSubSubscriberCount for missing Profile entries
341      * @throws Exception if something goes wrong in unsubscribe() method
342      */
343     public function garbageCollect()
344     {
345         if ($this->sub_state == '' || $this->sub_state == 'inactive') {
346             // No active PuSH subscription, we can just leave it be.
347             return true;
348         }
349
350         // PuSH subscription is either active or in an indeterminate state.
351         // Check if we're out of subscribers, and if so send an unsubscribe.
352         $count = 0;
353         Event::handle('FeedSubSubscriberCount', array($this, &$count));
354
355         if ($count > 0) {
356             common_log(LOG_INFO, __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this->getUri());
357             return false;
358         }
359
360         common_log(LOG_INFO, __METHOD__ . ': unsubscribing, no users left for ' . $this->getUri());
361         // Unsubscribe throws various Exceptions on failure
362         $this->unsubscribe();
363
364         return true;
365     }
366
367     static public function renewalCheck()
368     {
369         $fs = new FeedSub();
370         // the "" empty string check is because we historically haven't saved unsubscribed feeds as NULL
371         $fs->whereAdd('sub_end IS NOT NULL AND sub_end!="" AND sub_end < NOW() + INTERVAL 1 day');
372         if (!$fs->find()) { // find can be both false and 0, depending on why nothing was found
373             throw new NoResultException($fs);
374         }
375         return $fs;
376     }
377
378     public function renew()
379     {
380         common_debug('FeedSub is being renewed for uri=='._ve($this->uri).' on huburi=='._ve($this->huburi));
381         $this->subscribe();
382     }
383
384     /**
385      * Setting to subscribe means it is _waiting_ to become active. This
386      * cannot be done in a transaction because there is a chance that the
387      * remote script we're calling (as in the case of PuSHpress) performs
388      * the lookup _while_ we're POSTing data, which means the transaction
389      * never completes (PushcallbackAction gets an 'inactive' state).
390      *
391      * @return boolean true when everything is ok (throws Exception on fail)
392      * @throws Exception on failure, can be HTTPClient's or our own.
393      */
394     protected function doSubscribe($mode)
395     {
396         $msg = null;    // carries descriptive error message to enduser (no remote data strings!)
397
398         $orig = clone($this);
399         if ($mode == 'subscribe') {
400             $this->secret = common_random_hexstr(32);
401         }
402         $this->sub_state = $mode;
403         $this->update($orig);
404         unset($orig);
405
406         try {
407             $callback = common_local_url('pushcallback', array('feed' => $this->id));
408             $headers = array('Content-Type: application/x-www-form-urlencoded');
409             $post = array('hub.mode' => $mode,
410                           'hub.callback' => $callback,
411                           'hub.verify' => 'async',  // TODO: deprecated, remove when noone uses PuSH <0.4 (only 'async' method used there)
412                           'hub.verify_token' => 'Deprecated-since-PuSH-0.4', // TODO: rm!
413
414                           'hub.lease_seconds' => 2592000,   // 3600*24*30, request approximately month long lease (may be changed by hub)
415                           'hub.secret' => $this->secret,
416                           'hub.topic' => $this->getUri());
417             $client = new HTTPClient();
418             if ($this->huburi) {
419                 $hub = $this->huburi;
420             } else {
421                 if (common_config('feedsub', 'fallback_hub')) {
422                     $hub = common_config('feedsub', 'fallback_hub');
423                     if (common_config('feedsub', 'hub_user')) {
424                         $u = common_config('feedsub', 'hub_user');
425                         $p = common_config('feedsub', 'hub_pass');
426                         $client->setAuth($u, $p);
427                     }
428                 } else {
429                     throw new FeedSubException('Server could not find a usable PuSH hub.');
430                 }
431             }
432             $response = $client->post($hub, $headers, $post);
433             $status = $response->getStatus();
434             // PuSH specificed response status code
435             if ($status == 202  || $status == 204) {
436                 common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
437                 return;
438             } else if ($status >= 200 && $status < 300) {
439                 common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
440                 $msg = sprintf(_m("Unexpected HTTP status: %d"), $status);
441             } else if ($status == 422) {
442                 // Error code regarding something wrong in the data (it seems
443                 // that we're talking to a PuSH hub at least, so let's check
444                 // our own data to be sure we're not mistaken somehow.
445
446                 $this->ensureHub(true);
447             } else {
448                 common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
449             }
450         } catch (Exception $e) {
451             common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub {$this->huburi} subscribing to {$this->getUri()}");
452
453             // Reset the subscription state.
454             $orig = clone($this);
455             $this->sub_state = 'inactive';
456             $this->update($orig);
457
458             // Throw the Exception again.
459             throw $e;
460         }
461         throw new ServerException("{$mode} request failed" . (!is_null($msg) ? " ($msg)" : '.'));
462     }
463
464     /**
465      * Save PuSH subscription confirmation.
466      * Sets approximate lease start and end times and finalizes state.
467      *
468      * @param int $lease_seconds provided hub.lease_seconds parameter, if given
469      */
470     public function confirmSubscribe($lease_seconds)
471     {
472         $original = clone($this);
473
474         $this->sub_state = 'active';
475         $this->sub_start = common_sql_date(time());
476         if ($lease_seconds > 0) {
477             $this->sub_end = common_sql_date(time() + $lease_seconds);
478         } else {
479             $this->sub_end = null;  // Backwards compatibility to StatusNet (PuSH <0.4 supported permanent subs)
480         }
481         $this->modified = common_sql_now();
482
483         common_debug(__METHOD__ . ': Updating sub state and metadata for '.$this->getUri());
484         return $this->update($original);
485     }
486
487     /**
488      * Save PuSH unsubscription confirmation.
489      * Wipes active PuSH sub info and resets state.
490      */
491     public function confirmUnsubscribe()
492     {
493         $original = clone($this);
494
495         // @fixme these should all be null, but DB_DataObject doesn't save null values...?????
496         $this->secret = '';
497         $this->sub_state = '';
498         $this->sub_start = '';
499         $this->sub_end = '';
500         $this->modified = common_sql_now();
501
502         return $this->update($original);
503     }
504
505     /**
506      * Accept updates from a PuSH feed. If validated, this object and the
507      * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed
508      * and EndFeedSubHandleFeed events for processing.
509      *
510      * Not guaranteed to be running in an immediate POST context; may be run
511      * from a queue handler.
512      *
513      * Side effects: the feedsub record's lastupdate field will be updated
514      * to the current time (not published time) if we got a legit update.
515      *
516      * @param string $post source of Atom or RSS feed
517      * @param string $hmac X-Hub-Signature header, if present
518      */
519     public function receive($post, $hmac)
520     {
521         common_log(LOG_INFO, sprintf(__METHOD__.': packet for %s with HMAC %s', _ve($this->getUri()), _ve($hmac)));
522
523         if (!in_array($this->sub_state, array('active', 'nohub'))) {
524             common_log(LOG_ERR, sprintf(__METHOD__.': ignoring PuSH for inactive feed %s (in state %s)', _ve($this->getUri()), _ve($this->sub_state)));
525             return;
526         }
527
528         if ($post === '') {
529             common_log(LOG_ERR, __METHOD__ . ": ignoring empty post");
530             return;
531         }
532
533         try {
534             if (!$this->validatePushSig($post, $hmac)) {
535                 // Per spec we silently drop input with a bad sig,
536                 // while reporting receipt to the server.
537                 return;
538             }
539
540             $this->receiveFeed($post);
541
542         } catch (FeedSubBadPushSignatureException $e) {
543             // We got a signature, so something could be wrong. Let's check to see if
544             // maybe upstream has switched to another hub. Let's fetch feed and then
545             // compare rel="hub" with $this->huburi, which is done in $this->ensureHub()
546
547             $this->ensureHub(true);
548         }
549     }
550
551     /**
552      * All our feed URIs should be URLs.
553      */
554     public function importFeed()
555     {
556         $feed_url = $this->getUri();
557
558         // Fetch the URL
559         try {
560             common_log(LOG_INFO, sprintf('Importing feed backlog from %s', $feed_url));
561             $feed_xml = HTTPClient::quickGet($feed_url, 'application/atom+xml');
562         } catch (Exception $e) {
563             throw new FeedSubException("Could not fetch feed from URL '%s': %s (%d).\n", $feed_url, $e->getMessage(), $e->getCode());
564         }
565
566         return $this->receiveFeed($feed_xml);
567     }
568
569     protected function receiveFeed($feed_xml)
570     {
571         // We're passed the XML for the Atom feed as $feed_xml,
572         // so read it into a DOMDocument and process.
573         $feed = new DOMDocument();
574         if (!$feed->loadXML($feed_xml)) {
575             // @fixme might help to include the err message
576             common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML");
577             return;
578         }
579
580         $orig = clone($this);
581         $this->last_update = common_sql_now();
582         $this->update($orig);
583
584         Event::handle('StartFeedSubReceive', array($this, $feed));
585         Event::handle('EndFeedSubReceive', array($this, $feed));
586     }
587
588     /**
589      * Validate the given Atom chunk and HMAC signature against our
590      * shared secret that was set up at subscription time.
591      *
592      * If we don't have a shared secret, there should be no signature.
593      * If we do, our calculated HMAC should match theirs.
594      *
595      * @param string $post raw XML source as POSTed to us
596      * @param string $hmac X-Hub-Signature HTTP header value, or empty
597      * @return boolean true for a match
598      */
599     protected function validatePushSig($post, $hmac)
600     {
601         if ($this->secret) {
602             // {3,16} because shortest hash algorithm name is 3 characters (md2,md4,md5) and longest
603             // is currently 11 characters, but we'll leave some margin in the end...
604             if (preg_match('/^([0-9a-zA-Z\-\,]{3,16})=([0-9a-fA-F]+)$/', $hmac, $matches)) {
605                 $hash_algo  = strtolower($matches[1]);
606                 $their_hmac = strtolower($matches[2]);
607                 common_debug(sprintf(__METHOD__ . ': PuSH from feed %s uses HMAC algorithm %s with value: %s', _ve($this->getUri()), _ve($hash_algo), _ve($their_hmac)));
608
609                 if (!in_array($hash_algo, hash_algos())) {
610                     // 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)
611                     common_log(LOG_ERR, sprintf(__METHOD__.': HMAC algorithm %s unsupported, not found in PHP hash_algos()', _ve($hash_algo)));
612                     return false;
613                 } elseif (!is_null(common_config('security', 'hash_algos')) && !in_array($hash_algo, common_config('security', 'hash_algos'))) {
614                     // We _won't_ handle this because there is a list of accepted hash algorithms and this one is not in it.
615                     common_log(LOG_ERR, sprintf(__METHOD__.': Whitelist for HMAC algorithms exist, but %s is not included.', _ve($hash_algo)));
616                     return false;
617                 }
618
619                 $our_hmac = hash_hmac($hash_algo, $post, $this->secret);
620                 if ($their_hmac !== $our_hmac) {
621                     common_log(LOG_ERR, sprintf(__METHOD__.': ignoring 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)));
622                     throw new FeedSubBadPushSignatureException('Incoming PuSH signature did not match expected HMAC hash.');
623                 }
624                 return true;
625
626             } else {
627                 common_log(LOG_ERR, sprintf(__METHOD__.': ignoring PuSH with bogus HMAC==', _ve($hmac)));
628             }
629         } else {
630             if (empty($hmac)) {
631                 return true;
632             } else {
633                 common_log(LOG_ERR, sprintf(__METHOD__.': ignoring PuSH with unexpected HMAC==%s', _ve($hmac)));
634             }
635         }
636         return false;
637     }
638
639     public function delete($useWhere=false)
640     {
641         try {
642             $oprofile = Ostatus_profile::getKV('feeduri', $this->getUri());
643             if ($oprofile instanceof Ostatus_profile) {
644                 // Check if there's a profile. If not, handle the NoProfileException below
645                 $profile = $oprofile->localProfile();
646             }
647         } catch (NoProfileException $e) {
648             // If the Ostatus_profile has no local Profile bound to it, let's clean it out at the same time
649             $oprofile->delete();
650         } catch (NoUriException $e) {
651             // FeedSub->getUri() can throw a NoUriException, let's just go ahead and delete it
652         }
653         return parent::delete($useWhere);
654     }
655 }