/*
* 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);
* 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
* @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;
}
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);
}
// 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)) {
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
if (!empty($notice->id)) {
$notice->delete();
}
+ throw $e;
}
}
'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(),
$stored->verb = $act->verb;
// Use the local user's shortening preferences, if applicable.
- $stored->content = $actor->isLocal()
+ $stored->rendered = $actor->isLocal()
? $actor->shortenLinks($act->content)
: $act->content;
+ $stored->content = common_strip_html($stored->rendered);
$autosource = common_config('public', 'autosource');
try {
$stored->insert(); // throws exception on error
+ $orig = clone($stored); // for updating later in this try clause
+
+ // 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;
+ }
$object = null;
Event::handle('StoreActivityObject', array($act, $stored, $options, &$object));
if (empty($object)) {
throw new ServerException('No object from StoreActivityObject '.$stored->uri . ': '.$act->asString());
}
- $orig = clone($stored);
$stored->object_type = ActivityUtils::resolveUri($object->getObjectType(), true);
$stored->update($orig);
} catch (Exception $e) {
if ($this->isPublic()) {
$this->blowStream('public');
+ $this->blowStream('networkpublic');
}
self::blow('notice:list-ids:conversation:%s', $this->conversation);
if ($this->isPublic()) {
self::blow('public;last');
+ self::blow('networkpublic;last');
}
self::blow('fave:by_notice', $this->id);
*
* @return void
*/
- function saveKnownUrls($urls)
+ function saveKnownUrls(array $urls)
{
if (common_config('attachments', 'process_links')) {
// @fixme validation?
}
// 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)) {
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__);
$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;
}
}
$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;
}
/**
* 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));
}
$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);
$profile = $this->getProfile();
$act->actor = $profile->asActivityObject();
- $act->actor->extra[] = $profile->profileInfo($cur);
+ $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);
+ // TRANS: A repeat activity's title. %1$s is repeater's nickname
+ // and %2$s is the repeated user's nickname.
+ $act->title = sprintf(_('%1$s repeated a notice by %2$s'),
+ $this->getProfile()->getNickname(),
+ $repeated->getProfile()->getNickname());
+ $act->objects[] = $repeated->asActivity($scoped);
}
} else {
$act->objects[] = $this->asActivityObject();
$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);
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);
}
* 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 <statusnet:notice_info> element
*/
- function noticeInfo($cur)
+ function noticeInfo(Profile $scoped=null)
{
// local notice ID (useful to clients for ordering)
// favorite and repeated
- $scoped = null;
- if (!empty($cur)) {
- $scoped = $cur->getProfile();
+ if ($scoped instanceof Profile) {
$noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false";
}
$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,
$changed = true;
}
- // If it's not part of a conversation, it's
- // the beginning of a new conversation.
- if (empty($this->conversation)) {
- $conv = Conversation::create($this);
- $this->conversation = $conv->id;
- $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.
*/
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;
}
*/
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);
} 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;
}
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));
}
/**
*
* @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);
return ($result == 1) ? true : false;
}
- protected function _inScope($profile)
+ protected function _inScope(Profile $profile=null)
{
if (!is_null($this->scope)) {
$scope = $this->scope;
$reply = Reply::pkeyGet(array('notice_id' => $this->id,
'profile_id' => $profile->id));
-
+
if (!$reply instanceof Reply) {
return false;
}
}
}
- function isHiddenSpam($profile) {
-
+ function isHiddenSpam(Profile $profile=null) {
+
// Hide posts by silenced users from everyone but moderators.
if (common_config('notice', 'hidespam')) {
return $scope;
}
- static function fillProfiles($notices)
+ static function fillProfiles(array $notices)
{
$map = self::getProfiles($notices);
return array_values($map);
}
-
- static function getProfiles(&$notices)
+
+ static function getProfiles(array &$notices)
{
$ids = array();
foreach ($notices as $notice) {
return Profile::pivotGet('id', $ids);
}
-
- static function fillGroups(&$notices)
+
+ static function fillGroups(array &$notices)
{
$ids = self::_idsOf($notices);
return array_keys($ids);
}
- static function fillAttachments(&$notices)
+ static function fillAttachments(array &$notices)
{
$ids = self::_idsOf($notices);
}
}
- static function fillReplies(&$notices)
+ static function fillReplies(array &$notices)
{
$ids = self::_idsOf($notices);
$replyMap = Reply::listGet('notice_id', $ids);
return $this->_repeats[$this->id];
}
$repeatMap = Notice::listGet('repeat_of', array($this->id));
- $this->_repeats[$this->id] = $repeatMap[$this->id];
+ $this->_setRepeats($repeatMap[$this->id]);
return $this->_repeats[$this->id];
}
- function _setRepeats($repeats)
+ function _setRepeats(array $repeats)
{
$this->_repeats[$this->id] = $repeats;
}
- static function fillRepeats(&$notices)
+ static function fillRepeats(array &$notices)
{
$ids = self::_idsOf($notices);
$repeatMap = Notice::listGet('repeat_of', $ids);
foreach ($notices as $notice) {
- $repeats = $repeatMap[$notice->id];
+ $repeats = $repeatMap[$notice->id];
$notice->_setRepeats($repeats);
}
}
+
+ /**
+ * Checks whether this notice is in "private scope" (non-public notice)
+ *
+ * @return $isPrivate Whether this notice is private
+ */
+ public function isPrivateScope ()
+ {
+ return ($this->scope != Notice::SITE_SCOPE &&
+ $this->scope != Notice::PUBLIC_SCOPE);
+ }
}