From ef638b0f22e19e97cfac9fa00e19e75d51f1e05e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:30:31 -0400 Subject: [PATCH 01/31] Add scope bitmap for notices --- classes/Notice.php | 1 + classes/statusnet.ini | 1 + db/core.php | 3 +++ 3 files changed, 5 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index 664e5dab9f..36686f6f2d 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -73,6 +73,7 @@ class Notice extends Memcached_DataObject public $location_ns; // int(4) public $repeat_of; // int(4) public $object_type; // varchar(255) + public $scope; // int(4) /* Static get */ function staticGet($k,$v=NULL) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 338e5c5aea..b598f9fc68 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -337,6 +337,7 @@ location_id = 1 location_ns = 1 repeat_of = 1 object_type = 2 +scope = 1 [notice__keys] id = N diff --git a/db/core.php b/db/core.php index 16a59462d4..4881cc0fff 100644 --- a/db/core.php +++ b/db/core.php @@ -202,6 +202,9 @@ $schema['notice'] = array( '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'), + 'scope' => array('type' => 'int', + 'default' => '1', + 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers'), ), 'primary key' => array('id'), 'unique keys' => array( From b8735f49117f6e03b9d611e98bb5d82a43d19f00 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:54:57 -0400 Subject: [PATCH 02/31] add scope flags for Notice --- classes/Notice.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index 36686f6f2d..a4dcc10f5a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -90,6 +90,10 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const FOLLOWER_SCOPE = 4; + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); From 9af92f94bded2ee788785519f957756738e00738 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:55:14 -0400 Subject: [PATCH 03/31] function for checking scope rules for Profile --- classes/Profile.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index 88edf5cbb3..ba7d6296f8 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1076,4 +1076,44 @@ class Profile extends Memcached_DataObject return $profile; } + + function canRead(Notice $notice) + { + if ($notice->scope & Notice::SITE_SCOPE) { + $user = $this->getUser(); + if (empty($user)) { + return false; + } + } + + if ($notice->scope & Notice::ADDRESSEE_SCOPE) { + $replies = $notice->getReplies(); + + if (!in_array($this->id, $replies)) { + $groups = $notice->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($this->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + } + + if ($notice->scope & Notice::FOLLOWER_SCOPE) { + $author = $notice->getProfile(); + if (!Subscription::exists($this, $author)) { + return false; + } + } + + return true; + } } From 7f74aa6c203f0a21f634ea833679369b0f1fc2bf Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:30:31 -0400 Subject: [PATCH 04/31] Add scope bitmap for notices --- classes/Notice.php | 1 + classes/statusnet.ini | 1 + db/core.php | 3 +++ 3 files changed, 5 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index b228a49c7c..b432ad1ea5 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -73,6 +73,7 @@ class Notice extends Memcached_DataObject public $location_ns; // int(4) public $repeat_of; // int(4) public $object_type; // varchar(255) + public $scope; // int(4) /* Static get */ function staticGet($k,$v=NULL) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index f648fb3fbf..12c59daae0 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -337,6 +337,7 @@ location_id = 1 location_ns = 1 repeat_of = 1 object_type = 2 +scope = 1 [notice__keys] id = N diff --git a/db/core.php b/db/core.php index 928186d94d..3e439e5010 100644 --- a/db/core.php +++ b/db/core.php @@ -202,6 +202,9 @@ $schema['notice'] = array( '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'), + 'scope' => array('type' => 'int', + 'default' => '1', + 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers'), ), 'primary key' => array('id'), 'unique keys' => array( From 6cdbe47e722f8e77beade6f5c17b5a7adc99b1ec Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:54:57 -0400 Subject: [PATCH 05/31] add scope flags for Notice --- classes/Notice.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index b432ad1ea5..2b437c79f4 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -90,6 +90,10 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const FOLLOWER_SCOPE = 4; + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); From 7fc5679e7e09972fa88f711f0f88eeeab8131dad Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 16 Mar 2011 22:55:14 -0400 Subject: [PATCH 06/31] function for checking scope rules for Profile --- classes/Profile.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index 9566226951..fe097753d9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1156,4 +1156,44 @@ class Profile extends Memcached_DataObject return $profile; } + + function canRead(Notice $notice) + { + if ($notice->scope & Notice::SITE_SCOPE) { + $user = $this->getUser(); + if (empty($user)) { + return false; + } + } + + if ($notice->scope & Notice::ADDRESSEE_SCOPE) { + $replies = $notice->getReplies(); + + if (!in_array($this->id, $replies)) { + $groups = $notice->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($this->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + } + + if ($notice->scope & Notice::FOLLOWER_SCOPE) { + $author = $notice->getProfile(); + if (!Subscription::exists($this, $author)) { + return false; + } + } + + return true; + } } From 26a4bd7dbf5ed93c03ff5cb65d86f70792fcb94a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 22 Mar 2011 11:36:48 -0400 Subject: [PATCH 07/31] move scope check to Notice so we can have a null profile --- classes/Notice.php | 83 ++++++++++++++++++++++++++++++++++++++++++++- classes/Profile.php | 40 ---------------------- 2 files changed, 82 insertions(+), 41 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 2b437c79f4..03ce36640b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -92,7 +92,8 @@ class Notice extends Memcached_DataObject const SITE_SCOPE = 1; const ADDRESSEE_SCOPE = 2; - const FOLLOWER_SCOPE = 4; + const GROUP_SCOPE = 4; + const FOLLOWER_SCOPE = 8; function getProfile() { @@ -2186,4 +2187,84 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + /** + * Check that the given profile is allowed to read, respond to, or otherwise + * act on this notice. + * + * The $scope member is a bitmask of scopes, representing a logical AND of the + * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means + * "only visible to people who are mentioned in the notice AND are users on this site." + * Users on the site who are not mentioned in the notice will not be able to see the + * notice. + * + * @param Profile $profile The profile to check + * + * @return boolean whether the profile is in the notice's scope + */ + + function inScope($profile) + { + // If there's any scope, and there's no logged-in user, + // not allowed. + + if ($this->scope > 0 && empty($profile)) { + return false; + } + + // Only for users on this site + + if ($this->scope & Notice::SITE_SCOPE) { + $user = $profile->getUser(); + if (empty($user)) { + return false; + } + } + + // Only for users mentioned in the notice + + if ($this->scope & Notice::ADDRESSEE_SCOPE) { + + // XXX: just query for the single reply + + $replies = $this->getReplies(); + + if (!in_array($profile->id, $replies)) { + return false; + } + } + + // Only for members of the given group + + if ($this->scope & Notice::GROUP_SCOPE) { + + // XXX: just query for the single membership + + $groups = $this->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + + // Only for followers of the author + + if ($this->scope & Notice::FOLLOWER_SCOPE) { + $author = $this->getProfile(); + if (!Subscription::exists($profile, $author)) { + return false; + } + } + + return true; + } } diff --git a/classes/Profile.php b/classes/Profile.php index fe097753d9..9566226951 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1156,44 +1156,4 @@ class Profile extends Memcached_DataObject return $profile; } - - function canRead(Notice $notice) - { - if ($notice->scope & Notice::SITE_SCOPE) { - $user = $this->getUser(); - if (empty($user)) { - return false; - } - } - - if ($notice->scope & Notice::ADDRESSEE_SCOPE) { - $replies = $notice->getReplies(); - - if (!in_array($this->id, $replies)) { - $groups = $notice->getGroups(); - - $foundOne = false; - - foreach ($groups as $group) { - if ($this->isMember($group)) { - $foundOne = true; - break; - } - } - - if (!$foundOne) { - return false; - } - } - } - - if ($notice->scope & Notice::FOLLOWER_SCOPE) { - $author = $notice->getProfile(); - if (!Subscription::exists($this, $author)) { - return false; - } - } - - return true; - } } From 31e7d46a5b03c9082d38beed413b4af91a050a63 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 22 Mar 2011 18:15:53 -0400 Subject: [PATCH 08/31] add profile to stream function --- classes/Notice.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/classes/Notice.php b/classes/Notice.php index 03ce36640b..4e688218f8 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1546,8 +1546,17 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $profile=0) { + if ($profile === 0) { + $user = common_current_user(); + if (empty($user)) { + $profile = null; + } else { + $profile = $user->getProfile(); + } + } + $cache = Cache::instance(); if (empty($cache) || From 84984fdbfecdd79630801e2a6ca5b5e9e4b725a1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 25 Mar 2011 12:22:22 -0400 Subject: [PATCH 09/31] All notice streams check notice scope Added filtering code so that notice streams check notice scope. Added new class to implement filtering a stream, FilteringNoticeStream. Added a subclass that does the logic for checking Notice scope. And made all the streams use ScopingNoticeStream. --- lib/conversationnoticestream.php | 62 +++++++++++++++- lib/favenoticestream.php | 60 +++++++++++++++- lib/filenoticestream.php | 53 ++++++++++++-- lib/filteringnoticestream.php | 115 ++++++++++++++++++++++++++++++ lib/groupnoticestream.php | 62 ++++++++++++++-- lib/profilenoticestream.php | 6 +- lib/publicnoticestream.php | 61 +++++++++++++++- lib/repeatedbymenoticestream.php | 62 +++++++++++++++- lib/repeatsofmenoticestream.php | 61 +++++++++++++++- lib/replynoticestream.php | 62 +++++++++++++++- lib/scopingnoticestream.php | 78 ++++++++++++++++++++ lib/taggedprofilenoticestream.php | 64 +++++++++++++++-- lib/tagnoticestream.php | 62 +++++++++++++++- 13 files changed, 774 insertions(+), 34 deletions(-) create mode 100644 lib/filteringnoticestream.php create mode 100644 lib/scopingnoticestream.php diff --git a/lib/conversationnoticestream.php b/lib/conversationnoticestream.php index b26e898612..d7338d0245 100644 --- a/lib/conversationnoticestream.php +++ b/lib/conversationnoticestream.php @@ -1,14 +1,70 @@ . + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class ConversationNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Notice stream for a conversation + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ConversationNoticeStream extends ScopingNoticeStream { function __construct($id) { - parent::__construct(new RawConversationNoticeStream($id), - 'notice:conversation_ids:'.$id); + parent::__construct(new CachingNoticeStream(new RawConversationNoticeStream($id), + 'notice:conversation_ids:'.$id)); } } +/** + * Notice stream for a conversation + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawConversationNoticeStream extends NoticeStream { protected $id; diff --git a/lib/favenoticestream.php b/lib/favenoticestream.php index 5aaad5ce5b..987805cf9d 100644 --- a/lib/favenoticestream.php +++ b/lib/favenoticestream.php @@ -1,6 +1,51 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class FaveNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Notice stream for favorites + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class FaveNoticeStream extends ScopingNoticeStream { function __construct($user_id, $own) { @@ -10,10 +55,21 @@ class FaveNoticeStream extends CachingNoticeStream } else { $key = 'fave:ids_by_user:'.$user_id; } - parent::__construct($stream, $key); + parent::__construct(new CachingNoticeStream($stream, $key)); } } +/** + * Raw notice stream for favorites + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawFaveNoticeStream extends NoticeStream { protected $user_id; diff --git a/lib/filenoticestream.php b/lib/filenoticestream.php index fddc5d33ce..8c01893634 100644 --- a/lib/filenoticestream.php +++ b/lib/filenoticestream.php @@ -1,22 +1,67 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class FileNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +class FileNoticeStream extends ScopingNoticeStream { function __construct($file) { - parent::__construct(new RawFileNoticeStream($file), - 'file:notice-ids:'.$this->url); + parent::__construct(new CachingNoticeStream(new RawFileNoticeStream($file), + 'file:notice-ids:'.$this->url)); } } +/** + * Raw stream for a file + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawFileNoticeStream extends NoticeStream { protected $file = null; function __construct($file) { - $this->file = $file; parent::__construct(); + $this->file = $file; } /** diff --git a/lib/filteringnoticestream.php b/lib/filteringnoticestream.php new file mode 100644 index 0000000000..a3bdc08af6 --- /dev/null +++ b/lib/filteringnoticestream.php @@ -0,0 +1,115 @@ +. + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A class for presenting a filtered notice stream based on an upstream stream + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +abstract class FilteringNoticeStream extends NoticeStream +{ + protected $upstream; + + function __construct($upstream) + { + $this->upstream = $upstream; + } + + abstract function filter($notice); + + function getNotices($offset, $limit, $sinceId, $maxId) + { + // "offset" is virtual; we have to get a lot + $total = $offset + $limit; + + $filtered = array(); + + $startAt = 0; + $askFor = $total; + + // Keep going till we have $total notices in $notices array, + // or we get nothing from upstream. + + $results = null; + + do { + + $raw = $this->upstream->getNotices($startAt, $askFor, $sinceId, $maxId); + + $results = $raw->N; + + if ($results == 0) { + break; + } + + while ($raw->fetch()) { + if ($this->filter($raw)) { + $filtered[] = clone($raw); + if (count($filtered >= $total)) { + break; + } + } + } + + // XXX: make these smarter; factor hit rate into $askFor + + $startAt += $askFor; + $askFor = max($total - count($filtered), NOTICES_PER_PAGE); + + } while (count($filtered) < $total && $results !== 0); + + return new ArrayWrapper(array_slice($filtered, $offset, $limit)); + } + + function getNoticeIds($offset, $limit, $sinceId, $maxId) + { + $notices = $this->getNotices($offset, $limit, $sinceId, $maxId); + + $ids = array(); + + while ($notices->fetch()) { + $ids[] = $notice->id; + } + + return $ids; + } +} diff --git a/lib/groupnoticestream.php b/lib/groupnoticestream.php index a6aa2c352c..22d94d0482 100644 --- a/lib/groupnoticestream.php +++ b/lib/groupnoticestream.php @@ -1,14 +1,68 @@ -. + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class GroupNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of notices for a group + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class GroupNoticeStream extends ScopingNoticeStream { function __construct($group) { - parent::__construct(new RawGroupNoticeStream($group), - 'user_group:notice_ids:' . $group->id); + parent::__construct(new CachingNoticeStream(new RawGroupNoticeStream($group), + 'user_group:notice_ids:' . $group->id)); } } +/** + * Stream of notices for a group + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ class RawGroupNoticeStream extends NoticeStream { protected $group; diff --git a/lib/profilenoticestream.php b/lib/profilenoticestream.php index 9324bfb85d..ca54ad4892 100644 --- a/lib/profilenoticestream.php +++ b/lib/profilenoticestream.php @@ -45,12 +45,12 @@ if (!defined('STATUSNET')) { * @link http://status.net/ */ -class ProfileNoticeStream extends CachingNoticeStream +class ProfileNoticeStream extends ScopingNoticeStream { function __construct($profile) { - parent::__construct(new RawProfileNoticeStream($profile), - 'profile:notice_ids:' . $profile->id); + parent::__construct(new CachingNoticeStream(new RawProfileNoticeStream($profile), + 'profile:notice_ids:' . $profile->id)); } } diff --git a/lib/publicnoticestream.php b/lib/publicnoticestream.php index 0162375451..19d0ad96a0 100644 --- a/lib/publicnoticestream.php +++ b/lib/publicnoticestream.php @@ -1,13 +1,70 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class PublicNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Public stream + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class PublicNoticeStream extends ScopingNoticeStream { function __construct() { - parent::__construct(new RawPublicNoticeStream(), 'public'); + parent::__construct(new CachingNoticeStream(new RawPublicNoticeStream(), + 'public')); } } +/** + * Raw public stream + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawPublicNoticeStream extends NoticeStream { function getNoticeIds($offset=0, $limit=20, $since_id=0, $max_id=0) diff --git a/lib/repeatedbymenoticestream.php b/lib/repeatedbymenoticestream.php index 2c4c00ebf9..98c1583d6a 100644 --- a/lib/repeatedbymenoticestream.php +++ b/lib/repeatedbymenoticestream.php @@ -1,14 +1,70 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class RepeatedByMeNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of notices repeated by me + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class RepeatedByMeNoticeStream extends ScopingNoticeStream { function __construct($user) { - parent::__construct(new RawRepeatedByMeNoticeStream($user), - 'user:repeated_by_me:'.$user->id); + parent::__construct(new CachingNoticeStream(new RawRepeatedByMeNoticeStream($user), + 'user:repeated_by_me:'.$user->id)); } } +/** + * Raw stream of notices repeated by me + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawRepeatedByMeNoticeStream extends NoticeStream { protected $user; diff --git a/lib/repeatsofmenoticestream.php b/lib/repeatsofmenoticestream.php index 1441908e5a..f51fc9e447 100644 --- a/lib/repeatsofmenoticestream.php +++ b/lib/repeatsofmenoticestream.php @@ -1,14 +1,69 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class RepeatsOfMeNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of notices that are repeats of mine + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class RepeatsOfMeNoticeStream extends ScopingNoticeStream { function __construct($user) { - parent::__construct(new RawRepeatsOfMeNoticeStream($user), - 'user:repeats_of_me:'.$user->id); + parent::__construct(new CachingNoticeStream(new RawRepeatsOfMeNoticeStream($user), + 'user:repeats_of_me:'.$user->id)); } } +/** + * Raw stream of notices that are repeats of mine + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ class RawRepeatsOfMeNoticeStream extends NoticeStream { protected $user; diff --git a/lib/replynoticestream.php b/lib/replynoticestream.php index f358afcc54..d9214b7107 100644 --- a/lib/replynoticestream.php +++ b/lib/replynoticestream.php @@ -1,14 +1,70 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class ReplyNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of mentions of me + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ReplyNoticeStream extends ScopingNoticeStream { function __construct($userId) { - parent::__construct(new RawReplyNoticeStream($userId), - 'reply:stream:' . $userId); + parent::__construct(new CachingNoticeStream(new RawReplyNoticeStream($userId), + 'reply:stream:' . $userId)); } } +/** + * Raw stream of mentions of me + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawReplyNoticeStream extends NoticeStream { protected $userId; diff --git a/lib/scopingnoticestream.php b/lib/scopingnoticestream.php new file mode 100644 index 0000000000..a7ecbcd56b --- /dev/null +++ b/lib/scopingnoticestream.php @@ -0,0 +1,78 @@ +. + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Class comment + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ScopingNoticeStream extends FilteringNoticeStream +{ + protected $profile; + + function __construct($upstream, $profile = null) + { + parent::__construct($upstream); + + if (empty($profile)) { + $user = common_current_user(); + if (!empty($user)) { + $profile = $user->getProfile(); + } + } + $this->profile = $profile; + } + + /** + * Only return notices where the profile is in scope + * + * @param Notice $notice The notice to check + * + * @return boolean whether to include the notice + */ + + function filter($notice) + { + return $notice->inScope($this->profile); + } + +} diff --git a/lib/taggedprofilenoticestream.php b/lib/taggedprofilenoticestream.php index d1711876eb..83c304ed8f 100644 --- a/lib/taggedprofilenoticestream.php +++ b/lib/taggedprofilenoticestream.php @@ -1,14 +1,70 @@ -. + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class TaggedProfileNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of notices with a given profile and tag + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class TaggedProfileNoticeStream extends ScopingNoticeStream { function __construct($profile, $tag) { - parent::__construct(new RawTaggedProfileNoticeStream($profile, $tag), - 'profile:notice_ids_tagged:'.$profile->id.':'.Cache::keyize($tag)); + parent::__construct(new CachingNoticeStream(new RawTaggedProfileNoticeStream($profile, $tag), + 'profile:notice_ids_tagged:'.$profile->id.':'.Cache::keyize($tag))); } } +/** + * Raw stream of notices with a given profile and tag + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawTaggedProfileNoticeStream extends NoticeStream { protected $profile; diff --git a/lib/tagnoticestream.php b/lib/tagnoticestream.php index 0e287744dd..1dcf9f14bb 100644 --- a/lib/tagnoticestream.php +++ b/lib/tagnoticestream.php @@ -1,14 +1,70 @@ . + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ -class TagNoticeStream extends CachingNoticeStream +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Stream of notices with a given tag + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class TagNoticeStream extends ScopingNoticeStream { function __construct($tag) { - parent::__construct(new RawTagNoticeStream($tag), - 'notice_tag:notice_ids:' . Cache::keyize($tag)); + parent::__construct(new CachingNoticeStream(new RawTagNoticeStream($tag), + 'notice_tag:notice_ids:' . Cache::keyize($tag))); } } +/** + * Raw stream of notices with a given tag + * + * @category Stream + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class RawTagNoticeStream extends NoticeStream { protected $tag; From c7f866b03251dfdbab3d932f6d87ae2b349cd1c4 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 26 Mar 2011 15:49:46 -0400 Subject: [PATCH 10/31] Caller can set scope for Notice::saveNew() --- README | 2 ++ classes/Notice.php | 8 ++++++++ lib/default.php | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README b/README index 0dcbeea239..ef032cad84 100644 --- a/README +++ b/README @@ -1472,6 +1472,8 @@ Configuration options specific to notices. contentlimit: max length of the plain-text content of a notice. Default is null, meaning to use the site-wide text limit. 0 means no limit. +defaultscope: default scope for notices. Defaults to 0; set to + 1 to keep notices private to this site by default. message ------- diff --git a/classes/Notice.php b/classes/Notice.php index 83507f3bc0..b380f027a2 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -249,6 +249,7 @@ class Notice extends Memcached_DataObject * notice in place of extracting links from content * boolean 'distribute' whether to distribute the notice, default true * string 'object_type' URL of the associated object type (default ActivityObject::NOTE) + * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise * * @fixme tag override * @@ -260,6 +261,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -374,6 +376,12 @@ class Notice extends Memcached_DataObject $notice->object_type = $object_type; } + if (is_null($scope)) { // 0 is a valid value + $notice->scope = common_config('notice', 'defaultscope'); + } else { + $notice->scope = $scope; + } + if (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB diff --git a/lib/default.php b/lib/default.php index e6caf0301a..9872d8ffd3 100644 --- a/lib/default.php +++ b/lib/default.php @@ -288,7 +288,8 @@ $default = array('enabled' => true, 'css' => ''), 'notice' => - array('contentlimit' => null), + array('contentlimit' => null, + 'defaultscope' => 0), // set to 0 for default open 'message' => array('contentlimit' => null), 'location' => From 47b3fdf059c5b7a3f4e0b1b892c8b7f6439b4f7d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 26 Mar 2011 16:06:17 -0400 Subject: [PATCH 11/31] add scope limit flags to some notices in createsim.php --- scripts/createsim.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/createsim.php b/scripts/createsim.php index 3244cda104..b460be1dd2 100644 --- a/scripts/createsim.php +++ b/scripts/createsim.php @@ -71,7 +71,7 @@ function newNotice($i, $tagmax) { global $userprefix; - $options = array(); + $options = array('scope' => common_config('notice', 'defaultscope')); $n = rand(0, $i - 1); $user = User::staticGet('nickname', sprintf('%s%d', $userprefix, $n)); @@ -95,6 +95,10 @@ function newNotice($i, $tagmax) $rprofile = $notices->getProfile(); $content = "@".$rprofile->nickname." ".$content; } + $private_to_addressees = rand(0, 4); + if ($private_to_addressees == 0) { + $options['scope'] |= Notice::ADDRESSEE_SCOPE; + } } } @@ -120,9 +124,19 @@ function newNotice($i, $tagmax) } $options['groups'] = array($groups->id); $content = "!".$groups->nickname." ".$content; + $private_to_group = rand(0, 2); + if ($private_to_group == 0) { + $options['scope'] |= Notice::GROUP_SCOPE; + } } } + $private_to_site = rand(0, 4); + + if ($private_to_site == 0) { + $options['scope'] |= Notice::SITE_SCOPE; + } + $notice = Notice::saveNew($user->id, $content, 'system', $options); } From 82b38b62a4bc40d0782cd0be3ee57cf00e77dd36 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 26 Mar 2011 16:23:20 -0400 Subject: [PATCH 12/31] clearer scope rules for anonymous and author --- classes/Notice.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index b380f027a2..f6d0a689cb 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -2043,10 +2043,21 @@ class Notice extends Memcached_DataObject function inScope($profile) { - // If there's any scope, and there's no logged-in user, - // not allowed. + // If there's no scope, anyone (even anon) is in scope. - if ($this->scope > 0 && empty($profile)) { + if ($this->scope == 0) { + return true; + } + + // If there's scope, anon cannot be in scope + + if (empty($profile)) { + return false; + } + + // Author is always in scope + + if ($this->profile_id == $profile->id) { return false; } From e11c69fd81b8232cbb02aeb3aab32f04b928e321 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 26 Mar 2011 16:47:18 -0400 Subject: [PATCH 13/31] always allow author to see own notices --- classes/Notice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/Notice.php b/classes/Notice.php index f6d0a689cb..69ed959f38 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -2058,7 +2058,7 @@ class Notice extends Memcached_DataObject // Author is always in scope if ($this->profile_id == $profile->id) { - return false; + return true; } // Only for users on this site From 53a3fd822d86fdf36ace165a974d26372bf46ece Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 26 Mar 2011 16:47:36 -0400 Subject: [PATCH 14/31] add indicator for limited-scope notices --- lib/noticelistitem.php | 6 +++++- theme/rebase/css/display.css | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php index 17827d07ef..46f15f551d 100644 --- a/lib/noticelistitem.php +++ b/lib/noticelistitem.php @@ -170,7 +170,11 @@ class NoticeListItem extends Widget { if (Event::handle('StartOpenNoticeListItemElement', array($this))) { $id = (empty($this->repeat)) ? $this->notice->id : $this->repeat->id; - $this->out->elementStart('li', array('class' => 'hentry notice', + $class = 'hentry notice'; + if ($this->notice->scope != 0 && $this->notice->scope != 1) { + $class .= ' limited-scope'; + } + $this->out->elementStart('li', array('class' => $class, 'id' => 'notice-' . $id)); Event::handle('EndOpenNoticeListItemElement', array($this)); } diff --git a/theme/rebase/css/display.css b/theme/rebase/css/display.css index 6c3c9e6296..9c019e6289 100644 --- a/theme/rebase/css/display.css +++ b/theme/rebase/css/display.css @@ -1156,6 +1156,11 @@ width:auto; margin-left:0; } +.limited-scope .entry-content .timestamp:before { +content:'☠'; +font-size:150%; +} + /* override OStatus plugin style */ #form_ostatus_connect.form_settings.dialogbox, #form_ostatus_sub.dialogbox { From 5147404ea2cd5b41d5cad8f3c3769cbde0a2e2a9 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 11:02:20 -0400 Subject: [PATCH 15/31] ToSelector widget to send private notices A new widget, ToSelector (Sorry, couldn't think of anything better) that lets you select an addressee for a notice and whether it's private. --- actions/newnotice.php | 4 ++ lib/noticeform.php | 30 +++++++- lib/toselector.php | 156 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 lib/toselector.php diff --git a/actions/newnotice.php b/actions/newnotice.php index 7f697e23f3..f48134c3b2 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -209,6 +209,10 @@ class NewnoticeAction extends Action $author_id = $user->id; $text = $content_shortened; + // Does the heavy-lifting for getting "To:" information + + ToSelector::fillOptions($this, $options); + if (Event::handle('StartNoticeSaveWeb', array($this, &$author_id, &$text, &$options))) { $notice = Notice::saveNew($user->id, $content_shortened, 'web', $options); diff --git a/lib/noticeform.php b/lib/noticeform.php index 2cbacc9280..01a462aad1 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -79,6 +79,15 @@ class NoticeForm extends Form var $location_id; var $location_ns; + /** select this group from the drop-down by default. */ + var $to_group; + + /** select this user from the drop-down by default. */ + var $to_user; + + /** Pre-click the private checkbox. */ + var $private; + /** * Constructor * @@ -109,7 +118,8 @@ class NoticeForm extends Form $this->actionName = $action->trimmed('action'); $prefill = array('content', 'inreplyto', 'lat', - 'lon', 'location_id', 'location_ns'); + 'lon', 'location_id', 'location_ns', + 'to_group', 'to_profile', 'private'); foreach ($prefill as $fieldName) { if (array_key_exists($fieldName, $options)) { @@ -117,6 +127,16 @@ class NoticeForm extends Form } } + // Prefill the profile if we're replying + + if (empty($this->to_profile) && + !empty($this->inreplyto)) { + $notice = Notice::staticGet('id', $this->inreplyto); + if (!empty($notice)) { + $this->to_profile = $notice->getProfile(); + } + } + if (array_key_exists('user', $options)) { $this->user = $options['user']; } else { @@ -218,6 +238,14 @@ class NoticeForm extends Form } $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); + $this->out->elementStart('div'); + $toWidget = new ToSelector($this->out, + $this->user, + (!empty($this->to_group) ? $this->to_group : $this->to_user)); + + $toWidget->show(); + $this->out->elementEnd('div'); + if ($this->user->shareLocation()) { $this->out->hidden('notice_data-lat', empty($this->lat) ? (empty($this->profile->lat) ? null : $this->profile->lat) : $this->lat, 'lat'); $this->out->hidden('notice_data-lon', empty($this->lon) ? (empty($this->profile->lon) ? null : $this->profile->lon) : $this->lon, 'lon'); diff --git a/lib/toselector.php b/lib/toselector.php new file mode 100644 index 0000000000..6dd4b5c9f4 --- /dev/null +++ b/lib/toselector.php @@ -0,0 +1,156 @@ +. + * + * @category Widget + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Widget showing a drop-down of potential addressees + * + * @category Widget + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ToSelector extends Widget +{ + protected $user; + protected $to; + protected $id; + protected $name; + protected $private; + + /** + * Constructor + * + * @param HTMLOutputter $out output context + * @param User $user Current user + * @param mixed $to Default selection for addressee + */ + function __construct($out, $user, $to, $private=false, $id='notice_to', $name='notice_to') + { + parent::__construct($out); + + $this->user = $user; + $this->to = $to; + $this->private = $private; + $this->id = $id; + $this->name = $name; + } + + /** + * Constructor + * + * @param HTMLOutputter $out output context + * @param User $user Current user + * @param mixed $to Default selection for addressee + */ + function show() + { + $choices = array(); + $default = 'public:site'; + + if (!common_config('site', 'private')) { + $choices['public:everyone'] = _('Everyone'); + $default = 'public:everyone'; + } + // XXX: better name...? + $choices['public:site'] = sprintf(_('My colleagues at %s'), common_config('site', 'name')); + + $groups = $this->user->getGroups(); + + while ($groups->fetch()) { + $value = 'group:'.$groups->id; + if (($this->to instanceof User_group) && $this->to->id == $groups->id) { + $default = $value; + } + $choices[$value] = $groups->getBestName(); + } + + // XXX: add users...? + + if ($this->to instanceof Profile) { + $value = 'profile:'.$this->to->id; + $default = $value; + $choices[$value] = $this->to->getBestName(); + } + + $this->out->dropdown($this->id, + _('To:'), + $choices, + null, + false, + $default); + + $this->out->checkbox('notice_private', + _('Private'), + $this->private); + } + + static function fillOptions($action, &$options) + { + // XXX: make arg name selectable + $toArg = $action->trimmed('notice_to'); + $private = $action->boolean('notice_private'); + + list($prefix, $value) = explode(':', $toArg); + switch ($prefix) { + case 'group': + $options['groups'] = array($value); + if ($private) { + $options['scope'] = Notice::GROUP_SCOPE; + } + break; + case 'profile': + $profile = Profile::staticGet('id', $value); + $options['replies'] = $profile->getUri(); + if ($private) { + $options['scope'] = Notice::ADDRESSEE_SCOPE; + } + break; + case 'public': + if ($value == 'everyone' && !common_config('site', 'private')) { + $options['scope'] = 0; + } else if ($value == 'site') { + $options['scope'] = Notice::SITE_SCOPE; + } + break; + default: + throw new ClientException('Unknown to value: ' . toArg); + break; + } + } +} From b0deaad700e72a06bf11f044236ed9c27e0eccff Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 12:01:08 -0400 Subject: [PATCH 16/31] Add a check to prevent replying to an unscoped notice --- classes/Notice.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index 69ed959f38..3780d52d56 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -351,6 +351,10 @@ class Notice extends Memcached_DataObject if (!empty($notice->reply_to)) { $reply = Notice::staticGet('id', $notice->reply_to); + if (!$reply->inScope($profile)) { + throw new ClientException(sprintf(_("%s has no access to notice %d"), + $profile->nickname, $reply->id), 403); + } $notice->conversation = $reply->conversation; } From b1783e8d491aa82a1ca7049cd81187624cd09a37 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 12:13:46 -0400 Subject: [PATCH 17/31] make to_user/to_profile consistent in NoticeForm --- lib/noticeform.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/noticeform.php b/lib/noticeform.php index 01a462aad1..0f4207e4ae 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -83,7 +83,7 @@ class NoticeForm extends Form var $to_group; /** select this user from the drop-down by default. */ - var $to_user; + var $to_profile; /** Pre-click the private checkbox. */ var $private; @@ -241,7 +241,7 @@ class NoticeForm extends Form $this->out->elementStart('div'); $toWidget = new ToSelector($this->out, $this->user, - (!empty($this->to_group) ? $this->to_group : $this->to_user)); + (!empty($this->to_group) ? $this->to_group : $this->to_profile)); $toWidget->show(); $this->out->elementEnd('div'); From 683bd3f2b5bf8f9e5e2f566384184d4896bfc2e2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 12:56:18 -0400 Subject: [PATCH 18/31] don't push twitter stuff public if its not public --- plugins/TwitterBridge/TwitterBridgePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index d7c895d9cf..b6658b13a6 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -228,7 +228,7 @@ class TwitterBridgePlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - if (self::hasKeys() && $notice->isLocal()) { + if (self::hasKeys() && $notice->isLocal() && $notice->inScope(null)) { // Avoid a possible loop if ($notice->source != 'twitter') { array_push($transports, 'twitter'); From cbe003eb38a092127cddb95da2268fa8cc9d76e7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:10:24 -0400 Subject: [PATCH 19/31] don't leak private notices to facebook --- plugins/FacebookBridge/FacebookBridgePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/FacebookBridge/FacebookBridgePlugin.php b/plugins/FacebookBridge/FacebookBridgePlugin.php index 9c2a406090..d5a3f7c633 100644 --- a/plugins/FacebookBridge/FacebookBridgePlugin.php +++ b/plugins/FacebookBridge/FacebookBridgePlugin.php @@ -455,7 +455,7 @@ ENDOFSCRIPT; */ function onStartEnqueueNotice($notice, &$transports) { - if (self::hasApplication() && $notice->isLocal()) { + if (self::hasApplication() && $notice->isLocal() && $notice->inScope(null)) { array_push($transports, 'facebook'); } return true; From dca5e20b9122361b9cf0ea74bec5808f3d4a7ed2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:23:31 -0400 Subject: [PATCH 20/31] Make the to-selector clear left --- lib/noticeform.php | 2 +- theme/rebase/css/display.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/noticeform.php b/lib/noticeform.php index 0f4207e4ae..ee4e2ca967 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -238,7 +238,7 @@ class NoticeForm extends Form } $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); - $this->out->elementStart('div'); + $this->out->elementStart('div', 'to-selector'); $toWidget = new ToSelector($this->out, $this->user, (!empty($this->to_group) ? $this->to_group : $this->to_profile)); diff --git a/theme/rebase/css/display.css b/theme/rebase/css/display.css index 9c019e6289..45553e973a 100644 --- a/theme/rebase/css/display.css +++ b/theme/rebase/css/display.css @@ -361,6 +361,10 @@ address .poweredby { opacity:0; } +.form_notice .to-selector { + clear:left; +} + /* Local navigation */ #site_nav_local_views { From ec5a43bf4feb047d64728f4f95d06257750ec28a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:24:02 -0400 Subject: [PATCH 21/31] let actions set a default address for toselector --- lib/action.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/action.php b/lib/action.php index 654ec7aa43..3ac95b46a6 100644 --- a/lib/action.php +++ b/lib/action.php @@ -657,7 +657,8 @@ class Action extends HTMLOutputter // lawsuit if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) { if ($tag == 'status') { - $form = new NoticeForm($this); + $options = $this->noticeFormOptions(); + $form = new NoticeForm($this, $options); } Event::handle('EndMakeEntryForm', array($tag, $this, $form)); } @@ -673,6 +674,11 @@ class Action extends HTMLOutputter // lawsuit $this->elementEnd('div'); } + function noticeFormOptions() + { + return array(); + } + /** * Show anonymous message. * From a7380d593371b2e2d20fb2b03c2df825d5c18dff Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:24:17 -0400 Subject: [PATCH 22/31] set default address for showgroup --- actions/showgroup.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/actions/showgroup.php b/actions/showgroup.php index d77fbeed71..512ca6a0ee 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -365,6 +365,18 @@ class ShowgroupAction extends GroupDesignAction $this->raw(common_markup_to_html($m)); $this->elementEnd('div'); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (!empty($cur) && $cur->isMember($this->group)) { + $options['to_group'] = $this->group; + } + + return $options; + } } class GroupAdminSection extends ProfileSection From 798b03fb5f86ab7d67c35866d256eeb620f5c0cc Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:24:28 -0400 Subject: [PATCH 23/31] set default address for showstream --- actions/showstream.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/actions/showstream.php b/actions/showstream.php index 1a01812ec5..fed9685b35 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -278,6 +278,18 @@ class ShowstreamAction extends ProfileAction $cloud = new PersonalTagCloudSection($this, $this->user); $cloud->show(); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (empty($cur) || $cur->id != $this->profile->id) { + $options['to_profile'] = $this->profile; + } + + return $options; + } } // We don't show the author for a profile, since we already know who it is! From a74eda4e9ad344ba965c111567bd285266a95e23 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 16:37:35 -0400 Subject: [PATCH 24/31] don't send private notices over OStatus --- plugins/OStatus/OStatusPlugin.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index e75130b9e9..26b7ade64d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -111,7 +111,9 @@ class OStatusPlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - if ($notice->isLocal()) { + // FIXME: we don't do privacy-controlled OStatus updates yet. + // once that happens, finer grain of control here. + if ($notice->isLocal() && $notice->inScope(null)) { // put our transport first, in case there's any conflict (like OMB) array_unshift($transports, 'ostatus'); } From 7b8fc701e0f27114e310dcae53c6a727ab05254b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 22:21:41 -0400 Subject: [PATCH 25/31] don't show notices to out-of-scope viewers --- actions/shownotice.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/actions/shownotice.php b/actions/shownotice.php index f6074faddc..7127a60db4 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -79,7 +79,7 @@ class ShownoticeAction extends OwnerDesignAction $id = $this->arg('notice'); - $this->notice = Notice::staticGet($id); + $this->notice = Notice::staticGet('id', $id); if (empty($this->notice)) { // Did we used to have it, and it got deleted? @@ -94,6 +94,18 @@ class ShownoticeAction extends OwnerDesignAction return false; } + $cur = common_current_user(); + + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + throw new ClientException(_('Not available.'), 403); + } + $this->profile = $this->notice->getProfile(); if (empty($this->profile)) { From c1d4186c9839e4d03d00fd8829c363db921d3def Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 22:43:38 -0400 Subject: [PATCH 26/31] Disallow access to events and RSVPs out of scope --- plugins/Event/showevent.php | 10 ++++++++++ plugins/Event/showrsvp.php | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php index 7fb702f9db..1d4ec49205 100644 --- a/plugins/Event/showevent.php +++ b/plugins/Event/showevent.php @@ -77,6 +77,16 @@ class ShoweventAction extends ShownoticeAction throw new ClientException(_('No such event.'), 404); } + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + throw new ClientException(_('Not available.'), 403); + } + $this->user = User::staticGet('id', $this->event->profile_id); if (empty($this->user)) { diff --git a/plugins/Event/showrsvp.php b/plugins/Event/showrsvp.php index fde1d48f0e..0f13ca82ac 100644 --- a/plugins/Event/showrsvp.php +++ b/plugins/Event/showrsvp.php @@ -83,6 +83,16 @@ class ShowrsvpAction extends ShownoticeAction throw new ClientException(_('No such RSVP.'), 404); } + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + throw new ClientException(_('Not available.'), 403); + } + $this->user = User::staticGet('id', $this->rsvp->profile_id); if (empty($this->user)) { From 57dee164caf920be321004097b84b1fa2822650c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 22:50:29 -0400 Subject: [PATCH 27/31] fix missing cur in Event --- plugins/Event/showevent.php | 2 ++ plugins/Event/showrsvp.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php index 1d4ec49205..93ce6dd68b 100644 --- a/plugins/Event/showevent.php +++ b/plugins/Event/showevent.php @@ -77,6 +77,8 @@ class ShoweventAction extends ShownoticeAction throw new ClientException(_('No such event.'), 404); } + $cur = common_current_user(); + if (!empty($cur)) { $curProfile = $cur->getProfile(); } else { diff --git a/plugins/Event/showrsvp.php b/plugins/Event/showrsvp.php index 0f13ca82ac..3b7db6788a 100644 --- a/plugins/Event/showrsvp.php +++ b/plugins/Event/showrsvp.php @@ -83,6 +83,8 @@ class ShowrsvpAction extends ShownoticeAction throw new ClientException(_('No such RSVP.'), 404); } + $cur = common_current_user(); + if (!empty($cur)) { $curProfile = $cur->getProfile(); } else { From 2856982a1c378ec45722f04aadf37c82b87986cb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 22:50:45 -0400 Subject: [PATCH 28/31] disallow access to out-of-scope bookmark --- plugins/Bookmark/showbookmark.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/Bookmark/showbookmark.php b/plugins/Bookmark/showbookmark.php index 6bebffb68e..435d85940a 100644 --- a/plugins/Bookmark/showbookmark.php +++ b/plugins/Bookmark/showbookmark.php @@ -76,6 +76,16 @@ class ShowbookmarkAction extends ShownoticeAction throw new ClientException(_('No such bookmark.'), 404); } + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + throw new ClientException(_('Not available.'), 403); + } + $this->user = User::staticGet('id', $this->bookmark->profile_id); if (empty($this->user)) { From 908551ae3d20cab1a8b6c39eeda82c0c4af4c92b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 28 Mar 2011 22:50:52 -0400 Subject: [PATCH 29/31] disallow access to out-of-scope poll --- plugins/Poll/showpoll.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/Poll/showpoll.php b/plugins/Poll/showpoll.php index d95b1c512e..d59d9e28f3 100644 --- a/plugins/Poll/showpoll.php +++ b/plugins/Poll/showpoll.php @@ -76,6 +76,18 @@ class ShowPollAction extends ShownoticeAction throw new ClientException(_m('No such poll notice.'), 404); } + $cur = common_current_user(); + + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + throw new ClientException(_('Not available.'), 403); + } + $this->user = User::staticGet('id', $this->poll->profile_id); if (empty($this->user)) { From 32145484c28323c95c0f84a274fb13714b738a3d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 29 Mar 2011 11:53:26 -0400 Subject: [PATCH 30/31] Disallow repeats (retweets) of private notices We disallow repeating a notice (or whatever) if the scope of the notice is too private. So, only notices that are public scope (available to everyone in the world) or site scope (available to everyone on the site) can be repeated. Enforce this rule at a low level in Notice.php, and in the API, commands, and Web UI. Repeat button doesn't appear on tightly-scoped notices in the Web UI. --- actions/apistatusesretweet.php | 20 ++++++++++++++++++++ actions/repeat.php | 15 +++++++++++++++ classes/Notice.php | 14 ++++++++++++++ lib/command.php | 17 ++++++++++++++++- lib/noticelistitem.php | 26 +++++++++++++++----------- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/actions/apistatusesretweet.php b/actions/apistatusesretweet.php index ecc4a3f033..2bc9092ba6 100644 --- a/actions/apistatusesretweet.php +++ b/actions/apistatusesretweet.php @@ -85,8 +85,27 @@ class ApiStatusesRetweetAction extends ApiAuthAction return false; } + // Is it OK to repeat that notice (general enough scope)? + + if ($this->original->scope != Notice::SITE_SCOPE && + $this->original->scope != Notice::PUBLIC_SCOPE) { + $this->clientError(_('You may not repeat a private notice.'), + 403, + $this->format); + return false; + } + $profile = $this->user->getProfile(); + // Can the profile actually see that notice? + + if (!$this->original->inScope($profile)) { + $this->clientError(_('No access to that notice.'), + 403, + $this->format); + return false; + } + if ($profile->hasRepeated($id)) { // TRANS: Client error displayed trying to re-repeat a notice through the API. $this->clientError(_('Already repeated that notice.'), @@ -94,6 +113,7 @@ class ApiStatusesRetweetAction extends ApiAuthAction return false; } + return true; } diff --git a/actions/repeat.php b/actions/repeat.php index 869c2ddd4e..4201a4ce95 100644 --- a/actions/repeat.php +++ b/actions/repeat.php @@ -73,6 +73,14 @@ class RepeatAction extends Action return false; } + // Is it OK to repeat that notice (general enough scope)? + + if ($this->notice->scope != Notice::SITE_SCOPE && + $this->notice->scope != Notice::PUBLIC_SCOPE) { + $this->clientError(_('You may not repeat a private notice.'), + 403); + } + if ($this->user->id == $this->notice->profile_id) { // TRANS: Client error displayed when trying to repeat an own notice. $this->clientError(_('You cannot repeat your own notice.')); @@ -88,6 +96,13 @@ class RepeatAction extends Action $profile = $this->user->getProfile(); + // Can the profile actually see that notice? + + if (!$this->notice->inScope($profile)) { + $this->clientError(_('No access to that notice.'), 403); + } + + if ($profile->hasRepeated($id)) { // TRANS: Client error displayed when trying to repeat an already repeated notice. $this->clientError(_('You already repeated that notice.')); diff --git a/classes/Notice.php b/classes/Notice.php index 3780d52d56..a6e4566e4b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -90,6 +90,7 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const PUBLIC_SCOPE = 0; // Useful fake constant const SITE_SCOPE = 1; const ADDRESSEE_SCOPE = 2; const GROUP_SCOPE = 4; @@ -344,6 +345,19 @@ class Notice extends Memcached_DataObject // Handle repeat case if (isset($repeat_of)) { + + // Check for a private one + + $repeat = Notice::staticGet('id', $repeat_of); + + if (!empty($repeat) && + $repeat->scope != Notice::SITE_SCOPE && + $repeat->scope != Notice::PUBLIC_SCOPE) { + throw new ClientException(_('Cannot repeat a private notice.'), 403); + } + + // XXX: Check for access...? + $notice->repeat_of = $repeat_of; } else { $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); diff --git a/lib/command.php b/lib/command.php index 5b9964c5b1..35d0702684 100644 --- a/lib/command.php +++ b/lib/command.php @@ -544,7 +544,22 @@ class RepeatCommand extends Command return; } - if ($this->user->getProfile()->hasRepeated($notice->id)) { + // Is it OK to repeat that notice (general enough scope)? + + if ($notice->scope != Notice::SITE_SCOPE && + $notice->scope != Notice::PUBLIC_SCOPE) { + $channel->error($this->user, _('You may not repeat a private notice.')); + } + + $profile = $this->user->getProfile(); + + // Can the profile actually see that notice? + + if (!$notice->inScope($profile)) { + $channel->error($this->user, _('You have no access to that notice.')); + } + + if ($profile->hasRepeated($notice->id)) { // TRANS: Error text shown when trying to repeat an notice that was already repeated by the user. $channel->error($this->user, _('Already repeated that notice.')); return; diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php index 46f15f551d..097a5d06c4 100644 --- a/lib/noticelistitem.php +++ b/lib/noticelistitem.php @@ -596,17 +596,21 @@ class NoticeListItem extends Widget function showRepeatForm() { - $user = common_current_user(); - if ($user && $user->id != $this->notice->profile_id) { - $this->out->text(' '); - $profile = $user->getProfile(); - if ($profile->hasRepeated($this->notice->id)) { - $this->out->element('span', array('class' => 'repeated', - 'title' => _('Notice repeated')), - _('Repeated')); - } else { - $rf = new RepeatForm($this->out, $this->notice); - $rf->show(); + if ($this->notice->scope == Notice::PUBLIC_SCOPE || + $this->notice->scope == Notice::SITE_SCOPE) { + $user = common_current_user(); + if (!empty($user) && + $user->id != $this->notice->profile_id) { + $this->out->text(' '); + $profile = $user->getProfile(); + if ($profile->hasRepeated($this->notice->id)) { + $this->out->element('span', array('class' => 'repeated', + 'title' => _('Notice repeated')), + _('Repeated')); + } else { + $rf = new RepeatForm($this->out, $this->notice); + $rf->show(); + } } } } From 31fd4dbe3b45e481f273f3e6333e4ef8e728dccd Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 29 Mar 2011 12:12:08 -0400 Subject: [PATCH 31/31] Repeats keep the same scope as parent --- classes/Notice.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index a6e4566e4b..b5eafb0ffa 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1588,8 +1588,13 @@ class Notice extends Memcached_DataObject $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; } - return self::saveNew($repeater_id, $content, $source, - array('repeat_of' => $this->id)); + // Scope is same as this one's + + return self::saveNew($repeater_id, + $content, + $source, + array('repeat_of' => $this->id, + 'scope' => $this->scope)); } // These are supposed to be in chron order!