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/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/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/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/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 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)) { 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! diff --git a/classes/Notice.php b/classes/Notice.php index 114119bfc9..b5eafb0ffa 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) @@ -89,6 +90,12 @@ 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; + const FOLLOWER_SCOPE = 8; + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); @@ -243,6 +250,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 * @@ -254,6 +262,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -336,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); @@ -343,6 +365,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; } @@ -368,6 +394,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 @@ -1556,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! @@ -2011,4 +2048,95 @@ 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 no scope, anyone (even anon) is in scope. + + 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 true; + } + + // 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 032a370656..73a98d4ff5 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1087,4 +1087,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; + } } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index ab2c3d02ef..40d83f0a87 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 f44a85cad7..ab1f159743 100644 --- a/db/core.php +++ b/db/core.php @@ -203,6 +203,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( 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. * 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/conversationnoticestream.php b/lib/conversationnoticestream.php index dbba6cd6f0..addf8768ce 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/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' => 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 4b4fb00229..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/noticeform.php b/lib/noticeform.php index 2cbacc9280..ee4e2ca967 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_profile; + + /** 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', 'to-selector'); + $toWidget = new ToSelector($this->out, + $this->user, + (!empty($this->to_group) ? $this->to_group : $this->to_profile)); + + $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/noticelistitem.php b/lib/noticelistitem.php index 17827d07ef..097a5d06c4 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)); } @@ -592,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(); + } } } } diff --git a/lib/profilenoticestream.php b/lib/profilenoticestream.php index f62b787b04..7ea653f8c8 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 6a861ca26e..5c8d313d46 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, $limit, $since_id, $max_id) 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 d0ae5fc4a7..9de8d4efaf 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 291d3d6eb0..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; 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; + } + } +} 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)) { diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php index 7fb702f9db..93ce6dd68b 100644 --- a/plugins/Event/showevent.php +++ b/plugins/Event/showevent.php @@ -77,6 +77,18 @@ class ShoweventAction extends ShownoticeAction throw new ClientException(_('No such event.'), 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->event->profile_id); if (empty($this->user)) { diff --git a/plugins/Event/showrsvp.php b/plugins/Event/showrsvp.php index fde1d48f0e..3b7db6788a 100644 --- a/plugins/Event/showrsvp.php +++ b/plugins/Event/showrsvp.php @@ -83,6 +83,18 @@ class ShowrsvpAction extends ShownoticeAction throw new ClientException(_('No such RSVP.'), 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->rsvp->profile_id); if (empty($this->user)) { 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; 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'); } 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)) { 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'); 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); } diff --git a/theme/rebase/css/display.css b/theme/rebase/css/display.css index 6c3c9e6296..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 { @@ -1156,6 +1160,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 {