Example #1
0
 /**
  * Insert notifications for replies, mentions and repeats
  *
  * @return boolean hook flag
  */
 function onStartNoticeDistribute($notice)
 {
     assert($notice->id > 0);
     // since we removed tests below
     // repeats
     if ($notice->isRepeat()) {
         $repeated_notice = Notice::getKV('id', $notice->repeat_of);
         if ($repeated_notice instanceof Notice) {
             $this->insertNotification($repeated_notice->profile_id, $notice->profile_id, 'repeat', $repeated_notice->id);
             // mark reply/mention-notifications as read if we're repeating to a notice we're notified about
             self::markNotificationAsSeen($repeated_notice->id, $notice->profile_id, 'mention');
             self::markNotificationAsSeen($repeated_notice->id, $notice->profile_id, 'reply');
             // (no other notifications repeats)
             return true;
         }
     }
     // don't add notifications for activity/non-post-verb notices
     if ($notice->source == 'activity' || !ActivityUtils::compareVerbs($notice->verb, array(ActivityVerb::POST))) {
         return true;
     }
     // mark reply/mention-notifications as read if we're replying to a notice we're notified about
     if ($notice->reply_to) {
         self::markNotificationAsSeen($notice->reply_to, $notice->profile_id, 'mention');
         self::markNotificationAsSeen($notice->reply_to, $notice->profile_id, 'reply');
     }
     // replies and mentions
     $reply_notification_to = false;
     // check for reply to insert in notifications
     if ($notice->reply_to) {
         try {
             $replyauthor = $notice->getParent()->getProfile();
             $reply_notification_to = $replyauthor->id;
             $this->insertNotification($replyauthor->id, $notice->profile_id, 'reply', $notice->id);
             //} catch (NoParentNoticeException $e) {	// TODO: catch this when everyone runs latest GNU social!
             // This is not a reply to something (has no parent)
         } catch (NoResultException $e) {
             // Parent author's profile not found! Complain louder?
             common_log(LOG_ERR, "Parent notice's author not found: " . $e->getMessage());
         }
     }
     // check for mentions to insert in notifications
     $mentions = $notice->getReplies();
     $sender = Profile::getKV($notice->profile_id);
     $all_mentioned_user_ids = array();
     foreach ($mentions as $mentioned) {
         // no duplicate mentions
         if (in_array($mentioned, $all_mentioned_user_ids)) {
             continue;
         }
         $all_mentioned_user_ids[] = $mentioned;
         // only notify if mentioned user is not already notified for reply
         if ($reply_notification_to != $mentioned) {
             $this->insertNotification($mentioned, $notice->profile_id, 'mention', $notice->id);
         }
     }
     return true;
 }
 protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped)
 {
     switch (true) {
         case ActivityUtils::compareVerbs($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)):
             Fave::addNew($scoped, $target);
             break;
         case ActivityUtils::compareVerbs($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)):
             Fave::removeEntry($scoped, $target);
             break;
         default:
             throw new ServerException('ActivityVerb POST not handled by plugin that was supposed to do it.');
     }
     return false;
 }
