X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=classes%2FNotice.php;h=47f03099a0f009d74f51cdf0713e13914ac80f20;hb=d6b28c64830f632bb2f4b6f3c9369b9e56ad217a;hp=5fefa6c4ef5a958190d166bb5787ba46eaf6505e;hpb=c037f04d174f8359c77af03d323bca03032ef7fd;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 5fefa6c4ef..47f03099a0 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -65,10 +65,6 @@ class Notice extends Managed_DataObject public $is_local; // int(4) public $source; // varchar(32) public $conversation; // int(4) - public $lat; // decimal(10,7) - public $lon; // decimal(10,7) - public $location_id; // int(4) - public $location_ns; // int(4) public $repeat_of; // int(4) public $verb; // varchar(191) not 255 because utf8mb4 takes more space public $object_type; // varchar(191) not 255 because utf8mb4 takes more space @@ -93,12 +89,8 @@ class Notice extends Managed_DataObject 'is_local' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'notice was generated by a user'), 'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'), 'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'), - 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'), - 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), - 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), - 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), - 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => null), 'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), 'scope' => array('type' => 'int', 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'), @@ -166,45 +158,36 @@ class Notice extends Managed_DataObject $this->_profile[$this->profile_id] = $profile; } - function delete($useWhere=false) + public function deleteAs(Profile $actor, $delete_event=true) { - // For auditing purposes, save a record that the notice - // was deleted. - - // @fixme we have some cases where things get re-run and so the - // insert fails. - $deleted = Deleted_notice::getKV('id', $this->id); - - if (!$deleted instanceof Deleted_notice) { - $deleted = Deleted_notice::getKV('uri', $this->uri); - } - - if (!$deleted instanceof Deleted_notice) { - $deleted = new Deleted_notice(); - - $deleted->id = $this->id; - $deleted->profile_id = $this->profile_id; - $deleted->uri = $this->uri; - $deleted->created = $this->created; - $deleted->deleted = common_sql_now(); - - $deleted->insert(); + if (!$this->getProfile()->sameAs($actor) && !$actor->hasRight(Right::DELETEOTHERSNOTICE)) { + throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.')); } if (Event::handle('NoticeDeleteRelated', array($this))) { - // Clear related records - $this->clearReplies(); + $this->clearLocation(); $this->clearRepeats(); $this->clearTags(); $this->clearGroupInboxes(); $this->clearFiles(); $this->clearAttentions(); - // NOTE: we don't clear queue items } + $result = null; + if (!$delete_event || Event::handle('DeleteNoticeAsProfile', array($this, $actor, &$result))) { + // If $delete_event is true, we run the event. If the Event then + // returns false it is assumed everything was handled properly + // and the notice was deleted. + $result = $this->delete(); + } + return $result; + } + + public function delete($useWhere=false) + { $result = parent::delete($useWhere); $this->blowOnDelete(); @@ -274,6 +257,11 @@ class Notice extends Managed_DataObject return $this->content; } + public function getRendered() + { + return $this->rendered; + } + /* * Get the original representation URL of this notice. * @@ -297,10 +285,8 @@ class Notice extends Managed_DataObject } } - public function get_object_type($canonical=false) { - return $canonical - ? ActivityObject::canonicalType($this->object_type) - : $this->object_type; + public function getObjectType($canonical=false) { + return ActivityUtils::resolveUri($this->object_type, $canonical); } public static function getByUri($uri) @@ -332,7 +318,7 @@ class Notice extends Managed_DataObject * Record the given set of hash tags in the db for this notice. * Given tag strings will be normalized and checked for dupes. */ - function saveKnownTags($hashtags) + function saveKnownTags(array $hashtags) { //turn each into their canonical tag //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag @@ -416,7 +402,7 @@ class Notice extends Managed_DataObject * @return Notice * @throws ClientException */ - static function saveNew($profile_id, $content, $source, array $options=null) { + static function saveNew($profile_id, $content, $source, array $options=array()) { $defaults = array('uri' => null, 'url' => null, 'conversation' => null, // URI of conversation @@ -427,13 +413,16 @@ class Notice extends Managed_DataObject 'object_type' => null, 'verb' => null); - if (!empty($options) && is_array($options)) { + /* + * Above type-hint is already array, so simply count it, this saves + * "some" CPU cycles. + */ + if (count($options) > 0) { $options = array_merge($defaults, $options); - extract($options); - } else { - extract($defaults); } + extract($options); + if (!isset($is_local)) { $is_local = Notice::LOCAL_PUBLIC; } @@ -523,24 +512,18 @@ class Notice extends Managed_DataObject // Handle repeat case - if (isset($repeat_of)) { + if (!empty($options['repeat_of'])) { // Check for a private one - $repeat = Notice::getKV('id', $repeat_of); - - if (!($repeat instanceof Notice)) { - // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. - throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); - } + $repeat = Notice::getByID($options['repeat_of']); - if ($profile->id == $repeat->profile_id) { + if ($profile->sameAs($repeat->getProfile())) { // TRANS: Client error displayed when trying to repeat an own notice. throw new ClientException(_('You cannot repeat your own notice.')); } - if ($repeat->scope != Notice::SITE_SCOPE && - $repeat->scope != Notice::PUBLIC_SCOPE) { + if ($repeat->isPrivateScope()) { // TRANS: Client error displayed when trying to repeat a non-public notice. throw new ClientException(_('Cannot repeat a private notice.'), 403); } @@ -610,12 +593,13 @@ class Notice extends Managed_DataObject if (empty($notice->conversation) and !empty($options['conversation'])) { $conv = Conversation::getKV('uri', $options['conversation']); if ($conv instanceof Conversation) { - common_debug('Conversation stitched together from (probably) reply to unknown remote user. Activity creation time ('.$notice->created.') should maybe be compared to conversation creation time ('.$conv->created.').'); + common_debug('Conversation stitched together from (probably) a reply to unknown remote user. Activity creation time ('.$notice->created.') should maybe be compared to conversation creation time ('.$conv->created.').'); $notice->conversation = $conv->id; } else { // Conversation URI was not found, so we must create it. But we can't create it // until we have a Notice ID because of the database layout... - $notice->tmp_conv_uri = $options['conversation']; + // $options['conversation'] will be used later after the $notice->insert(); + common_debug('Conversation URI not found, so we have to create it after inserting this Notice: '.$options['conversation']); } } else { // If we're not using the attached conversation URI let's remove it @@ -625,20 +609,23 @@ class Notice extends Managed_DataObject } } + $notloc = new Notice_location(); if (!empty($lat) && !empty($lon)) { - $notice->lat = $lat; - $notice->lon = $lon; + $notloc->lat = $lat; + $notloc->lon = $lon; } if (!empty($location_ns) && !empty($location_id)) { - $notice->location_id = $location_id; - $notice->location_ns = $location_ns; + $notloc->location_id = $location_id; + $notloc->location_ns = $location_ns; } if (!empty($rendered)) { $notice->rendered = $rendered; } else { - $notice->rendered = common_render_content($final, $notice); + $notice->rendered = common_render_content($final, + $notice->getProfile(), + $notice->hasParent() ? $notice->getParent() : null); } if (empty($verb)) { @@ -671,12 +658,19 @@ class Notice extends Managed_DataObject // XXX: some of these functions write to the DB try { - $notice->insert(); // throws exception on failure + $notice->insert(); // throws exception on failure, if successful we have an ->id + + if (($notloc->lat && $notloc->lon) || ($notloc->location_id && $notloc->location_ns)) { + $notloc->notice_id = $notice->getID(); + $notloc->insert(); // store the notice location if it had any information + } + // If it's not part of a conversation, it's // the beginning of a new conversation. if (empty($notice->conversation)) { $orig = clone($notice); // $act->context->conversation will be null if it was not provided + $conv = Conversation::create($notice, $options['conversation']); $notice->conversation = $conv->id; $notice->update($orig); @@ -692,33 +686,33 @@ class Notice extends Managed_DataObject } } - // Clear the cache for subscribed users, so they'll update at next request - // XXX: someone clever could prepend instead of clearing the cache - - // Save per-notice metadata... - - if (isset($replies)) { - $notice->saveKnownReplies($replies); - } else { - $notice->saveReplies(); - } + // 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($notice->verb, array(ActivityVerb::POST))) { + if (isset($replies)) { + $notice->saveKnownReplies($replies); + } else { + $notice->saveReplies(); + } - if (isset($tags)) { - $notice->saveKnownTags($tags); - } else { - $notice->saveTags(); - } + if (isset($tags)) { + $notice->saveKnownTags($tags); + } else { + $notice->saveTags(); + } - // Note: groups may save tags, so must be run after tags are saved - // to avoid errors on duplicates. - // Note: groups should always be set. + // Note: groups may save tags, so must be run after tags are saved + // to avoid errors on duplicates. + // Note: groups should always be set. - $notice->saveKnownGroups($groups); + $notice->saveKnownGroups($groups); - if (isset($urls)) { - $notice->saveKnownUrls($urls); - } else { - $notice->saveUrls(); + if (isset($urls)) { + $notice->saveKnownUrls($urls); + } else { + $notice->saveUrls(); + } } if ($distribute) { @@ -746,6 +740,7 @@ class Notice extends Managed_DataObject } // Get ActivityObject properties + $actobj = null; if (!empty($act->id)) { // implied object $options['uri'] = $act->id; @@ -764,7 +759,7 @@ class Notice extends Managed_DataObject $defaults = array( 'groups' => array(), - 'is_local' => self::LOCAL_PUBLIC, + 'is_local' => $actor->isLocal() ? self::LOCAL_PUBLIC : self::REMOTE, 'mentions' => array(), 'reply_to' => null, 'repeat_of' => null, @@ -784,12 +779,36 @@ class Notice extends Managed_DataObject } extract($options, EXTR_SKIP); + // dupe check $stored = new Notice(); - if (!empty($uri)) { + 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); - throw new Exception('Notice URI already exists'); + // 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.'); } } @@ -799,18 +818,24 @@ class Notice extends Managed_DataObject $stored->url = $url; $stored->verb = $act->verb; - // Use the local user's shortening preferences, if applicable. - $stored->rendered = $actor->isLocal() - ? $actor->shortenLinks($act->content) - : $act->content; + // 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); $stored->content = common_strip_html($stored->rendered); - $autosource = common_config('public', 'autosource'); - - // Sandboxed are non-false, but not 1, either - if (!$actor->hasRight(Right::PUBLICNOTICE) || - ($source && $autosource && in_array($source, $autosource))) { - $stored->is_local = Notice::LOCAL_NONPUBLIC; + // Reject notice if it is too long (without the HTML) + // FIXME: Reject if too short (empty) too? But we have to pass the + if ($actor->isLocal() && Notice::contentTooLong($stored->content)) { + // TRANS: Client error displayed when the parameter "status" is missing. + // TRANS: %d is the maximum number of character for a notice. + throw new ClientException(sprintf(_m('That\'s too long. Maximum notice size is %d character.', + 'That\'s too long. Maximum notice size is %d characters.', + Notice::maxContent()), + Notice::maxContent())); } // Maybe a missing act-time should be fatal if the actor is not local? @@ -841,7 +866,6 @@ class Notice extends Managed_DataObject // 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)) { - $groups = array(); $replyGroups = $reply->getGroups(); foreach ($replyGroups as $group) { if ($actor->isMember($group)) { @@ -853,17 +877,28 @@ class Notice extends Managed_DataObject 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.').'); + $stored->conversation = $conv->getID(); + } else { + // Conversation URI was not found, so we must create it. But we can't create it + // until we have a Notice ID because of the database layout... + // $options['conversation'] will be used later after the $stored->insert(); + common_debug('Conversation URI from activity context not found, so we have to create it after inserting this Notice: '.$act->context->conversation); + } + } } + $notloc = null; if ($act->context instanceof ActivityContext) { - $location = $act->context->location; - if ($location) { - $stored->lat = $location->lat; - $stored->lon = $location->lon; - if ($location->location_id) { - $stored->location_ns = $location->location_ns; - $stored->location_id = $location->location_id; - } + if ($act->context->location instanceof Location) { + $notloc = Notice_location::fromLocation($act->context->location); } } else { $act->context = new ActivityContext(); @@ -885,25 +920,45 @@ class Notice extends Managed_DataObject $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 { - $stored->insert(); // throws exception on error + $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->uri . ': '.$act->asString()); + throw new ServerException('Unsuccessful call to StoreActivityObject '.$stored->getUri() . ': '.$act->asString()); } - // If it's not part of a conversation, it's - // the beginning of a new conversation. + // If it's not part of a conversation, it's the beginning + // of a new one (or belongs to a previously unknown URI). if (empty($stored->conversation)) { // $act->context->conversation will be null if it was not provided + common_debug('Creating a new conversation for stored notice ID='.$stored->getID().' with URI: '.$act->context->conversation); $conv = Conversation::create($stored, $act->context->conversation); - $stored->conversation = $conv->id; + $stored->conversation = $conv->getID(); } $stored->update($orig); @@ -923,34 +978,39 @@ class Notice extends Managed_DataObject // Save per-notice metadata... $mentions = array(); - $groups = array(); + $group_ids = array(); // This event lets plugins filter out non-local recipients (attentions we don't care about) // Used primarily for OStatus (and if we don't federate, all attentions would be local anyway) - Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$groups)); - - if (!empty($mentions)) { - $stored->saveKnownReplies($mentions); - } else { - $stored->saveReplies(); - } + Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$group_ids)); + + // 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($mentions)) { + $stored->saveKnownReplies($mentions); + } else { + $stored->saveReplies(); + } - if (!empty($tags)) { - $stored->saveKnownTags($tags); - } else { - $stored->saveTags(); - } + 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. - // Note: groups should always be set. + // Note: groups may save tags, so must be run after tags are saved + // to avoid errors on duplicates. + // Note: groups should always be set. - $stored->saveKnownGroups($groups); + $stored->saveKnownGroups($group_ids); - if (!empty($urls)) { - $stored->saveKnownUrls($urls); - } else { - $stored->saveUrls(); + if (!empty($urls)) { + $stored->saveKnownUrls($urls); + } else { + $stored->saveUrls(); + } } if ($distribute) { @@ -962,15 +1022,13 @@ class Notice extends Managed_DataObject } static public function figureOutScope(Profile $actor, array $groups, $scope=null) { - if (is_null($scope)) { - $scope = self::defaultScope(); - } + $scope = is_null($scope) ? self::defaultScope() : intval($scope); // For private streams try { $user = $actor->getUser(); // FIXME: We can't do bit comparison with == (Legacy StatusNet thing. Let's keep it for now.) - if ($user->private_stream && ($scope == Notice::PUBLIC_SCOPE || $scope == Notice::SITE_SCOPE)) { + if ($user->private_stream && ($scope === Notice::PUBLIC_SCOPE || $scope === Notice::SITE_SCOPE)) { $scope |= Notice::FOLLOWER_SCOPE; } } catch (NoSuchUserException $e) { @@ -1002,8 +1060,10 @@ class Notice extends Managed_DataObject $this->blowStream('networkpublic'); } - self::blow('notice:list-ids:conversation:%s', $this->conversation); - self::blow('conversation:notice_count:%d', $this->conversation); + if ($this->conversation) { + self::blow('notice:list-ids:conversation:%s', $this->conversation); + self::blow('conversation:notice_count:%d', $this->conversation); + } if ($this->isRepeat()) { // XXX: we should probably only use one of these @@ -1113,7 +1173,7 @@ class Notice extends Managed_DataObject * * @return void */ - function saveKnownUrls($urls) + function saveKnownUrls(array $urls) { if (common_config('attachments', 'process_links')) { // @fixme validation? @@ -1230,7 +1290,7 @@ class Notice extends Managed_DataObject } $stream = new ConversationNoticeStream($this->conversation); - $notices = $stream->getNotices(/*offset*/ 1, /*limit*/ 1); + $notice = $stream->getNotices(/*offset*/ 1, /*limit*/ 1); // if our "offset 1, limit 1" query got a result, return true else false return $notice->N > 0; @@ -1297,6 +1357,10 @@ class Notice extends Managed_DataObject } } catch (NoParentNoticeException $e) { // Latest notice has no parent + } catch (NoResultException $e) { + // Notice was not found, so we can't go further up in the tree. + // FIXME: Maybe we should do this in a more stable way where deleted + // notices won't break conversation chains? } // No parent, or parent out of scope $root = $last; @@ -1468,13 +1532,8 @@ class Notice extends Managed_DataObject * best with generalizations on user_group to support * remote groups better. */ - function saveKnownGroups($group_ids) + function saveKnownGroups(array $group_ids) { - if (!is_array($group_ids)) { - // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). - throw new ServerException(_('Bad type provided to saveKnownGroups.')); - } - $groups = array(); foreach (array_unique($group_ids) as $id) { $group = User_group::getKV('id', $id); @@ -1550,7 +1609,7 @@ class Notice extends Managed_DataObject return; } - $sender = Profile::getKV($this->profile_id); + $sender = $this->getProfile(); foreach (array_unique($uris) as $uri) { try { @@ -1565,11 +1624,9 @@ class Notice extends Managed_DataObject continue; } - $this->saveReply($profile->id); - self::blow('reply:stream:%d', $profile->id); + $this->saveReply($profile->getID()); + self::blow('reply:stream:%d', $profile->getID()); } - - return; } /** @@ -1584,12 +1641,6 @@ class Notice extends Managed_DataObject function saveReplies() { - // Don't save reply data for repeats - - if ($this->isRepeat()) { - return array(); - } - $sender = $this->getProfile(); $replied = array(); @@ -1598,17 +1649,21 @@ class Notice extends Managed_DataObject try { $parent = $this->getParent(); $parentauthor = $parent->getProfile(); - $this->saveReply($parentauthor->id); - $replied[$parentauthor->id] = 1; - self::blow('reply:stream:%d', $parentauthor->id); + $this->saveReply($parentauthor->getID()); + $replied[$parentauthor->getID()] = 1; + self::blow('reply:stream:%d', $parentauthor->getID()); } catch (NoParentNoticeException $e) { // Not a reply, since it has no parent! + $parent = null; + } catch (NoResultException $e) { + // Parent notice was probably deleted + $parent = null; } // @todo ideally this parser information would only // be calculated once. - $mentions = common_find_mentions($this->content, $this); + $mentions = common_find_mentions($this->content, $sender, $parent); // store replied only for first @ (what user/notice what the reply directed, // we assume first @ is it) @@ -1656,32 +1711,22 @@ class Notice extends Managed_DataObject protected $_replies = array(); /** - * Pull the complete list of @-reply targets for this notice. + * Pull the complete list of @-mentioned profile IDs for this notice. * * @return array of integer profile ids */ function getReplies() { - if (isset($this->_replies[$this->id])) { - return $this->_replies[$this->id]; + if (!isset($this->_replies[$this->getID()])) { + $mentions = Reply::multiGet('notice_id', array($this->getID())); + $this->_replies[$this->getID()] = $mentions->fetchAll('profile_id'); } - - $replyMap = Reply::listGet('notice_id', array($this->id)); - - $ids = array(); - - foreach ($replyMap[$this->id] as $reply) { - $ids[] = $reply->profile_id; - } - - $this->_replies[$this->id] = $ids; - - return $ids; + return $this->_replies[$this->getID()]; } - function _setReplies($replies) + function _setReplies(array $replies) { - $this->_replies[$this->id] = $replies; + $this->_replies[$this->getID()] = $replies; } /** @@ -1707,7 +1752,6 @@ class Notice extends Managed_DataObject function sendReplyNotifications() { // Don't send reply notifications for repeats - if ($this->isRepeat()) { return array(); } @@ -1717,9 +1761,11 @@ class Notice extends Managed_DataObject require_once INSTALLDIR.'/lib/mail.php'; foreach ($recipientIds as $recipientId) { - $user = User::getKV('id', $recipientId); - if ($user instanceof User) { + try { + $user = User::getByID($recipientId); mail_notify_attn($user, $this); + } catch (NoResultException $e) { + // No such user } } Event::handle('EndNotifyMentioned', array($this, $recipientIds)); @@ -1756,11 +1802,11 @@ class Notice extends Managed_DataObject } $groups = User_group::multiGet('id', $ids); - $this->_groups[$this->id] = $groups->fetchAll(); + $this->_setGroups($groups->fetchAll()); return $this->_groups[$this->id]; } - function _setGroups($groups) + function _setGroups(array $groups) { $this->_groups[$this->id] = $groups; } @@ -1838,9 +1884,15 @@ class Notice extends Managed_DataObject $ctx->replyToUrl = $reply->getUrl(true); // true for fallback to local URL, less messy } catch (NoParentNoticeException $e) { // This is not a reply to something + } catch (NoResultException $e) { + // Parent notice was probably deleted } - $ctx->location = $this->getLocation(); + try { + $ctx->location = Notice_location::locFromStored($this); + } catch (ServerException $e) { + $ctx->location = null; + } $conv = null; @@ -2018,6 +2070,7 @@ class Notice extends Managed_DataObject if (Event::handle('StartActivityObjectFromNotice', array($this, &$object))) { $object->type = $this->object_type ?: ActivityObject::NOTE; $object->id = $this->getUri(); + //FIXME: = $object->title ?: sprintf(... because we might get a title from StartActivityObjectFromNotice $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname()); $object->content = $this->rendered; $object->link = $this->getUrl(); @@ -2086,23 +2139,6 @@ class Notice extends Managed_DataObject return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit)); } - function getLocation() - { - $location = null; - - if (!empty($this->location_id) && !empty($this->location_ns)) { - $location = Location::fromId($this->location_id, $this->location_ns); - } - - if (is_null($location)) { // no ID, or Location::fromId() failed - if (!empty($this->lat) && !empty($this->lon)) { - $location = Location::fromLatLon($this->lat, $this->lon); - } - } - - return $location; - } - /** * Convenience function for posting a repeat of an existing message. * @@ -2273,6 +2309,16 @@ class Notice extends Managed_DataObject $reply->free(); } + function clearLocation() + { + $loc = new Notice_location(); + $loc->notice_id = $this->id; + + if ($loc->find()) { + $loc->delete(); + } + } + function clearFiles() { $f2p = new File_to_post(); @@ -2397,7 +2443,7 @@ class Notice extends Managed_DataObject $this->uri = sprintf('%s%s=%d:%s=%s', TagURI::mint(), 'noticeId', $this->id, - 'objectType', $this->get_object_type(true)); + 'objectType', $this->getObjectType(true)); $changed = true; } @@ -2459,8 +2505,13 @@ class Notice extends Managed_DataObject public function isLocal() { - return ($this->is_local == Notice::LOCAL_PUBLIC || - $this->is_local == Notice::LOCAL_NONPUBLIC); + $is_local = intval($this->is_local); + return ($is_local === self::LOCAL_PUBLIC || $is_local === self::LOCAL_NONPUBLIC); + } + + public function getScope() + { + return intval($this->scope); } public function isRepeat() @@ -2475,6 +2526,37 @@ class Notice extends Managed_DataObject */ public function getTags() { + // Check default scope (non-private notices) + $inScope = (!$this->isPrivateScope()); + + // Get current profile + $profile = Profile::current(); + + // Is the general scope check okay and the user in logged in? + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ',profile[]=' . gettype($profile)); + if (($inScope === TRUE) && ($profile instanceof Profile)) { + /* + * Check scope, else a privacy leaks happens this way: + * + * 1) Bob and Alice follow each other and write private notices + * (this->scope=2) to each other. + * 2) Bob uses tags in his private notice to alice (which she can + * read from him). + * 3) Alice adds that notice (with tags) to her favorites + * ("faving") it. + * 4) The tags from Bob's private notice becomes visible in Alice's + * profile. + * + * This has the simple background that the scope is not being + * re-checked. This has to be done here at this point because given + * above scenario is a privacy leak as the tags may be *really* + * private (nobody else shall see them) such as initmate words or + * very political words. + */ + $inScope = $this->inScope($profile); + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ' - After inScope() has been called.'); + } + $tags = array(); $keypart = sprintf('notice:tags:%d', $this->id); @@ -2486,7 +2568,9 @@ class Notice extends Managed_DataObject } else { $tag = new Notice_tag(); $tag->notice_id = $this->id; - if ($tag->find()) { + + // Check scope for privacy-leak protection (see some lines above why) + if (($inScope === TRUE) && ($tag->find())) { while ($tag->fetch()) { $tags[] = $tag->tag; } @@ -2614,6 +2698,11 @@ class Notice extends Managed_DataObject ($this->is_local != Notice::GATEWAY)); } + public function isPrivateScope () { + return ($this->scope != Notice::SITE_SCOPE && + $this->scope != Notice::PUBLIC_SCOPE); + } + /** * Check that the given profile is allowed to read, respond to, or otherwise * act on this notice. @@ -2628,7 +2717,7 @@ class Notice extends Managed_DataObject * * @return boolean whether the profile is in the notice's scope */ - function inScope($profile) + function inScope(Profile $profile=null) { if (is_null($profile)) { $keypart = sprintf('notice:in-scope-for:%d:null', $this->id); @@ -2651,15 +2740,11 @@ class Notice extends Managed_DataObject return ($result == 1) ? true : false; } - protected function _inScope($profile) + protected function _inScope(Profile $profile=null) { - if (!is_null($this->scope)) { - $scope = $this->scope; - } else { - $scope = self::defaultScope(); - } + $scope = is_null($this->scope) ? self::defaultScope() : $this->getScope(); - if ($scope == 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public. + if ($scope === 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public. return !$this->isHiddenSpam($profile); } @@ -2720,7 +2805,7 @@ class Notice extends Managed_DataObject return !$this->isHiddenSpam($profile); } - function isHiddenSpam($profile) { + function isHiddenSpam(Profile $profile=null) { // Hide posts by silenced users from everyone but moderators. @@ -2744,12 +2829,35 @@ class Notice extends Managed_DataObject return false; } + public function hasParent() + { + try { + $this->getParent(); + } catch (NoParentNoticeException $e) { + return false; + } + return true; + } + public function getParent() { + $reply_to_id = null; + if (empty($this->reply_to)) { throw new NoParentNoticeException($this); } - return self::getByID($this->reply_to); + + // The reply_to ID in the table Notice could exist with a number + // however, the replied to notice might not exist in the database. + // Thus we need to catch the exception and throw the NoParentNoticeException else + // the timeline will not display correctly. + try { + $reply_to_id = self::getByID($this->reply_to); + } catch(Exception $e){ + throw new NoParentNoticeException($this); + } + + return $reply_to_id; } /** @@ -2782,7 +2890,7 @@ class Notice extends Managed_DataObject return $scope; } - static function fillProfiles($notices) + static function fillProfiles(array $notices) { $map = self::getProfiles($notices); foreach ($notices as $entry=>$notice) { @@ -2799,7 +2907,7 @@ class Notice extends Managed_DataObject return array_values($map); } - static function getProfiles(&$notices) + static function getProfiles(array &$notices) { $ids = array(); foreach ($notices as $notice) { @@ -2809,7 +2917,7 @@ class Notice extends Managed_DataObject return Profile::pivotGet('id', $ids); } - static function fillGroups(&$notices) + static function fillGroups(array &$notices) { $ids = self::_idsOf($notices); $gis = Group_inbox::listGet('notice_id', $ids); @@ -2844,7 +2952,7 @@ class Notice extends Managed_DataObject return array_keys($ids); } - static function fillAttachments(&$notices) + static function fillAttachments(array &$notices) { $ids = self::_idsOf($notices); $f2pMap = File_to_post::listGet('post_id', $ids); @@ -2868,7 +2976,7 @@ class Notice extends Managed_DataObject } } - static function fillReplies(&$notices) + static function fillReplies(array &$notices) { $ids = self::_idsOf($notices); $replyMap = Reply::listGet('notice_id', $ids); @@ -2881,4 +2989,84 @@ class Notice extends Managed_DataObject $notice->_setReplies($ids); } } + + /** + * Checks whether the current profile is allowed (in scope) to see this notice. + * + * @return $inScope Whether the current profile is allowed to see this notice + */ + function isCurrentProfileInScope () { + // Check scope, default is allowed + $inScope = TRUE; + + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] this->tag=' . $this->tag . ',this->id=' . $this->id . ',this->scope=' . $this->scope); + + // Is it private scope? + if ($this->isPrivateScope()) { + // 2) Get current profile + $profile = Profile::current(); + + // Is the profile not set? + if (!$profile instanceof Profile) { + // Public viewer shall not see a tag from a private dent (privacy leak) + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] Not logged in (public view).'); + $inScope = FALSE; + } elseif (!$this->inScope($profile)) { + // Current profile is not in scope (not allowed to see) of notice + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] profile->id=' . $profile->id . ' is not allowed to see this notice.'); + $inScope = FALSE; + } + } + + // Return result + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] this->tag=' . $this->tag . ',this->weight=' . $this->weight . ',inScope=' . intval($inScope) . ' - EXIT!'); + return $inScope; + } + + static public function beforeSchemaUpdate() + { + $table = strtolower(get_called_class()); + $schema = Schema::get(); + $schemadef = $schema->getTableDef($table); + + // 2015-09-04 We move Notice location data to Notice_location + // First we see if we have to do this at all + if (!isset($schemadef['fields']['lat']) + && !isset($schemadef['fields']['lon']) + && !isset($schemadef['fields']['location_id']) + && !isset($schemadef['fields']['location_ns'])) { + // We have already removed the location fields, so no need to migrate. + return; + } + // Then we make sure the Notice_location table is created! + $schema->ensureTable('notice_location', Notice_location::schemaDef()); + + // Then we continue on our road to migration! + echo "\nFound old $table table, moving location data to 'notice_location' table... (this will probably take a LONG time, but can be aborted and continued)"; + + $notice = new Notice(); + $notice->query(sprintf('SELECT id, lat, lon, location_id, location_ns FROM %1$s ' . + 'WHERE lat IS NOT NULL ' . + 'OR lon IS NOT NULL ' . + 'OR location_id IS NOT NULL ' . + 'OR location_ns IS NOT NULL', + $schema->quoteIdentifier($table))); + print "\nFound {$notice->N} notices with location data, inserting"; + while ($notice->fetch()) { + $notloc = Notice_location::getKV('notice_id', $notice->id); + if ($notloc instanceof Notice_location) { + print "-"; + continue; + } + $notloc = new Notice_location(); + $notloc->notice_id = $notice->id; + $notloc->lat= $notice->lat; + $notloc->lon= $notice->lon; + $notloc->location_id= $notice->location_id; + $notloc->location_ns= $notice->location_ns; + $notloc->insert(); + print "."; + } + print "\n"; + } }