]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/microappplugin.php
MicroAppPlugin extends to intermediate ActivityHandlerPlugin
[quix0rs-gnu-social.git] / lib / microappplugin.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2011, StatusNet, Inc.
5  *
6  * Superclass for microapp plugin
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  Microapp
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2011 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * Superclass for microapp plugins
39  *
40  * This class lets you define micro-applications with different kinds of activities.
41  *
42  * The applications work more-or-less like other
43  *
44  * @category  Microapp
45  * @package   StatusNet
46  * @author    Evan Prodromou <evan@status.net>
47  * @copyright 2011 StatusNet, Inc.
48  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
49  * @link      http://status.net/
50  */
51 abstract class MicroAppPlugin extends ActivityHandlerPlugin
52 {
53     /**
54      * Returns a localized string which represents this micro-app,
55      * to be shown to users selecting what type of post to make.
56      * This is paired with the key string in $this->tag().
57      *
58      * All micro-app classes must override this method.
59      *
60      * @return string
61      */
62     abstract function appTitle();
63
64     /**
65      * Returns a key string which represents this micro-app in HTML
66      * ids etc, as when offering selection of what type of post to make.
67      * This is paired with the user-visible localizable $this->appTitle().
68      *
69      * All micro-app classes must override this method.
70      */
71     abstract function tag();
72
73     /**
74      * Given a parsed ActivityStreams activity, your plugin
75      * gets to figure out how to actually save it into a notice
76      * and any additional data structures you require.
77      *
78      * This will handle things received via AtomPub, OStatus
79      * (PuSH and Salmon transports), or ActivityStreams-based
80      * backup/restore of account data.
81      *
82      * You should be able to accept as input the output from your
83      * $this->activityObjectFromNotice(). Where applicable, try to
84      * use existing ActivityStreams structures and object types,
85      * and be liberal in accepting input from what might be other
86      * compatible apps.
87      *
88      * All micro-app classes must override this method.
89      *
90      * @fixme are there any standard options?
91      *
92      * @param Activity $activity
93      * @param Profile $actor
94      * @param array $options=array()
95      *
96      * @return Notice the resulting notice
97      */
98     abstract function saveNoticeFromActivity($activity, $actor, $options=array());
99
100     /**
101      * Given an existing Notice object, your plugin gets to
102      * figure out how to arrange it into an ActivityStreams
103      * object.
104      *
105      * This will be how your specialized notice gets output in
106      * Atom feeds and JSON-based ActivityStreams output, including
107      * account backup/restore and OStatus (PuSH and Salmon transports).
108      *
109      * You should be able to round-trip data from this format back
110      * through $this->saveNoticeFromActivity(). Where applicable, try
111      * to use existing ActivityStreams structures and object types,
112      * and consider interop with other compatible apps.
113      *
114      * All micro-app classes must override this method.
115      *
116      * @fixme this outputs an ActivityObject, not an Activity. Any compat issues?
117      *
118      * @param Notice $notice
119      *
120      * @return ActivityObject
121      */
122     abstract function activityObjectFromNotice($notice);
123
124     /**
125      * When building the primary notice form, we'll fetch also some
126      * alternate forms for specialized types -- that's you!
127      *
128      * Return a custom Widget or Form object for the given output
129      * object, and it'll be included in the HTML output. Beware that
130      * your form may be initially hidden.
131      *
132      * All micro-app classes must override this method.
133      *
134      * @param HTMLOutputter $out
135      * @return Widget
136      */
137     abstract function entryForm($out);
138
139     /**
140      * When a notice is deleted, you'll be called here for a chance
141      * to clean up any related resources.
142      *
143      * All micro-app classes must override this method.
144      *
145      * @param Notice $notice
146      */
147     abstract function deleteRelated($notice);
148
149     /**
150      *
151      */
152     public function newFormAction() {
153         // such as 'newbookmark' or 'newevent' route
154         return 'new'.$this->tag();
155     }
156
157     /**
158      * Called when generating Atom XML ActivityStreams output from an
159      * ActivityObject belonging to this plugin. Gives the plugin
160      * a chance to add custom output.
161      *
162      * Note that you can only add output of additional XML elements,
163      * not change existing stuff here.
164      *
165      * If output is already handled by the base Activity classes,
166      * you can leave this base implementation as a no-op.
167      *
168      * @param ActivityObject $obj
169      * @param XMLOutputter $out to add elements at end of object
170      */
171     function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
172     {
173         // default is a no-op
174     }
175
176     /**
177      * Called when generating JSON ActivityStreams output from an
178      * ActivityObject belonging to this plugin. Gives the plugin
179      * a chance to add custom output.
180      *
181      * Modify the array contents to your heart's content, and it'll
182      * all get serialized out as JSON.
183      *
184      * If output is already handled by the base Activity classes,
185      * you can leave this base implementation as a no-op.
186      *
187      * @param ActivityObject $obj
188      * @param array &$out JSON-targeted array which can be modified
189      */
190     public function activityObjectOutputJson(ActivityObject $obj, array &$out)
191     {
192         // default is a no-op
193     }
194
195     /**
196      * When a notice is deleted, delete the related objects
197      * by calling the overridable $this->deleteRelated().
198      *
199      * @param Notice $notice Notice being deleted
200      *
201      * @return boolean hook value
202      */
203     function onNoticeDeleteRelated($notice)
204     {
205         if (!$this->isMyNotice($notice)) {
206             return true;
207         }
208
209         $this->deleteRelated($notice);
210     }
211
212     /**
213      * Output the HTML for this kind of object in a list
214      *
215      * @param NoticeListItem $nli The list item being shown.
216      *
217      * @return boolean hook value
218      *
219      * @fixme WARNING WARNING WARNING this closes a 'div' that is implicitly opened in BookmarkPlugin's showNotice implementation
220      */
221     function onStartShowNoticeItem($nli)
222     {
223         if (!$this->isMyNotice($nli->notice)) {
224             return true;
225         }
226
227         $adapter = $this->adaptNoticeListItem($nli);
228
229         if (!empty($adapter)) {
230             $adapter->showNotice();
231             $adapter->showNoticeAttachments();
232             $adapter->showNoticeInfo();
233             $adapter->showNoticeOptions();
234         } else {
235             $this->oldShowNotice($nli);
236         }
237
238         return false;
239     }
240
241     /**
242      * Given a notice list item, returns an adapter specific
243      * to this plugin.
244      *
245      * @param NoticeListItem $nli item to adapt
246      *
247      * @return NoticeListItemAdapter adapter or null
248      */
249     function adaptNoticeListItem($nli)
250     {
251       return null;
252     }
253
254     function oldShowNotice($nli)
255     {
256         $out = $nli->out;
257         $notice = $nli->notice;
258
259         try {
260             $this->showNotice($notice, $out);
261         } catch (Exception $e) {
262             common_log(LOG_ERR, $e->getMessage());
263             // try to fall back
264             $out->elementStart('div');
265             $nli->showAuthor();
266             $nli->showContent();
267         }
268
269         $nli->showNoticeLink();
270         $nli->showNoticeSource();
271         $nli->showNoticeLocation();
272         $nli->showContext();
273         $nli->showRepeat();
274
275         $out->elementEnd('div');
276
277         $nli->showNoticeOptions();
278     }
279
280     /**
281      * Render a notice as one of our objects
282      *
283      * @param Notice         $notice  Notice to render
284      * @param ActivityObject &$object Empty object to fill
285      *
286      * @return boolean hook value
287      */
288     function onStartActivityObjectFromNotice($notice, &$object)
289     {
290         if (!$this->isMyNotice($notice)) {
291             return true;
292         }
293
294         $object = $this->activityObjectFromNotice($notice);
295         return false;
296     }
297
298     /**
299      * Handle a posted object from PuSH
300      *
301      * @param Activity        $activity activity to handle
302      * @param Ostatus_profile $oprofile Profile for the feed
303      *
304      * @return boolean hook value
305      */
306     function onStartHandleFeedEntryWithProfile($activity, $oprofile, &$notice)
307     {
308         if (!$this->isMyActivity($activity)) {
309             return true;
310         }
311
312         $actor = $oprofile->checkAuthorship($activity);
313
314         if (!$actor instanceof Ostatus_profile) {
315             // TRANS: Client exception thrown when no author for an activity was found.
316             throw new ClientException(_('Cannot get author for activity.'));
317         }
318
319         $object = $activity->objects[0];
320
321         $options = array('uri' => $object->id,
322                          'url' => $object->link,
323                          'is_local' => Notice::REMOTE,
324                          'source' => 'ostatus');
325
326         // $actor is an ostatus_profile
327         $notice = $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
328
329         return false;
330     }
331
332     /**
333      * Handle a posted object from Salmon
334      *
335      * @param Activity $activity activity to handle
336      * @param mixed    $target   user or group targeted
337      *
338      * @return boolean hook value
339      */
340
341     function onStartHandleSalmonTarget($activity, $target)
342     {
343         if (!$this->isMyActivity($activity)) {
344             return true;
345         }
346
347         $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
348
349         if ($target instanceof User_group) {
350             $uri = $target->getUri();
351             if (!array_key_exists($uri, $activity->context->attention)) {
352                 // @todo FIXME: please document (i18n).
353                 // TRANS: Client exception thrown when ...
354                 throw new ClientException(_('Object not posted to this group.'));
355             }
356         } else if ($target instanceof User) {
357             $uri      = $target->uri;
358             $original = null;
359             if (!empty($activity->context->replyToID)) {
360                 $original = Notice::getKV('uri', $activity->context->replyToID);
361             }
362             if (!array_key_exists($uri, $activity->context->attention) &&
363                 (empty($original) ||
364                  $original->profile_id != $target->id)) {
365                 // @todo FIXME: Please document (i18n).
366                 // TRANS: Client exception when ...
367                 throw new ClientException(_('Object not posted to this user.'));
368             }
369         } else {
370             // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled.
371             throw new ServerException(_('Do not know how to handle this kind of target.'));
372         }
373
374         $actor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
375
376         $object = $activity->objects[0];
377
378         $options = array('uri' => $object->id,
379                          'url' => $object->link,
380                          'is_local' => Notice::REMOTE,
381                          'source' => 'ostatus');
382
383         // $actor is an ostatus_profile
384         $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
385
386         return false;
387     }
388
389     /**
390      * Handle object posted via AtomPub
391      *
392      * @param Activity &$activity Activity that was posted
393      * @param User     $user      User that posted it
394      * @param Notice   &$notice   Resulting notice
395      *
396      * @return boolean hook value
397      */
398     function onStartAtomPubNewActivity(&$activity, $user, &$notice)
399     {
400         if (!$this->isMyActivity($activity)) {
401             return true;
402         }
403
404         $options = array('source' => 'atompub');
405
406         // $user->getProfile() is a Profile
407         $notice = $this->saveNoticeFromActivity($activity,
408                                                 $user->getProfile(),
409                                                 $options);
410
411         return false;
412     }
413
414     /**
415      * Handle object imported from a backup file
416      *
417      * @param User           $user     User to import for
418      * @param ActivityObject $author   Original author per import file
419      * @param Activity       $activity Activity to import
420      * @param boolean        $trusted  Is this a trusted user?
421      * @param boolean        &$done    Is this done (success or unrecoverable error)
422      *
423      * @return boolean hook value
424      */
425     function onStartImportActivity($user, $author, $activity, $trusted, &$done)
426     {
427         if (!$this->isMyActivity($activity)) {
428             return true;
429         }
430
431         $obj = $activity->objects[0];
432
433         $options = array('uri' => $object->id,
434                          'url' => $object->link,
435                          'source' => 'restore');
436
437         // $user->getProfile() is a Profile
438         $saved = $this->saveNoticeFromActivity($activity,
439                                                $user->getProfile(),
440                                                $options);
441
442         if (!empty($saved)) {
443             $done = true;
444         }
445
446         return false;
447     }
448
449     /**
450      * Event handler gives the plugin a chance to add custom
451      * Atom XML ActivityStreams output from a previously filled-out
452      * ActivityObject.
453      *
454      * The atomOutput method is called if it's one of
455      * our matching types.
456      *
457      * @param ActivityObject $obj
458      * @param XMLOutputter $out to add elements at end of object
459      * @return boolean hook return value
460      */
461     function onEndActivityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
462     {
463         if (in_array($obj->type, $this->types())) {
464             $this->activityObjectOutputAtom($obj, $out);
465         }
466         return true;
467     }
468
469     /**
470      * Event handler gives the plugin a chance to add custom
471      * JSON ActivityStreams output from a previously filled-out
472      * ActivityObject.
473      *
474      * The activityObjectOutputJson method is called if it's one of
475      * our matching types.
476      *
477      * @param ActivityObject $obj
478      * @param array &$out JSON-targeted array which can be modified
479      * @return boolean hook return value
480      */
481     function onEndActivityObjectOutputJson(ActivityObject $obj, array &$out)
482     {
483         if (in_array($obj->type, $this->types())) {
484             $this->activityObjectOutputJson($obj, $out);
485         }
486         return true;
487     }
488
489     function onStartShowEntryForms(&$tabs)
490     {
491         $tabs[$this->tag()] = array('title' => $this->appTitle(),
492                                     'href'  => common_local_url($this->newFormAction()),
493                                    );
494         return true;
495     }
496
497     function onStartMakeEntryForm($tag, $out, &$form)
498     {
499         if ($tag == $this->tag()) {
500             $form = $this->entryForm($out);
501             return false;
502         }
503
504         return true;
505     }
506
507     function showNotice($notice, $out)
508     {
509         // TRANS: Server exception thrown when a micro app plugin developer has not done his job too well.
510         throw new ServerException(_('You must implement either adaptNoticeListItem() or showNotice().'));
511     }
512 }