Projet

Général

Profil

Paste
Télécharger (39,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / cas / cas.module @ 4f315dab

1
<?php
2

    
3
/**
4
 * @file
5
 * Enables users to authenticate via a Central Authentication Service (CAS)
6
 * Cas will currently work if the auto registration is turned on and will
7
 * create user accounts automatically.
8
 */
9

    
10
define('CAS_NO_LINK', 0);
11
define('CAS_ADD_LINK', 1);
12
define('CAS_MAKE_DEFAULT', 2);
13
define('CAS_REDIRECT', 3);
14
define('CAS_LOGIN_INVITE_DEFAULT', 'Log in using CAS');
15
define('CAS_LOGIN_DRUPAL_INVITE_DEFAULT', 'Cancel CAS login');
16
define('CAS_LOGIN_REDIR_MESSAGE', 'You will be redirected to the secure CAS login page.');
17
define('CAS_EXCLUDE', 'services/*');
18

    
19
// Frequency of CAS Gateway checking.
20
define('CAS_CHECK_NEVER', -2);
21
define('CAS_CHECK_ONCE', -1);
22
define('CAS_CHECK_ALWAYS', 0);
23

    
24
/**
25
 * Implements hook_init().
26
 *
27
 * Traps a page load to see if authentication is required.
28
 */
29
function cas_init() {
30
  global $user;
31

    
32
  if (module_exists('cas_test') && arg(0) == 'cas_test') {
33
    // We are destined for a page handled by the cas_test module, so do not
34
    // do any processing here. Necessary for CAS gateway tests.
35
    return;
36
  }
37
  elseif (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'install') {
38
    // Do nothing on profile install.
39
    return;
40
  }
41

    
42
  // Process a single-sign out request.
43
  _cas_single_sign_out_check();
44

    
45
  // If a user is not logged in, consider using CAS authentication.
46
  if (!$user->uid) {
47
    $force_authentication = _cas_force_login();
48
    $check_authentication = _cas_allow_check_for_login();
49
    $request_type = $_SERVER['REQUEST_METHOD'];
50
    $perform_login_check = $force_authentication || ($check_authentication && ($request_type == 'GET'));
51
    if ($perform_login_check) {
52
      cas_login_check($force_authentication);
53
    }
54
  }
55
}
56

    
57
/**
58
 * Checks to see if the user needs to be logged in.
59
 *
60
 * @param $force_authentication
61
 *   If TRUE, require that the user be authenticated with the CAS server
62
 *   before proceeding. Otherwise, check with the CAS server to see if the
63
 *   user is already logged in.
64
 */
65
function cas_login_check($force_authentication = TRUE) {
66
  global $user;
67
  if ($user->uid) {
68
    //Don't Login  because we already are
69
    return;
70
  }
71

    
72
  if (!cas_phpcas_load()) {
73
    // No need to print a message, as the user will already see the failed
74
    // include_once calls.
75
    return;
76
  }
77

    
78
  // Start a drupal session
79
  drupal_session_start();
80
  _cas_single_sign_out_save_ticket();  // We use this later for CAS 3 logoutRequests
81

    
82
  // Initialize phpCAS.
83
  cas_phpcas_init();
84

    
85
  // We're going to try phpCAS auth test
86
  if ($force_authentication) {
87
    phpCAS::forceAuthentication();
88
  }
89
  else {
90
    $logged_in = phpCAS::checkAuthentication();
91

    
92
    // We're done cause we're not logged in.
93
    if (!$logged_in) {
94
      return;
95
    }
96
  }
97

    
98
  // Build the cas_user object and allow modules to alter it.
99
  $cas_user = array(
100
    'name' => phpCAS::getUser(),
101
    'login' => TRUE,
102
    'register' => variable_get('cas_user_register', TRUE),
103
    'attributes' => cas_phpcas_attributes(),
104
  );
105
  drupal_alter('cas_user', $cas_user);
106

    
107
  // Bail out if a module denied login access for this user or unset the user
108
  // name.
109
  if (empty($cas_user['login']) || empty($cas_user['name'])) {
110
    // Only set a warning if we forced login.
111
    if ($force_authentication) {
112
      drupal_set_message(t('The user account %name is not available on this site.', array('%name' => $cas_user['name'])), 'error');
113
    }
114
    return;
115
  }
116

    
117
  // Proceed with the login process, using the altered CAS username.
118
  $cas_name = $cas_user['name'];
119

    
120
  // blocked user check
121
  $blocked = FALSE;
122
  if (_cas_external_user_is_blocked($cas_name)) {
123
      $blocked = 'The username %cas_name has been blocked.';
124
  }
125
  // @todo The D7 equivalent here must have been renamed.
126
  // elseif (drupal_is_denied('user', $cas_name)) {
127
  //   // denied by access controls
128
  //   return 'The name %cas_name is a reserved username.';
129
  // }
130

    
131
  if ($blocked) {
132
    // Only display error messages only if the user intended to log in.
133
    if ($force_authentication) {
134
      watchdog('cas', $blocked, array('%cas_name' => $cas_name), WATCHDOG_WARNING);
135
      drupal_set_message(t($blocked, array('%cas_name' => $cas_name)), 'error');
136
    }
137
    return;
138
  }
139

    
140
  $account = cas_user_load_by_name($cas_name);
141

    
142
  // Automatic user registration.
143
  if (!$account && $cas_user['register']) {
144
    // No account could be found and auto registration is enabled, so attempt
145
    // to register a new user.
146
    $account = cas_user_register($cas_name);
147
    if (!$account) {
148
      // The account could not be created, set a message.
149
      if ($force_authentication) {
150
        drupal_set_message(t('A new account could not be created for %cas_name. The username is already in use on this site.', array('%cas_name' => $cas_name)), 'error');
151
      }
152
      return;
153
    }
154
  }
155

    
156
  // final check to make sure we have a good user
157
  if ($account && $account->uid > 0) {
158
    // Save the altered CAS name for future use.
159
    $_SESSION['cas_name'] = $cas_name;
160

    
161
    $cas_first_login = !$account->login;
162

    
163
    // Save single sign out information
164
    if (!empty($_SESSION['cas_ticket'])) {
165
      _cas_single_sign_out_save_token($account);
166
    }
167

    
168
    // Populate $edit with some basic properties.
169
    $edit['cas_user'] = $cas_user;
170
    $edit['roles'] = $account->roles + cas_roles();
171
    if (module_exists('persistent_login') && !empty($_SESSION['cas_remember'])) {
172
      $edit['values']['persistent_login'] = 1;
173
    }
174
    // Allow other modules to make their own custom changes.
175
    cas_user_module_invoke('presave', $edit, $account);
176

    
177
    // Save the user account and log the user in.
178
    $user = user_save($account, $edit);
179
    user_login_finalize($edit);
180

    
181
    drupal_set_message(t(variable_get('cas_login_message', 'Logged in via CAS as %cas_username.'), array('%cas_username' => $user->name)));
182
    if (!empty($edit['persistent_login'])) {
183
      drupal_set_message(t('You will remain logged in on this computer even after you close your browser.'));
184
    }
185

    
186
    _cas_redirect_after_login($cas_first_login);
187

    
188
  }
189
  else {
190
    $user = drupal_anonymous_user();
191
    unset($_SESSION['phpCAS']);
192

    
193
    // Only display error messages only if the user intended to log in.
194
    if ($force_authentication) {
195
      drupal_set_message(t('No account found for %cas_name.', array('%cas_name' => $cas_name)), 'error');
196
    }
197
  }
198
}
199

    
200
/**
201
 * Loads the phpCAS library.
202
 *
203
 * @param $path
204
 *   Attempt to load phpCAS using this path. If omitted, phpCAS will be loaded
205
 *   using Libraries API or the configured value.
206
 *
207
 * @return
208
 *   The phpCAS version if the phpCAS was successfully loaded, FALSE otherwise.
209
 */
210
function cas_phpcas_load($path = NULL) {
211
  if (!isset($path)) {
212
    if (module_exists('libraries')) {
213
      $path = libraries_get_path('CAS');
214
    }
215
    else {
216
      $path = variable_get('cas_library_dir', 'CAS');
217
    }
218
  }
219

    
220
  // Build the name of the file to load.
221
  if ($path != '') {
222
    $path = rtrim($path, '/') . '/';
223
  }
224
  $filename = $path . 'CAS.php';
225

    
226
  include_once($filename);
227

    
228
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
229
    // The file could not be loaded successfully.
230
    return FALSE;
231
  }
232
  return PHPCAS_VERSION;
233
}
234

    
235
/**
236
 * Initialize phpCAS.
237
 *
238
 * Will load phpCAS if necessary.
239
 */
