/[drupal]/contributions/modules/invite/invite.module
ViewVC logotype

Contents of /contributions/modules/invite/invite.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.26 - (show annotations) (download) (as text)
Fri Oct 9 20:28:19 2009 UTC (6 weeks, 4 days ago) by smk
Branch: MAIN
Changes since 1.25: +107 -58 lines
File MIME type: text/x-php
Synced with DRUPAL-6--2.

#364971 by jaydub: Fixed administrative overview query for PostgreSQL.
#322748: Fixed only administrator can send invitations on multilingual installation.
#374869 by webchick, smk-ka: Denying access to the user registration form at the menu callback level, not at the form level, for better compatibility with other modules.
#467486 by neilnz: Grammar fix.
#310775 by Barrett, sun: Fixed SQL error when no emails are left after filtering out registered users.
#323661 by tcconway: Removed "Track" from local task title "Track Invitations".
#214426 by barako: Updated French translation.
#317552 by Stella: Use UTF8-safe string functions and other minor changes.
1 <?php
2 // $Id: invite.module,v 1.25 2008/09/02 17:02:14 smk Exp $
3
4 /**
5 * @file
6 * Allows your users to send and track invitations to join your site.
7 */
8
9 /**
10 * Session names.
11 */
12 define('INVITE_SESSION', 'invite_code');
13 define('INVITE_ADMIN_SESSION', 'invite_admin_filter');
14
15 /**
16 * Value for unlimited invites.
17 */
18 define('INVITE_UNLIMITED', -1);
19
20 /**
21 * Include token support.
22 */
23 require_once drupal_get_path('module', 'invite') .'/invite_token.inc';
24
25 /**
26 * Implementation of hook_help().
27 */
28 function invite_help($path, $arg) {
29 switch ($path) {
30 // Display module help
31 case 'admin/help#invite':
32 return _invite_module_help();
33
34 // Display introductory text on user profile pages
35 case 'user/%/invites':
36 case 'user/%/invites/accepted':
37 $output = '<p>'. t("The invitations shown on this page have been used to join the site. Clicking on an e-mail address takes you to the user's profile page.");
38 break;
39 case 'user/%/invites/pending':
40 $output = '<p>'. t("The invitations shown on this page haven't been accepted yet.");
41 break;
42 case 'user/%/invites/expired':
43 $output = '<p>'. t('The invitations shown on this page have not been used to register on the site within the expiration period of @count days.', array('@count' => variable_get('invite_expiry', 30)));
44 break;
45
46 default:
47 return;
48 }
49 $output .= ' '. t('The status <em>deleted</em> means the user account has been terminated.') .'</p>';
50 if (!user_access('withdraw accepted invitations')) {
51 $output .= '<p>'. t("At any time, you may withdraw either pending or expired invitations. Accepted invitations can't be withdrawn and count permanently toward your invitation allotment.") .'</p>';
52 }
53 return $output;
54 }
55
56 /**
57 * Display module help.
58 */
59 function _invite_module_help() {
60 $file = drupal_get_path('module', 'invite') .'/README.txt';
61 if (file_exists($file)) {
62 return _filter_autop(check_plain(file_get_contents($file)));
63 }
64 }
65
66 /**
67 * Implementation of hook_theme().
68 */
69 function invite_theme() {
70 return array(
71 'invite_form' => array(
72 'arguments' => array('form' => NULL),
73 ),
74 'invite_user_overview' => array(
75 'arguments' => array('items' => NULL),
76 'file' => 'invite_admin.inc',
77 ),
78 'invite_token_help' => array(
79 'arguments' => array('type' => NULL, 'prefix' => NULL, 'suffix' => NULL),
80 'file' => 'invite_token.inc',
81 ),
82 );
83 }
84
85 /**
86 * Implementation of hook_perm().
87 */
88 function invite_perm() {
89 return array(
90 'send invitations',
91 'send mass invitations',
92 'track invitations',
93 'withdraw accepted invitations'
94 );
95 }
96
97 /**
98 * Implements hook_init().
99 */
100 function invite_init() {
101 global $user;
102
103 // Notify current user about newly joined invitees.
104 if (!empty($user->invite_sent) && !module_invoke('throttle', 'status')) {
105 invite_notify($user->uid);
106 }
107 }
108
109 /**
110 * Implementation of hook_menu().
111 */
112 function invite_menu() {
113 // Admin menu items
114 $items['admin/user/invite'] = array(
115 'title' => 'Invites',
116 'page callback' => 'invite_admin_overview',
117 'access arguments' => array('administer site configuration'),
118 'type' => MENU_NORMAL_ITEM,
119 'file' => 'invite_admin.inc',
120 );
121 $items['admin/user/invite/list'] = array(
122 'title' => 'Inviters',
123 'type' => MENU_DEFAULT_LOCAL_TASK,
124 'weight' => -10,
125 );
126 $items['admin/user/invite/settings'] = array(
127 'title' => 'Settings',
128 'page callback' => 'drupal_get_form',
129 'page arguments' => array('invite_settings'),
130 'access arguments' => array('administer site configuration'),
131 'type' => MENU_LOCAL_TASK,
132 'weight' => 10,
133 'file' => 'invite_admin.inc',
134 );
135 $items['admin/user/invite/details/%user'] = array(
136 'title callback' => 'invite_admin_details_page_title',
137 'title arguments' => array(4),
138 'page callback' => 'invite_admin_details',
139 'page arguments' => array(4),
140 'access arguments' => array('administer site configuration'),
141 'type' => MENU_LOCAL_TASK,
142 'file' => 'invite_admin.inc',
143 );
144
145 // Frontend menu items
146 $items['invite'] = array(
147 'title' => 'Invite a friend',
148 'title callback' => 'invite_page_title',
149 'page callback' => 'drupal_get_form',
150 'page arguments' => array('invite_form', 'page', array()),
151 'access arguments' => array('send invitations'),
152 'type' => MENU_NORMAL_ITEM,
153 );
154 $items['invite/accept/%invite'] = array(
155 'page callback' => 'invite_accept',
156 'page arguments' => array(2),
157 'access callback' => TRUE,
158 'type' => MENU_CALLBACK,
159 );
160 $items['invite/withdraw'] = array(
161 'page callback' => 'drupal_get_form',
162 'page arguments' => array('invite_cancel'),
163 'access arguments' => array('track invitations'),
164 'type' => MENU_CALLBACK,
165 );
166 $items['invite/resend/%invite'] = array(
167 'title' => 'Resend invitation',
168 'page callback' => 'invite_resend',
169 'page arguments' => array(2),
170 'access arguments' => array('send invitations'),
171 'type' => MENU_CALLBACK,
172 );
173
174 // User profile tabs
175 $items['user/%user/invites'] = array(
176 'title' => 'Invitations',
177 'page callback' => 'invite_user_overview',
178 'access callback' => 'invite_user_access',
179 'access arguments' => array('track invitations', 1),
180 'type' => MENU_LOCAL_TASK,
181 'file' => 'invite_admin.inc',
182 );
183 $items['user/%user/invites/accepted'] = array(
184 'title' => 'Accepted',
185 'page callback' => 'invite_user_overview',
186 'page arguments' => array('accepted'),
187 'access callback' => 'invite_user_access',
188 'access arguments' => array('track invitations', 1),
189 'type' => MENU_DEFAULT_LOCAL_TASK,
190 'weight' => -5,
191 'file' => 'invite_admin.inc',
192 );
193 $items['user/%user/invites/pending'] = array(
194 'title' => 'Pending',
195 'page callback' => 'invite_user_overview',
196 'page arguments' => array('pending'),
197 'access callback' => 'invite_user_access',
198 'access arguments' => array('track invitations', 1),
199 'type' => MENU_LOCAL_TASK,
200 'file' => 'invite_admin.inc',
201 );
202 $items['user/%user/invites/expired'] = array(
203 'title' => 'Expired',
204 'page callback' => 'invite_user_overview',
205 'page arguments' => array('expired'),
206 'access callback' => 'invite_user_access',
207 'access arguments' => array('track invitations', 1),
208 'type' => MENU_LOCAL_TASK,
209 'weight' => 5,
210 'file' => 'invite_admin.inc',
211 );
212 $items['user/%user/invites/new'] = array(
213 'title' => 'New invitation',
214 'page callback' => 'drupal_get_form',
215 'page arguments' => array('invite_form', 'page', array()),
216 'access callback' => 'invite_user_access',
217 'access arguments' => array('send invitations', 1),
218 'type' => MENU_LOCAL_TASK,
219 'weight' => 10,
220 );
221
222 return $items;
223 }
224
225 /**
226 * Implementation of hook_menu_alter().
227 *
228 * Override the user/register menu access handler with a custom
229 * implementation.
230 */
231 function invite_menu_alter(&$items) {
232 if (invite_user_registration_by_invite_only()) {
233 $items['user/register']['access callback'] = 'invite_user_register_access';
234 }
235 }
236
237 /**
238 * Determine if user registration mode is set to invite only.
239 */
240 function invite_user_registration_by_invite_only() {
241 return (variable_get('user_register', 1) === '1-inviteonly');
242 }
243
244 /**
245 * Access callback; determine access to user registration form.
246 */
247 function invite_user_register_access() {
248 $invite = invite_load_from_session();
249
250 // Legacy url support (user/register/regcode).
251 if (!$invite && $code = arg(2)) {
252 if ($invite = invite_load($code)) {
253 if (invite_validate($invite)) {
254 $_SESSION[INVITE_SESSION] = $invite->reg_code;
255 }
256 }
257 }
258 if (!$invite && !user_access('administer users')) {
259 drupal_set_message(t('Sorry, new user registration by invitation only.'));
260 return FALSE;
261 }
262
263 // Let the default handler take care of standard conditions.
264 return user_register_access();
265 }
266
267 /**
268 * Title callback allowing for customization of the invite page title.
269 *
270 * @param $title
271 * The default page title, ie. non-overridden.
272 */
273 function invite_page_title($title) {
274 return variable_get('invite_page_title', $title);
275 }
276
277 /**
278 * Title callback for the user details administration page.
279 *
280 * @param $account
281 */
282 function invite_admin_details_page_title($account) {
283 return t('Invitees of @name', array('@name' => $account->name));
284 }
285
286 /**
287 * Access callback ensuring the user profile tabs are visible only to their
288 * owner.
289 *
290 * @param $permission
291 * Required permission to view the item.
292 * @param $account
293 * A user object.
294 */
295 function invite_user_access($permission, $account) {
296 return ($account->uid == $GLOBALS['user']->uid && user_access($permission));
297 }
298
299 /**
300 * Displays a notification message when an invited user has registered.
301 *
302 * @param $uid
303 * The user id to check accepted invitations for.
304 */
305 function invite_notify($uid) {
306 $result = db_query('SELECT invitee FROM {invite_notifications} WHERE uid = %d', $uid);
307 while ($row = db_fetch_object($result)) {
308 $account = user_load(array('uid' => $row->invitee, 'status' => 1));
309 if ($account) {
310 drupal_set_message(t('!user (@email) has joined @site-name!', array('!user' => theme('username', $account), '@email' => $account->mail, '@site-name' => variable_get('site_name', t('Drupal')))));
311 db_query("DELETE FROM {invite_notifications} WHERE uid = %d AND invitee = %d", $uid, $row->invitee);
312 }
313 }
314 }
315
316 /**
317 * Menu callback; handle incoming requests for accepting an invite.
318 *
319 * @param $invite
320 * A (unvalidated) invite object.
321 */
322 function invite_accept($invite) {
323 global $user;
324
325 if (!$user->uid && invite_validate($invite)) {
326 $_SESSION[INVITE_SESSION] = $invite->reg_code;
327 drupal_goto('user/register');
328 }
329
330 drupal_goto();
331 }
332
333 /**
334 * Implementation of hook_form_alter().
335 */
336 function invite_form_alter(&$form, $form_state, $form_id) {
337 switch ($form_id) {
338 case 'user_admin_settings':
339 // Add new registration mode 'by invitation only'. By prepending the
340 // option value with a numeric value, other modules still work as
341 // expected, as long as they are using the non-strict PHP comparison
342 // operator (since '1-inviteonly' == 1 yields TRUE). To determine the real
343 // setting use invite_user_registration_by_invite_only().
344 //
345 // However, setting the new mode is only allowed if no other module
346 // has overridden the menu access handler for the user registration form.
347 $item = menu_get_item('user/register');
348 if (in_array($item['access_callback'], array('user_register_access', 'invite_user_register_access'))) {
349 $form['registration']['user_register']['#options']['1-inviteonly'] = t('New user registration by invitation only.');
350 }
351 // Clear menu cache on submit to allow our custom access handler to
352 // snap in.
353 $form['#submit'][] = 'menu_rebuild';
354 break;
355
356 case 'user_register':
357 // In order to prevent caching of the preset e-mail address, we have to
358 // disable caching for user/register.
359 $GLOBALS['conf']['cache'] = CACHE_DISABLED;
360
361 $invite = invite_load_from_session();
362
363 // Legacy url support (user/register/regcode).
364 if (!$invite && $code = arg(2)) {
365 if ($invite = invite_load($code)) {
366 if (invite_validate($invite)) {
367 $_SESSION[INVITE_SESSION] = $invite->reg_code;
368 }
369 }
370 }
371 if ($invite) {
372 // Preset the e-mail field.
373 if (isset($form['account'])) {
374 $field = &$form['account'];
375 }
376 else {
377 $field = &$form;
378 }
379 if (isset($field['mail'])) {
380 $field['mail']['#default_value'] = $invite->email;
381 }
382 }
383 break;
384
385 case 'user_login_block':
386 // Remove temptation for non members to try and register.
387 if (invite_user_registration_by_invite_only()) {
388 $new_items = array();
389 $new_items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
390 $form['links']['#value'] = theme('item_list', $new_items);
391 }
392 break;
393 }
394 }
395
396 /**
397 * Load an invite record for a tracking code.
398 *
399 * @param $code
400 * A registration code to load the invite record for.
401 * @return
402 * An invite record.
403 */
404 function invite_load($code) {
405 $result = db_query("SELECT * FROM {invite} WHERE reg_code = '%s' AND canceled = 0", $code);
406 if ($invite = db_fetch_object($result)) {
407 $invite->inviter = user_load(array('uid' => $invite->uid));
408 $invite->data = (array)unserialize($invite->data);
409 }
410 return $invite;
411 }
412
413 /**
414 * Returns an invite record from an invite code stored in the user's session.
415 *
416 * @return
417 * An invite record, or FALSE if there is no invite code stored in the
418 * user's session.
419 */
420 function invite_load_from_session() {
421 if (isset($_SESSION[INVITE_SESSION])) {
422 return invite_load($_SESSION[INVITE_SESSION]);
423 }
424 return FALSE;
425 }
426
427 /**
428 * Validates an invite record.
429 *
430 * @param $invite
431 * An invite record as returned by invite_load().
432 * @return
433 * TRUE if the invite is valid, otherwise this function won't return.
434 */
435 function invite_validate($invite) {
436 if (!$invite || !$invite->inviter) {
437 drupal_set_message(t('This invitation has been withdrawn.'));
438 drupal_goto();
439 }
440 else if ($invite->joined != 0) {
441 drupal_set_message(t('This invitation has already been used. Please login now with your username and password.'));
442 drupal_goto('user');
443 }
444 else if ($invite->expiry < time()) {
445 drupal_set_message(t('Sorry, this invitation has expired.'));
446 drupal_goto();
447 }
448 else {
449 return TRUE;
450 }
451 }
452
453 /**
454 * Implementation of hook_user().
455 */
456 function invite_user($op, &$edit, &$account, $category = NULL) {
457 switch ($op) {
458 case 'insert':
459 $invite = invite_load_from_session();
460
461 if (!$invite) {
462 // Try to look up an invitation in case a user has been invited to join
463 // the site, but did go straight to the site and signed up without
464 // using the invite link.
465 $code = db_result(db_query("SELECT reg_code FROM {invite} WHERE email = '%s'", $account->mail));
466 if ($code) {
467 $invite = invite_load($code);
468 }
469 }
470 if ($invite) {
471 _invite_accept($invite, $account);
472
473 // Flag the inviting user, this triggers status notifications and
474 // saves us some queries otherwise.
475 if ($invite->inviter->uid) {
476 user_save($invite->inviter, array('invite_sent' => TRUE));
477 }
478
479 unset($_SESSION[INVITE_SESSION]);
480 }
481 break;
482
483 case 'delete':
484 invite_delete($account->uid);
485 break;
486 }
487 }
488
489 /**
490 * Set an invitation's status to accepted.
491 *
492 * @param $invite
493 * An invite object.
494 * @param $account
495 * The user object of the invitee.
496 */
497 function _invite_accept($invite, $account) {
498 // Update the invitation record.
499 db_query("UPDATE {invite} SET email = '%s', invitee = %d, joined = %d WHERE reg_code = '%s'", $account->mail, $account->uid, time(), $invite->reg_code);
500 // Delete all invites to these e-mail addresses, except this one.
501 db_query("DELETE FROM {invite} WHERE (email = '%s' OR email = '%s') AND reg_code <> '%s'", $invite->email, $account->mail, $invite->reg_code);
502 // Add all users who invited this particular e-mail address to the
503 // notification queue.
504 db_query("INSERT INTO {invite_notifications} (uid, invitee) SELECT uid, %d from {invite} WHERE (email = '%s' OR email = '%s') AND canceled = 0", $account->uid, $invite->email, $account->mail);
505 // Escalate the invitee's role.
506 _invite_escalate_role($account);
507 // Unblock user account.
508 db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $account->uid);
509 }
510
511 /**
512 * Escalates an invited user's role, based on the role(s) of the inviter.
513 *
514 * @param $account
515 * The user object of the invitee.
516 */
517 function _invite_escalate_role($account) {
518 // Add a dummy entry to retrieve the default target role setting.
519 $roles = array('default' => 'default');
520
521 // Add roles of inviter.
522 $inviter_uid = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $account->uid));
523 if ($inviter_uid && $inviter = user_load(array('uid' => $inviter_uid))) {
524 $roles = array_merge($roles, array_intersect($inviter->roles, user_roles(FALSE, 'send invitations')));
525 }
526
527 // Map to configured target roles.
528 $targets = array();
529 foreach ($roles as $rid => $role) {
530 $target = variable_get('invite_target_role_'. $rid, DRUPAL_AUTHENTICATED_RID);
531 if ($target != DRUPAL_AUTHENTICATED_RID) {
532 $targets[$target] = $target;
533 }
534 }
535
536 // Notify other modules of changed user.
537 $edit = array('roles' => $targets);
538 user_module_invoke('update', $edit, $account);
539
540 // Save new user role(s).
541 foreach ($targets as $target) {
542 db_query("DELETE FROM {users_roles} WHERE uid = %d AND rid = %d", $account->uid, $target);
543 db_query("INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)", $account->uid, $target);
544 }
545
546 // Notify other modules of role escalation.
547 $args = array('invitee' => $account, 'inviter' => $inviter, 'roles' => $targets);
548 module_invoke_all('invite', 'escalate', $args);
549 }
550
551 /**
552 * Physically delete all invites from and to a user.
553 *
554 * @param $uid
555 * The user id to delete invites for.
556 */
557 function invite_delete($uid) {
558 // Delete invite for this user if the originating user has the permission.
559 $origin = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $uid));
560 if ($origin && $inviter = user_load(array('uid' => $origin))) {
561 if (user_access('withdraw accepted invitations', $inviter)) {
562 db_query("DELETE FROM {invite} WHERE invitee = %d", $uid);
563 }
564 }
565 // Delete any invites originating from this user.
566 db_query("DELETE FROM {invite} WHERE uid = %d", $uid);
567 // Clean up the notification queue.
568 db_query("DELETE FROM {invite_notifications} WHERE uid = %d OR invitee = %d", $uid, $uid);
569 }
570
571 /**
572 * Implementation of hook_block().
573 */
574 function invite_block($op = 'list', $delta = 0, $edit = array()) {
575 if ($op == 'list') {
576 $blocks[0] = array('info' => t('Invite a friend'), 'cache' => BLOCK_CACHE_PER_ROLE);
577 return $blocks;
578 }
579 else if ($op == 'view') {
580 $block = array();
581 switch ($delta) {
582 case 0:
583 if (user_access('send invitations')) {
584 $block = array(
585 'subject' => t('Invite a friend'),
586 'content' => drupal_get_form('invite_form', 'block'),
587 );
588 }
589 break;
590 }
591 return $block;
592 }
593 }
594
595 /**
596 * Generate the invite forms.
597 *
598 * @param $form_satate
599 * A keyed array containing the current state of the form.
600 * @param $op
601 * The type of form to generate, 'page' or 'block'.
602 * @param $edit
603 * Previous values when resending an invite.
604 * @return
605 * A form definition.
606 */
607 function invite_form(&$form_state, $op = 'page', $edit = array()) {
608 global $user;
609
610 if (!is_array($edit)) {
611 $edit = (array)$edit;
612 }
613
614 $remaining_invites = invite_get_remaining_invites($user);
615
616 if ($remaining_invites == 0) {
617 if ($op == 'block') {
618 // Hide block.
619 $form['#access'] = FALSE;
620 return $form;
621 }
622 else if (!$edit) {
623 // Deny access when NOT resending an invite.
624 drupal_set_message(t("Sorry, you've reached the maximum number of invitations."), 'error');
625 drupal_goto(referer_uri());
626 }
627 }
628
629 $form['resent'] = array(
630 '#type' => 'value',
631 '#value' => $edit ? $edit['resent'] + 1 : 0,
632 );
633 $form['reg_code'] = array(
634 '#type' => 'value',
635 '#value' => $edit ? $edit['reg_code'] : NULL,
636 );
637 if ($remaining_invites != INVITE_UNLIMITED) {
638 $form['remaining_invites'] = array(
639 '#type' => 'value',
640 '#value' => $remaining_invites,
641 );
642 }
643 switch ($op) {
644 case 'page':
645 default:
646 $form += invite_page_form($remaining_invites, $edit);
647 break;
648 case 'block':
649 $form += invite_block_form($remaining_invites);
650 break;
651 }
652
653 return $form;
654 }
655
656 /**
657 * Calculate the remaining invites of a user.
658 *
659 * @param $account
660 * A user object.
661 * @return
662 * The number of remaining invites.
663 */
664 function invite_get_remaining_invites($account) {
665 if ($account->uid == 1) {
666 return INVITE_UNLIMITED;
667 }
668
669 // Check user property for remaining invites.
670 $data = unserialize($account->data);
671 if (isset($data['invites'])) {
672 $remaining = $data['invites'];
673 }
674 else {
675 $remaining = invite_get_role_limit($account);
676 if ($remaining > 0) {
677 // Legacy support.
678 $sent = db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d", $account->uid));
679 $remaining = max($remaining - $sent, 0);
680 if ($sent > 0) {
681 // Update user property for faster lookup next time.
682 user_save($account, array('invites' => $remaining));
683 }
684 }
685 }
686
687 return $remaining;
688 }
689
690 /**
691 * Calculate the max. number of invites based on a user's role.
692 *
693 * @param $account
694 * A user object.
695 * @return
696 * The configured maximum of invites.
697 */
698 function invite_get_role_limit($account) {
699 if (!isset($account->roles)) {
700 $account = user_load(array('uid' => $account->uid));
701 }
702
703 $role_limit = 0;
704 foreach (user_roles(FALSE, 'send invitations') as $rid => $role) {
705 if (array_key_exists($rid, $account->roles)) {
706 $role_max = variable_get('invite_maxnum_'. $rid, INVITE_UNLIMITED);
707 if ($role_max == INVITE_UNLIMITED) {
708 return INVITE_UNLIMITED;
709 }
710 $role_limit = max($role_max, $role_limit);
711 }
712 }
713 return $role_limit;
714 }
715
716 /**
717 * Generate the invite page form.
718 *
719 * @param $remaining_invite
720 * Number of remaining invites.
721 * @param $edit
722 * Previous values when resending an invite.
723 * @return
724 * A form definition.
725 */
726 function invite_page_form($remaining_invites, $edit = array()) {
727 global $user;
728
729 // Remaining invites.
730 if ($remaining_invites != INVITE_UNLIMITED) {
731 $form['remaining_invites_markup']['#value'] = format_plural($remaining_invites, 'You have 1 invite remaining.', 'You have @count invites remaining.');
732 }
733
734 // Sender e-mail address.
735 if ($user->uid && variable_get('invite_use_users_email', 0)) {
736 $from = $user->mail;
737 }
738 else {
739 $from = variable_get('site_mail', ini_get('sendmail_from'));
740 }
741 // Personalize displayed e-mail address.
742 // @see http://drupal.org/project/pmail
743 if (module_exists('pmail')) {
744 $from = personalize_email($from);
745 }
746 $form['from'] = array(
747 '#type' => 'item',
748 '#title' => t('From'),
749 '#value' => check_plain($from),
750 );
751
752 // Recipient email address.
753 if (!$edit) {
754 $failed_emails = '';
755 $allow_multiple = user_access('send mass invitations');
756 if (isset($_SESSION['invite_failed_emails'])) {
757 $failed_emails = implode("\n", (array)unserialize($_SESSION['invite_failed_emails']));
758 unset($_SESSION['invite_failed_emails']);
759 }
760 $form['email'] = array(
761 '#title' => t('To'),
762 '#default_value' => $failed_emails,
763 '#description' => format_plural($allow_multiple ? 99 : 1, 'Enter the e-mail address of the person you would like to invite.', 'Enter the e-mail addresses of the persons you would like to invite. To specify multiple recipients, enter one e-mail address per line or separate each address with a comma.'),
764 '#required' => TRUE,
765 );
766 if ($allow_multiple) {
767 $form['email']['#type'] = 'textarea';
768 $form['email']['#rows'] = 3;
769 }
770 else {
771 $form['email']['#type'] = 'textfield';
772 $form['email']['#maxlength'] = 64;
773 }
774 if ($failed_emails) {
775 $form['email']['#attributes']['class'] = 'error';
776 }
777 }
778 else {
779 // The email is not editable when resending an invite.
780 $allow_multiple = FALSE;
781 $form['email_markup'] = array(
782 '#type' => 'item',
783 '#title' => t('To'),
784 '#value' => check_plain($edit['email']),
785 );
786 $form['email'] = array(
787 '#type' => 'value',
788 '#value' => $edit['email'],
789 );
790 }
791
792 // Message subject.
793 if ($edit && !empty($edit['data']['subject'])) {
794 $subject = $edit['data']['subject'];
795 }
796 else {
797 $subject = invite_get_subject();
798 }
799 // Add prefix.
800 $prefix = t('Re:');
801 if ($edit && drupal_substr($subject, 0, strlen($prefix)) != $prefix) {
802 $subject = $prefix .' '. $subject;
803 }
804 if (variable_get('invite_subject_editable', FALSE)) {
805 $form['subject'] = array(
806 '#type' => 'textfield',
807 '#title' => t('Subject'),
808 '#default_value' => $subject,
809 '#maxlength' => 64,
810 '#description' => t('Type the subject of the invitation e-mail.'),
811 '#required' => TRUE,
812 );
813 }
814 else {
815 $form['subject'] = array(
816 '#type' => 'item',
817 '#title' => t('Subject'),
818 '#value' => check_plain($subject),
819 );
820 }
821
822 // Message body.
823 $form['body'] = array(
824 '#type' => 'item',
825 '#title' => t('Message'),
826 );
827 $form['message'] = array(
828 '#type' => 'textarea',
829 '#default_value' => ($edit && !empty($edit['data']['message'])) ? $edit['data']['message'] : '',
830 '#description' => format_plural($allow_multiple ? 1 : 99, 'This message will be added to the mail sent to the person you are inviting.', 'This message will be added to the mail sent to the persons you are inviting.'),
831 );
832
833 $form['submit'] = array(
834 '#type' => 'submit',
835 '#value' => t('Send invite'),
836 );
837
838 return $form;
839 }
840
841 /**
842 * Generate the invite block form.
843 *
844 * @param $remaining_invite
845 * Number of remaining invites.
846 * @return
847 * A form definition.
848 */
849 function invite_block_form($remaining_invites) {
850 global $user;
851
852 $form['#action'] = url('invite');
853
854 $form['invite'] = array(
855 '#value' => t('Recommend @site-name to:', array('@site-name' => variable_get('site_name', t('Drupal')))),
856 );
857 $description = '';
858 if ($remaining_invites != INVITE_UNLIMITED) {
859 $description = format_plural($remaining_invites, '1 invite remaining', '@count invites remaining');
860 }
861 $form['email'] = array(
862 '#type' => 'textfield',
863 '#size' => 20,
864 '#maxlength' => 64,
865 '#description' => $description,
866 '#required' => TRUE,
867 );
868 $form['submit'] = array(
869 '#type' => 'submit',
870 '#value' => t('Send invite'),
871 );
872 $form['link'] = array(
873 '#prefix' => '<div><small>',
874 '#value' => l(t('View your invites'), "user/$user->uid/invites"),
875 '#suffix' => '</small></div>',
876 '#access' => user_access('track invitations') && $user->uid,
877 );
878
879 return $form;
880 }
881
882 /**
883 * Theme function for the invite form.
884 *
885 * @ingroup themeable
886 */
887 function theme_invite_form($form) {
888 $output = '';
889 $op = $form['#parameters'][2];
890
891 if ($op == 'page') {
892 // Show form elements.
893 $output .= drupal_render($form['remaining_invites_markup']);
894 $output .= drupal_render($form['remaining_invites']);
895 $output .= drupal_render($form['from']);
896 if (isset($form['email_markup'])) {
897 $output .= drupal_render($form['email_markup']);
898 }
899 $output .= drupal_render($form['email']);
900 $output .= drupal_render($form['subject']);
901
902 // Show complete invitation message.
903 $output .= drupal_render($form['body']);
904 $output .= '<div class="invite-message"><div class="opening">';
905
906 // Prepare invitation message.
907 $message_form = "</p></div>\n". drupal_render($form['message']) ."\n".'<div class="closing"><p>';
908 $body = _filter_autop(t(_invite_get_mail_template()));
909
910 // Perform token replacement on message body.
911 $types = _invite_token_types(array('data' => array('message' => $message_form)));
912 $output .= token_replace_multiple($body, $types);
913
914 $output .= "</div></div>\n";
915 }
916
917 // Render all missing form elements.
918 $output .= drupal_render($form);
919
920 return $output;
921 }
922
923 /**
924 * Forms API callback; validate submitted form data.
925 *
926 * Filters out e-mails that are already registered or have been invited before.
927 * Checks the invite limit of the user and the max. number of invites per turn.
928 */
929 function invite_form_validate($form, &$form_state) {
930 global $user;
931
932 $emails = _invite_get_emails($form_state['values']['email']);
933
934 if (!$form_state['values']['resent']) {
935 if (count($emails) > 0) {
936 // Filter out already registered users, but pass validation.
937 $failed_emails = _invite_validate_emails("SELECT mail AS email FROM {users} WHERE mail IN (". db_placeholders($emails, 'varchar') .")", $emails);
938 if (count($failed_emails)) {
939 $error = format_plural(count($failed_emails), 'The following recipient is already a member:', 'The following recipients are already members:') .'<br />';
940 foreach ($failed_emails as $key => $email) {
941 $account = user_load(array('mail' => $email));
942 $failed_emails[$key] = theme('username', $account) .' ('. check_plain($email) .')';
943 }
944 $error .= implode(', ', $failed_emails);
945 drupal_set_message($error, 'error');
946 }
947 }
948
949 if (!empty($emails)) {
950 // Filter out already invited users, but pass validation.
951 $failed_emails = _invite_validate_emails("SELECT email FROM {invite} WHERE email IN (". db_placeholders($emails, 'varchar') .") AND uid = %d AND canceled = 0", $emails, $user->uid);
952 if (count($failed_emails)) {
953 $error = format_plural(count($failed_emails), 'You have already invited the following recipient:', 'You have already invited the following recipients:') .'<br />';
954 $error .= implode(', ', array_map('check_plain', $failed_emails));
955 drupal_set_message($error, 'error');
956 }
957 }
958
959 // Check that there is at least one valid e-mail remaining after filtering
960 // out dupes.
961 if (count($emails) == 0) {
962 form_set_error('email');
963 return;
964 }
965
966 // Check invite limit, fail to let the user choose which ones to send.
967 if (isset($form_state['values']['remaining_invites']) && count($emails) > $form_state['values']['remaining_invites']) {
968 form_set_error('email', format_plural($form_state['values']['remaining_invites'], 'You have only 1 invite left.', 'You have only @count invites left.'));
969 return;
970 }
971
972 // Check number of e-mails.
973 if (!user_access('send mass invitations') && count($emails) > 1) {
974 form_set_error('email', t('You cannot send more than one invitation.'));
975 return;
976 }
977 }
978
979 // Save valid emails.
980 $form_state['values']['valid_emails'] = $emails;
981 }
982
983 /**
984 * Extract valid e-mail addresses from a string.
985 *
986 * E-mails must be separated by newlines or commas. E-mails are allowed to
987 * include a display name (eg. Some Name <foo@example.com>). Invalid addresses
988 * are filtered out and stored in a session variable for re-display.
989 *
990 * @param $string
991 * The string to process. Recognized delimiters are comma, NL and CR.
992 * @return
993 * Array of valid e-mail addresses.
994 */
995 function _invite_get_emails($string) {
996 $valid_emails = $failed_emails = array();
997 $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\']+';
998 $domain = '(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.?)+';
999 $ipv4 = '[0-9]{1,3}(\.[0-9]{1,3}){3}';
1000 $ipv6 = '[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7}';
1001 $rx = "/($user@($domain|(\[($ipv4|$ipv6)\])))>?$/";
1002
1003 $emails = array_unique(split("[,\n\r]", $string));
1004 foreach ($emails as $email) {
1005 $email = preg_replace('/^.*<(.*)>$/', '${1}', trim($email));
1006 if ($email) {
1007 if (preg_match($rx, $email, $match)) {
1008 $valid_emails[] = $match[1];
1009 }
1010 else {
1011 $failed_emails[] = $email;
1012 }
1013 }
1014 }
1015
1016 if (count($failed_emails)) {
1017 $_SESSION['invite_failed_emails'] = serialize($failed_emails);
1018 }
1019
1020 return $valid_emails;
1021 }
1022
1023 /**
1024 * Filter out e-mails based on a database query.
1025 *
1026 * @param $sql
1027 * The query to execute.
1028 * @param &$emails
1029 * The list of e-mail addresses to validate. When this function returns, all
1030 * invalid e-mails have already been removed.
1031 * @param ...
1032 * More query arguments.
1033 * @return
1034 * An array of invalid e-mail addresses.
1035 */
1036 function _invite_validate_emails($sql, &$emails) {
1037 $failed_emails = array();
1038 // Build query arguments.
1039 $args = func_get_args();
1040 $args = array_merge($emails, array_slice($args, 2));
1041 $result = db_query($sql, $args);
1042 while ($row = db_fetch_object($result)) {
1043 $failed_emails[] = $row->email;
1044 }
1045 // Keep only valid e-mails.
1046 $emails = array_diff($emails, $failed_emails);
1047 return $failed_emails;
1048 }
1049
1050 /**
1051 * Forms API callback; process submitted form data.
1052 */
1053 function invite_form_submit($form, &$form_state) {
1054 global $user, $language;
1055
1056 // Set this now, so other modules can change it later.
1057 $form_state['redirect'] = 'invite';
1058
1059 $failed_emails = array();
1060 $num_failed = $num_succeeded = 0;
1061
1062 // Get e-mails that failed validation.
1063 if (isset($_SESSION['invite_failed_emails'])) {
1064 $failed_emails = (array)unserialize($_SESSION['invite_failed_emails']);
1065 $num_failed = count($failed_emails);
1066 }
1067
1068 $subject = isset($form_state['values']['subject']) ? trim($form_state['values']['subject']) : invite_get_subject();
1069 $message = isset($form_state['values']['message']) ? trim($form_state['values']['message']) : NULL;
1070
1071 if (!variable_get('invite_use_users_email', 0)) {
1072 $from = variable_get('invite_manual_from', '');
1073 }
1074 else if ($user->uid) {
1075 $from = $user->mail;
1076 }
1077 if (!$from) {
1078 // Never pass an empty string to drupal_mail()
1079 $from = NULL;
1080 }
1081
1082 foreach ($form_state['values']['valid_emails'] as $email) {
1083 // Create the invite object.
1084 $code = $form_state['values']['reg_code'] ? $form_state['values']['reg_code'] : invite_generate_code();
1085 $invite = _invite_substitutions(array(
1086 'email' => $email,
1087 'code' => $code,
1088 'resent' => $form_state['values']['resent'],
1089 'data' => array('subject' => $subject, 'message' => $message),
1090 ));
1091
1092 // Send e-mail.
1093 $params = array('invite' => $invite);
1094 $message = drupal_mail('invite', 'invite', $email, $language, $params, $from, TRUE);
1095 if (1 || $message['result']) {
1096 // Save invite.
1097 invite_save($invite);
1098
1099 // Notify other modules.
1100 if (!$form_state['values']['resent']) {
1101 $args = array('inviter' => $invite->inviter, 'email' => $invite->email, 'code' => $invite->code);
1102 module_invoke_all('invite', 'invite', $args);
1103 }
1104
1105 $num_succeeded++;
1106 }
1107 else {
1108 $failed_emails[] = $email;
1109 }
1110 }
1111
1112 // Store failed e-mails for re-display.
1113 if ($failed_emails) {
1114 $_SESSION['invite_failed_emails'] = serialize($failed_emails);
1115 }
1116
1117 if ($num_succeeded) {
1118 if (isset($form_state['values']['remaining_invites'])) {
1119 // Update user property if user is limited.
1120 user_save($user, array('invites' => $form_state['values']['remaining_invites'] - $num_succeeded));
1121 }
1122 $message = format_plural($num_succeeded, 'Your invitation has been successfully sent. You will be notified when the invitee joins the site.', '@count invitations have been successfully sent. You will be notified when any invitee joins the site.');
1123 drupal_set_message($message);
1124 }
1125 if ($num_failed) {
1126 $message = format_plural($num_failed, 'The entered e-mail address is invalid. Please correct it.', '@count entered e-mail addresses are invalid. Please correct them.');
1127 drupal_set_message($message, 'error');
1128 }
1129 else if (user_access('track invitations') && $user->uid) {
1130 // Everything went well: redirect to pending invites page.
1131 $form_state['redirect'] = "user/$user->uid/invites/pending";
1132 }
1133 }
1134
1135 /**
1136 * Return the invite e-mail subject.
1137 *
1138 * @param $substitutions
1139 * Associative array of substitutions for token replacement.
1140 * @return
1141 * The e-mail subject.
1142 */
1143 function invite_get_subject($substitutions = array()) {
1144 $subject = t(variable_get('invite_subject', t('[inviter-raw] has sent you an invite!')));
1145 return token_replace_multiple($subject, _invite_token_types($substitutions));
1146 }
1147
1148 /**
1149 * Generates a unique tracking code.
1150 *
1151 * @return
1152 * An 8-digit unique tracking code.
1153 */
1154 function invite_generate_code() {
1155 do {
1156 $reg_code = user_password(8);
1157 $result = db_query("SELECT COUNT(*) FROM {invite} WHERE reg_code = '%s'", $reg_code);
1158 } while (db_result($result));
1159
1160 return $reg_code;
1161 }
1162
1163 /**
1164 * Implementation of hook_mail().
1165 */
1166 function invite_mail($key, &$message, $params) {
1167 global $user;
1168
1169 $invite = $params['invite'];
1170
1171 // Override Reply-To address.
1172 if (!variable_get('invite_use_users_email_replyto', 0)) {
1173 $reply_to = variable_get('invite_manual_reply_to', '');
1174 }
1175 else if ($user->uid) {
1176 $reply_to = $user->mail;
1177 }
1178 if ($reply_to) {
1179 $message['headers']['Reply-To'] = $reply_to;
1180 }
1181
1182 $message['subject'] = $invite->data['subject'];
1183
1184 $template = t(_invite_get_mail_template());
1185 $tokens = _invite_token_types($invite);
1186 $message['body'][] = token_replace_multiple($template, $tokens);
1187 }
1188
1189 /**
1190 * Save an invite to the database.
1191 *
1192 * @param $edit
1193 * Associative array of data to store.
1194 * @return
1195 * The result of the database operation.
1196 */
1197 function invite_save($edit) {
1198 $edit = (array)$edit;
1199 $data = serialize($edit['data']);
1200 $now = time();
1201 $expiry = $now + (variable_get('invite_expiry', 30) * 60 * 60 * 24);
1202 if ($edit['resent']) {
1203 $result = db_query("UPDATE {invite} SET expiry = %d, resent = %d, data = '%s' WHERE reg_code = '%s' AND uid = %d", $expiry, $edit['resent'], $data, $edit['code'], $edit['inviter']->uid);
1204 }
1205 else {
1206 $result = db_query("INSERT INTO {invite} (reg_code, email, uid, created, expiry, data) VALUES ('%s', '%s', %d, %d, %d, '%s')", $edit['code'], $edit['email'],