X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=classes%2FNotice.php;fp=classes%2FNotice.php;h=cc00cd1dd6ac5432b9036c28c404664339b9503d;hb=4160a3fb730113f3d712bd777884c4b0482f6df1;hp=269b846f24d4f71a87b47b328764d83eece2982c;hpb=abd90bbdf562614755802885dfb5673645df8575;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 269b846f24..cc00cd1dd6 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 @@ -84,7 +80,7 @@ class Notice extends Managed_DataObject 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'), 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'), - 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'), + 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8mb4_general_ci'), 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), 'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), @@ -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'), @@ -118,6 +110,7 @@ class Notice extends Managed_DataObject 'notice_profile_id_idx' => array('profile_id', 'created', 'id'), 'notice_repeat_of_created_id_idx' => array('repeat_of', 'created', 'id'), 'notice_conversation_created_id_idx' => array('conversation', 'created', 'id'), + 'notice_verb_idx' => array('verb'), 'notice_replyto_idx' => array('reply_to') ) ); @@ -166,45 +159,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 +258,20 @@ class Notice extends Managed_DataObject return $this->content; } + public function getRendered() + { + if (is_null($this->rendered) || $this->rendered === '') { + // update to include rendered content on-the-fly, so we don't have to have a fix-up script in upgrade.php + common_debug('Rendering notice '.$this->getID().' as it had no rendered HTML content.'); + $orig = clone($this); + $this->rendered = common_render_content($this->getContent(), + $this->getProfile(), + $this->hasParent() ? $this->getParent() : null); + $this->update($orig); + } + return $this->rendered; + } + /* * Get the original representation URL of this notice. * @@ -284,23 +282,23 @@ class Notice extends Managed_DataObject // The risk is we start having empty urls and non-http uris... // and we can't really handle any other protocol right now. switch (true) { + case $this->isLocal(): + return common_local_url('shownotice', array('notice' => $this->getID()), null, null, false); case common_valid_http_url($this->url): // should we allow non-http/https URLs? return $this->url; - case !$this->isLocal() && common_valid_http_url($this->uri): // Sometimes we only have the URI for remote posts. + case common_valid_http_url($this->uri): // Sometimes we only have the URI for remote posts. return $this->uri; - case $this->isLocal() || $fallback: + case $fallback: // let's generate a valid link to our locally available notice on demand - return common_local_url('shownotice', array('notice' => $this->id), null, null, false); + return common_local_url('shownotice', array('notice' => $this->getID()), null, null, false); default: - common_debug('No URL available for notice: id='.$this->id); + common_debug('No URL available for notice: id='.$this->getID()); throw new InvalidUrlException($this->url); } } - 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) @@ -313,16 +311,6 @@ class Notice extends Managed_DataObject return $notice; } - public static function getById($id) - { - $notice = new Notice(); - $notice->id = $id; - if (!$notice->find(true)) { - throw new NoResultException($notice); - } - return $notice; - } - /** * Extract #hashtags from this notice's content and save them to the database. */ @@ -533,18 +521,13 @@ 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.')); } @@ -620,35 +603,43 @@ 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.').'); - $notice->conversation = $conv->id; + 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.').'); } 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']; + // 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 options to Notice::saveNew: '.$options['conversation']); + // The insert in Conversation::create throws exception on failure + $conv = Conversation::create($options['conversation'], $notice->created); } - } else { - // If we're not using the attached conversation URI let's remove it - // so we don't mistake ourselves later, when creating our own Conversation. - // This implies that the notice knows which conversation it belongs to. - $options['conversation'] = null; + $notice->conversation = $conv->getID(); + unset($conv); } } + // If it's not part of a conversation, it's the beginning of a new conversation. + if (empty($notice->conversation)) { + $conv = Conversation::create(); + $notice->conversation = $conv->getID(); + unset($conv); + } + + + $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)) { @@ -681,15 +672,11 @@ class Notice extends Managed_DataObject // XXX: some of these functions write to the DB try { - $notice->insert(); // throws exception on failure - // 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); + $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 } } catch (Exception $e) { // Let's test if we managed initial insert, which would imply @@ -702,33 +689,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) { @@ -756,6 +743,7 @@ class Notice extends Managed_DataObject } // Get ActivityObject properties + $actobj = null; if (!empty($act->id)) { // implied object $options['uri'] = $act->id; @@ -774,7 +762,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, @@ -794,12 +782,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.'); } } @@ -809,19 +821,15 @@ 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; - $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; + // 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)) { @@ -851,7 +859,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)) { @@ -863,17 +870,36 @@ 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.').'); + } 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) { - $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(); @@ -895,27 +921,39 @@ 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()); - } - - // If it's not part of a conversation, it's - // the beginning of a new conversation. - if (empty($stored->conversation)) { - // $act->context->conversation will be null if it was not provided - $conv = Conversation::create($stored, $act->context->conversation); - $stored->conversation = $conv->id; + 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)) { @@ -931,36 +969,26 @@ class Notice extends Managed_DataObject throw new ServerException('StartNoticeSave did not give back a Notice'); } - // Save per-notice metadata... - $mentions = array(); - $groups = 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(); - } - - if (!empty($tags)) { - $stored->saveKnownTags($tags); - } else { - $stored->saveTags(); - } + // 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))) { - // Note: groups may save tags, so must be run after tags are saved - // to avoid errors on duplicates. - // Note: groups should always be set. + if (!empty($tags)) { + $stored->saveKnownTags($tags); + } else { + $stored->saveTags(); + } - $stored->saveKnownGroups($groups); + // 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 (!empty($urls)) { + $stored->saveKnownUrls($urls); + } else { + $stored->saveUrls(); + } } if ($distribute) { @@ -972,15 +1000,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) { @@ -989,12 +1015,14 @@ class Notice extends Managed_DataObject // Force the scope for private groups foreach ($groups as $group_id) { - $group = User_group::staticGet('id', $group_id); - if ($group instanceof User_group) { + try { + $group = User_group::getByID($group_id); if ($group->force_scope) { $scope |= Notice::GROUP_SCOPE; break; } + } catch (Exception $e) { + common_log(LOG_ERR, 'Notice figureOutScope threw exception: '.$e->getMessage()); } } @@ -1010,8 +1038,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 @@ -1109,7 +1139,7 @@ class Notice extends Managed_DataObject */ function saveUrls() { if (common_config('attachments', 'process_links')) { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this); } } @@ -1126,11 +1156,7 @@ class Notice extends Managed_DataObject if (common_config('attachments', 'process_links')) { // @fixme validation? foreach (array_unique($urls) as $url) { - try { - File::processNew($url, $this->id); - } catch (ServerException $e) { - // Could not save URL. Log it? - } + $this->saveUrl($url, $this); } } } @@ -1138,9 +1164,9 @@ class Notice extends Managed_DataObject /** * @private callback */ - function saveUrl($url, $notice_id) { + function saveUrl($url, Notice $notice) { try { - File::processNew($url, $notice_id); + File::processNew($url, $notice); } catch (ServerException $e) { // Could not save URL. Log it? } @@ -1215,17 +1241,15 @@ class Notice extends Managed_DataObject $this->_attachments[$this->id] = $attachments; } - function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) + static function publicStream($offset=0, $limit=20, $since_id=null, $max_id=null) { $stream = new PublicNoticeStream(); return $stream->getNotices($offset, $limit, $since_id, $max_id); } - - function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) + static function conversationStream($id, $offset=0, $limit=20, $since_id=null, $max_id=null) { $stream = new ConversationNoticeStream($id); - return $stream->getNotices($offset, $limit, $since_id, $max_id); } @@ -1237,18 +1261,17 @@ class Notice extends Managed_DataObject */ function hasConversation() { - if (!empty($this->conversation)) { - $conversation = Notice::conversationStream( - $this->conversation, - 1, - 1 - ); - - if ($conversation->N > 0) { - return true; - } + if (empty($this->conversation)) { + // this notice is not part of a conversation apparently + // FIXME: all notices should have a conversation value, right? + return false; } - return false; + + $stream = new ConversationNoticeStream($this->conversation); + $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; } /** @@ -1283,8 +1306,7 @@ class Notice extends Managed_DataObject $root = new Notice; $root->conversation = $this->conversation; $root->orderBy('notice.created ASC'); - $root->find(); - $root->fetch(); + $root->find(true); // true means "fetch first result" $root->free(); return $root; } @@ -1311,8 +1333,12 @@ class Notice extends Managed_DataObject $last = $parent; continue; } - } catch (Exception $e) { + } 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; @@ -1484,13 +1510,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); @@ -1548,6 +1569,55 @@ class Notice extends Managed_DataObject return true; } + function saveAttentions(array $uris) + { + foreach ($uris as $uri=>$type) { + try { + $target = Profile::fromUri($uri); + } catch (UnknownUriException $e) { + common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'"); + continue; + } + + try { + $this->saveAttention($target); + } catch (AlreadyFulfilledException $e) { + common_debug('Attention already exists: '.var_export($e->getMessage(),true)); + } catch (Exception $e) { + common_log(LOG_ERR, "Could not save notice id=={$this->getID()} attention for profile id=={$target->getID()}: {$e->getMessage()}"); + } + } + } + + /** + * Saves an attention for a profile (user or group) which means + * it shows up in their home feed and such. + */ + function saveAttention(Profile $target, $reason=null) + { + if ($target->isGroup()) { + // FIXME: Make sure we check (for both local and remote) users are in the groups they send to! + + // legacy notification method, will still be in use for quite a while I think + $this->addToGroupInbox($target->getGroup()); + } else { + if ($target->hasBlocked($this->getProfile())) { + common_log(LOG_INFO, "Not saving reply to profile {$target->id} ($uri) from sender {$sender->id} because of a block."); + return false; + } + } + + if ($target->isLocal()) { + // legacy notification method, will still be in use for quite a while I think + $this->saveReply($target->getID()); + } + + $att = Attention::saveNew($this, $target, $reason); + + self::blow('reply:stream:%d', $target->getID()); + return true; + } + /** * Save reply records indicating that this notice needs to be * delivered to the local users with the given URIs. @@ -1566,7 +1636,7 @@ class Notice extends Managed_DataObject return; } - $sender = Profile::getKV($this->profile_id); + $sender = $this->getProfile(); foreach (array_unique($uris) as $uri) { try { @@ -1581,11 +1651,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; } /** @@ -1600,12 +1668,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(); @@ -1614,35 +1676,33 @@ 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); - } catch (Exception $e) { + $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); - - // store replied only for first @ (what user/notice what the reply directed, - // we assume first @ is it) + $mentions = common_find_mentions($this->content, $sender, $parent); foreach ($mentions as $mention) { foreach ($mention['mentioned'] as $mentioned) { // skip if they're already covered - - if (!empty($replied[$mentioned->id])) { + if (array_key_exists($mentioned->id, $replied)) { continue; } // Don't save replies from blocked profile to local user - - $mentioned_user = User::getKV('id', $mentioned->id); - if ($mentioned_user instanceof User && $mentioned_user->hasBlocked($sender)) { + if ($mentioned->hasBlocked($sender)) { continue; } @@ -1670,35 +1730,42 @@ class Notice extends Managed_DataObject return $reply; } - protected $_replies = array(); + protected $_attentionids = array(); /** - * Pull the complete list of @-reply targets for this notice. + * Pull the complete list of known activity context attentions for this notice. * - * @return array of integer profile ids + * @return array of integer profile ids (also group profiles) */ - function getReplies() + function getAttentionProfileIDs() { - if (isset($this->_replies[$this->id])) { - return $this->_replies[$this->id]; + if (!isset($this->_attentionids[$this->getID()])) { + $atts = Attention::multiGet('notice_id', array($this->getID())); + // (array)null means empty array + $this->_attentionids[$this->getID()] = (array)$atts->fetchAll('profile_id'); } + return $this->_attentionids[$this->getID()]; + } - $replyMap = Reply::listGet('notice_id', array($this->id)); - - $ids = array(); + protected $_replies = array(); - foreach ($replyMap[$this->id] as $reply) { - $ids[] = $reply->profile_id; + /** + * Pull the complete list of @-mentioned profile IDs for this notice. + * + * @return array of integer profile ids + */ + function getReplies() + { + if (!isset($this->_replies[$this->getID()])) { + $mentions = Reply::multiGet('notice_id', array($this->getID())); + $this->_replies[$this->getID()] = $mentions->fetchAll('profile_id'); } - - $this->_setReplies($ids); - - return $ids; + return $this->_replies[$this->getID()]; } function _setReplies(array $replies) { - $this->_replies[$this->id] = $replies; + $this->_replies[$this->getID()] = $replies; } /** @@ -1706,11 +1773,11 @@ class Notice extends Managed_DataObject * * @return array of Profiles */ - function getReplyProfiles() + function getAttentionProfiles() { - $ids = $this->getReplies(); + $ids = array_unique(array_merge($this->getReplies(), $this->getGroupProfileIDs(), $this->getAttentionProfileIDs())); - $profiles = Profile::multiGet('id', $ids); + $profiles = Profile::multiGet('id', (array)$ids); return $profiles->fetchAll(); } @@ -1724,7 +1791,6 @@ class Notice extends Managed_DataObject function sendReplyNotifications() { // Don't send reply notifications for repeats - if ($this->isRepeat()) { return array(); } @@ -1734,15 +1800,34 @@ 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)); } } + /** + * Pull list of Profile IDs of groups this notice addresses. + * + * @return array of Group _profile_ IDs + */ + + function getGroupProfileIDs() + { + $ids = array(); + + foreach ($this->getGroups() as $group) { + $ids[] = $group->profile_id; + } + + return $ids; + } + /** * Pull list of groups this notice needs to be delivered to, * as previously recorded by saveKnownGroups(). @@ -1809,7 +1894,7 @@ class Notice extends Managed_DataObject // The notice is probably a share or similar, which don't // have a representational URL of their own. } - $act->content = common_xml_safe_str($this->rendered); + $act->content = common_xml_safe_str($this->getRendered()); $profile = $this->getProfile(); @@ -1852,12 +1937,18 @@ class Notice extends Managed_DataObject try { $reply = $this->getParent(); $ctx->replyToID = $reply->getUri(); - $ctx->replyToUrl = $reply->getUrl(); - } catch (Exception $e) { + $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; @@ -1868,19 +1959,11 @@ class Notice extends Managed_DataObject } } - $reply_ids = $this->getReplies(); - - foreach ($reply_ids as $id) { - $rprofile = Profile::getKV('id', $id); - if ($rprofile instanceof Profile) { - $ctx->attention[$rprofile->getUri()] = ActivityObject::PERSON; - } - } - - $groups = $this->getGroups(); - - foreach ($groups as $group) { - $ctx->attention[$group->getUri()] = ActivityObject::GROUP; + // This covers the legacy getReplies and getGroups too which get their data + // from entries stored via Notice::saveNew (which we want to move away from)... + foreach ($this->getAttentionProfiles() as $target) { + // User and group profiles which get the attention of this notice + $ctx->attention[$target->getUri()] = $target->getObjectType(); } switch ($this->scope) { @@ -2035,8 +2118,9 @@ 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->content = $this->getRendered(); $object->link = $this->getUrl(); $object->extra[] = array('status_net', array('notice_id' => $this->id)); @@ -2103,23 +2187,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. * @@ -2206,7 +2273,7 @@ class Notice extends Managed_DataObject return $notice->fetchAll('id'); } - function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null) + static function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null) { $options = array(); @@ -2290,6 +2357,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(); @@ -2364,8 +2441,11 @@ class Notice extends Managed_DataObject // If there's a failure, we want to _force_ // distribution at this point. try { + $json = json_encode((object)array('id' => $this->getID(), + 'type' => 'Notice', + )); $qm = QueueManager::get(); - $qm->enqueue($this, 'distrib'); + $qm->enqueue($json, 'distrib'); } catch (Exception $e) { // If the exception isn't transient, this // may throw more exceptions as DQH does @@ -2414,7 +2494,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; } @@ -2476,8 +2556,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() @@ -2670,13 +2755,9 @@ class Notice extends Managed_DataObject 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); } @@ -2761,15 +2842,35 @@ class Notice extends Managed_DataObject return false; } + public function hasParent() + { + try { + $this->getParent(); + } catch (NoParentNoticeException $e) { + return false; + } + return true; + } + public function getParent() { - $parent = Notice::getKV('id', $this->reply_to); + $reply_to_id = null; - if (!$parent instanceof Notice) { - throw new ServerException('Notice has no parent'); + if (empty($this->reply_to)) { + throw new NoParentNoticeException($this); } - return $parent; + // 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; } /** @@ -2901,4 +3002,51 @@ class Notice extends Managed_DataObject $notice->_setReplies($ids); } } + + 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"; + } }