240
function cas_phpcas_init() {
241
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
242
    cas_phpcas_load();
243
  }
244

    
245
  $initialized = &drupal_static(__FUNCTION__, FALSE);
246
  if ($initialized) {
247
    // phpCAS cannot be initialized twice. If you need to force this function
248
    // to run again, call drupal_static_reset('cas_phpcas_init') first.
249
    return;
250
  }
251
  $initialized = TRUE;
252

    
253
  // Variable set
254
  $server_version    = (string)variable_get('cas_version', '3.0');
255
  $server_cas_server = (string)variable_get('cas_server', 'sso-cas.univ-rennes1.fr');
256
  $server_port       = (int)variable_get('cas_port', '443');
257
  $server_uri        = (string)variable_get('cas_uri', '');
258
  $cas_cert          = (string)variable_get('cas_cert', '');
259
  $debug_file        = (string)variable_get('cas_debugfile', '');
260
  if ($debug_file != '') {
261
    phpCAS::setDebug($debug_file);
262
  }
263
  $start_session = (boolean)FALSE;
264
  if (variable_get('cas_proxy', 0)) {
265
    phpCAS::proxy($server_version, $server_cas_server, $server_port, $server_uri, $start_session);
266
    $cas_pgt_storage_path = variable_get('cas_pgtpath', '');
267
    if ($cas_pgt_storage_path != '') {
268
      if (version_compare(PHPCAS_VERSION, '1.3', '>=')) {
269
        phpCAS::setPGTStorageFile($cas_pgt_storage_path);
270
      }
271
      else {
272
        $cas_pgt_format = variable_get('cas_pgtformat', 'plain');
273
        phpCAS::setPGTStorageFile($cas_pgt_format, $cas_pgt_storage_path);
274
      }
275
    }
276
  }
277
  else {
278
    phpCAS::client($server_version, $server_cas_server, $server_port, $server_uri, $start_session);
279
  }
280

    
281
  //Add CAS proxy lists allowed
282
  $proxy_list = variable_get('cas_proxy_list', '');
283
  if ($proxy_list) {
284
    $proxy_list = explode("\n", $proxy_list);
285
    phpCAS::allowProxyChain(new CAS_ProxyChain($proxy_list));
286
  }
287

    
288
  // force CAS authentication
289
  if ($cas_cert = variable_get('cas_cert', '')) {
290
    phpCAS::setCasServerCACert($cas_cert);
291
  }
292
  else {
293
    phpCAS::setNoCasServerValidation();
294
  }
295

    
296
  phpCAS::setFixedServiceURL(url(current_path(), array('query' => drupal_get_query_parameters(), 'absolute' => TRUE)));
297
  phpCAS::setCacheTimesForAuthRecheck((int) variable_get('cas_check_frequency', CAS_CHECK_NEVER));
298

    
299
  // Allow other modules to call phpCAS routines. We do not call
300
  // drupal_alter() since there are no parameters to pass.
301
  module_invoke_all('cas_phpcas_alter');
302
}
303

    
304

    
305
/**
306
 * Implements hook_permission().
307
 */
308
function cas_permission() {
309
  return array(
310
    'administer cas' => array(
311
      'title' => t('Administer CAS'),
312
      'description' => t('Configure CAS server, default CAS user roles, login/logout redirection, and other settings.'),
313
      'restrict access' => TRUE,
314
    )
315
  );
316
}
317

    
318
/**
319
 * Implements hook_help().
320
 */
321
function cas_help($section) {
322
  switch ($section) {
323
    case 'admin/help#cas':
324
      return t("Allows users to authenticate via a Central Authentication Service.");
325
  }
326
}
327

    
328
/**
329
 * Implements hook_menu().
330
 */
