From 8597856b56ca728a2473b67f7f14e1aa3234220c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 9 May 2011 17:07:36 -0700 Subject: [PATCH] Fancier invitation form for whitelisted domains Squashed commit of the following: commit 1c0766e8f9d9e962ec553e2fb35bd2f944ffb4b0 Author: Zach Copley Date: Mon May 9 17:00:51 2011 -0700 Make the invites from the fancier invite form save commit 9ea45b7cf38eda8dad1d82e87b3400413a532079 Author: Zach Copley Date: Fri May 6 16:14:40 2011 -0700 .js to let the user add (and remove) additional invitees from their domain commit b2a02339bd11d02c7cba24629dde359e22de32b6 Author: Zach Copley Date: Thu May 5 15:44:49 2011 -0700 Load special whitelist invite .js when loading the invite page commit 132fed7550b40cd1d46ee506fd83974a116bce32 Author: Zach Copley Date: Wed May 4 18:35:49 2011 -0700 Remove settings class from whitelist inviter form commit a38437351b505594aead5da86af9a5ed089666b6 Author: Zach Copley Date: Wed May 4 18:21:18 2011 -0700 Make a fancier form for whitelist domain invites commit 710d4f41edf412871a9c1fbf33af317226485325 Author: Zach Copley Date: Wed May 4 17:34:09 2011 -0700 Add some more events to the invitation page commit 2449e4e0c1bf11568968cfc3ea2d8e69db2d875e Author: Zach Copley Date: Wed May 4 17:12:36 2011 -0700 Refactor invite action a bit --- EVENTS.txt | 18 ++ actions/invite.php | 247 ++++++++---------- lib/inviteform.php | 145 ++++++++++ .../DomainWhitelist/DomainWhitelistPlugin.php | 94 ++++++- plugins/DomainWhitelist/js/whitelistinvite.js | 40 +++ .../lib/whitelistinviteform.php | 184 +++++++++++++ 6 files changed, 595 insertions(+), 133 deletions(-) create mode 100644 lib/inviteform.php create mode 100644 plugins/DomainWhitelist/js/whitelistinvite.js create mode 100644 plugins/DomainWhitelist/lib/whitelistinviteform.php diff --git a/EVENTS.txt b/EVENTS.txt index f3f45cd084..0a9759c246 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1400,3 +1400,21 @@ EndReadWriteTables: after noting which tables must be read-write, even on read-o - $tables: list of table names - $rwdb: read-write database URI +StartShowInviteForm: Right before displaying the invitations form +- $action: invitation action + +EndShowInviteForm: After displaying the invitations form +- $action: invitation action + +StartSendInvitations: Right before sending invitations +- $action: invitation action + +EndSendInvitations: Right after sending invitations +- $action: invitation action + +StartShowInvitationSuccess: Right before showing invitations success msg +- $action: invitation action + +EndShowInvitationSuccess: After showing invitations success msg +- $action: invitation action + diff --git a/actions/invite.php b/actions/invite.php index 40376532b4..0e3a878f2e 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -1,7 +1,7 @@ trimmed('token'); - if (!$token || $token != common_session_token()) { - // TRANS: Client error displayed when the session token does not match or is not given. - $this->showForm(_('There was a problem with your session token. Try again, please.')); - return; - } + if (Event::handle('StartSendInvitations', array(&$this))) { - $user = common_current_user(); - $profile = $user->getProfile(); - - $bestname = $profile->getBestName(); - $sitename = common_config('site', 'name'); - $personal = $this->trimmed('personal'); - - $addresses = explode("\n", $this->trimmed('addresses')); - - foreach ($addresses as $email) { - $email = trim($email); - $valid = null; - - try { - - if (Event::handle('StartValidateUserEmail', array(null, $email, &$valid))) { - $valid = Validate::email($email, common_config('email', 'check_domain')); - Event::handle('EndValidateUserEmail', array(null, $email, &$valid)); - } - - if ($valid) { - if (Event::handle('StartValidateEmailInvite', array($user, $email, &$valid))) { - $valid = true; - Event::handle('EndValidateEmailInvite', array($user, $email, &$valid)); - } - } - - if (!$valid) { - // TRANS: Form validation message when providing an e-mail address that does not validate. - // TRANS: %s is an invalid e-mail address. - $this->showForm(sprintf(_('Invalid email address: %s.'), $email)); - return; - } - } catch (ClientException $e) { - $this->showForm($e->getMessage()); + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + // TRANS: Client error displayed when the session token does not match or is not given. + $this->showForm(_('There was a problem with your session token. Try again, please.')); return; } - } - $this->already = array(); - $this->subbed = array(); + $user = common_current_user(); + $profile = $user->getProfile(); - foreach ($addresses as $email) { - $email = common_canonical_email($email); - $other = User::staticGet('email', $email); - if ($other) { - if ($user->isSubscribed($other)) { - $this->already[] = $other; - } else { - subs_subscribe_to($user, $other); - $this->subbed[] = $other; + $bestname = $profile->getBestName(); + $sitename = common_config('site', 'name'); + $personal = $this->trimmed('personal'); + + $addresses = explode("\n", $this->trimmed('addresses')); + foreach ($addresses as $email) { + $email = trim($email); + $valid = null; + + try { + + if (Event::handle('StartValidateUserEmail', array(null, $email, &$valid))) { + $valid = Validate::email($email, common_config('email', 'check_domain')); + Event::handle('EndValidateUserEmail', array(null, $email, &$valid)); + } + + if ($valid) { + if (Event::handle('StartValidateEmailInvite', array($user, $email, &$valid))) { + $valid = true; + Event::handle('EndValidateEmailInvite', array($user, $email, &$valid)); + } + } + + if (!$valid) { + // TRANS: Form validation message when providing an e-mail address that does not validate. + // TRANS: %s is an invalid e-mail address. + $this->showForm(sprintf(_('Invalid email address: %s.'), $email)); + return; + } + } catch (ClientException $e) { + $this->showForm($e->getMessage()); + return; } - } else { - $this->sent[] = $email; - $this->sendInvitation($email, $user, $personal); } + + $this->already = array(); + $this->subbed = array(); + + foreach ($addresses as $email) { + $email = common_canonical_email($email); + $other = User::staticGet('email', $email); + if ($other) { + if ($user->isSubscribed($other)) { + $this->already[] = $other; + } else { + subs_subscribe_to($user, $other); + $this->subbed[] = $other; + } + } else { + $this->sent[] = $email; + $this->sendInvitation($email, $user, $personal); + } + } + + $this->mode = 'sent'; + + $this->showPage(); + Event::handle('EndSendInvitations', array($this)); } - - $this->mode = 'sent'; - - $this->showPage(); } function showScripts() @@ -158,50 +161,54 @@ class InviteAction extends CurrentUserDesignAction function showInvitationSuccess() { - if ($this->already) { - // TRANS: Message displayed inviting users to use a StatusNet site while the inviting user - // TRANS: is already subscribed to one or more users with the given e-mail address(es). - // TRANS: Plural form is based on the number of reported already subscribed e-mail addresses. - // TRANS: Followed by a bullet list. - $this->element('p', null, _m('You are already subscribed to this user:', - 'You are already subscribed to these users:', - count($this->already))); - $this->elementStart('ul'); - foreach ($this->already as $other) { - // TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address). - $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + if (Event::handle('StartShowInvitationSuccess', array($this))) { + + if ($this->already) { + // TRANS: Message displayed inviting users to use a StatusNet site while the inviting user + // TRANS: is already subscribed to one or more users with the given e-mail address(es). + // TRANS: Plural form is based on the number of reported already subscribed e-mail addresses. + // TRANS: Followed by a bullet list. + $this->element('p', null, _m('You are already subscribed to this user:', + 'You are already subscribed to these users:', + count($this->already))); + $this->elementStart('ul'); + foreach ($this->already as $other) { + // TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address). + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + } + $this->elementEnd('ul'); } - $this->elementEnd('ul'); - } - if ($this->subbed) { - // TRANS: Message displayed inviting users to use a StatusNet site while the invited user - // TRANS: already uses a this StatusNet site. Plural form is based on the number of - // TRANS: reported already present people. Followed by a bullet list. - $this->element('p', null, _m('This person is already a user and you were automatically subscribed:', - 'These people are already users and you were automatically subscribed to them:', - count($this->subbed))); - $this->elementStart('ul'); - foreach ($this->subbed as $other) { - // TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address). - $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + if ($this->subbed) { + // TRANS: Message displayed inviting users to use a StatusNet site while the invited user + // TRANS: already uses a this StatusNet site. Plural form is based on the number of + // TRANS: reported already present people. Followed by a bullet list. + $this->element('p', null, _m('This person is already a user and you were automatically subscribed:', + 'These people are already users and you were automatically subscribed to them:', + count($this->subbed))); + $this->elementStart('ul'); + foreach ($this->subbed as $other) { + // TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address). + $this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email)); + } + $this->elementEnd('ul'); } - $this->elementEnd('ul'); - } - if ($this->sent) { - // TRANS: Message displayed inviting users to use a StatusNet site. Plural form is - // TRANS: based on the number of invitations sent. Followed by a bullet list of - // TRANS: e-mail addresses to which invitations were sent. - $this->element('p', null, _m('Invitation sent to the following person:', - 'Invitations sent to the following people:', - count($this->sent))); - $this->elementStart('ul'); - foreach ($this->sent as $other) { - $this->element('li', null, $other); + if ($this->sent) { + // TRANS: Message displayed inviting users to use a StatusNet site. Plural form is + // TRANS: based on the number of invitations sent. Followed by a bullet list of + // TRANS: e-mail addresses to which invitations were sent. + $this->element('p', null, _m('Invitation sent to the following person:', + 'Invitations sent to the following people:', + count($this->sent))); + $this->elementStart('ul'); + foreach ($this->sent as $other) { + $this->element('li', null, $other); + } + $this->elementEnd('ul'); + // TRANS: Generic message displayed after sending out one or more invitations to + // TRANS: people to join a StatusNet site. + $this->element('p', null, _('You will be notified when your invitees accept the invitation and register on the site. Thanks for growing the community!')); } - $this->elementEnd('ul'); - // TRANS: Generic message displayed after sending out one or more invitations to - // TRANS: people to join a StatusNet site. - $this->element('p', null, _('You will be notified when your invitees accept the invitation and register on the site. Thanks for growing the community!')); + Event::handle('EndShowInvitationSuccess', array($this)); } } @@ -229,35 +236,11 @@ class InviteAction extends CurrentUserDesignAction function showInviteForm() { - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_invite', - 'class' => 'form_settings', - 'action' => common_local_url('invite'))); - $this->elementStart('fieldset'); - // TRANS: Form legend. - $this->element('legend', null, 'Send an invitation'); - $this->hidden('token', common_session_token()); - - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - // TRANS: Field label for a list of e-mail addresses. - $this->textarea('addresses', _('Email addresses'), - $this->trimmed('addresses'), - // TRANS: Tooltip for field label for a list of e-mail addresses. - _('Addresses of friends to invite (one per line).')); - $this->elementEnd('li'); - $this->elementStart('li'); - // TRANS: Field label for a personal message to send to invitees. - $this->textarea('personal', _('Personal message'), - $this->trimmed('personal'), - // TRANS: Tooltip for field label for a personal message to send to invitees. - _('Optionally add a personal message to the invitation.')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - // TRANS: Send button for inviting friends - $this->submit('send', _m('BUTTON', 'Send')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); + if (Event::handle('StartShowInviteForm', array($this))) { + $form = new InviteForm($this); + $form->show(); + Event::handle('EndShowInviteForm', array($this)); + } } function sendInvitation($email, $user, $personal) diff --git a/lib/inviteform.php b/lib/inviteform.php new file mode 100644 index 0000000000..7b3c164cc8 --- /dev/null +++ b/lib/inviteform.php @@ -0,0 +1,145 @@ +. + * + * @category Form + * @package StatusNet + * @author Zach Copley + * @copyright 2011 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')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/form.php'; + +/** + * Form for inviting collegues and friends + * + * @category Form + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + */ +class InviteForm extends Form +{ + /** + * Constructor + * + * @param Action $out output channel + */ + function __construct($out=null) + { + parent::__construct($out); + } + + /** + * ID of the form + * + * @return string ID of the form + */ + function id() + { + return 'form_invite'; + } + + /** + * class of the form + * + * @return string of the form class + */ + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('invite'); + } + + /** + * Name of the form + * + * @return void + */ + function formLegend() + { + // TRANS: Form legend. + $this->out->element('legend', null, _('Invite collegues')); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->elementStart('ul', 'form_data'); + $this->out->elementStart('li'); + // TRANS: Field label for a list of e-mail addresses. + $this->out->textarea( + 'addresses', + _('Email addresses'), + $this->out->trimmed('addresses'), + // TRANS: Tooltip for field label for a list of e-mail addresses. + _('Addresses of friends to invite (one per line).') + ); + $this->out->elementEnd('li'); + $this->out->elementStart('li'); + // TRANS: Field label for a personal message to send to invitees. + $this->out->textarea( + 'personal', _('Personal message'), + $this->out->trimmed('personal'), + // TRANS: Tooltip for field label for a personal message to send to invitees. + _('Optionally add a personal message to the invitation.') + ); + $this->out->elementEnd('li'); + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Send button for inviting friends + $this->out->submit( + 'send', + _m('BUTTON','Send'), 'submit form_action-primary', + // TRANS: Submit button title. + 'send', + _('Send') + ); + } +} diff --git a/plugins/DomainWhitelist/DomainWhitelistPlugin.php b/plugins/DomainWhitelist/DomainWhitelistPlugin.php index 4f8513238e..4bd9d59983 100644 --- a/plugins/DomainWhitelist/DomainWhitelistPlugin.php +++ b/plugins/DomainWhitelist/DomainWhitelistPlugin.php @@ -23,6 +23,7 @@ * @category Cache * @package StatusNet * @author Evan Prodromou + * @author Zach Copley * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ @@ -40,12 +41,64 @@ if (!defined('STATUSNET')) { * @category General * @package StatusNet * @author Evan Prodromou + * @author Zach Copley * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ class DomainWhitelistPlugin extends Plugin { + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false + * means stop. + */ + function onAutoload($cls) { + $base = dirname(__FILE__); + $lower = strtolower($cls); + + $files = array("$base/classes/$cls.php", + "$base/lib/$lower.php"); + if (substr($lower, -6) == 'action') { + $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; + } + foreach ($files as $file) { + if (file_exists($file)) { + include_once $file; + return false; + } + } + return true; + } + + /** + * Get the path to the plugin's installation directory. Used + * to link in js files and whatnot. + * + * @return String the absolute path + */ + protected function getPath() { + return preg_replace('/^' . preg_quote(INSTALLDIR, '/') . '\//', '', dirname(__FILE__)); + } + + /** + * Link in a JavaScript script for the whitelist invite form + * + * @param Action $action Action being shown + * + * @return boolean hook flag + */ + function onEndShowStatusNetScripts($action) { + $name = $action->arg('action'); + if ($name == 'invite') { + $action->script($this->getPath() . '/js/whitelistinvite.js'); + } + return true; + } + function onRequireValidatedEmailPlugin_Override($user, &$knownGood) { $knownGood = (!empty($user->email) && $this->matchesWhitelist($user->email)); @@ -120,11 +173,50 @@ class DomainWhitelistPlugin extends Plugin } } + /** + * Show a fancier invite form when domains are restricted to the + * whitelist. + * + * @param action $action the invite action + * @return boolean hook value + */ + function onStartShowInviteForm($action) + { + $form = new WhitelistInviteForm($action, $this->getWhitelist()); + $form->show(); + return false; + } + + /** + * This is a bit of a hack. We take the values from the custom + * whitelist invite form and reformat them so they look like + * their coming from the the normal invite form. + * + * @param action &$action the invite action + * @return boolean hook value + */ + function onStartSendInvitations(&$action) + { + $emails = array(); + $usernames = $action->arg('username'); + $domains = $action->arg('domain'); + + for($i = 0; $i < count($usernames); $i++) { + if (!empty($usernames[$i])) { + $emails[] = $usernames[$i] . '@' . $domains[$i] . "\n"; + } + } + + $action->args['addresses'] = implode($emails); + + return true; + } + function onPluginVersion(&$versions) { $versions[] = array('name' => 'DomainWhitelist', 'version' => STATUSNET_VERSION, - 'author' => 'Evan Prodromou', + 'author' => 'Evan Prodromou, Zach Copley', 'homepage' => 'http://status.net/wiki/Plugin:DomainWhitelist', 'rawdescription' => // TRANS: Plugin description. diff --git a/plugins/DomainWhitelist/js/whitelistinvite.js b/plugins/DomainWhitelist/js/whitelistinvite.js new file mode 100644 index 0000000000..d6a5a29eed --- /dev/null +++ b/plugins/DomainWhitelist/js/whitelistinvite.js @@ -0,0 +1,40 @@ +// XXX: Should I do crazy SN.X.Y.Z.A namespace instead? +var SN_WHITELIST = SN_WHITELIST || {}; + +SN_WHITELIST.updateButtons = function() { + var lis = $('ul > li > input[name^="username[]"]'); + if (lis.length === 1) { + $("ul > li > a.remove_row").hide(); + } else { + $("ul > li > a.remove_row:first").show(); + } +}; + +SN_WHITELIST.resetRow = function(row) { + $("input", row).val(''); + // Make sure the default domain is the first selection + $("select option:first", row).val(); + $("a.remove_row", row).show(); +}; + +SN_WHITELIST.addRow = function() { + var row = $(this).closest("li"); + var newRow = row.clone(); + SN_WHITELIST.resetRow(newRow); + $(newRow).insertAfter(row).show("blind", "slow", function() { + SN_WHITELIST.updateButtons(); + }); +}; + +SN_WHITELIST.removeRow = function() { + $(this).closest("li").hide("blind", "slow", function() { + $(this).remove(); + SN_WHITELIST.updateButtons(); + }); +}; + +$(document).ready(function() { + $('.add_row').live('click', SN_WHITELIST.addRow); + $('.remove_row').live('click', SN_WHITELIST.removeRow); +}); + diff --git a/plugins/DomainWhitelist/lib/whitelistinviteform.php b/plugins/DomainWhitelist/lib/whitelistinviteform.php new file mode 100644 index 0000000000..72029bc3e8 --- /dev/null +++ b/plugins/DomainWhitelist/lib/whitelistinviteform.php @@ -0,0 +1,184 @@ +. + * + * @category Form + * @package StatusNet + * @author Zach Copley + * @copyright 2011 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')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/form.php'; + +/** + * Form for inviting collegues and friends + * + * @category Form + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + */ +class WhitelistInviteForm extends Form +{ + private $whitelist = null; + + /** + * Constructor + * + * @param Action $out output channel + */ + function __construct($out, $whitelist) + { + parent::__construct($out); + $this->whitelist = $whitelist; + } + + /** + * ID of the form + * + * @return string ID of the form + */ + function id() + { + return 'form_invite'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('invite'); + } + + /** + * Name of the form + * + * @return void + */ + function formLegend() + { + // TRANS: Form legend. + $this->out->element('legend', null, _('Invite collegues')); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->elementStart('ul', 'form_data'); + for ($i = 0; $i < 3; $i++) { + $this->showEmailLI(); + } + $this->out->elementStart('li'); + // TRANS: Field label for a personal message to send to invitees. + $this->out->textarea( + 'personal', _('Personal message'), + $this->out->trimmed('personal'), + // TRANS: Tooltip for field label for a personal message to send to invitees. + _('Optionally add a personal message to the invitation.') + ); + $this->out->elementEnd('li'); + $this->out->elementEnd('ul'); + } + + function showEmailLI() + { + $this->out->elementStart('li'); + $this->out->input('username[]', ''); + $this->out->text('@'); + if (count($this->whitelist) == 1) { + $this->out->element( + 'span', + array('class' => 'email_invite'), + $this->whitelist[0] + ); + $this->out->hidden('domain[]', $this->whitelist[0]); + } else { + $content = array(); + foreach($this->whitelist as $domain) { + $content[$domain] = $domain; + } + $this->out->dropdown('domain[]', '', $content); + } + $this->showMultiControls(); + $this->out->elementEnd('li'); + } + + function showMultiControls() + { + $this->out->element( + 'a', + array( + 'class' => 'remove_row', + 'href' => 'javascript://', + ), + '-' + ); + + $this->out->element( + 'a', + array( + 'class' => 'add_row', + 'href' => 'javascript://', + ), + '+' + ); + } + + function getUsersDomain() + { + $user = common_current_user(); + + assert(!empty($user)); + + + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Send button for inviting friends + $this->out->submit( + 'send', + _m('BUTTON','Send'), 'submit form_action-primary', + // TRANS: Submit button title. + 'send', + _('Send') + ); + } +}