3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2011, StatusNet, Inc.
6 * Superclass for microapp plugin
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.
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.
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/>.
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/
31 if (!defined('STATUSNET')) {
32 // This check helps protect against security problems;
33 // your code file can't be executed directly from the web.
38 * Superclass for microapp plugins
40 * This class lets you define micro-applications with different kinds of activities.
42 * The applications work more-or-less like other
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/
51 abstract class MicroAppPlugin extends ActivityHandlerPlugin
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().
58 * All micro-app classes must override this method.
62 abstract function appTitle();
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().
69 * All micro-app classes must override this method.
71 abstract function tag();
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.
78 * This will handle things received via AtomPub, OStatus
79 * (PuSH and Salmon transports), or ActivityStreams-based
80 * backup/restore of account data.
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
88 * All micro-app classes must override this method.
90 * @fixme are there any standard options?
92 * @param Activity $activity
93 * @param Profile $actor
94 * @param array $options=array()
96 * @return Notice the resulting notice
98 abstract function saveNoticeFromActivity($activity, $actor, $options=array());
101 * Given an existing Notice object, your plugin gets to
102 * figure out how to arrange it into an ActivityStreams
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).
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.
114 * All micro-app classes must override this method.
116 * @fixme this outputs an ActivityObject, not an Activity. Any compat issues?
118 * @param Notice $notice
120 * @return ActivityObject
122 abstract function activityObjectFromNotice($notice);
125 * When building the primary notice form, we'll fetch also some
126 * alternate forms for specialized types -- that's you!
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.
132 * All micro-app classes must override this method.
134 * @param HTMLOutputter $out
137 abstract function entryForm($out);
140 * When a notice is deleted, you'll be called here for a chance
141 * to clean up any related resources.
143 * All micro-app classes must override this method.
145 * @param Notice $notice
147 abstract function deleteRelated($notice);
152 public function newFormAction() {
153 // such as 'newbookmark' or 'newevent' route
154 return 'new'.$this->tag();
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.
162 * Note that you can only add output of additional XML elements,
163 * not change existing stuff here.
165 * If output is already handled by the base Activity classes,
166 * you can leave this base implementation as a no-op.
168 * @param ActivityObject $obj
169 * @param XMLOutputter $out to add elements at end of object
171 function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
173 // default is a no-op
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.
181 * Modify the array contents to your heart's content, and it'll
182 * all get serialized out as JSON.
184 * If output is already handled by the base Activity classes,
185 * you can leave this base implementation as a no-op.
187 * @param ActivityObject $obj
188 * @param array &$out JSON-targeted array which can be modified
190 public function activityObjectOutputJson(ActivityObject $obj, array &$out)
192 // default is a no-op
196 * When a notice is deleted, delete the related objects
197 * by calling the overridable $this->deleteRelated().
199 * @param Notice $notice Notice being deleted
201 * @return boolean hook value
203 function onNoticeDeleteRelated($notice)
205 if (!$this->isMyNotice($notice)) {
209 $this->deleteRelated($notice);
213 * Output the HTML for this kind of object in a list
215 * @param NoticeListItem $nli The list item being shown.
217 * @return boolean hook value
219 * @fixme WARNING WARNING WARNING this closes a 'div' that is implicitly opened in BookmarkPlugin's showNotice implementation
221 function onStartShowNoticeItem($nli)
223 if (!$this->isMyNotice($nli->notice)) {
227 $adapter = $this->adaptNoticeListItem($nli);
229 if (!empty($adapter)) {
230 $adapter->showNotice();
231 $adapter->showNoticeAttachments();
232 $adapter->showNoticeInfo();
233 $adapter->showNoticeOptions();
235 $this->oldShowNotice($nli);
242 * Given a notice list item, returns an adapter specific
245 * @param NoticeListItem $nli item to adapt
247 * @return NoticeListItemAdapter adapter or null
249 function adaptNoticeListItem($nli)
254 function oldShowNotice($nli)
257 $notice = $nli->notice;
260 $this->showNotice($notice, $out);
261 } catch (Exception $e) {
262 common_log(LOG_ERR, $e->getMessage());
264 $out->elementStart('div');
269 $nli->showNoticeLink();
270 $nli->showNoticeSource();
271 $nli->showNoticeLocation();
275 $out->elementEnd('div');
277 $nli->showNoticeOptions();
281 * Render a notice as one of our objects
283 * @param Notice $notice Notice to render
284 * @param ActivityObject &$object Empty object to fill
286 * @return boolean hook value
288 function onStartActivityObjectFromNotice($notice, &$object)
290 if (!$this->isMyNotice($notice)) {
294 $object = $this->activityObjectFromNotice($notice);
299 * Handle a posted object from PuSH
301 * @param Activity $activity activity to handle
302 * @param Ostatus_profile $oprofile Profile for the feed
304 * @return boolean hook value
306 function onStartHandleFeedEntryWithProfile($activity, $oprofile, &$notice)
308 if (!$this->isMyActivity($activity)) {
312 $actor = $oprofile->checkAuthorship($activity);
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.'));
319 $object = $activity->objects[0];
321 $options = array('uri' => $object->id,
322 'url' => $object->link,
323 'is_local' => Notice::REMOTE,
324 'source' => 'ostatus');
326 // $actor is an ostatus_profile
327 $notice = $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
333 * Handle a posted object from Salmon
335 * @param Activity $activity activity to handle
336 * @param mixed $target user or group targeted
338 * @return boolean hook value
341 function onStartHandleSalmonTarget($activity, $target)
343 if (!$this->isMyActivity($activity)) {
347 $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
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.'));
356 } else if ($target instanceof User) {
359 if (!empty($activity->context->replyToID)) {
360 $original = Notice::getKV('uri', $activity->context->replyToID);
362 if (!array_key_exists($uri, $activity->context->attention) &&
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.'));
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.'));
374 $actor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
376 $object = $activity->objects[0];
378 $options = array('uri' => $object->id,
379 'url' => $object->link,
380 'is_local' => Notice::REMOTE,
381 'source' => 'ostatus');
383 // $actor is an ostatus_profile
384 $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options);
390 * Handle object posted via AtomPub
392 * @param Activity &$activity Activity that was posted
393 * @param User $user User that posted it
394 * @param Notice &$notice Resulting notice
396 * @return boolean hook value
398 function onStartAtomPubNewActivity(&$activity, $user, &$notice)
400 if (!$this->isMyActivity($activity)) {
404 $options = array('source' => 'atompub');
406 // $user->getProfile() is a Profile
407 $notice = $this->saveNoticeFromActivity($activity,
415 * Handle object imported from a backup file
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)
423 * @return boolean hook value
425 function onStartImportActivity($user, $author, $activity, $trusted, &$done)
427 if (!$this->isMyActivity($activity)) {
431 $obj = $activity->objects[0];
433 $options = array('uri' => $object->id,
434 'url' => $object->link,
435 'source' => 'restore');
437 // $user->getProfile() is a Profile
438 $saved = $this->saveNoticeFromActivity($activity,
442 if (!empty($saved)) {
450 * Event handler gives the plugin a chance to add custom
451 * Atom XML ActivityStreams output from a previously filled-out
454 * The atomOutput method is called if it's one of
455 * our matching types.
457 * @param ActivityObject $obj
458 * @param XMLOutputter $out to add elements at end of object
459 * @return boolean hook return value
461 function onEndActivityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
463 if (in_array($obj->type, $this->types())) {
464 $this->activityObjectOutputAtom($obj, $out);
470 * Event handler gives the plugin a chance to add custom
471 * JSON ActivityStreams output from a previously filled-out
474 * The activityObjectOutputJson method is called if it's one of
475 * our matching types.
477 * @param ActivityObject $obj
478 * @param array &$out JSON-targeted array which can be modified
479 * @return boolean hook return value
481 function onEndActivityObjectOutputJson(ActivityObject $obj, array &$out)
483 if (in_array($obj->type, $this->types())) {
484 $this->activityObjectOutputJson($obj, $out);
489 function onStartShowEntryForms(&$tabs)
491 $tabs[$this->tag()] = array('title' => $this->appTitle(),
492 'href' => common_local_url($this->newFormAction()),
497 function onStartMakeEntryForm($tag, $out, &$form)
499 if ($tag == $this->tag()) {
500 $form = $this->entryForm($out);
507 function showNotice($notice, $out)
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().'));