331
function cas_menu() {
332
  global $user;
333
  $items = array();
334
  //cas_login_check();
335
  $items['admin/config/people/cas'] = array(
336
    'title' => 'CAS settings',
337
    'description' => 'Configure central authentication services',
338
    'page callback' => 'drupal_get_form',
339
    'page arguments' => array('cas_admin_settings'),
340
    'access arguments' => array('administer cas'),
341
    'type' => MENU_NORMAL_ITEM,
342
    'file' => 'cas.admin.inc',
343
  );
344
  $items['admin/config/people/cas/settings'] = array(
345
    'title' => 'CAS',
346
    'type' => MENU_DEFAULT_LOCAL_TASK,
347
    'weight' => -10,
348
  );
349
  $items['admin/people/cas/create'] = array(
350
    'title' => 'Add CAS user(s)',
351
    'page callback' => 'drupal_get_form',
352
    'page arguments' => array('cas_add_user_form'),
353
    'access arguments' => array('administer users'),
354
    'type' => MENU_LOCAL_ACTION,
355
    'file' => 'cas.user.inc',
356
    'tab_parent' => 'admin/people',
357
    'weight' => 1,
358
  );
359
  $items['user/%user/cas'] = array(
360
    'title' => 'CAS',
361
    'page callback' => 'cas_user_identities',
362
    'page arguments' => array(1),
363
    'access arguments' => array('administer users'),
364
    'type' => MENU_LOCAL_TASK,
365
    'file' => 'cas.pages.inc',
366
    'tab_parent' => 'user/%/edit',
367
    'weight' => 1,
368
  );
369
  $items['user/%user/cas/delete'] = array(
370
    'title' => 'Delete CAS username',
371
    'page callback' => 'drupal_get_form',
372
    'page arguments' => array('cas_user_delete_form', 1),
373
    'access arguments' => array('administer users'),
374
    'file' => 'cas.pages.inc',
375
  );
376
  $items['cas'] = array(
377
    'path' => 'cas',
378
    'title' => 'CAS Login',
379
    'page callback' => 'cas_login_page',
380
    'access callback' => 'user_is_anonymous',
381
    'type' => MENU_SUGGESTED_ITEM,
382
  );
383
  $items['caslogout'] = array(
384
    'title' => 'CAS Logout',
385
    'page callback' => 'cas_logout',
386
    'access callback' => 'cas_user_is_logged_in',
387
    'type' => MENU_SUGGESTED_ITEM,
388
  );
389
  return $items;
390
}
391

    
392
function cas_user_is_logged_in() {
393
  return user_is_logged_in() || !empty($_SESSION['phpCAS']['user']);
394
}
395

    
396
/**
397
 * Implements hook_menu_site_status_alter().
398
 */
399
function cas_menu_site_status_alter(&$menu_site_status, $path) {
400
  if (user_is_logged_in() && strtolower($path) == 'cas') {
401
    // If user is logged in, redirect to '<front>' instead of giving 403.
402
    drupal_goto('');
403
  }
404
}
405

    
406
/**
407
 * Implements hook_menu_link_alter().
408
 *
409
 * Flag this link as needing alter at display time.
410
 * @see cas_translated_menu_link_alter()
411
 */
412
function cas_menu_link_alter(&$item) {
413
  if (strtolower($item['link_path']) == 'cas' || strtolower($item['link_path']) == 'caslogout') {
414
    $item['options']['alter'] = TRUE;
415
  }
416
}
417

    
418
/**
419
 * Implements hook_translated_menu_item_alter().
420
 *
421
 * Append dynamic query 'destination' to several menu items.
422
 */
423
function cas_translated_menu_link_alter(&$item) {
424
  if (strtolower($item['href']) == 'cas') {
425
    $item['localized_options']['query'] = drupal_get_destination();
426
  }
427
  elseif (strtolower($item['href']) == 'caslogout' && !variable_get('cas_logout_destination', '')) {
428
    $item['localized_options']['query'] = drupal_get_destination();
429
  }
430
}
431

    
432
/**
433
 * Implements hook_user_operations().
434
 */
435
function cas_user_operations($form = array(), $form_state = array()) {
436
  $operations['cas_create'] = array(
437
    'label' => t('Create CAS username'),
438
    'callback' => 'cas_user_operations_create_username',
439
  );
440
  $operations['cas_remove'] = array(
441
    'label' => t('Remove CAS usernames'),
442
    'callback' => 'cas_user_operations_remove_usernames',
443
  );
444
  return $operations;
445
}
446

    
447
/**
448
 * Callback function to assign a CAS username to the account.
449
 *
450
 * @param $uids
451
 *   An array of user ids. For each account, a CAS username is created with
452
 *   the same name as the Drupal username.
453
 *
454
 * @see cas_user_operations()
455
 */
456
function cas_user_operations_create_username($uids) {
457
  $accounts = user_load_multiple($uids);
458
  foreach ($accounts as $account) {
459
    $count = db_select('cas_user', 'c')
460
      ->condition('cas_name', $account->name)
461
      ->condition('uid', $account->uid, '<>')
462
      ->countQuery()->execute()->fetchfield();
463
    if ($count) {
464
      drupal_set_message(t('CAS username %username already in use.', array('%username' => $account->name)), 'error');
465
      continue;
466
    }
467
    db_merge('cas_user')
468
      ->key(array('cas_name' => $account->name))
469
      ->fields(array('uid' => $account->uid))
470
      ->execute();
471
  }
472
}
473

    
474
/**
475
 * Callback function to remove CAS usernames from the account.
476
 *
477
 * @param $uids
478
 *   An array of user ids. For each account, all CAS usernames are removed.
479
 *
480
 * @see cas_user_operations()
481
 */
482
function cas_user_operations_remove_usernames($uids) {
483
  db_delete('cas_user')
484
    ->condition('uid', $uids, 'IN')
485
    ->execute();
486
}
487

    
488
/**
489
 * Implements hook_admin_paths().
490
 */
491
function cas_admin_paths() {
492
  $paths = array(
493
    'user/*/cas' => TRUE,
494
    'user/*/cas/delete/*' => TRUE,
495
  );
496
  return $paths;
497
}
498

    
499
/**
500
 * Implements hook_user_load().
501
 *
502
 * Adds an associative array 'cas_names' to each user. The array keys are
503
 * unique authentication mapping ids, with CAS usernames as the values.
504
 */
