diff --git a/actions/approvegroup.php b/actions/approvegroup.php index 5039cfae6b..55a0a6c3fd 100644 --- a/actions/approvegroup.php +++ b/actions/approvegroup.php @@ -152,12 +152,12 @@ class ApprovegroupAction extends Action try { if ($this->approve) { - $this->profile->completeJoinGroup($this->group); + $this->request->complete(); } elseif ($this->cancel) { - $this->profile->cancelJoinGroup($this->group); + $this->request->abort(); } } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), diff --git a/actions/approvesub.php b/actions/approvesub.php new file mode 100644 index 0000000000..d30da25d32 --- /dev/null +++ b/actions/approvesub.php @@ -0,0 +1,145 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApprovesubAction extends Action +{ + var $profile = null; + + /** + * Prepare to run + */ + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (empty($cur)) { + // TRANS: Client error displayed trying to approve group membership while not logged in. + $this->clientError(_('Must be logged in.'), 403); + return false; + } + if ($this->arg('profile_id')) { + $this->profile = Profile::staticGet('id', $this->arg('profile_id')); + } else { + // TRANS: Client error displayed trying to approve subscriptionswithout specifying a profile to approve. + $this->clientError(_('Must specify a profile.')); + return false; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $this->profile->id, + 'subscribed' => $cur->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed trying to approve subscription for a non-existing request. + $this->clientError(sprintf(_('%s is not in the moderation queue for your subscriptions.'), $this->profile->nickname), 403); + } + + $this->approve = (bool)$this->arg('approve'); + $this->cancel = (bool)$this->arg('cancel'); + if (!$this->approve && !$this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription. + $this->clientError(_('Internal error: received neither cancel nor abort.')); + } + if ($this->approve && $this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription + $this->clientError(_('Internal error: received both cancel and abort.')); + } + return true; + } + + /** + * Handle the request + * + * On POST, add the current user to the group + * + * @param array $args unused + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $cur = common_current_user(); + + try { + if ($this->approve) { + $this->request->complete(); + } elseif ($this->cancel) { + $this->request->abort(); + } + } catch (Exception $e) { + common_log(LOG_ERR, "Exception canceling sub: " . $e->getMessage()); + // TRANS: Server error displayed when cancelling a queued subscription request fails. + // TRANS: %1$s is the leaving user's nickname, $2$s is the nickname for which the leave failed. + $this->serverError(sprintf(_('Could not cancel or approve request for user %1$s to join group %2$s.'), + $this->profile->nickname, $cur->nickname)); + return; + } + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title for subscription approval ajax return + // TRANS: %1$s is the approved user's nickname + $this->element('title', null, sprintf(_m('TITLE','%1$s\'s request'), + $this->profile->nickname)); + $this->elementEnd('head'); + $this->elementStart('body'); + if ($this->approve) { + // TRANS: Message on page for user after approving a subscription request. + $this->element('p', 'success', _('Subscription approved.')); + } elseif ($this->cancel) { + // TRANS: Message on page for user after rejecting a subscription request. + $this->element('p', 'success', _('Subscription canceled.')); + } + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subqueue', array('nickname' => + $cur->nickname)), + 303); + } + } +} diff --git a/actions/cancelgroup.php b/actions/cancelgroup.php index 57df1a10a7..3074e3ffa3 100644 --- a/actions/cancelgroup.php +++ b/actions/cancelgroup.php @@ -139,9 +139,9 @@ class CancelgroupAction extends Action parent::handle($args); try { - $this->profile->cancelJoinGroup($this->group); + $this->request->abort(); } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), diff --git a/actions/cancelsubscription.php b/actions/cancelsubscription.php new file mode 100644 index 0000000000..367fbfa138 --- /dev/null +++ b/actions/cancelsubscription.php @@ -0,0 +1,119 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class CancelsubscriptionAction extends Action +{ + + function handle($args) + { + parent::handle($args); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); + } + if (!common_logged_in()) { + $this->clientError(_('Not logged in.')); + return; + } + + $user = common_current_user(); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname))); + return; + } + + /* Use a session token for CSRF protection. */ + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token. ' . + 'Try again, please.')); + return; + } + + $other_id = $this->arg('unsubscribeto'); + + if (!$other_id) { + $this->clientError(_('No profile ID in request.')); + return; + } + + $other = Profile::staticGet('id', $other_id); + + if (!$other) { + $this->clientError(_('No profile with that ID.')); + return; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $user->id, + 'subscribed' => $other->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed when trying to approve a non-existing group join request. + // TRANS: %s is a user nickname. + $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); + } + + $this->request->abort(); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _('Unsubscribed')); + $this->elementEnd('head'); + $this->elementStart('body'); + $subscribe = new SubscribeForm($this, $other); + $subscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname)), + 303); + } + } +} diff --git a/actions/groupqueue.php b/actions/groupqueue.php index dca0ff7bd5..c6f54d57d7 100644 --- a/actions/groupqueue.php +++ b/actions/groupqueue.php @@ -156,7 +156,7 @@ class GroupqueueAction extends GroupDesignAction $members->free(); $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, - $this->page, 'groupmembers', + $this->page, 'groupqueue', array('nickname' => $this->group->nickname)); } } diff --git a/actions/profilesettings.php b/actions/profilesettings.php index e1d686ca29..7aa987f3a5 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -192,6 +192,17 @@ class ProfilesettingsAction extends SettingsAction ($this->arg('autosubscribe')) ? $this->boolean('autosubscribe') : $user->autosubscribe); $this->elementEnd('li'); + $this->elementStart('li'); + $this->dropdown('subscribe_policy', + // TRANS: Dropdown field label on profile settings, for what policies to apply when someone else tries to subscribe to your updates. + _('Subscription policy'), + array(User::SUBSCRIBE_POLICY_OPEN => _('Let anyone follow me'), + User::SUBSCRIBE_POLICY_MODERATE => _('Ask me first')), + // TRANS: Dropdown field title on group edit form. + _('Whether other users need your permission to follow your updates.'), + false, + (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy); + $this->elementEnd('li'); } $this->elementEnd('ul'); // TRANS: Button to save input in profile settings. @@ -234,6 +245,7 @@ class ProfilesettingsAction extends SettingsAction $bio = $this->trimmed('bio'); $location = $this->trimmed('location'); $autosubscribe = $this->boolean('autosubscribe'); + $subscribe_policy = $this->trimmed('subscribe_policy'); $language = $this->trimmed('language'); $timezone = $this->trimmed('timezone'); $tagstring = $this->trimmed('tags'); @@ -333,11 +345,12 @@ class ProfilesettingsAction extends SettingsAction } // XXX: XOR - if ($user->autosubscribe ^ $autosubscribe) { + if (($user->autosubscribe ^ $autosubscribe) || $user->subscribe_policy != $subscribe_policy) { $original = clone($user); $user->autosubscribe = $autosubscribe; + $user->subscribe_policy = $subscribe_policy; $result = $user->update($original); @@ -345,7 +358,7 @@ class ProfilesettingsAction extends SettingsAction common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown when user profile settings could not be updated to // TRANS: automatically subscribe to any subscriber. - $this->serverError(_('Could not update user for autosubscribe.')); + $this->serverError(_('Could not update user for autosubscribe or subscribe_policy.')); return; } } diff --git a/actions/subqueue.php b/actions/subqueue.php new file mode 100644 index 0000000000..38bc16e562 --- /dev/null +++ b/actions/subqueue.php @@ -0,0 +1,142 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/lib/profilelist.php'); + +/** + * List of group members + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class SubqueueAction extends GalleryAction +{ + var $page = null; + + function isReadOnly($args) + { + return true; + } + + // @todo FIXME: most of this belongs in a base class, sounds common to most group actions? + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (!$cur || $cur->id != $this->profile->id) { + // TRANS: Client error displayed when trying to approve group applicants without being a group administrator. + $this->clientError(_('You may only approve your own pending subscriptions.')); + return false; + } + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Title of the first page showing pending subscribers still awaiting approval. + // TRANS: %s is the name of the user. + return sprintf(_('%s subscribers awaiting approval'), + $this->profile->nickname); + } else { + // TRANS: Title of all but the first page showing pending subscribersmembers still awaiting approval. + // TRANS: %1$s is the name of the user, %2$d is the page number of the members list. + return sprintf(_('%1$s subscribers awaiting approval, page %2$d'), + $this->profile->nickname, + $this->page); + } + } + + function showPageNotice() + { + $this->element('p', 'instructions', + // TRANS: Page notice for group members page. + _('A list of users awaiting approval to subscribe to you.')); + } + + + function showContent() + { + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + $cnt = 0; + + $members = $this->profile->getRequests($offset, $limit); + + if ($members) { + // @fixme change! + $member_list = new SubQueueList($members, $this); + $cnt = $member_list->show(); + } + + $members->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'subqueue', + array('nickname' => $this->profile->nickname)); // urgh + } +} + +class SubQueueList extends ProfileList +{ + function newListItem($profile) + { + return new SubQueueListItem($profile, $this->action); + } +} + +class SubQueueListItem extends ProfileListItem +{ + function showActions() + { + $this->startActions(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showApproveButtons(); + Event::handle('EndProfileListItemActionElements', array($this)); + } + $this->endActions(); + } + + function showApproveButtons() + { + $this->out->elementStart('li', 'entity_approval'); + $form = new ApproveSubForm($this->out, $this->profile); + $form->show(); + $this->out->elementEnd('li'); + } +} diff --git a/actions/subscribe.php b/actions/subscribe.php index 3837915d53..fad153fc6e 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -139,8 +139,8 @@ class SubscribeAction extends Action { // Throws exception on error - Subscription::start($this->user->getProfile(), - $this->other); + $sub = Subscription::start($this->user->getProfile(), + $this->other); if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); @@ -149,8 +149,12 @@ class SubscribeAction extends Action $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $this->other); - $unsubscribe->show(); + if ($sub instanceof Subscription) { + $form = new UnsubscribeForm($this, $this->other); + } else { + $form = new CancelSubscriptionForm($this, $this->other); + } + $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); } else { diff --git a/classes/Group_join_queue.php b/classes/Group_join_queue.php index 48b36cae2d..acf3a13957 100644 --- a/classes/Group_join_queue.php +++ b/classes/Group_join_queue.php @@ -56,6 +56,71 @@ class Group_join_queue extends Managed_DataObject return $rq; } + function getMember() + { + $member = Profile::staticGet('id', $this->profile_id); + + if (empty($member)) { + // TRANS: Exception thrown providing an invalid profile ID. + // TRANS: %s is the invalid profile ID. + throw new Exception(sprintf(_("Profile ID %s is invalid."),$this->profile_id)); + } + + return $member; + } + + function getGroup() + { + $group = User_group::staticGet('id', $this->group_id); + + if (empty($group)) { + // TRANS: Exception thrown providing an invalid group ID. + // TRANS: %s is the invalid group ID. + throw new Exception(sprintf(_("Group ID %s is invalid."),$this->group_id)); + } + + return $group; + } + + /** + * Abort the pending group join... + * + * @param User_group $group + */ + function abort() + { + $profile = $this->getMember(); + $group = $this->getGroup(); + if ($request) { + if (Event::handle('StartCancelJoinGroup', array($profile, $group))) { + $this->delete(); + Event::handle('EndCancelJoinGroup', array($profile, $group)); + } + } + } + + /** + * Complete a pending group join... + * + * @return Group_member object on success + */ + function complete(User_group $group) + { + $join = null; + $profile = $this->getMember(); + $group = $this->getGroup(); + if (Event::handle('StartJoinGroup', array($profile, $group))) { + $join = Group_member::join($group->id, $profile->id); + $this->delete(); + Event::handle('EndJoinGroup', array($profile, $group)); + } + if (!$join) { + throw new Exception('Internal error: group join failed.'); + } + $join->notify(); + return $join; + } + /** * Send notifications via email etc to group administrators about * this exciting new pending moderation queue item! diff --git a/classes/Profile.php b/classes/Profile.php index b6b05f6a4f..d86bf98e83 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -297,49 +297,6 @@ class Profile extends Memcached_DataObject return $join; } - /** - * Cancel a pending group join... - * - * @param User_group $group - */ - function cancelJoinGroup(User_group $group) - { - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartCancelJoinGroup', array($group, $this))) { - $request->delete(); - Event::handle('EndCancelJoinGroup', array($group, $this)); - } - } - } - - /** - * Complete a pending group join on our end... - * - * @param User_group $group - */ - function completeJoinGroup(User_group $group) - { - $join = null; - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartJoinGroup', array($group, $this))) { - $join = Group_member::join($group->id, $this->id); - $request->delete(); - Event::handle('EndJoinGroup', array($group, $this)); - } - } else { - // TRANS: Exception thrown trying to approve a non-existing group join request. - throw new Exception(_('Invalid group join approval: not pending.')); - } - if ($join) { - $join->notify(); - } - return $join; - } - /** * Leave a group that this profile is a member of. * @@ -399,6 +356,36 @@ class Profile extends Memcached_DataObject return new ArrayWrapper($profiles); } + /** + * Get pending subscribers, who have not yet been approved. + * + * @param int $offset + * @param int $limit + * @return Profile + */ + function getRequests($offset=0, $limit=null) + { + $qry = + 'SELECT profile.* ' . + 'FROM profile JOIN subscription_queue '. + 'ON profile.id = subscription_queue.subscriber ' . + 'WHERE subscription_queue.subscribed = %d ' . + 'ORDER BY subscription_queue.created DESC '; + + if ($limit != null) { + if (common_config('db','type') == 'pgsql') { + $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $qry .= ' LIMIT ' . $offset . ', ' . $limit; + } + } + + $members = new Profile(); + + $members->query(sprintf($qry, $this->id)); + return $members; + } + function subscriptionCount() { $c = Cache::instance(); @@ -456,6 +443,17 @@ class Profile extends Memcached_DataObject { return Subscription::exists($this, $other); } + + /** + * Check if a pending subscription request is outstanding for this... + * + * @param Profile $other + * @return boolean + */ + function hasPendingSubscription($other) + { + return Subscription_queue::exists($this, $other); + } /** * Are these two profiles subscribed to each other? diff --git a/classes/Subscription.php b/classes/Subscription.php index 797e6fef1c..438f1dfe50 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -27,6 +27,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class Subscription extends Memcached_DataObject { const CACHE_WINDOW = 201; + const FORCE = true; ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -58,11 +59,12 @@ class Subscription extends Memcached_DataObject * * @param Profile $subscriber party to receive new notices * @param Profile $other party sending notices; publisher + * @param bool $force pass Subscription::FORCE to override local subscription approval * - * @return Subscription new subscription + * @return mixed Subscription or Subscription_queue: new subscription info */ - static function start($subscriber, $other) + static function start($subscriber, $other, $force=false) { // @fixme should we enforce this as profiles in callers instead? if ($subscriber instanceof User) { @@ -88,35 +90,39 @@ class Subscription extends Memcached_DataObject } if (Event::handle('StartSubscribe', array($subscriber, $other))) { - $sub = self::saveNew($subscriber->id, $other->id); - $sub->notify(); - - self::blow('user:notices_with_friends:%d', $subscriber->id); - - self::blow('subscription:by-subscriber:'.$subscriber->id); - self::blow('subscription:by-subscribed:'.$other->id); - - $subscriber->blowSubscriptionCount(); - $other->blowSubscriberCount(); - $otherUser = User::staticGet('id', $other->id); + if ($otherUser && $otherUser->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE && !$force) { + $sub = Subscription_queue::saveNew($subscriber, $other); + $sub->notify(); + } else { + $sub = self::saveNew($subscriber->id, $other->id); + $sub->notify(); - if (!empty($otherUser) && - $otherUser->autosubscribe && - !self::exists($other, $subscriber) && - !$subscriber->hasBlocked($other)) { + self::blow('user:notices_with_friends:%d', $subscriber->id); - try { - self::start($other, $subscriber); - } catch (Exception $e) { - common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + self::blow('subscription:by-subscriber:'.$subscriber->id); + self::blow('subscription:by-subscribed:'.$other->id); + + $subscriber->blowSubscriptionCount(); + $other->blowSubscriberCount(); + + if (!empty($otherUser) && + $otherUser->autosubscribe && + !self::exists($other, $subscriber) && + !$subscriber->hasBlocked($other)) { + + try { + self::start($other, $subscriber); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + } } } Event::handle('EndSubscribe', array($subscriber, $other)); } - return true; + return $sub; } /** diff --git a/classes/Subscription_queue.php b/classes/Subscription_queue.php new file mode 100644 index 0000000000..19cd71c6a8 --- /dev/null +++ b/classes/Subscription_queue.php @@ -0,0 +1,105 @@ + 'Holder for subscription requests awaiting moderation.', + 'fields' => array( + 'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile making the request'), + 'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile being subscribed to'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('subscriber', 'subscribed'), + 'indexes' => array( + 'subscription_queue_subscriber_created_idx' => array('subscriber', 'created'), + 'subscription_queue_subscribed_created_idx' => array('subscribed', 'created'), + ), + 'foreign keys' => array( + 'subscription_queue_subscriber_fkey' => array('profile', array('subscriber' => 'id')), + 'subscription_queue_subscribed_fkey' => array('profile', array('subscribed' => 'id')), + ) + ); + } + + public static function saveNew(Profile $subscriber, Profile $subscribed) + { + $rq = new Subscription_queue(); + $rq->subscriber = $subscriber->id; + $rq->subscribed = $subscribed->id; + $rq->created = common_sql_now(); + $rq->insert(); + return $rq; + } + + function exists($subscriber, $other) + { + $sub = Subscription_queue::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + return (empty($sub)) ? false : true; + } + + /** + * Complete a pending subscription, as we've got approval of some sort. + * + * @return Subscription + */ + public function complete() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + $sub = Subscription::start($subscriber, $subscribed, Subscription::FORCE); + if ($sub) { + $this->delete(); + } + return $sub; + } + + /** + * Cancel an outstanding subscription request to the other profile. + */ + public function abort() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + if (Event::handle('StartCancelSubscription', array($subscriber, $subscribed))) { + $this->delete(); + Event::handle('EndCancelSubscription', array($subscriber, $subscribed)); + } + } + + /** + * Send notifications via email etc to group administrators about + * this exciting new pending moderation queue item! + */ + public function notify() + { + $other = Profile::staticGet('id', $this->subscriber); + $listenee = User::staticGet('id', $this->subscribed); + mail_subscribe_pending_notify_profile($listenee, $other); + } +} diff --git a/classes/User.php b/classes/User.php index 1a3a7dfd72..f395dc1e49 100644 --- a/classes/User.php +++ b/classes/User.php @@ -30,6 +30,9 @@ require_once 'Validate.php'; class User extends Memcached_DataObject { + const SUBSCRIBE_POLICY_OPEN = 0; + const SUBSCRIBE_POLICY_MODERATE = 1; + ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -55,6 +58,7 @@ class User extends Memcached_DataObject public $smsemail; // varchar(255) public $uri; // varchar(255) unique_key public $autosubscribe; // tinyint(1) + public $subscribe_policy; // tinyint(1) public $urlshorteningservice; // varchar(50) default_ur1.ca public $inboxed; // tinyint(1) public $design_id; // int(4) @@ -86,6 +90,12 @@ class User extends Memcached_DataObject return $profile->isSubscribed($other); } + function hasPendingSubscription($other) + { + $profile = $this->getProfile(); + return $profile->hasPendingSubscription($other); + } + // 'update' won't write key columns, so we have to do it ourselves. function updateKeys(&$orig) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 12c59daae0..40d83f0a87 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -591,6 +591,7 @@ smsreplies = 17 smsemail = 2 uri = 2 autosubscribe = 17 +subscribe_policy = 17 urlshorteningservice = 2 inboxed = 17 design_id = 1 diff --git a/db/core.php b/db/core.php index 3e439e5010..ab1f159743 100644 --- a/db/core.php +++ b/db/core.php @@ -118,6 +118,7 @@ $schema['user'] = array( 'smsemail' => array('type' => 'varchar', 'length' => 255, 'description' => 'built from sms and carrier'), 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), 'autosubscribe' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'automatically subscribe to users who subscribe to us'), + 'subscribe_policy' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'), 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'internal', 'description' => 'service to use for auto-shortening URLs'), 'inboxed' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'has an inbox been created for this user?'), 'design_id' => array('type' => 'int', 'description' => 'id of a design'), @@ -1031,3 +1032,5 @@ $schema['schema_version'] = array( ); $schema['group_join_queue'] = Group_join_queue::schemaDef(); + +$schema['subscription_queue'] = Subscription_queue::schemaDef(); diff --git a/lib/accountprofileblock.php b/lib/accountprofileblock.php index a8bdb4715b..6a4021bf04 100644 --- a/lib/accountprofileblock.php +++ b/lib/accountprofileblock.php @@ -144,6 +144,9 @@ class AccountProfileBlock extends ProfileBlock if ($cur->isSubscribed($this->profile)) { $usf = new UnsubscribeForm($this->out, $this->profile); $usf->show(); + } else if ($cur->hasPendingSubscription($this->profile)) { + $sf = new CancelSubscriptionForm($this->out, $this->profile); + $sf->show(); } else { $sf = new SubscribeForm($this->out, $this->profile); $sf->show(); diff --git a/lib/approvesubform.php b/lib/approvesubform.php new file mode 100644 index 0000000000..820f648d0f --- /dev/null +++ b/lib/approvesubform.php @@ -0,0 +1,114 @@ +. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou + * @author Sarven Capadisli + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/form.php'; + +/** + * Form for leaving a group + * + * @category Form + * @package StatusNet + * @author Evan Prodromou + * @author Sarven Capadisli + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see UnsubscribeForm + */ +class ApproveSubForm extends Form +{ + var $profile = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param Profile $profile user whose request to accept or drop + */ + function __construct($out=null, $profile=null) + { + parent::__construct($out); + + $this->profile = $profile; + } + + /** + * ID of the form + * + * @return string ID of the form + */ + function id() + { + return 'sub-queue-' . $this->profile->id; + } + + /** + * class of the form + * + * @return string of the form class + */ + function formClass() + { + return 'form_sub_queue ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + $params = array(); + if ($this->profile) { + $params['profile_id'] = $this->profile->id; + } + return common_local_url('approvesub', + array(), $params); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Submit button text to accept a subscription request on approve sub form. + $this->out->submit('approve', _m('BUTTON','Accept')); + // TRANS: Submit button text to reject a subscription request on approve sub form. + $this->out->submit('cancel', _m('BUTTON','Reject')); + } +} diff --git a/lib/cancelsubscriptionform.php b/lib/cancelsubscriptionform.php new file mode 100644 index 0000000000..1173152c63 --- /dev/null +++ b/lib/cancelsubscriptionform.php @@ -0,0 +1,129 @@ +. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou + * @author Sarven Capadisli + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/form.php'; + +/** + * Form for leaving a group + * + * @category Form + * @package StatusNet + * @author Evan Prodromou + * @author Sarven Capadisli + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see UnsubscribeForm + */ + +class CancelSubscriptionForm extends Form +{ + /** + * user being subscribed to + */ + + var $profile = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param Profile $profile being subscribed to + */ + + function __construct($out=null, $profile=null) + { + parent::__construct($out); + + $this->profile = $profile; + } + + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'subscription-cancel-' . $this->profile->id; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_unsubscribe ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('cancelsubscription', + array(), + array('id' => $this->profile->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->hidden('unsubscribeto-' . $this->profile->id, + $this->profile->id, + 'unsubscribeto'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Cancel sub request')); + } +} diff --git a/lib/mail.php b/lib/mail.php index 708e5349b1..e8c5d3c4e7 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -259,6 +259,49 @@ function mail_subscribe_notify_profile($listenee, $other) } } +function mail_subscribe_pending_notify_profile($listenee, $other) +{ + if ($other->hasRight(Right::EMAILONSUBSCRIBE) && + $listenee->email && $listenee->emailnotifysub) { + + $profile = $listenee->getProfile(); + + $name = $profile->getBestName(); + + $long_name = ($other->fullname) ? + ($other->fullname . ' (' . $other->nickname . ')') : $other->nickname; + + $recipients = $listenee->email; + + // use the recipient's localization + common_switch_locale($listenee->language); + + $headers = _mail_prepare_headers('subscribe', $listenee->nickname, $other->nickname); + $headers['From'] = mail_notify_from(); + $headers['To'] = $name . ' <' . $listenee->email . '>'; + // TRANS: Subject of pending new-subscriber notification e-mail. + // TRANS: %1$s is the subscribing user's nickname, %2$s is the StatusNet sitename. + $headers['Subject'] = sprintf(_('%1$s would like to listen to '. + 'your notices on %2$s.'), + $other->getBestName(), + common_config('site', 'name')); + + // TRANS: Main body of pending new-subscriber notification e-mail. + // TRANS: %1$s is the subscriber's long name, %2$s is the StatusNet sitename. + $body = sprintf(_('%1$s would like to listen to your notices on %2$s. ' . + 'You may approve or reject their subscription at %3$s'), + $long_name, + common_config('site', 'name'), + common_local_url('subqueue', array('nickname' => $listenee->nickname))) . + mail_profile_block($other) . + mail_footer_block(); + + // reset localization + common_switch_locale(); + mail_send($recipients, $headers, $body); + } +} + function mail_footer_block() { // TRANS: Common footer block for StatusNet notification emails. diff --git a/lib/router.php b/lib/router.php index fa9fe9aee1..156bc10c7f 100644 --- a/lib/router.php +++ b/lib/router.php @@ -199,7 +199,8 @@ class Router // main stuff is repetitive $main = array('login', 'logout', 'register', 'subscribe', - 'unsubscribe', 'confirmaddress', 'recoverpassword', + 'unsubscribe', 'cancelsubscription', 'approvesub', + 'confirmaddress', 'recoverpassword', 'invite', 'favor', 'disfavor', 'sup', 'block', 'unblock', 'subedit', 'groupblock', 'groupunblock', @@ -843,6 +844,10 @@ class Router array('tag' => self::REGEX_TAG)); } + $m->connect('subscribers/pending', + array('action' => 'subqueue', + 'nickname' => $nickname)); + foreach (array('rss', 'groups') as $a) { $m->connect($a, array('action' => 'user'.$a, @@ -899,6 +904,9 @@ class Router array('action' => $a), array('nickname' => Nickname::DISPLAY_FMT)); } + $m->connect(':nickname/subscribers/pending', + array('action' => 'subqueue'), + array('nickname' => Nickname::DISPLAY_FMT)); foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', diff --git a/lib/subgroupnav.php b/lib/subgroupnav.php index ee4b0a8dff..b0ec364bff 100644 --- a/lib/subgroupnav.php +++ b/lib/subgroupnav.php @@ -96,6 +96,20 @@ class SubGroupNav extends Menu $this->user->nickname), $action == 'subscribers', 'nav_subscribers'); + if ($cur && $cur->id == $this->user->id) { + // Possibly site admins should be able to get in here too + $pending = $this->countPendingSubs(); + if ($pending || $cur->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE) { + $this->out->menuItem(common_local_url('subqueue', + array('nickname' => + $this->user->nickname)), + sprintf(_('Pending (%d)'), $pending), + sprintf(_('Approve pending subscription requests'), + $this->user->nickname), + $action == 'subqueueaction', + 'nav_subscribers'); + } + } $this->out->menuItem(common_local_url('usergroups', array('nickname' => $this->user->nickname)), @@ -118,4 +132,11 @@ class SubGroupNav extends Menu $this->out->elementEnd('ul'); } + + function countPendingSubs() + { + $req = new Subscription_queue(); + $req->subscribed = $this->user->id; + return $req->count(); + } } diff --git a/plugins/ClientSideShorten/shorten.php b/plugins/ClientSideShorten/shorten.php index 53c2cf5d1a..3e7121db8b 100644 --- a/plugins/ClientSideShorten/shorten.php +++ b/plugins/ClientSideShorten/shorten.php @@ -57,7 +57,7 @@ class ShortenAction extends Action return true; } - function handle($args) + function handle($args=null) { parent::handle($args); header('Content-Type: text/plain');