X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=b828678d87c92f338ee3538404a900f9b2324f39;hb=3d6e25ee5f78d4fc3e00335d39724694264bbd53;hp=1afc6d09772d583df574ec4395f0053e97bc16d5;hpb=3251ef3b518ef9db55fbab0693d8b0f654dcda58;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 1afc6d0977..b828678d87 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -88,7 +88,7 @@ class Notice extends Managed_DataObject 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'), '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'), + 'conversation' => array('type' => 'int', 'description' => 'the local numerical conversation id'), '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' => null), 'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), @@ -110,6 +110,10 @@ 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_object_type_idx' => array('object_type'), + 'notice_verb_idx' => array('verb'), + 'notice_profile_id_verb_idx' => array('profile_id', 'verb'), + 'notice_url_idx' => array('url'), // Qvitter wants this 'notice_replyto_idx' => array('reply_to') ) ); @@ -164,6 +168,18 @@ class Notice extends Managed_DataObject throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.')); } + $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; + } + + protected function deleteRelated() + { if (Event::handle('NoticeDeleteRelated', array($this))) { // Clear related records $this->clearReplies(); @@ -175,19 +191,12 @@ class Notice extends Managed_DataObject $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) { + $this->deleteRelated(); + $result = parent::delete($useWhere); $this->blowOnDelete(); @@ -239,10 +248,10 @@ class Notice extends Managed_DataObject return common_local_url('shownotice', array('notice' => $this->id), null, null, false); } - public function getTitle() + public function getTitle($imply=true) { $title = null; - if (Event::handle('GetNoticeTitle', array($this, &$title))) { + if (Event::handle('GetNoticeTitle', array($this, &$title)) && $imply) { // TRANS: Title of a notice posted without a title value. // TRANS: %1$s is a user name, %2$s is the notice creation date/time. $title = sprintf(_('%1$s\'s status on %2$s'), @@ -259,9 +268,34 @@ class Notice extends Managed_DataObject public function getRendered() { + // we test $this->id because if it's not inserted yet, we can't update the field + if (!empty($this->id) && (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; } + public function getCreated() + { + return $this->created; + } + + public function getVerb($make_relative=false) + { + return ActivityUtils::resolveUri($this->verb, $make_relative); + } + + public function isVerb(array $verbs) + { + return ActivityUtils::compareVerbs($this->getVerb(), $verbs); + } + /* * Get the original representation URL of this notice. * @@ -272,31 +306,34 @@ 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); throw new InvalidUrlException($this->url); } } public function getObjectType($canonical=false) { + if (is_null($this->object_type) || $this->object_type==='') { + throw new NoObjectTypeException($this); + } return ActivityUtils::resolveUri($this->object_type, $canonical); } - public static function getByUri($uri) + public function isObjectType(array $types) { - $notice = new Notice(); - $notice->uri = $uri; - if (!$notice->find(true)) { - throw new NoResultException($notice); + try { + return ActivityUtils::compareTypes($this->getObjectType(), $types); + } catch (NoObjectTypeException $e) { + return false; } - return $notice; } /** @@ -467,12 +504,7 @@ class Notice extends Managed_DataObject $notice = new Notice(); $notice->profile_id = $profile_id; - $autosource = common_config('public', 'autosource'); - - // Sandboxed are non-false, but not 1, either - - if (!$profile->hasRight(Right::PUBLICNOTICE) || - ($source && $autosource && in_array($source, $autosource))) { + if ($source && in_array($source, common_config('public', 'autosource'))) { $notice->is_local = Notice::LOCAL_NONPUBLIC; } else { $notice->is_local = $is_local; @@ -486,9 +518,6 @@ class Notice extends Managed_DataObject if (!$notice->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.'); } @@ -592,21 +621,25 @@ class Notice extends Managed_DataObject $conv = Conversation::getKV('uri', $options['conversation']); if ($conv instanceof Conversation) { 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... - // $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']); + // 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)) { $notloc->lat = $lat; @@ -662,17 +695,6 @@ class Notice extends Managed_DataObject $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); - } } catch (Exception $e) { // Let's test if we managed initial insert, which would imply // failing on some update-part (check 'insert()'). Delete if @@ -744,7 +766,7 @@ class Notice extends Managed_DataObject $options['uri'] = $act->id; $options['url'] = $act->link; } else { - $actobj = count($act->objects)==1 ? $act->objects[0] : null; + $actobj = count($act->objects)===1 ? $act->objects[0] : null; if (!is_null($actobj) && !empty($actobj->id)) { $options['uri'] = $actobj->id; if ($actobj->link) { @@ -789,12 +811,12 @@ class Notice extends Managed_DataObject } } - $autosource = common_config('public', 'autosource'); + // NOTE: Sandboxed users previously got all the notices _during_ + // sandbox period set to to is_local=Notice::LOCAL_NONPUBLIC here. + // Since then we have started just filtering _when_ it gets shown + // instead of creating a mixed jumble of differently scoped notices. - // 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... + if ($source && in_array($source, common_config('public', 'autosource'))) { $stored->is_local = Notice::LOCAL_NONPUBLIC; } else { $stored->is_local = intval($is_local); @@ -810,31 +832,25 @@ class Notice extends Managed_DataObject } } - $stored->profile_id = $actor->id; + $stored->profile_id = $actor->getID(); $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; + // we use mb_strlen because it _might_ be that the content is just the string "0"... + $content = mb_strlen($act->content) ? $act->content : $act->summary; + if (mb_strlen($content)===0 && !is_null($actobj)) { + $content = mb_strlen($actobj->content) ? $actobj->content : $actobj->summary; } - $stored->rendered = $actor->isLocal() ? $content : common_purify($content); - $stored->content = common_strip_html($stored->rendered); - - // 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())); + // Strip out any bad HTML from $content. URI.Base is used to sort out relative URLs. + $stored->rendered = common_purify($content, ['URI.Base' => $stored->url ?: null]); + $stored->content = common_strip_html($stored->getRendered(), true, true); + if (trim($stored->content) === '') { + // TRANS: Error message when the plain text content of a notice has zero length. + throw new ClientException(_('Empty notice content, will not save this.')); } + unset($content); // garbage collect // Maybe a missing act-time should be fatal if the actor is not local? if (!empty($act->time)) { @@ -843,13 +859,31 @@ class Notice extends Managed_DataObject $stored->created = common_sql_now(); } - $reply = null; + $reply = null; // this will store the in-reply-to Notice if found + $replyUris = []; // this keeps a list of possible URIs to look up if ($act->context instanceof ActivityContext && !empty($act->context->replyToID)) { - $reply = self::getKV('uri', $act->context->replyToID); + $replyUris[] = $act->context->replyToID; } - if (!$reply instanceof Notice && $act->target instanceof ActivityObject) { - $reply = self::getKV('uri', $act->target->id); + if ($act->target instanceof ActivityObject && !empty($act->target->id)) { + $replyUris[] = $act->target->id; } + foreach (array_unique($replyUris) as $replyUri) { + $reply = self::getKV('uri', $replyUri); + // Only do remote fetching if we're not a private site + if (!common_config('site', 'private') && !$reply instanceof Notice) { + // the URI is the object we're looking for, $actor is a + // Profile that surely knows of it and &$reply where it + // will be stored when fetched + Event::handle('FetchRemoteNotice', array($replyUri, $actor, &$reply)); + } + // we got what we're in-reply-to now, so let's move on + if ($reply instanceof Notice) { + break; + } + // otherwise reset whatever we might've gotten from the event + $reply = null; + } + unset($replyUris); // garbage collect if ($reply instanceof Notice) { if (!$reply->inScope($actor)) { @@ -883,15 +917,24 @@ class Notice extends Managed_DataObject $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); + // 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); } } + unset($reply); // garbage collect + + // 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) { @@ -902,7 +945,13 @@ class Notice extends Managed_DataObject $act->context = new ActivityContext(); } - $stored->scope = self::figureOutScope($actor, $groups, $scope); + if (array_key_exists(ActivityContext::ATTN_PUBLIC, $act->context->attention)) { + $stored->scope = Notice::PUBLIC_SCOPE; + // TODO: maybe we should actually keep this? if the saveAttentions thing wants to use it... + unset($act->context->attention[ActivityContext::ATTN_PUBLIC]); + } else { + $stored->scope = self::figureOutScope($actor, $groups, $scope); + } foreach ($act->categories as $cat) { if ($cat->term) { @@ -947,18 +996,11 @@ class Notice extends Managed_DataObject $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 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->getID(); + throw new NoticeSaveException('Unsuccessful call to StoreActivityObject '._ve($stored->getUri()) . ': '._ve($act->asString())); } + unset($object); + // If something changed in the Notice during StoreActivityObject $stored->update($orig); } catch (Exception $e) { if (empty($stored->id)) { @@ -970,27 +1012,18 @@ class Notice extends Managed_DataObject throw $e; } } + unset($notloc); // garbage collect + if (!$stored instanceof Notice) { - throw new ServerException('StartNoticeSave did not give back a Notice'); + throw new ServerException('StartNoticeSave did not give back a Notice.'); + } elseif (empty($stored->id)) { + throw new ServerException('Supposedly saved Notice has no ID.'); } - // Save per-notice metadata... - $mentions = 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, &$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); @@ -1000,9 +1033,7 @@ class Notice extends Managed_DataObject // 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($group_ids); + $stored->saveAttentions($act->context->attention); if (!empty($urls)) { $stored->saveKnownUrls($urls); @@ -1251,14 +1282,12 @@ class Notice extends Managed_DataObject $ids[] = $f2p->file_id; } - $files = File::multiGet('id', $ids); - $this->_attachments[$this->id] = $files->fetchAll(); - return $this->_attachments[$this->id]; + return $this->_setAttachments(File::multiGet('id', $ids)->fetchAll()); } - function _setAttachments($attachments) + public function _setAttachments(array $attachments) { - $this->_attachments[$this->id] = $attachments; + return $this->_attachments[$this->id] = $attachments; } static function publicStream($offset=0, $limit=20, $since_id=null, $max_id=null) @@ -1267,9 +1296,9 @@ class Notice extends Managed_DataObject return $stream->getNotices($offset, $limit, $since_id, $max_id); } - static function conversationStream($id, $offset=0, $limit=20, $since_id=null, $max_id=null) + static function conversationStream($id, $offset=0, $limit=20, $since_id=null, $max_id=null, Profile $scoped=null) { - $stream = new ConversationNoticeStream($id); + $stream = new ConversationNoticeStream($id, $scoped); return $stream->getNotices($offset, $limit, $since_id, $max_id); } @@ -1287,8 +1316,9 @@ class Notice extends Managed_DataObject return false; } - $stream = new ConversationNoticeStream($this->conversation); - $notice = $stream->getNotices(/*offset*/ 1, /*limit*/ 1); + //FIXME: Get the Profile::current() stuff some other way + // to avoid confusion between queue processing and session. + $notice = self::conversationStream($this->conversation, 1, 1, null, null, Profile::current()); // if our "offset 1, limit 1" query got a result, return true else false return $notice->N > 0; @@ -1497,12 +1527,16 @@ class Notice extends Managed_DataObject function getProfileTags() { - $profile = $this->getProfile(); - $list = $profile->getOtherTags($profile); $ptags = array(); + try { + $profile = $this->getProfile(); + $list = $profile->getOtherTags($profile); - while($list->fetch()) { - $ptags[] = clone($list); + while($list->fetch()) { + $ptags[] = clone($list); + } + } catch (Exception $e) { + common_log(LOG_ERR, "Error during Notice->getProfileTags() for id=={$this->getID()}: {$e->getMessage()}"); } return $ptags; @@ -1589,6 +1623,53 @@ 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); + return true; + } + /** * Save reply records indicating that this notice needs to be * delivered to the local users with the given URIs. @@ -1663,9 +1744,6 @@ class Notice extends Managed_DataObject $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) - foreach ($mentions as $mention) { foreach ($mention['mentioned'] as $mentioned) { @@ -1676,9 +1754,7 @@ class Notice extends Managed_DataObject } // 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; } @@ -1706,6 +1782,23 @@ class Notice extends Managed_DataObject return $reply; } + protected $_attentionids = array(); + + /** + * Pull the complete list of known activity context attentions for this notice. + * + * @return array of integer profile ids (also group profiles) + */ + function getAttentionProfileIDs() + { + 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()]; + } + protected $_replies = array(); /** @@ -1732,11 +1825,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(); } @@ -1761,7 +1854,7 @@ class Notice extends Managed_DataObject foreach ($recipientIds as $recipientId) { try { $user = User::getByID($recipientId); - mail_notify_attn($user, $this); + mail_notify_attn($user->getProfile(), $this); } catch (NoResultException $e) { // No such user } @@ -1770,6 +1863,23 @@ class Notice extends Managed_DataObject } } + /** + * 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(). @@ -1836,7 +1946,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(); @@ -1901,19 +2011,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) { @@ -2070,7 +2172,7 @@ class Notice extends Managed_DataObject $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)); @@ -2078,6 +2180,11 @@ class Notice extends Managed_DataObject Event::handle('EndActivityObjectFromNotice', array($this, &$object)); } + if (!$object instanceof ActivityObject) { + common_log(LOG_ERR, 'Notice asActivityObject created something else for uri=='._ve($this->getUri()).': '._ve($object)); + throw new ServerException('Notice asActivityObject created something else.'); + } + return $object; } @@ -2391,8 +2498,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 @@ -2562,21 +2672,21 @@ class Notice extends Managed_DataObject */ public static function getAsTimestamp($id) { - if (!$id) { - return false; + if (empty($id)) { + throw new EmptyPkeyValueException('Notice', 'id'); } - $notice = Notice::getKV('id', $id); - if ($notice) { - return $notice->created; + $timestamp = null; + if (Event::handle('GetNoticeSqlTimestamp', array($id, &$timestamp))) { + // getByID throws exception if $id isn't found + $notice = Notice::getByID($id); + $timestamp = $notice->created; } - $deleted = Deleted_notice::getKV('id', $id); - if ($deleted) { - return $deleted->created; + if (empty($timestamp)) { + throw new ServerException('No timestamp found for Notice with id=='._ve($id)); } - - return false; + return $timestamp; } /** @@ -2592,11 +2702,12 @@ class Notice extends Managed_DataObject */ public static function whereSinceId($id, $idField='id', $createdField='created') { - $since = Notice::getAsTimestamp($id); - if ($since) { - return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since); + try { + $since = Notice::getAsTimestamp($id); + } catch (Exception $e) { + return false; } - return false; + return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since); } /** @@ -2631,11 +2742,12 @@ class Notice extends Managed_DataObject */ public static function whereMaxId($id, $idField='id', $createdField='created') { - $max = Notice::getAsTimestamp($id); - if ($max) { - return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id); + try { + $max = Notice::getAsTimestamp($id); + } catch (Exception $e) { + return false; } - return false; + return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id); } /** @@ -2657,10 +2769,10 @@ class Notice extends Managed_DataObject } } - function isPublic() + public function isPublic() { - return (($this->is_local != Notice::LOCAL_NONPUBLIC) && - ($this->is_local != Notice::GATEWAY)); + $is_local = intval($this->is_local); + return !($is_local === Notice::LOCAL_NONPUBLIC || $is_local === Notice::GATEWAY); } /** @@ -2930,6 +3042,19 @@ class Notice extends Managed_DataObject $files = array(); $f2ps = $f2pMap[$notice->id]; foreach ($f2ps as $f2p) { + if (!isset($fileMap[$f2p->file_id])) { + // We have probably deleted value from fileMap since + // it as a NULL entry (see the following elseif). + continue; + } elseif (is_null($fileMap[$f2p->file_id])) { + // If the file id lookup returned a NULL value, it doesn't + // exist in our file table! So this is a remnant file_to_post + // entry that is no longer valid and should be removed. + common_debug('ATTACHMENT deleting f2p for post_id='.$f2p->post_id.' file_id='.$f2p->file_id); + $f2p->delete(); + unset($fileMap[$f2p->file_id]); + continue; + } $files[] = $fileMap[$f2p->file_id]; } $notice->_setAttachments($files); @@ -2958,42 +3083,113 @@ class Notice extends Managed_DataObject // 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; + if (isset($schemadef['fields']['lat']) + && isset($schemadef['fields']['lon']) + && isset($schemadef['fields']['location_id']) + && isset($schemadef['fields']['location_ns'])) { + // 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"; + } + + /** + * Make sure constraints are met before upgrading, if foreign keys + * are not already in use. + * 2016-03-31 + */ + if (!isset($schemadef['foreign keys'])) { + $newschemadef = self::schemaDef(); + printfnq("\nConstraint checking Notice table...\n"); + /** + * Improve typing and make sure no NULL values in any id-related columns are 0 + * 2016-03-31 + */ + foreach (['reply_to', 'repeat_of'] as $field) { + $notice = new Notice(); // reset the object + $notice->query(sprintf('UPDATE %1$s SET %2$s=NULL WHERE %2$s=0', $notice->escapedTableName(), $field)); + // Now we're sure that no Notice entries have repeat_of=0, only an id > 0 or NULL + unset($notice); + } + + /** + * This Will find foreign keys which do not fulfill the constraints and fix + * where appropriate, such as delete when "repeat_of" ID not found in notice.id + * or set to NULL for "reply_to" in the same case. + * 2016-03-31 + * + * XXX: How does this work if we would use multicolumn foreign keys? + */ + foreach (['reply_to' => 'reset', 'repeat_of' => 'delete', 'profile_id' => 'delete'] as $field=>$action) { + $notice = new Notice(); + + $fkeyname = $notice->tableName().'_'.$field.'_fkey'; + assert(isset($newschemadef['foreign keys'][$fkeyname])); + assert($newschemadef['foreign keys'][$fkeyname]); + + $foreign_key = $newschemadef['foreign keys'][$fkeyname]; + $fkeytable = $foreign_key[0]; + assert(isset($foreign_key[1][$field])); + $fkeycol = $foreign_key[1][$field]; + + printfnq("* {$fkeyname} ({$field} => {$fkeytable}.{$fkeycol})\n"); + + // NOTE: Above we set all repeat_of to NULL if they were 0, so this really gets them all. + $notice->whereAdd(sprintf('%1$s NOT IN (SELECT %2$s FROM %3$s)', $field, $fkeycol, $fkeytable)); + if ($notice->find()) { + printfnq("\tFound {$notice->N} notices with {$field} NOT IN notice.id, {$action}ing..."); + switch ($action) { + case 'delete': // since it's a directly dependant notice for an unknown ID we don't want it in our DB + while ($notice->fetch()) { + $notice->delete(); + } + break; + case 'reset': // just set it to NULL to be compatible with our constraints, if it was related to an unknown ID + $ids = []; + foreach ($notice->fetchAll('id') as $id) { + settype($id, 'int'); + $ids[] = $id; + } + unset($notice); + $notice = new Notice(); + $notice->query(sprintf('UPDATE %1$s SET %2$s=NULL WHERE id IN (%3$s)', + $notice->escapedTableName(), + $field, + implode(',', $ids))); + break; + default: + throw new ServerException('The programmer sucks, invalid action name when fixing table.'); + } + printfnq("DONE.\n"); + } + unset($notice); } - $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"; } }