505
function cas_user_load($users) {
506
  foreach (array_keys($users) as $uid) {
507
    $users[$uid]->cas_names = array();
508
  }
509
  $result = db_query('SELECT aid, uid, cas_name FROM {cas_user} WHERE uid IN (:uids)', array(':uids' => array_keys($users)));
510
  foreach ($result as $record) {
511
    $users[$record->uid]->cas_names[$record->aid] = $record->cas_name;
512
  }
513
  foreach (array_keys($users) as $uid) {
514
    $users[$uid]->cas_name = reset($users[$uid]->cas_names);
515
  }
516
}
517

    
518
/**
519
 * Implements hook_user_insert().
520
 *
521
 * When a user is created, record their CAS username if provided.
522
 */
523
function cas_user_insert(&$edit, $account, $category) {
524
  if (!empty($edit['cas_name'])) {
525
    db_insert('cas_user')
526
      ->fields(array(
527
        'cas_name' => $edit['cas_name'],
528
        'uid' => $account->uid,
529
      ))
530
      ->execute();
531
  }
532
  // Update $account to reflect changes.
533
  $users = array($account->uid => $account);
534
  cas_user_load($users);
535
}
536

    
537
/**
538
 * Implements hook_user_update().
539
 *
540
 * When a user is updated, change their CAS username if provided.
541
 */
542
function cas_user_update(&$edit, $account, $category) {
543
  if (!array_key_exists('cas_name', $edit)) {
544
    // If the cas_name key is not provided, there is nothing to do.
545
    return;
546
  }
547
  $cas_name = $edit['cas_name'];
548

    
549
  // See if the user currently has any CAS names.
550
  reset($account->cas_names);
551
  if ($aid = key($account->cas_names)) {
552
    // The user already has CAS username(s) set.
553
    if (empty($cas_name)) {
554
      // Remove a CAS username.
555
      db_delete('cas_user')
556
        ->condition('uid', $account->uid)
557
        ->condition('aid', $aid)
558
        ->execute();
559
    }
560
    else {
561
      // Change a CAS username.
562
      if ($cas_name != $account->cas_names[$aid]) {
563
        db_update('cas_user')
564
          ->fields(array('cas_name' => $cas_name))
565
          ->condition('uid', $account->uid)
566
          ->condition('aid', $aid)
567
          ->execute();
568
      }
569
    }
570
  }
571
  else {
572
    // No current CAS usernames.
573
    if (!empty($cas_name)) {
574
      // Add a CAS username.
575
      db_insert('cas_user')
576
        ->fields(array(
577
          'uid' => $account->uid,
578
          'cas_name' => $cas_name,
579
        ))
580
        ->execute();
581
    }
582
  }
583
  // Update $account to reflect changes.
584
  $users = array($account->uid => $account);
585
  cas_user_load($users);
586
}
587

    
588
/**
589
 * Implement hook_user_delete().
590
 *
591
 * When a CAS user is deleted, we need to clean up the entry in {cas_user}.
592
 */
593
function cas_user_delete($account) {
594
  db_delete('cas_user')
595
    ->condition('uid', $account->uid)
596
    ->execute();
597
}
598

    
599
/**
600
 * Fetch a user object by CAS name.
601
 *
602
 * @param $cas_name
603
 *   The name of the CAS user.
604
 * @param $alter
605
 *   If TRUE, run the CAS username through hook_cas_user_alter() before
606
 *   loading the account.
607
 * @param $reset
608
 *   TRUE to reset the internal cache and load from the database; FALSE
609
 *   (default) to load from the internal cache, if set.
610
 *
611
 * @return
612
 *   A fully-loaded $user object upon successful user load or FALSE if user
613
 *   cannot be loaded.
614
 */
615
function cas_user_load_by_name($cas_name, $alter = FALSE, $reset = FALSE) {
616
  if ($alter) {
617
    $cas_user = array(
618
      'name' => $cas_name,
619
      'login' => TRUE,
620
      'register' => FALSE,
621
    );
622
    drupal_alter('cas_user', $cas_user);
623
    $cas_name = $cas_user['name'];
624
  }
625

    
626
  $uid = db_select('cas_user')->fields('cas_user', array('uid'))->condition('cas_name', db_like($cas_name), 'LIKE')->range(0, 1)->execute()->fetchField();
627
  if ($uid) {
628
    return user_load($uid, $reset);
629
  }
630
  return FALSE;
631
}
632

    
633
/**
634
 * This is the page callback for the /cas page, which is used only to
635
 * trigger a forced CAS authentication.
636
 *
637
 * In almost all cases, the user will have been redirected before even
638
 * hitting this page (see hook_init implementation). But as a stop gap
639
 * just redirect to the homepage.
640
 */
641
function cas_login_page() {
642
  drupal_goto('');
643
}
644

    
645
/**
646
 * Logs a user out of Drupal and then out of CAS.
647
 *
648
 * This function does not return, but instead immediately redirects the user
649
 * to the CAS server to complete the CAS logout process.
650
 *
651
 * Other modules intending to call this from their implementation of
652
 * hook_user_logout() will need to pass $invoke_hook = FALSE to avoid an
653
 * infinite recursion. WARNING: since this function does not return, any
654
 * later implementations of hook_user_logout() will not run. You may wish to
655
 * adjust the hook execution order using hook_module_implements_alter().
656
 *
657
 * @param $invoke_hook
658
 *   If TRUE, invoke hook_user_logout() and save a watchdog message indicating
659
 *   that the user has logged out.
660
 */
661
function cas_logout($invoke_hook = TRUE) {
662
  global $user;
663

    
664
  // Build the logout URL.
665
  cas_phpcas_init();
666

    
667
  if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
668
    // Add destination override so that a destination can be specified on the
669
    // logout link, e.g., caslogout?destination=http://foo.bar.com/foobar. We do
670
    // not allow absolute URLs to be passed via $_GET, as this can be an attack
671
    // vector.
672
    $destination = $_GET['destination'];
673
  }