Example #3
0
 static function saveActivity(Activity $act, Profile $actor, array $options = array())
 {
     // First check if we're going to let this Activity through from the specific actor
     if (!$actor->hasRight(Right::NEWNOTICE)) {
         common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $actor->getNickname());
         // TRANS: Client exception thrown when a user tries to post while being banned.
         throw new ClientException(_m('You are banned from posting notices on this site.'), 403);
     }
     if (common_config('throttle', 'enabled') && !self::checkEditThrottle($actor->id)) {
         common_log(LOG_WARNING, 'Excessive posting by profile #' . $actor->id . '; throttled.');
         // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame.
         throw new ClientException(_m('Too many notices too fast; take a breather ' . 'and post again in a few minutes.'));
     }
     // Get ActivityObject properties
     $actobj = null;
     if (!empty($act->id)) {
         // implied object
         $options['uri'] = $act->id;
         $options['url'] = $act->link;
     } else {
         $actobj = count($act->objects) == 1 ? $act->objects[0] : null;
         if (!is_null($actobj) && !empty($actobj->id)) {
             $options['uri'] = $actobj->id;
             if ($actobj->link) {
                 $options['url'] = $actobj->link;
             } elseif (preg_match('!^https?://!', $actobj->id)) {
                 $options['url'] = $actobj->id;
             }
         }
     }
     $defaults = array('groups' => array(), 'is_local' => $actor->isLocal() ? self::LOCAL_PUBLIC : self::REMOTE, 'mentions' => array(), 'reply_to' => null, 'repeat_of' => null, 'scope' => null, 'source' => 'unknown', 'tags' => array(), 'uri' => null, 'url' => null, 'urls' => array(), 'distribute' => true);
     // options will have default values when nothing has been supplied
     $options = array_merge($defaults, $options);
     foreach (array_keys($defaults) as $key) {
         // Only convert the keynames we specify ourselves from 'defaults' array into variables
         ${$key} = $options[$key];
     }
     extract($options, EXTR_SKIP);
     // dupe check
     $stored = new Notice();
     if (!empty($uri) && !ActivityUtils::compareVerbs($act->verb, array(ActivityVerb::DELETE))) {
         $stored->uri = $uri;
         if ($stored->find()) {
             common_debug('cannot create duplicate Notice URI: ' . $stored->uri);
             // I _assume_ saving a Notice with a colliding URI means we're really trying to
             // save the same notice again...
             throw new AlreadyFulfilledException('Notice URI already exists');
         }
     }
     $autosource = common_config('public', 'autosource');
     // Sandboxed are non-false, but not 1, either
     if (!$actor->hasRight(Right::PUBLICNOTICE) || $source && $autosource && in_array($source, $autosource)) {
         // FIXME: ...what about remote nonpublic? Hmmm. That is, if we sandbox remote profiles...
         $stored->is_local = Notice::LOCAL_NONPUBLIC;
     } else {
         $stored->is_local = intval($is_local);
     }
     if (!$stored->isLocal()) {
         // Only do these checks for non-local notices. Local notices will generate these values later.
         if (!common_valid_http_url($url)) {
             common_debug('Bad notice URL: [' . $url . '], URI: [' . $uri . ']. Cannot link back to original! This is normal for shared notices etc.');
         }
         if (empty($uri)) {
             throw new ServerException('No URI for remote notice. Cannot accept that.');
         }
     }
     $stored->profile_id = $actor->id;
     $stored->source = $source;
     $stored->uri = $uri;
     $stored->url = $url;
     $stored->verb = $act->verb;
     // Notice content. We trust local users to provide HTML we like, but of course not remote users.
     // FIXME: What about local users importing feeds? Mirror functions must filter out bad HTML first...
     $content = $act->content ?: $act->summary;
     if (is_null($content) && !is_null($actobj)) {
         $content = $actobj->content ?: $actobj->summary;
     }
     $stored->rendered = $actor->isLocal() ? $content : common_purify($content);
     // yeah, just don't use getRendered() here since it's not inserted yet ;)
     $stored->content = common_strip_html($stored->rendered);
     // Maybe a missing act-time should be fatal if the actor is not local?
     if (!empty($act->time)) {
         $stored->created = common_sql_date($act->time);
     } else {
         $stored->created = common_sql_now();
     }
     $reply = null;
     if ($act->context instanceof ActivityContext && !empty($act->context->replyToID)) {
         $reply = self::getKV('uri', $act->context->replyToID);
     }
     if (!$reply instanceof Notice && $act->target instanceof ActivityObject) {
         $reply = self::getKV('uri', $act->target->id);
     }
     if ($reply instanceof Notice) {
         if (!$reply->inScope($actor)) {
             // TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
             // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
             throw new ClientException(sprintf(_m('%1$s has no right to reply to notice %2$d.'), $actor->getNickname(), $reply->id), 403);
         }
         $stored->reply_to = $reply->id;
         $stored->conversation = $reply->conversation;
         // If the original is private to a group, and notice has no group specified,
         // make it to the same group(s)
         if (empty($groups) && $reply->scope & Notice::GROUP_SCOPE) {
             $replyGroups = $reply->getGroups();
             foreach ($replyGroups as $group) {
                 if ($actor->isMember($group)) {
                     $groups[] = $group->id;
                 }
             }
         }
         if (is_null($scope)) {
             $scope = $reply->scope;
         }
     } else {
         // If we don't know the reply, we might know the conversation!
         // This will happen if a known remote user replies to an
         // unknown remote user - within a known conversation.
         if (empty($stored->conversation) and !empty($act->context->conversation)) {
             $conv = Conversation::getKV('uri', $act->context->conversation);
             if ($conv instanceof Conversation) {
                 common_debug('Conversation stitched together from (probably) a reply activity to unknown remote user. Activity creation time (' . $stored->created . ') should maybe be compared to conversation creation time (' . $conv->created . ').');
             } else {
                 // Conversation entry with specified URI was not found, so we must create it.
                 common_debug('Conversation URI not found, so we will create it with the URI given in the context of the activity: ' . $act->context->conversation);
                 // The insert in Conversation::create throws exception on failure
                 $conv = Conversation::create($act->context->conversation, $stored->created);
             }
             $stored->conversation = $conv->getID();
             unset($conv);
         }
     }
     // If it's not part of a conversation, it's the beginning of a new conversation.
     if (empty($stored->conversation)) {
         $conv = Conversation::create();
         $stored->conversation = $conv->getID();
         unset($conv);
     }
     $notloc = null;
     if ($act->context instanceof ActivityContext) {
         if ($act->context->location instanceof Location) {
             $notloc = Notice_location::fromLocation($act->context->location);
         }
     } else {
         $act->context = new ActivityContext();
     }
     $stored->scope = self::figureOutScope($actor, $groups, $scope);
     foreach ($act->categories as $cat) {
         if ($cat->term) {
             $term = common_canonical_tag($cat->term);
             if (!empty($term)) {
                 $tags[] = $term;
             }
         }
     }
     foreach ($act->enclosures as $href) {
         // @todo FIXME: Save these locally or....?
         $urls[] = $href;
     }
     if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) {
         if (empty($act->objects[0]->type)) {
             // Default type for the post verb is 'note', but we know it's
             // a 'comment' if it is in reply to something.
             $stored->object_type = empty($stored->reply_to) ? ActivityObject::NOTE : ActivityObject::COMMENT;
         } else {
             //TODO: Is it safe to always return a relative URI? The
             // JSON version of ActivityStreams always use it, so we
             // should definitely be able to handle it...
             $stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type, true);
         }
     }
     if (Event::handle('StartNoticeSave', array(&$stored))) {
         // XXX: some of these functions write to the DB
         try {
             $result = $stored->insert();
             // throws exception on error
             if ($notloc instanceof Notice_location) {
                 $notloc->notice_id = $stored->getID();
                 $notloc->insert();
             }
             $orig = clone $stored;
             // for updating later in this try clause
             $object = null;
             Event::handle('StoreActivityObject', array($act, $stored, $options, &$object));
             if (empty($object)) {
                 throw new ServerException('Unsuccessful call to StoreActivityObject ' . $stored->getUri() . ': ' . $act->asString());
             }
             // If something changed in the Notice during StoreActivityObject
             $stored->update($orig);
         } catch (Exception $e) {
             if (empty($stored->id)) {
                 common_debug('Failed to save stored object entry in database (' . $e->getMessage() . ')');
             } else {
                 common_debug('Failed to store activity object in database (' . $e->getMessage() . '), deleting notice id ' . $stored->id);
                 $stored->delete();
             }
             throw $e;
         }
     }
     if (!$stored instanceof Notice) {
         throw new ServerException('StartNoticeSave did not give back a Notice');
     }
     // Only save 'attention' and metadata stuff (URLs, tags...) stuff if
     // the activityverb is a POST (since stuff like repeat, favorite etc.
     // reasonably handle notifications themselves.
     if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) {
         if (!empty($tags)) {
             $stored->saveKnownTags($tags);
         } else {
             $stored->saveTags();
         }
         // Note: groups may save tags, so must be run after tags are saved
         // to avoid errors on duplicates.
         $stored->saveAttentions($act->context->attention);
         if (!empty($urls)) {
             $stored->saveKnownUrls($urls);
         } else {
             $stored->saveUrls();
         }
     }
     if ($distribute) {
         // Prepare inbox delivery, may be queued to background.
         $stored->distribute();
     }
     return $stored;
 }
 /**
  * This is run before ->insert, so our task in this function is just to
  * delete if it is the delete verb.
  */
 public function onStartNoticeSave(Notice $stored)
 {
     // DELETE is a bit special, we have to remove the existing entry and then
     // add a new one with the same URI in order to trigger the distribution.
     // (that's why we don't use $this->isMyNotice(...))
     if (!ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::DELETE))) {
         return true;
     }
     try {
         $target = Notice::getByUri($stored->uri);
     } catch (NoResultException $e) {
         throw new AlreadyFulfilledException('Notice URI not found, so we have nothing to delete.');
     }
     $actor = $stored->getProfile();
     $owner = $target->getProfile();
     if ($owner->hasRole(Profile_role::DELETED)) {
         // Don't bother with replacing notices if its author is being deleted.
         // The later "StoreActivityObject" will pick this up and execute
         // the deletion then.
         // (the "delete verb notice" is too new to ever pass through Notice::saveNew
         // which otherwise wouldn't execute the StoreActivityObject event)
         return true;
     }
     // Since the user deleting may not be the same as the notice's owner,
     // double-check this and also set the "re-stored" notice profile_id.
     if (!$actor->sameAs($owner) && !$actor->hasRight(Right::DELETEOTHERSNOTICE)) {
         throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.'));
     }
     // We copy the identifying fields and replace the sensitive ones.
     //$stored->id = $target->id;    // We can't copy this since DB_DataObject won't inject it anyway
     $props = array('uri', 'profile_id', 'conversation', 'reply_to', 'created', 'repeat_of', 'object_type', 'is_local', 'scope');
     foreach ($props as $prop) {
         $stored->{$prop} = $target->{$prop};
     }
     // Let's see if this has been deleted already.
     try {
         $deleted = Deleted_notice::getByKeys(['uri' => $stored->getUri()]);
         return $deleted;
     } catch (NoResultException $e) {
         $deleted = new Deleted_notice();
         $deleted->id = $target->getID();
         $deleted->profile_id = $actor->getID();
         $deleted->uri = $stored->getUri();
         $deleted->act_created = $stored->created;
         $deleted->created = common_sql_now();
         // throws exception on error
         $result = $deleted->insert();
     }
     // Now we delete the original notice, leaving the id and uri free.
     $target->delete();
     return true;
 }
