diff --git a/actions/approvegroup.php b/actions/approvegroup.php index 95338a4af3..55a0a6c3fd 100644 --- a/actions/approvegroup.php +++ b/actions/approvegroup.php @@ -157,7 +157,7 @@ class ApprovegroupAction extends Action $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 15eb2b5dc3..3074e3ffa3 100644 --- a/actions/cancelgroup.php +++ b/actions/cancelgroup.php @@ -141,7 +141,7 @@ class CancelgroupAction extends Action try { $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/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/classes/Profile.php b/classes/Profile.php index 94e709b508..3d989dd974 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -356,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(); diff --git a/classes/Subscription.php b/classes/Subscription.php index 70d351a0f7..438f1dfe50 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -122,7 +122,7 @@ class Subscription extends Memcached_DataObject Event::handle('EndSubscribe', array($subscriber, $other)); } - return true; + return $sub; } /** diff --git a/classes/Subscription_queue.php b/classes/Subscription_queue.php index 3e254dfce1..41c0a54fa8 100644 --- a/classes/Subscription_queue.php +++ b/classes/Subscription_queue.php @@ -72,7 +72,7 @@ class Subscription_queue extends Managed_DataObject { $subscriber = Profile::staticGet('id', $this->subscriber); $subscribed = Profile::staticGet('id', $this->subscribed); - $sub = Subscription::start($subscriber, $other, Subscription::FORCE); + $sub = Subscription::start($subscriber, $subscribed, Subscription::FORCE); if ($sub) { $this->delete(); } @@ -82,7 +82,7 @@ class Subscription_queue extends Managed_DataObject /** * Cancel an outstanding subscription request to the other profile. */ - public function abort($profile) + public function abort() { $subscriber = Profile::staticGet('id', $this->subscriber); $subscribed = Profile::staticGet('id', $this->subscribed); 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/router.php b/lib/router.php index 2cbb1ad649..156bc10c7f 100644 --- a/lib/router.php +++ b/lib/router.php @@ -199,7 +199,7 @@ class Router // main stuff is repetitive $main = array('login', 'logout', 'register', 'subscribe', - 'unsubscribe', 'cancelsubscription', + 'unsubscribe', 'cancelsubscription', 'approvesub', 'confirmaddress', 'recoverpassword', 'invite', 'favor', 'disfavor', 'sup', 'block', 'unblock', 'subedit', @@ -844,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, @@ -900,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(); + } }