674
  else {
675
    $destination = variable_get('cas_logout_destination', '');
676
  }
677

    
678
  //Make it an absolute url.  This will also convert <front> to the front page.
679
  if ($destination) {
680
    $destination_url = url($destination, array('absolute' => TRUE));
681
    $options = array(
682
      'service' => $destination_url,
683
      'url' => $destination_url,
684
    );
685
  }
686
  else {
687
    $options = array();
688
  }
689

    
690
  // Mimic user_logout().
691
  if ($invoke_hook) {
692
    watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
693
    module_invoke_all('user_logout', $user);
694
  }
695

    
696
  // phpCAS automatically calls session_destroy().
697
  phpCAS::logout($options);
698
}
699

    
700
/**
701
 * Implements hook_block_info().
702
 */
703
function cas_block_info() {
704
  $blocks['login']['info'] = t('CAS login');
705
  // Not worth caching.
706
  $blocks['login']['cache'] = DRUPAL_NO_CACHE;
707

    
708
  return $blocks;
709
}
710

    
711
/**
712
 * Implements hook_block_view().
713
 */
714
function cas_block_view($delta = '') {
715
  global $user;
716

    
717
  $block = array();
718

    
719
  switch ($delta) {
720
    case 'login':
721
      // For usability's sake, avoid showing two login forms on one page.
722
      if (!$user->uid && !(arg(0) == 'user' && !is_numeric(arg(1)))) {
723
        $block['subject'] = t('User login');
724
        $block['content'] = drupal_get_form('cas_login_block');
725
      }
726
      return $block;
727
  }
728
}
729

    
730
/**
731
 * Login form for the CAS login block.
732
 */
733
function cas_login_block($form) {
734
  $form['#action'] = url('cas', array('query' => drupal_get_destination()));
735
  $form['#id'] = 'cas-login-form';
736

    
737
  $form['cas_login_redirection_message'] = array(
738
    '#type' => 'item',
739
    '#markup' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
740
    '#weight' => -1,
741
  );
742
  $form['actions'] = array('#type' => 'actions');
743
  $form['actions']['submit'] = array(
744
    '#type' => 'submit',
745
    '#value' => t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)),
746
  );
747

    
748
  return $form;
749
}
750

    
751
/**
752
 * Determine if we should automatically check if the user is authenticated.
753
 *
754
 * This implements part of the CAS gateway feature.
755
 * @see phpCAS::checkAuthentication()
756
 *
757
 * @return
758
 *   TRUE if we should query the CAS server to see if the user is already
759
 *   authenticated, FALSE otherwise.
760
 */
761
function _cas_allow_check_for_login() {
762
  // Do not process in maintenance mode.
763
  if (variable_get('maintenance_mode', 0)) {
764
    return FALSE;
765
  }
766

    
767
  if (variable_get('cas_check_frequency', CAS_CHECK_NEVER) == CAS_CHECK_NEVER) {
768
    // The user has disabled the feature.
769
    return FALSE;
770
  }
771

    
772
  // Check to see if we've got a search bot.
773
  if (isset($_SERVER['HTTP_USER_AGENT'])) {
774
    $crawlers = array(
775
      'Google',
776
      'msnbot',
777
      'Rambler',
778
      'Yahoo',
779
      'AbachoBOT',
780
      'accoona',
781
      'AcoiRobot',
782
      'ASPSeek',
783
      'CrocCrawler',
784
      'Dumbot',
785
      'FAST-WebCrawler',
786
      'GeonaBot',
787
      'Gigabot',
788
      'Lycos',
789
      'MSRBOT',
790
      'Scooter',
791
      'AltaVista',
792
      'IDBot',
793
      'eStyle',
794
      'Scrubby',
795
      'gsa-crawler',
796
      );
797
    // Return on the first find.
798
    foreach ($crawlers as $c) {
799
      if (stripos($_SERVER['HTTP_USER_AGENT'], $c) !== FALSE) {
800
        return FALSE;
801
      }
802
    }
803
  }
804

    
805
  // Do not force login for XMLRPC, Cron, or Drush.
806
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'xmlrpc.php')) {
807
    return FALSE;
808
  }
809
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'cron.php')) {
810
    return FALSE;
811
  }
812
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'drush')) {
813
    return FALSE;
814
  }
815
  if (!empty($_SERVER['argv'][0]) && stristr($_SERVER['argv'][0], 'drush')) {
816
    return FALSE;
817
  }
818

    
819
  // Test against exclude pages.
820
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
821
    $path = drupal_get_path_alias($_GET['q']);
822
    if (drupal_match_path($path, $pages)) {
823
      return FALSE;
824
    }
825
  }
826

    
827
  return TRUE;
828
}
829

    
830
/**
831
 * Determine if we should require the user be authenticated.
832
 *
833
 * @return
834
 *   TRUE if we should require the user be authenticated, FALSE otherwise.
835
 */
836
function _cas_force_login() {
837
  // The 'cas' page is a shortcut to force authentication.
838
  if (strtolower(arg(0)) == 'cas') {
839
    return TRUE;
840
  }
841

    
842
  // Do not process in maintenance mode.
843
  if (variable_get('maintenance_mode', 0)) {
844
    return FALSE;
845
  }
846

    
847
  // Do not force login for XMLRPC, Cron, or Drush.
848
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'xmlrpc.php')) {
849
    return FALSE;
850
  }
851
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'cron.php')) {
852
    return FALSE;
853
  }
854
  if (function_exists('drush_verify_cli') && drush_verify_cli()) {
855
    return FALSE;
856
  }
857

    
858
  // Excluded page do not need login.
859
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
860
    $path = drupal_get_path_alias($_GET['q']);
861
    if (drupal_match_path($path, $pages)) {
862
      return FALSE;
863
    }
864
  }
865

    
866
  // Set the default behavior.
867
  $force_login = variable_get('cas_access', 0);
868

    
869
  // If we match the specified paths, reverse the behavior.
870
  if ($pages = variable_get('cas_pages', '')) {
871
    $path = drupal_get_path_alias($_GET['q']);
872
    if (drupal_match_path($path, $pages)) {
873
      $force_login = !$force_login;
874
    }
875
  }
876

    
877
  return $force_login;