Example #5
0
 function _fromAtomEntry($entry, $feed)
 {
     $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
     if (!empty($pubEl)) {
         $this->time = strtotime($pubEl->textContent);
     } else {
         // XXX technically an error; being liberal. Good idea...?
         $updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
         if (!empty($updateEl)) {
             $this->time = strtotime($updateEl->textContent);
         } else {
             $this->time = null;
         }
     }
     $this->link = ActivityUtils::getPermalink($entry);
     $verbEl = $this->_child($entry, self::VERB);
     if (!empty($verbEl)) {
         $this->verb = trim($verbEl->textContent);
     } else {
         $this->verb = ActivityVerb::POST;
         // XXX: do other implied stuff here
     }
     // get immediate object children
     $objectEls = ActivityUtils::children($entry, self::OBJECT, self::SPEC);
     if (count($objectEls) > 0) {
         foreach ($objectEls as $objectEl) {
             // Special case for embedded activities
             $objectType = ActivityUtils::childContent($objectEl, self::OBJECTTYPE, self::SPEC);
             if (!empty($objectType) && $objectType == ActivityObject::ACTIVITY) {
                 $this->objects[] = new Activity($objectEl);
             } else {
                 $this->objects[] = new ActivityObject($objectEl);
             }
         }
     } else {
         // XXX: really?
         $this->objects[] = new ActivityObject($entry);
     }
     $actorEl = $this->_child($entry, self::ACTOR);
     if (!empty($actorEl)) {
         // Standalone <activity:actor> elements are a holdover from older
         // versions of ActivityStreams. Newer feeds should have this data
         // integrated straight into <atom:author>.
         $this->actor = new ActivityObject($actorEl);
         // Cliqset has bad actor IDs (just nickname of user). We
         // work around it by getting the author data and using its
         // id instead
         if (!preg_match('/^\\w+:/', $this->actor->id)) {
             $authorEl = ActivityUtils::child($entry, 'author');
             if (!empty($authorEl)) {
                 $authorObj = new ActivityObject($authorEl);
                 $this->actor->id = $authorObj->id;
             }
         }
     } else {
         if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
             // An <atom:author> in the entry overrides any author info on
             // the surrounding feed.
             $this->actor = new ActivityObject($authorEl);
         } else {
             if (!empty($feed) && ($subjectEl = $this->_child($feed, self::SUBJECT))) {
                 // Feed subject is used for things like groups.
                 // Should actually possibly not be interpreted as an actor...?
                 $this->actor = new ActivityObject($subjectEl);
             } else {
                 if (!empty($feed) && ($authorEl = $this->_child($feed, self::AUTHOR, self::ATOM))) {
                     // If there's no <atom:author> on the entry, it's safe to assume
                     // the containing feed's authorship info applies.
                     $this->actor = new ActivityObject($authorEl);
                 }
             }
         }
     }
     $contextEl = $this->_child($entry, self::CONTEXT);
     if (!empty($contextEl)) {
         $this->context = new ActivityContext($contextEl);
     } else {
         $this->context = new ActivityContext($entry);
     }
     $targetEl = $this->_child($entry, self::TARGET);
     if (!empty($targetEl)) {
         $this->target = new ActivityObject($targetEl);
     } elseif (ActivityUtils::compareVerbs($this->verb, array(ActivityVerb::FAVORITE))) {
         // StatusNet didn't send a 'target' for their Favorite atom entries
         $this->target = clone $this->objects[0];
     }
     $this->summary = ActivityUtils::childContent($entry, 'summary');
     $this->id = ActivityUtils::childContent($entry, 'id');
     $this->content = ActivityUtils::getContent($entry);
     $catEls = $entry->getElementsByTagNameNS(self::ATOM, 'category');
     if ($catEls) {
         for ($i = 0; $i < $catEls->length; $i++) {
             $catEl = $catEls->item($i);
             $this->categories[] = new AtomCategory($catEl);
         }
     }
     foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) {
         $this->enclosures[] = $link->getAttribute('href');
     }
     // From APP. Might be useful.
     $this->selfLink = ActivityUtils::getLink($entry, 'self', 'application/atom+xml');
     $this->editLink = ActivityUtils::getLink($entry, 'edit', 'application/atom+xml');
 }
 /**
  * Handle a posted object from Salmon
  *
  * @param Activity $activity activity to handle
  * @param mixed    $target   user or group targeted
  *
  * @return boolean hook value
  */
 function onStartHandleSalmonTarget(Activity $activity, $target)
 {
     if (!$this->isMyActivity($activity)) {
         return true;
     }
     $this->log(LOG_INFO, get_called_class() . " checking {$activity->id} as a valid Salmon slap.");
     if ($target instanceof User_group || $target->isGroup()) {
         $uri = $target->getUri();
         if (!array_key_exists($uri, $activity->context->attention)) {
             // @todo FIXME: please document (i18n).
             // TRANS: Client exception thrown when ...
             throw new ClientException(_('Object not posted to this group.'));
         }
     } elseif ($target instanceof Profile && $target->isLocal()) {
         $original = null;
         // FIXME: Shouldn't favorites show up with a 'target' activityobject?
         if (!ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) {
             // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note)
             if (!empty($activity->objects[0]->id)) {
                 $activity->context->replyToID = $activity->objects[0]->id;
             }
         }
         if (!empty($activity->context->replyToID)) {
             $original = Notice::getKV('uri', $activity->context->replyToID);
         }
         if ((!$original instanceof Notice || $original->profile_id != $target->id) && !array_key_exists($target->getUri(), $activity->context->attention)) {
             // @todo FIXME: Please document (i18n).
             // TRANS: Client exception when ...
             throw new ClientException(_('Object not posted to this user.'));
         }
     } else {
         // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled.
         throw new ServerException(_('Do not know how to handle this kind of target.'));
     }
     $oactor = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
     $actor = $oactor->localProfile();
     // FIXME: will this work in all cases? I made it work for Favorite...
     if (ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST))) {
         $object = $activity->objects[0];
     } else {
         $object = $activity;
     }
     $options = array('uri' => $object->id, 'url' => $object->link, 'is_local' => Notice::REMOTE, 'source' => 'ostatus');
     if (!isset($this->oldSaveNew)) {
         $notice = Notice::saveActivity($activity, $actor, $options);
     } else {
         $notice = $this->saveNoticeFromActivity($activity, $actor, $options);
     }
     return false;
 }
Example #7
0
 public static function extendActivity(Notice $stored, Activity $act, Profile $scoped = null)
 {
     $target = self::getTargetFromStored($stored);
     // The following logic was copied from StatusNet's Activity plugin
     if (ActivityUtils::compareVerbs($target->verb, array(ActivityVerb::POST))) {
         // "I like the thing you posted"
         $act->objects = $target->asActivity()->objects;
     } else {
         // "I like that you did whatever you did"
         $act->target = $target->asActivityObject();
         $act->objects = array(clone $act->target);
     }
     $act->context->replyToID = $target->getUri();
     $act->context->replyToUrl = $target->getUrl();
     $act->title = ActivityUtils::verbToTitle($act->verb);
 }