X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=83d5d5fd577e73cb953ccf31c45915e1c900b8f2;hb=3bdb80a182b0fca7840d097be628a29a56e0c542;hp=e71500145d2e000410f6542ad64f34c4164c4873;hpb=c44146d6f83c06d1d10dac6a1e35754c7c783974;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index e71500145d..83d5d5fd57 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -55,10 +55,10 @@ class Notice extends Managed_DataObject public $__table = 'notice'; // table name public $id; // int(4) primary_key not_null public $profile_id; // int(4) multiple_key not_null - public $uri; // varchar(255) unique_key + public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space public $content; // text public $rendered; // text - public $url; // varchar(255) + public $url; // varchar(191) not 255 because utf8mb4 takes more space public $created; // datetime multiple_key not_null default_0000-00-00%2000%3A00%3A00 public $modified; // timestamp not_null default_CURRENT_TIMESTAMP public $reply_to; // int(4) @@ -70,8 +70,8 @@ class Notice extends Managed_DataObject public $location_id; // int(4) public $location_ns; // int(4) public $repeat_of; // int(4) - public $verb; // varchar(255) - public $object_type; // varchar(255) + public $verb; // varchar(191) not 255 because utf8mb4 takes more space + public $object_type; // varchar(191) not 255 because utf8mb4 takes more space public $scope; // int(4) /* the code above is auto generated do not remove the tag below */ @@ -83,10 +83,10 @@ class Notice extends Managed_DataObject 'fields' => array( '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' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + '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'), 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), - 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), + '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'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'), @@ -98,8 +98,8 @@ class Notice extends Managed_DataObject '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' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), - 'verb' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), + 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + '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'), ), @@ -128,7 +128,7 @@ class Notice extends Managed_DataObject return $def; } - + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE = 0; @@ -142,7 +142,7 @@ class Notice extends Managed_DataObject const FOLLOWER_SCOPE = 8; protected $_profile = array(); - + /** * Will always return a profile, if anything fails it will * (through _setProfile) throw a NoProfileException. @@ -150,11 +150,14 @@ class Notice extends Managed_DataObject public function getProfile() { if (!isset($this->_profile[$this->profile_id])) { - $this->_setProfile(Profile::getKV('id', $this->profile_id)); + // We could've sent getKV directly to _setProfile, but occasionally we get + // a "false" (instead of null), likely because it indicates a cache miss. + $profile = Profile::getKV('id', $this->profile_id); + $this->_setProfile($profile instanceof Profile ? $profile : null); } return $this->_profile[$this->profile_id]; } - + public function _setProfile(Profile $profile=null) { if (!$profile instanceof Profile) { @@ -266,21 +269,28 @@ class Notice extends Managed_DataObject return $title; } + public function getContent() + { + return $this->content; + } + /* * Get the original representation URL of this notice. + * + * @param boolean $fallback Whether to fall back to generate a local URL or throw InvalidUrlException */ - public function getUrl() + public function getUrl($fallback=false) { // 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 common_valid_http_url($this->url): // should we allow non-http/https URLs? return $this->url; - case $this->isLocal(): + case !$this->isLocal() && common_valid_http_url($this->uri): // Sometimes we only have the URI for remote posts. + return $this->uri; + case $this->isLocal() || $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); - case common_valid_http_url($this->uri): - return $this->uri; default: common_debug('No URL available for notice: id='.$this->id); throw new InvalidUrlException($this->url); @@ -303,6 +313,16 @@ 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. */ @@ -322,7 +342,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 @@ -406,23 +426,27 @@ 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, - 'reply_to' => null, - 'repeat_of' => null, + 'conversation' => null, // URI of conversation + 'reply_to' => null, // This will override convo URI if the parent is known + 'repeat_of' => null, // This will override convo URI if the repeated notice is known 'scope' => null, 'distribute' => true, '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; } @@ -528,8 +552,7 @@ class Notice extends Managed_DataObject 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); } @@ -569,7 +592,7 @@ class Notice extends Managed_DataObject } // If it's a repeat, the reply_to should be to the original - if (!empty($reply->repeat_of)) { + if ($reply->isRepeat()) { $notice->reply_to = $reply->repeat_of; } else { $notice->reply_to = $reply->id; @@ -592,6 +615,26 @@ class Notice extends Managed_DataObject // Scope set below } + + // 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($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; + } 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']; + } + } 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; + } } if (!empty($lat) && !empty($lon)) { @@ -611,7 +654,7 @@ class Notice extends Managed_DataObject } if (empty($verb)) { - if (!empty($notice->repeat_of)) { + if ($notice->isRepeat()) { $notice->verb = ActivityVerb::SHARE; $notice->object_type = ActivityObject::ACTIVITY; } else { @@ -639,54 +682,31 @@ class Notice extends Managed_DataObject // XXX: some of these functions write to the DB - $id = $notice->insert(); - - if (!$id) { - common_log_db_error($notice, 'INSERT', __FILE__); - // TRANS: Server exception thrown when a notice cannot be saved. - throw new ServerException(_('Problem saving notice.')); - } - - // Update ID-dependent columns: URI, conversation - - $orig = clone($notice); - - $changed = false; - - // We can only get here if it's a local notice, since remote notices - // should've bailed out earlier due to lacking a URI. - if (empty($notice->uri)) { - $notice->uri = sprintf('%s%s=%d:%s=%s', - TagURI::mint(), - 'noticeId', $notice->id, - 'objectType', $notice->get_object_type(true)); - $changed = true; - } - - // If it's not part of a conversation, it's - // the beginning of a new conversation. - - if (empty($notice->conversation)) { - $conv = Conversation::create($notice); - $notice->conversation = $conv->id; - $changed = true; - } - - if ($changed) { - if ($notice->update($orig) === false) { - common_log_db_error($notice, 'UPDATE', __FILE__); - // TRANS: Server exception thrown when a notice cannot be updated. - throw new ServerException(_('Problem saving notice.')); + 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); } + } catch (Exception $e) { + // Let's test if we managed initial insert, which would imply + // failing on some update-part (check 'insert()'). Delete if + // something had been stored to the database. + if (!empty($notice->id)) { + $notice->delete(); + } + throw $e; } - } // Clear the cache for subscribed users, so they'll update at next request // XXX: someone clever could prepend instead of clearing the cache - $notice->blowOnInsert(); - // Save per-notice metadata... if (isset($replies)) { @@ -737,24 +757,22 @@ class Notice extends Managed_DataObject 'and post again in a few minutes.')); } -/* This interferes with stuff like Favorites from old StatusNet installations (first object in objects is the favored notice) // Get ActivityObject properties - $actobj = count($act->objects)==1 ? $act->objects[0] : null; - if (!is_null($actobj) && $actobj->id) { - $options['uri'] = $actobj->id; - if ($actobj->link) { - $options['url'] = $actobj->link; - } elseif ($act->link) { - $options['url'] = $act->link; - } elseif (preg_match('!^https?://!', $actobj->id)) { - $options['url'] = $actobj->id; - } - } else { + if (!empty($act->id)) { // implied object $options['uri'] = $act->id; $options['url'] = $act->link; + } else { + $actobj = count($act->objects)==1 ? $act->objects[0] : null; + if (!is_null($actobj) && !empty($actobj->id)) { + $options['uri'] = $actobj->id; + if ($actobj->link) { + $options['url'] = $actobj->link; + } elseif (preg_match('!^https?://!', $actobj->id)) { + $options['url'] = $actobj->id; + } + } } -*/ $defaults = array( 'groups' => array(), @@ -771,7 +789,7 @@ class Notice extends Managed_DataObject 'distribute' => true); // options will have default values when nothing has been supplied - $options = array_merge($defaults, $options); + $options = array_merge($defaults, $options); foreach (array_keys($defaults) as $key) { // Only convert the keynames we specify ourselves from 'defaults' array into variables $$key = $options[$key]; @@ -793,6 +811,12 @@ 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 @@ -878,14 +902,22 @@ class Notice extends Managed_DataObject try { $stored->insert(); // throws exception on error + $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('No object from StoreActivityObject '.$stored->uri . ': '.$act->asString()); + 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; } - $orig = clone($stored); - $stored->object_type = ActivityUtils::resolveUri($object->getObjectType(), true); + $stored->update($orig); } catch (Exception $e) { if (empty($stored->id)) { @@ -897,8 +929,10 @@ class Notice extends Managed_DataObject throw $e; } } + if (!$stored instanceof Notice) { + throw new ServerException('StartNoticeSave did not give back a Notice'); + } - common_debug(get_called_class().' looking up mentions metadata etc.'); // Save per-notice metadata... $mentions = array(); $groups = array(); @@ -935,8 +969,6 @@ class Notice extends Managed_DataObject // Prepare inbox delivery, may be queued to background. $stored->distribute(); } - - common_debug(get_called_class().' returning stored activity - success!'); return $stored; } @@ -977,12 +1009,13 @@ class Notice extends Managed_DataObject if ($this->isPublic()) { $this->blowStream('public'); + $this->blowStream('networkpublic'); } self::blow('notice:list-ids:conversation:%s', $this->conversation); self::blow('conversation:notice_count:%d', $this->conversation); - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { // XXX: we should probably only use one of these $this->blowStream('notice:repeats:%d', $this->repeat_of); self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of); @@ -1021,6 +1054,7 @@ class Notice extends Managed_DataObject if ($this->isPublic()) { self::blow('public;last'); + self::blow('networkpublic;last'); } self::blow('fave:by_notice', $this->id); @@ -1045,13 +1079,9 @@ class Notice extends Managed_DataObject } $args = func_get_args(); - $format = array_shift($args); - $keyPart = vsprintf($format, $args); - $cacheKey = Cache::key($keyPart); - $c->delete($cacheKey); // delete the "last" stream, too, if this notice is @@ -1093,7 +1123,7 @@ class Notice extends Managed_DataObject * * @return void */ - function saveKnownUrls($urls) + function saveKnownUrls(array $urls) { if (common_config('attachments', 'process_links')) { // @fixme validation? @@ -1165,24 +1195,20 @@ class Notice extends Managed_DataObject } protected $_attachments = array(); - + function attachments() { if (isset($this->_attachments[$this->id])) { return $this->_attachments[$this->id]; } - + $f2ps = File_to_post::listGet('post_id', array($this->id)); - $ids = array(); - foreach ($f2ps[$this->id] as $f2p) { - $ids[] = $f2p->file_id; + $ids[] = $f2p->file_id; } - - $files = File::multiGet('id', $ids); + $files = File::multiGet('id', $ids); $this->_attachments[$this->id] = $files->fetchAll(); - return $this->_attachments[$this->id]; } @@ -1253,12 +1279,18 @@ class Notice extends Managed_DataObject } // If this isn't a reply to anything, then it's its own - // root. + // root if it's the earliest notice in the conversation: if (empty($this->reply_to)) { - return $this; + $root = new Notice; + $root->conversation = $this->conversation; + $root->orderBy('notice.created ASC'); + $root->find(); + $root->fetch(); + $root->free(); + return $root; } - + if (is_null($profile)) { $keypart = sprintf('notice:conversation_root:%d:null', $this->id); } else { @@ -1266,7 +1298,7 @@ class Notice extends Managed_DataObject $this->id, $profile->id); } - + $root = self::cacheGet($keypart); if ($root !== false && $root->inScope($profile)) { @@ -1358,7 +1390,7 @@ class Notice extends Managed_DataObject // Exclude any deleted, non-local, or blocking recipients. $profile = $this->getProfile(); $originalProfile = null; - if ($this->repeat_of) { + if ($this->isRepeat()) { // Check blocks against the original notice's poster as well. $original = Notice::getKV('id', $this->repeat_of); if ($original instanceof Notice) { @@ -1465,7 +1497,7 @@ class Notice extends Managed_DataObject foreach (array_unique($group_ids) as $id) { $group = User_group::getKV('id', $id); if ($group instanceof User_group) { - common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + common_log(LOG_DEBUG, "Local delivery to group id $id, $group->nickname"); $result = $this->addToGroupInbox($group); if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); @@ -1572,7 +1604,7 @@ class Notice extends Managed_DataObject { // Don't save reply data for repeats - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { return array(); } @@ -1661,12 +1693,12 @@ class Notice extends Managed_DataObject $ids[] = $reply->profile_id; } - $this->_replies[$this->id] = $ids; + $this->_setReplies($ids); return $ids; } - function _setReplies($replies) + function _setReplies(array $replies) { $this->_replies[$this->id] = $replies; } @@ -1679,9 +1711,9 @@ class Notice extends Managed_DataObject function getReplyProfiles() { $ids = $this->getReplies(); - + $profiles = Profile::multiGet('id', $ids); - + return $profiles->fetchAll(); } @@ -1695,17 +1727,21 @@ class Notice extends Managed_DataObject { // Don't send reply notifications for repeats - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { return array(); } $recipientIds = $this->getReplies(); + if (Event::handle('StartNotifyMentioned', array($this, &$recipientIds))) { + require_once INSTALLDIR.'/lib/mail.php'; - foreach ($recipientIds as $recipientId) { - $user = User::getKV('id', $recipientId); - if ($user instanceof User) { - mail_notify_attn($user, $this); + foreach ($recipientIds as $recipientId) { + $user = User::getKV('id', $recipientId); + if ($user instanceof User) { + mail_notify_attn($user, $this); + } } + Event::handle('EndNotifyMentioned', array($this, $recipientIds)); } } @@ -1715,9 +1751,9 @@ class Notice extends Managed_DataObject * * @return array of Group objects */ - + protected $_groups = array(); - + function getGroups() { // Don't save groups for repeats @@ -1725,28 +1761,25 @@ class Notice extends Managed_DataObject if (!empty($this->repeat_of)) { return array(); } - + if (isset($this->_groups[$this->id])) { return $this->_groups[$this->id]; } - + $gis = Group_inbox::listGet('notice_id', array($this->id)); $ids = array(); - foreach ($gis[$this->id] as $gi) - { + foreach ($gis[$this->id] as $gi) { $ids[] = $gi->group_id; } - + $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; } @@ -1754,12 +1787,12 @@ class Notice extends Managed_DataObject /** * Convert a notice into an activity for export. * - * @param User $cur Current user + * @param Profile $scoped The currently logged in/scoped profile * * @return Activity activity object representing this Notice. */ - function asActivity($cur=null) + function asActivity(Profile $scoped=null) { $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); @@ -1768,7 +1801,7 @@ class Notice extends Managed_DataObject } $act = new Activity(); - if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { + if (Event::handle('StartNoticeAsActivity', array($this, $act, $scoped))) { $act->id = $this->uri; $act->time = strtotime($this->created); @@ -1782,18 +1815,13 @@ class Notice extends Managed_DataObject $profile = $this->getProfile(); - $act->actor = ActivityObject::fromProfile($profile); - $act->actor->extra[] = $profile->profileInfo($cur); + $act->actor = $profile->asActivityObject(); + $act->actor->extra[] = $profile->profileInfo($scoped); $act->verb = $this->verb; - if ($this->repeat_of) { - $repeated = Notice::getKV('id', $this->repeat_of); - if ($repeated instanceof Notice) { - $act->objects[] = $repeated->asActivity($cur); - } - } else { - $act->objects[] = ActivityObject::fromNotice($this); + if (!$this->repeat_of) { + $act->objects[] = $this->asActivityObject(); } // XXX: should this be handled by default processing for object entry? @@ -1815,9 +1843,9 @@ class Notice extends Managed_DataObject $attachments = $this->attachments(); foreach ($attachments as $attachment) { - // Save local attachments + // Include local attachments in Activity if (!empty($attachment->filename)) { - $act->attachments[] = ActivityObject::fromFile($attachment); + $act->enclosures[] = $attachment->getEnclosure(); } } @@ -1915,7 +1943,7 @@ class Notice extends Managed_DataObject $act->editLink = $act->selfLink; } - Event::handle('EndNoticeAsActivity', array($this, &$act)); + Event::handle('EndNoticeAsActivity', array($this, $act, $scoped)); } self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); @@ -1929,10 +1957,10 @@ class Notice extends Managed_DataObject function asAtomEntry($namespace=false, $source=false, $author=true, - $cur=null) + Profile $scoped=null) { - $act = $this->asActivity($cur); - $act->extra[] = $this->noticeInfo($cur); + $act = $this->asActivity($scoped); + $act->extra[] = $this->noticeInfo($scoped); return $act->asString($namespace, $author, $source); } @@ -1942,12 +1970,12 @@ class Notice extends Managed_DataObject * Clients use some extra notice info in the atom stream. * This gives it to them. * - * @param User $cur Current user + * @param Profile $scoped The currently logged in/scoped profile * * @return array representation of element */ - function noticeInfo($cur) + function noticeInfo(Profile $scoped=null) { // local notice ID (useful to clients for ordering) @@ -1973,9 +2001,7 @@ class Notice extends Managed_DataObject // favorite and repeated - $scoped = null; - if (!empty($cur)) { - $scoped = $cur->getProfile(); + if ($scoped instanceof Profile) { $noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false"; } @@ -2000,10 +2026,29 @@ class Notice extends Managed_DataObject function asActivityNoun($element) { - $noun = ActivityObject::fromNotice($this); + $noun = $this->asActivityObject(); return $noun->asString('activity:' . $element); } + public function asActivityObject() + { + $object = new ActivityObject(); + + if (Event::handle('StartActivityObjectFromNotice', array($this, &$object))) { + $object->type = $this->object_type ?: ActivityObject::NOTE; + $object->id = $this->getUri(); + $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname()); + $object->content = $this->rendered; + $object->link = $this->getUrl(); + + $object->extra[] = array('status_net', array('notice_id' => $this->id)); + + Event::handle('EndActivityObjectFromNotice', array($this, &$object)); + } + + return $object; + } + /** * Determine which notice, if any, a new notice is in reply to. * @@ -2096,6 +2141,18 @@ class Notice extends Managed_DataObject $author->getNickname(), $this->content); + $maxlen = self::maxContent(); + if ($maxlen > 0 && mb_strlen($content) > $maxlen) { + // Web interface and current Twitter API clients will + // pull the original notice's text, but some older + // clients and RSS/Atom feeds will see this trimmed text. + // + // Unfortunately this is likely to lose tags or URLs + // at the end of long notices. + $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; + } + + // Scope is same as this one's return self::saveNew($repeater->id, $content, @@ -2330,20 +2387,47 @@ class Notice extends Managed_DataObject { $result = parent::insert(); - if ($result !== false) { - // Profile::hasRepeated() abuses pkeyGet(), so we - // have to clear manually - if (!empty($this->repeat_of)) { - $c = self::memcache(); - if (!empty($c)) { - $ck = self::multicacheKey('Notice', - array('profile_id' => $this->profile_id, - 'repeat_of' => $this->repeat_of)); - $c->delete($ck); - } + if ($result === false) { + common_log_db_error($this, 'INSERT', __FILE__); + // TRANS: Server exception thrown when a stored object entry cannot be saved. + throw new ServerException('Could not save Notice'); + } + + // Profile::hasRepeated() abuses pkeyGet(), so we + // have to clear manually + if (!empty($this->repeat_of)) { + $c = self::memcache(); + if (!empty($c)) { + $ck = self::multicacheKey('Notice', + array('profile_id' => $this->profile_id, + 'repeat_of' => $this->repeat_of)); + $c->delete($ck); } } + // Update possibly ID-dependent columns: URI, conversation + // (now that INSERT has added the notice's local id) + $orig = clone($this); + $changed = false; + + // We can only get here if it's a local notice, since remote notices + // should've bailed out earlier due to lacking a URI. + if (empty($this->uri)) { + $this->uri = sprintf('%s%s=%d:%s=%s', + TagURI::mint(), + 'noticeId', $this->id, + 'objectType', $this->get_object_type(true)); + $changed = true; + } + + if ($changed && $this->update($orig) === false) { + common_log_db_error($notice, 'UPDATE', __FILE__); + // TRANS: Server exception thrown when a notice cannot be updated. + throw new ServerException(_('Problem saving notice.')); + } + + $this->blowOnInsert(); + return $result; } @@ -2355,31 +2439,34 @@ class Notice extends Managed_DataObject */ function getSource() { + if (empty($this->source)) { + return false; + } + $ns = new Notice_source(); - if (!empty($this->source)) { - switch ($this->source) { - case 'web': - case 'xmpp': - case 'mail': - case 'omb': - case 'system': - case 'api': + switch ($this->source) { + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'system': + case 'api': + $ns->code = $this->source; + break; + default: + $ns = Notice_source::getKV($this->source); + if (!$ns) { + $ns = new Notice_source(); $ns->code = $this->source; - break; - default: - $ns = Notice_source::getKV($this->source); - if (!$ns) { - $ns = new Notice_source(); - $ns->code = $this->source; - $app = Oauth_application::getKV('name', $this->source); - if ($app) { - $ns->name = $app->name; - $ns->url = $app->source_url; - } + $app = Oauth_application::getKV('name', $this->source); + if ($app) { + $ns->name = $app->name; + $ns->url = $app->source_url; } - break; } + break; } + return $ns; } @@ -2395,6 +2482,11 @@ class Notice extends Managed_DataObject $this->is_local == Notice::LOCAL_NONPUBLIC); } + public function isRepeat() + { + return !empty($this->repeat_of); + } + /** * Get the list of hash tags saved with this notice. * @@ -2402,6 +2494,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); @@ -2413,7 +2536,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; } @@ -2537,12 +2662,13 @@ class Notice extends Managed_DataObject function isPublic() { - if (common_config('public', 'localonly')) { - return ($this->is_local == Notice::LOCAL_PUBLIC); - } else { - return (($this->is_local != Notice::LOCAL_NONPUBLIC) && - ($this->is_local != Notice::GATEWAY)); - } + return (($this->is_local != Notice::LOCAL_NONPUBLIC) && + ($this->is_local != Notice::GATEWAY)); + } + + public function isPrivateScope () { + return ($this->scope != Notice::SITE_SCOPE && + $this->scope != Notice::PUBLIC_SCOPE); } /** @@ -2559,7 +2685,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); @@ -2582,7 +2708,7 @@ 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; @@ -2590,89 +2716,69 @@ class Notice extends Managed_DataObject $scope = self::defaultScope(); } - // If there's no scope, anyone (even anon) is in scope. - - if ($scope == 0) { // Not private - + if ($scope == 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public. return !$this->isHiddenSpam($profile); + } - } else { // Private, somehow - - // If there's scope, anon cannot be in scope + // If there's scope, anon cannot be in scope + if (empty($profile)) { + return false; + } - if (empty($profile)) { - return false; - } + // Author is always in scope + if ($this->profile_id == $profile->id) { + return true; + } - // Author is always in scope + // Only for users on this site + if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) { + return false; + } - if ($this->profile_id == $profile->id) { - return true; - } + // Only for users mentioned in the notice + if ($scope & Notice::ADDRESSEE_SCOPE) { - // Only for users on this site + $reply = Reply::pkeyGet(array('notice_id' => $this->id, + 'profile_id' => $profile->id)); - if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) { + if (!$reply instanceof Reply) { return false; } + } - // Only for users mentioned in the notice - - if ($scope & Notice::ADDRESSEE_SCOPE) { - - $reply = Reply::pkeyGet(array('notice_id' => $this->id, - 'profile_id' => $profile->id)); - - if (!$reply instanceof Reply) { - return false; - } - } - - // Only for members of the given group - - if ($scope & Notice::GROUP_SCOPE) { - - // XXX: just query for the single membership + // Only for members of the given group + if ($scope & Notice::GROUP_SCOPE) { - $groups = $this->getGroups(); + // XXX: just query for the single membership - $foundOne = false; + $groups = $this->getGroups(); - foreach ($groups as $group) { - if ($profile->isMember($group)) { - $foundOne = true; - break; - } - } + $foundOne = false; - if (!$foundOne) { - return false; + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; } } - // Only for followers of the author - - $author = null; + if (!$foundOne) { + return false; + } + } - if ($scope & Notice::FOLLOWER_SCOPE) { + if ($scope & Notice::FOLLOWER_SCOPE || $this->getProfile()->isPrivateStream()) { - try { - $author = $this->getProfile(); - } catch (Exception $e) { - return false; - } - - if (!Subscription::exists($profile, $author)) { - return false; - } + if (!Subscription::exists($profile, $this->getProfile())) { + return false; } - - return !$this->isHiddenSpam($profile); } + + return !$this->isHiddenSpam($profile); } - function isHiddenSpam($profile) { - + function isHiddenSpam(Profile $profile=null) { + // Hide posts by silenced users from everyone but moderators. if (common_config('notice', 'hidespam')) { @@ -2722,7 +2828,7 @@ class Notice extends Managed_DataObject $skip = array('_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats'); return array_diff($vars, $skip); } - + static function defaultScope() { $scope = common_config('notice', 'defaultscope'); @@ -2736,10 +2842,9 @@ 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) { try { if (array_key_exists($notice->profile_id, $map)) { @@ -2750,42 +2855,35 @@ class Notice extends Managed_DataObject unset($notices[$entry]); } } - + return array_values($map); } - - static function getProfiles(&$notices) + + static function getProfiles(array &$notices) { $ids = array(); foreach ($notices as $notice) { $ids[] = $notice->profile_id; } - $ids = array_unique($ids); - - return Profile::pivotGet('id', $ids); + 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); - $gids = array(); - foreach ($gis as $id => $gi) - { + foreach ($gis as $id => $gi) { foreach ($gi as $g) { $gids[] = $g->group_id; } } - + $gids = array_unique($gids); - $group = User_group::pivotGet('id', $gids); - foreach ($notices as $notice) { $grps = array(); @@ -2806,24 +2904,19 @@ 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); - $fileIds = array(); - foreach ($f2pMap as $noticeId => $f2ps) { foreach ($f2ps as $f2p) { - $fileIds[] = $f2p->file_id; + $fileIds[] = $f2p->file_id; } } $fileIds = array_unique($fileIds); - $fileMap = File::pivotGet('id', $fileIds); - foreach ($notices as $notice) { $files = array(); @@ -2835,7 +2928,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); @@ -2849,30 +2942,36 @@ class Notice extends Managed_DataObject } } - protected $_repeats = array(); + /** + * 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; - function getRepeats() - { - if (isset($this->_repeats[$this->id])) { - return $this->_repeats[$this->id]; - } - $repeatMap = Notice::listGet('repeat_of', array($this->id)); - $this->_repeats[$this->id] = $repeatMap[$this->id]; - return $this->_repeats[$this->id]; - } + //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] this->tag=' . $this->tag . ',this->id=' . $this->id . ',this->scope=' . $this->scope); - function _setRepeats($repeats) - { - $this->_repeats[$this->id] = $repeats; - } + // Is it private scope? + if ($this->isPrivateScope()) { + // 2) Get current profile + $profile = Profile::current(); - static function fillRepeats(&$notices) - { - $ids = self::_idsOf($notices); - $repeatMap = Notice::listGet('repeat_of', $ids); - foreach ($notices as $notice) { - $repeats = $repeatMap[$notice->id]; - $notice->_setRepeats($repeats); + // 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; } }