878
}
879
/**
880
 * Implements hook_form_alter().
881
 *
882
 * Overrides specific from settings based on user policy.
883
 */
884
function cas_form_alter(&$form, &$form_state, $form_id) {
885
  // Special handling of the user login page when the CAS login form is set to
886
  // redirect.
887
  if ($form_id == 'user_login' && variable_get('cas_login_form', CAS_NO_LINK) == CAS_REDIRECT) {
888
    drupal_goto('cas');
889
  }
890

    
891
  switch ($form_id) {
892
    case 'user_login':
893
    case 'user_login_block':
894
      if (variable_get('cas_login_form', CAS_NO_LINK) != CAS_NO_LINK) {
895
        $form['#attached']['css'][] = drupal_get_path('module', 'cas') . '/cas.css';
896
        $form['#attached']['js'][] = drupal_get_path('module', 'cas') . '/cas.js';
897

    
898
        if (!empty($form_state['input']['cas_identifier'])) {
899
          $form['name']['#required'] = FALSE;
900
          $form['pass']['#required'] = FALSE;
901
          unset($form['#validate']);
902
          $form['#submit'] = array('cas_login_submit');
903
        }
904

    
905
        $items = array();
906
        $items[] = array(
907
          'data' => l(t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)), '#'),
908
          'class' => array('cas-link'),
909
        );
910

    
911
        $items[] = array(
912
          'data' => l(t(variable_get('cas_login_drupal_invite', CAS_LOGIN_DRUPAL_INVITE_DEFAULT)), '#'),
913
          'class' => array('uncas-link'),
914
        );
915

    
916
        $form['cas_links'] = array(
917
          '#theme' => 'item_list',
918
          '#items' => $items,
919
          '#attributes' => array('class' => array('cas-links')),
920
          '#weight' => 1,
921
        );
922

    
923
        $form['links']['#weight'] = 2;
924

    
925
        $form['cas_login_redirection_message'] = array(
926
          '#type' => 'item',
927
          '#markup' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
928
          '#weight' => -1,
929
        );
930

    
931
        $form['cas_identifier'] = array(
932
          '#type' => 'checkbox',
933
          '#title' => t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)),
934
          '#default_value' => variable_get('cas_login_form', CAS_NO_LINK) == CAS_MAKE_DEFAULT,
935
          '#weight' => -1,
936
          '#description' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
937
        );
938
        $form['cas.return_to'] = array('#type' => 'hidden', '#value' => user_login_destination());
939
      }
940
      break;
941

    
942
    case 'user_profile_form':
943
      $account = $form['#user'];
944
      if (user_access('administer users')) {
945
        // The user is an administrator, so add fields to allow changing the
946
        // CAS username(s) associated with the account.
947
        $cas_names = $account->cas_names;
948
        $aids = array_keys($cas_names);
949

    
950
        $element = array(
951
          '#type' => 'textfield',
952
          '#title' => t('CAS username'),
953
          '#default_value' => array_shift($cas_names),
954
          '#cas_user_aid' => array_shift($aids),
955
          '#description' => t('<a href="@url">Create, edit or delete</a> additional CAS usernames associated with this account.', array('@url' => url('user/' . $account->uid . '/cas'))),
956
          '#element_validate' => array('_cas_name_element_validate'),
957
          '#weight' => -9,
958
        );
959

    
960
        // See if any additional CAS usernames exist.
961
        if (!empty($cas_names)) {
962
          $element['#description'] .= ' <br />' . t('Other CAS usernames: %cas_names.', array('%cas_names' => implode(', ', $cas_names)));
963
        }
964
        $form['account']['cas_name'] = $element;
965
      }
966
      elseif (cas_is_external_user($account)) {
967
        // The user is not an administrator, so selectively remove the e-mail
968
        // and password fields.
969
        if (variable_get('cas_hide_email', 0)) {
970
          $form['account']['mail']['#access'] = FALSE;
971
        }
972
        if (variable_get('cas_hide_password', 0)) {
973
          $form['account']['pass']['#access'] = FALSE;
974
        }
975
      }
976
      if (cas_is_external_user($account) && variable_get('cas_hide_password', 0)) {
977
        // Also remove requirement to validate your current password before
978
        // changing your e-mail address.
979
        $form['account']['current_pass']['#access'] = FALSE;
980
        $form['account']['current_pass_required_values']['#access'] = FALSE;
981
        $form['account']['current_pass_required_values']['#value'] = array();
982
        $form['#validate'] = array_diff($form['#validate'], array('user_validate_current_pass'));
983
      }
984
      break;
985

    
986
    case 'user_pass':
987
      if (!user_access('administer users') && variable_get('cas_changePasswordURL', '') != '') {
988
        drupal_goto(variable_get('cas_changePasswordURL', ''));
989
      }
990
      break;
991

    
992
    case 'user_register_form':
993
      if (user_access('administer users')) {
994
        $form['account']['cas_name'] = array(
995
          '#type' => 'textfield',
996
          '#title' => t('CAS username'),
997
          '#default_value' => '',
998
          '#description' => t('If necessary, additional CAS usernames can be added after the account is created.'),
999
          '#element_validate' => array('_cas_name_element_validate'),
1000
          '#weight' => -9,
1001
        );
1002
      }
1003
      elseif (variable_get('cas_registerURL', '') != '') {
1004
        drupal_goto(variable_get('cas_registerURL', ''));
1005
      }
1006
      break;
1007

    
1008
    case 'user_admin_account':
1009
      // Insert the CAS username into the second column.
1010
      _cas_array_insert($form['accounts']['#header'], 1, array(
1011
        'cas' => array(
1012
          'data' => 'CAS usernames',
1013
        ),
1014
      ));
1015
      foreach ($form['accounts']['#options'] as $uid => &$row) {
1016
        $cas_usernames = db_query('SELECT cas_name FROM {cas_user} WHERE uid = :uid', array(':uid' => $uid))->fetchCol();
1017
        $row['cas'] = theme('item_list', array('items' => $cas_usernames));
1018
      }
1019
      break;
1020
  }
1021
}
1022

    
1023
/**
1024
 * Form element 'cas_name' validator.
1025
 *
1026
 * If the element is displaying an existing {cas_user} entry, set
1027
 * #cas_user_aid to the corresponding authmap id to avoid spurious
1028
 * validation errors.
1029
 */
1030
function _cas_name_element_validate($element, &$form_state) {
1031
  if (empty($element['#value'])) {
1032
    // Nothing to validate if the name is empty.
1033
    return;
1034
  }
1035

    
1036
  $query = db_select('cas_user')
1037
    ->fields('cas_user', array('uid'))
1038
    ->condition('cas_name', $element['#value']);
1039

    
1040
  // If set, we ignore entries with a specified authmap id. This is used on
1041
  // the user/%user/edit page to not throw validation errors when we do not
1042
  // change the CAS username.
1043
  if (isset($element['#cas_user_aid'])) {
1044
    $query->condition('aid', $element['#cas_user_aid'], '<>');
1045
  }
1046

    
1047
  $uid = $query->execute()->fetchField();
1048

    
1049
  if ($uid !== FALSE) {
1050
    // Another user is using this CAS username.
1051
    form_set_error('cas_name', t('The CAS username is <a href="@edit-user-url">already in use</a> on this site.', array('@edit-user-url' => url('user/' . $uid . '/edit'))));
1052
  }
1053
}
1054

    
1055
/**
1056
 * Login form _validate hook
1057
 */
1058
function cas_login_submit(&$form, &$form_state) {
1059
  if (!empty($form_state['values']['persistent_login'])) {
1060
    $_SESSION['cas_remember'] = 1;
1061
  }
1062
  // Force redirection.
1063
  unset($_GET['destination']);
1064
  drupal_goto('cas', array('query' => $form_state['values']['cas.return_to']));
1065
}
1066

    
1067
function _cas_single_sign_out_check() {
1068
  if (isset($_POST["logoutRequest"])) {
1069
    $cas_logout_request_xml_string = utf8_encode(urldecode($_POST["logoutRequest"]));
1070
    $cas_logout_request_xml = new SimpleXMLElement($cas_logout_request_xml_string);
1071
    if (is_object($cas_logout_request_xml)) {
1072
      $namespaces = $cas_logout_request_xml->getNameSpaces();
1073
      $xsearch = 'SessionIndex';
1074
      if (isset($namespaces['samlp'])) {
1075
        $cas_session_indexes = $cas_logout_request_xml->children($namespaces['samlp'])->SessionIndex;
1076
      }
1077
      else {
1078
        $cas_session_indexes = $cas_logout_request_xml->xpath($xsearch);
1079
      }
1080
      if ($cas_session_indexes) {
1081
        $cas_session_index = (string)$cas_session_indexes[0];
1082
        // Log them out now.
1083
        // first lets find out who we want to log off
1084

    
1085

    
1086
        $record = db_query_range("SELECT cld.uid, u.name FROM {users} u JOIN {cas_login_data} cld ON u.uid = cld.uid WHERE cld.cas_session_id = :ticket", 0, 1, array(':ticket' => $cas_session_index))->fetchObject();
1087
        if ($record) {
1088
          watchdog('user', 'Session closed for %name by CAS logout request.', array('%name' => $record->name));
1089
          //remove all entry for user id in cas_login_data
1090
          db_delete('cas_login_data')
1091
            ->condition('uid', $record->uid)
1092
            ->execute();
1093

    
1094
          // remove their session
1095
          db_delete('sessions')
1096
            ->condition('uid', $record->uid)
1097
            ->execute();
1098
        }
1099
      }
1100
    }
1101
    // This request is done, so just exit.
1102
    exit();
1103
  }
1104
}
1105

    
1106
/**
1107
 * Return the current CAS username.
1108
 */
1109
function cas_current_user() {
1110
  return isset($_SESSION['cas_name']) ? $_SESSION['cas_name'] : FALSE;
1111
}
1112

    
1113
/**
1114
 * Determine whether the specified user is an "external" CAS user.
1115
 * When settings are set to use drupal as the user repository, then this
1116
 * function will always return true.
1117
 *
1118
 * @param $account
1119
 *   The user object for the user to query. If omitted, the current user is
1120
 *   used.
1121
 *
1122
 * @return
1123
 *   TRUE if the user is logged in via CAS.
1124
 */
1125
function cas_is_external_user($account = NULL) {
1126
  if (!isset($account)) {
1127
    $account = $GLOBALS['user'];
1128
  }
1129
  return in_array(cas_current_user(), $account->cas_names);
1130
}
1131

    
1132

    
1133
function _cas_single_sign_out_save_token($user) {
1134
  // Ok lets save the CAS service ticket to DB so
1135
  // we can handle CAS logoutRequests when they come
1136
  if ($user->uid && $user->uid > 0 && !empty($_SESSION['cas_ticket'])) {
1137
    db_insert('cas_login_data')
1138
      ->fields(array(
1139
        'cas_session_id' => $_SESSION['cas_ticket'],
1140
        'uid' => $user->uid,
1141
        ))
1142
      ->execute();
1143
    unset($_SESSION['cas_ticket']);
1144
  }
1145
}
1146

    
1147
/**
1148
 * Make sure that we persist ticket because of redirects performed by CAS.
1149
 *
1150
 */
1151
function _cas_single_sign_out_save_ticket() {
1152
  if (isset($_GET['ticket'])) {
1153
    $_SESSION['cas_ticket'] = $_GET['ticket'];
1154
  }
1155
}
1156

    
1157
/**
1158
 * Determine whether a CAS user is blocked.
1159
 *
1160
 * @param $cas_name
1161
 *   The CAS username.
1162
 *
1163
 * @return
1164
 *   Boolean TRUE if the user is blocked, FALSE if the user is active.
1165
 */
1166
function _cas_external_user_is_blocked($cas_name) {
1167
  return db_query("SELECT name FROM {users} u JOIN {cas_user} c ON u.uid = c.uid WHERE u.status = 0 AND c.cas_name = :cas_name", array(':cas_name' => $cas_name))->fetchField();
1168
}
1169

    
1170
/**
1171
 * Invokes hook_cas_user_TYPE() in every module.
1172
 *
1173
 * We cannot use module_invoke() because the arguments need to be passed by
1174
 * reference.
1175
 */
1176
function cas_user_module_invoke($type, &$edit, $account) {
1177
  foreach (module_implements('cas_user_' . $type) as $module) {
1178
    $function = $module . '_cas_user_' . $type;
1179
    $function($edit, $account);
1180
  }
1181
}
1182

    
1183
/**
1184
 * Roles which should be granted to all CAS users.
1185
 *
1186
 * @return
1187
 *   An associative array with the role id as the key and the role name as value.
1188
 */
1189
function cas_roles() {
1190
  $cas_roles = &drupal_static(__FUNCTION__);
1191
  if (!isset($cas_roles)) {
1192
    $cas_roles = array_intersect_key(user_roles(), array_filter(variable_get('cas_auto_assigned_role', array(DRUPAL_AUTHENTICATED_RID => TRUE))));
1193
  }
1194
  return $cas_roles;
1195
}
1196

    
1197
/**
1198
 * Register a CAS user with some default values.
1199
 *
1200
 * @param $cas_name
1201
 *   The name of the CAS user.
1202
 * @param $options
1203
 *   An associative array of options, with the following elements:
1204
 *    - 'edit': An array of fields and values for the new user. If omitted,
1205
 *      reasonable defaults are used.
1206
 *    - 'invoke_cas_user_presave': Defaults to FALSE. Whether or not to invoke
1207
 *      hook_cas_user_presave() on the newly created account.
1208
 *
1209
 * @return
1210
 *   The user object of the created user, or FALSE if the user cannot be
1211
 *   created.
1212
 */
1213
function cas_user_register($cas_name, $options = array()) {
1214
  // Add some reasonable defaults if they have not yet been provided.
1215
  $edit = isset($options['edit']) ? $options['edit'] : array();
1216
  $edit += array(
1217
    'name' => $cas_name,
1218
    'pass' => user_password(),
1219
    'init' => $cas_name,
1220
    'mail' => variable_get('cas_domain', '') ? $cas_name . '@' . variable_get('cas_domain', '') : '',
1221
    'status' => 1,
1222
    'roles' => array(),
1223
  );
1224
  $edit['roles'] += cas_roles();
1225
  $edit['cas_name'] = $cas_name;
1226

    
1227
  // See if the user name is already taken.
1228
  if ((bool) db_select('users')->fields('users', array('name'))->condition('name', db_like($edit['name']), 'LIKE')->range(0, 1)->execute()->fetchField()) {
1229
    return FALSE;
1230
  }
1231

    
1232
  // Create the user account.
1233
  $account = user_save(drupal_anonymous_user(), $edit);
1234
  watchdog("user", 'new user: %n (CAS)', array('%n' => $account->name), WATCHDOG_NOTICE, l(t("edit user"), "admin/user/edit/$account->uid"));
1235

    
1236
  if (!empty($options['invoke_cas_user_presave'])) {
1237
    // Populate $edit with some basic properties.
1238
    $edit = array(
1239
      'cas_user' => array(
1240
        'name' => $cas_name,
1241
      ),
1242
    );
1243

    
1244
    // Allow other modules to make their own custom changes.
1245
    cas_user_module_invoke('presave', $edit, $account);
1246

    
1247
    // Clean up extra variables before saving.
1248
    unset($edit['cas_user']);
1249

    
1250
    $account = user_save($account, $edit);
1251
  }
1252

    
1253
  // Reload to ensure that we have a fully populated user object.
1254
  return user_load($account->uid);
1255
}
1256

    
1257
/**
1258
 * Get the CAS attributes of the current CAS user.
1259
 *
1260
 * Ensures that phpCAS is properly initialized before getting the attributes.
1261
 * @see phpCAS::getAttributes()
1262
 *
1263
 * @param $cas_name
1264
 *   If provided, ensure that the currently logged in CAS user matches this
1265
 *   CAS username.
1266
 *
1267
 * @return
1268
 *   An associative array of CAS attributes.
1269
 */
1270
function cas_phpcas_attributes($cas_name = NULL) {
1271
  if (isset($cas_name) && $cas_name != cas_current_user()) {
1272
    // Attributes cannot be extracted for other users, since they are
1273
    // stored in the session variable.
1274
    return array();
1275
  }
1276

    
1277
  cas_phpcas_init();
1278
  if (phpCAS::isAuthenticated()) {
1279
    if (method_exists('phpCAS', 'getAttributes')) {
1280
      return phpCAS::getAttributes();
1281
    }
1282
  }
1283

    
1284
  return array();
1285
}
1286

    
1287

    
1288
/**
1289
 * Insert an array into the specified position of another array.
1290
 *
1291
 * Preserves keys in associative arrays.
1292
 * @see http://www.php.net/manual/en/function.array-splice.php#56794
1293
 */
1294
function _cas_array_insert(&$array, $position, $insert_array) {
1295
  $first_array = array_splice($array, 0, $position);
1296
  $array = array_merge($first_array, $insert_array, $array);
1297
}
1298

    
1299
/**
1300
 * Implements hook_views_api().
1301
 */
1302
function cas_views_api() {
1303
  return array(
1304
    'api' => 3,
1305
    'path' => drupal_get_path('module', 'cas') . '/includes/views',
1306
  );
1307
}
1308

    
1309
/**
1310
 * Redirect a user after they have logged into the website through CAS
1311
 *
1312
 * @param $cas_first_login - TRUE if this is the first time the CAS user
1313
 * logged into the site
1314
 */
1315
function _cas_redirect_after_login($cas_first_login) {
1316
  // When users first log in, we may want to redirect them to a special page if specified
1317
  if ($cas_first_login && variable_get('cas_first_login_destination', '')) {
1318
    $destination = variable_get('cas_first_login_destination', '');
1319
    drupal_goto($destination);
1320
  }
1321
  else {
1322
    // If logged in through forced authentication ('/cas'), then redirect user to the
1323
    // homepage, or to wherever the current "destination" parameter points.
1324
    if (strtolower(current_path()) == 'cas') {
1325
      drupal_goto('');
1326
    }
1327
    // If logged in through gateway feature, then just reload the current path
1328
    // and preserve any query string args that were set
1329
    else {
1330
      drupal_goto(current_path(), array('query' => drupal_get_query_parameters()));
1331
    }
1332
  }
1333
}