Projet

Général

Profil

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

root / drupal7 / sites / all / modules / cas / cas.module @ e9f59589

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_LOGIN_INVITE_DEFAULT', 'Log in using CAS');
14
define('CAS_LOGIN_DRUPAL_INVITE_DEFAULT', 'Cancel CAS login');
15
define('CAS_LOGIN_REDIR_MESSAGE', 'You will be redirected to the secure CAS login page.');
16
define('CAS_EXCLUDE', 'services/*');
17

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

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

    
31
  if (module_exists('cas_test') && arg(0) == 'cas_test') {
32
    // We are destined for a page handled by the cas_test module, so do not
33
    // do any processing here. Necessary for CAS gateway tests.
34
    return;
35
  }
36

    
37
  // Process a single-sign out request.
38
  _cas_single_sign_out_check();
39

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

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

    
67
  if (!cas_phpcas_load()) {
68
    // No need to print a message, as the user will already see the failed
69
    // include_once calls.
70
    return;
71
  }
72

    
73
  // Start a drupal session
74
  drupal_session_start();
75
  _cas_single_sign_out_save_ticket();  // We use this later for CAS 3 logoutRequests
76

    
77
  // Initialize phpCAS.
78
  cas_phpcas_init();
79

    
80
  // We're going to try phpCAS auth test
81
  if ($force_authentication) {
82
    phpCAS::forceAuthentication();
83
  }
84
  else {
85
    $logged_in = phpCAS::checkAuthentication();
86

    
87
    // We're done cause we're not logged in.
88
    if (!$logged_in) {
89
      return;
90
    }
91
  }
92

    
93
  // Build the cas_user object and allow modules to alter it.
94
  $cas_user = array(
95
    'name' => phpCAS::getUser(),
96
    'login' => TRUE,
97
    'register' => variable_get('cas_user_register', TRUE),
98
    'attributes' => cas_phpcas_attributes(),
99
  );
100
  drupal_alter('cas_user', $cas_user);
101

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

    
112
  // Proceed with the login process, using the altered CAS username.
113
  $cas_name = $cas_user['name'];
114

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

    
126
  if ($blocked) {
127
    // Only display error messages only if the user intended to log in.
128
    if ($force_authentication) {
129
      watchdog('cas', $blocked, array('%cas_name' => $cas_name), WATCHDOG_WARNING);
130
      drupal_set_message(t($blocked, array('%cas_name' => $cas_name)), 'error');
131
    }
132
    return;
133
  }
134

    
135
  $account = cas_user_load_by_name($cas_name);
136

    
137
  // Automatic user registration.
138
  if (!$account && $cas_user['register']) {
139
    // No account could be found and auto registration is enabled, so attempt
140
    // to register a new user.
141
    $account = cas_user_register($cas_name);
142
    if (!$account) {
143
      // The account could not be created, set a message.
144
      if ($force_authentication) {
145
        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');
146
      }
147
      return;
148
    }
149
  }
150

    
151
  // final check to make sure we have a good user
152
  if ($account && $account->uid > 0) {
153
    // Save the altered CAS name for future use.
154
    $_SESSION['cas_name'] = $cas_name;
155

    
156
    $cas_first_login = !$account->login;
157

    
158
    // Save single sign out information
159
    if (!empty($_SESSION['cas_ticket'])) {
160
      _cas_single_sign_out_save_token($account);
161
    }
162

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

    
172
    // Save the user account and log the user in.
173
    $user = user_save($account, $edit);
174
    user_login_finalize($edit);
175

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

    
181
    _cas_redirect_after_login($cas_first_login);
182

    
183
  }
184
  else {
185
    $user = drupal_anonymous_user();
186
    unset($_SESSION['phpCAS']);
187

    
188
    // Only display error messages only if the user intended to log in.
189
    if ($force_authentication) {
190
      drupal_set_message(t('No account found for %cas_name.', array('%cas_name' => $cas_name)), 'error');
191
    }
192
  }
193
}
194

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

    
215
  // Build the name of the file to load.
216
  if ($path != '') {
217
    $path = rtrim($path, '/') . '/';
218
  }
219
  $filename = $path . 'CAS.php';
220

    
221
  include_once($filename);
222

    
223
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
224
    // The file could not be loaded successfully.
225
    return FALSE;
226
  }
227
  return PHPCAS_VERSION;
228
}
229

    
230
/**
231
 * Initialize phpCAS.
232
 *
233
 * Will load phpCAS if necessary.
234
 */
235
function cas_phpcas_init() {
236
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
237
    cas_phpcas_load();
238
  }
239

    
240
  $initialized = &drupal_static(__FUNCTION__, FALSE);
241
  if ($initialized) {
242
    // phpCAS cannot be initialized twice. If you need to force this function
243
    // to run again, call drupal_static_reset('cas_phpcas_init') first.
244
    return;
245
  }
246
  $initialized = TRUE;
247

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

    
276
  //Add CAS proxy lists allowed
277
  $proxy_list = variable_get('cas_proxy_list', '');
278
  if ($proxy_list) {
279
    $proxy_list = explode("\n", $proxy_list);
280
    phpCAS::allowProxyChain(new CAS_ProxyChain($proxy_list));
281
  }
282

    
283
  // force CAS authentication
284
  if ($cas_cert = variable_get('cas_cert', '')) {
285
    phpCAS::setCasServerCACert($cas_cert);
286
  }
287
  else {
288
    phpCAS::setNoCasServerValidation();
289
  }
290

    
291
  phpCAS::setFixedServiceURL(url(current_path(), array('query' => drupal_get_query_parameters(), 'absolute' => TRUE)));
292
  phpCAS::setCacheTimesForAuthRecheck((int) variable_get('cas_check_frequency', CAS_CHECK_NEVER));
293

    
294
  // Allow other modules to call phpCAS routines. We do not call
295
  // drupal_alter() since there are no parameters to pass.
296
  module_invoke_all('cas_phpcas_alter');
297
}
298

    
299

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

    
313
/**
314
 * Implements hook_help().
315
 */
316
function cas_help($section) {
317
  switch ($section) {
318
    case 'admin/help#cas':
319
      return t("Allows users to authenticate via a Central Authentication Service.");
320
  }
321
}
322

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

    
387
function cas_user_is_logged_in() {
388
  return user_is_logged_in() || !empty($_SESSION['phpCAS']['user']);
389
}
390

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

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

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

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

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

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

    
483
/**
484
 * Implements hook_admin_paths().
485
 */
486
function cas_admin_paths() {
487
  $paths = array(
488
    'user/*/cas' => TRUE,
489
    'user/*/cas/delete/*' => TRUE,
490
  );
491
  return $paths;
492
}
493

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

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

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

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

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

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

    
621
  $uid = db_select('cas_user')->fields('cas_user', array('uid'))->condition('cas_name', db_like($cas_name), 'LIKE')->range(0, 1)->execute()->fetchField();
622
  if ($uid) {
623
    return user_load($uid, $reset);
624
  }
625
  return FALSE;
626
}
627

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

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

    
659
  // Build the logout URL.
660
  cas_phpcas_init();
661

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

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

    
685
  // Mimic user_logout().
686
  if ($invoke_hook) {
687
    watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
688
    module_invoke_all('user_logout', $user);
689
  }
690

    
691
  // phpCAS automatically calls session_destroy().
692
  phpCAS::logout($options);
693
}
694

    
695
/**
696
 * Implements hook_block_info().
697
 */
698
function cas_block_info() {
699
  $blocks['login']['info'] = t('CAS login');
700
  // Not worth caching.
701
  $blocks['login']['cache'] = DRUPAL_NO_CACHE;
702

    
703
  return $blocks;
704
}
705

    
706
/**
707
 * Implements hook_block_view().
708
 */
709
function cas_block_view($delta = '') {
710
  global $user;
711

    
712
  $block = array();
713

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

    
725
/**
726
 * Login form for the CAS login block.
727
 */
728
function cas_login_block($form) {
729
  $form['#action'] = url('cas', array('query' => drupal_get_destination()));
730
  $form['#id'] = 'cas-login-form';
731

    
732
  $form['cas_login_redirection_message'] = array(
733
    '#type' => 'item',
734
    '#markup' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
735
    '#weight' => -1,
736
  );
737
  $form['actions'] = array('#type' => 'actions');
738
  $form['actions']['submit'] = array(
739
    '#type' => 'submit',
740
    '#value' => t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)),
741
  );
742

    
743
  return $form;
744
}
745

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

    
762
  if (variable_get('cas_check_frequency', CAS_CHECK_NEVER) == CAS_CHECK_NEVER) {
763
    // The user has disabled the feature.
764
    return FALSE;
765
  }
766

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

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

    
814
  // Test against exclude pages.
815
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
816
    $path = drupal_get_path_alias($_GET['q']);
817
    if (drupal_match_path($path, $pages)) {
818
      return FALSE;
819
    }
820
  }
821

    
822
  return TRUE;
823
}
824

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

    
837
  // Do not process in maintenance mode.
838
  if (variable_get('maintenance_mode', 0)) {
839
    return FALSE;
840
  }
841

    
842
  // Do not force login for XMLRPC, Cron, or Drush.
843
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'xmlrpc.php')) {
844
    return FALSE;
845
  }
846
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'cron.php')) {
847
    return FALSE;
848
  }
849
  if (function_exists('drush_verify_cli') && drush_verify_cli()) {
850
    return FALSE;
851
  }
852

    
853
  // Excluded page do not need login.
854
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
855
    $path = drupal_get_path_alias($_GET['q']);
856
    if (drupal_match_path($path, $pages)) {
857
      return FALSE;
858
    }
859
  }
860

    
861
  // Set the default behavior.
862
  $force_login = variable_get('cas_access', 0);
863

    
864
  // If we match the speficied paths, reverse the behavior.
865
  if ($pages = variable_get('cas_pages', '')) {
866
    $path = drupal_get_path_alias($_GET['q']);
867
    if (drupal_match_path($path, $pages)) {
868
      $force_login = !$force_login;
869
    }
870
  }
871

    
872
  return $force_login;
873
}
874
/**
875
 * Implements hook_form_alter().
876
 *
877
 * Overrides specific from settings based on user policy.
878
 */
879
function cas_form_alter(&$form, &$form_state, $form_id) {
880

    
881
  switch ($form_id) {
882
    case 'user_login':
883
    case 'user_login_block':
884
      if (variable_get('cas_login_form', CAS_NO_LINK) != CAS_NO_LINK) {
885
        $form['#attached']['css'][] = drupal_get_path('module', 'cas') . '/cas.css';
886
        $form['#attached']['js'][] = drupal_get_path('module', 'cas') . '/cas.js';
887

    
888
        if (!empty($form_state['input']['cas_identifier'])) {
889
          $form['name']['#required'] = FALSE;
890
          $form['pass']['#required'] = FALSE;
891
          unset($form['#validate']);
892
          $form['#submit'] = array('cas_login_submit');
893
        }
894

    
895
        $items = array();
896
        $items[] = array(
897
          'data' => l(t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)), '#'),
898
          'class' => array('cas-link'),
899
        );
900

    
901
        $items[] = array(
902
          'data' => l(t(variable_get('cas_login_drupal_invite', CAS_LOGIN_DRUPAL_INVITE_DEFAULT)), '#'),
903
          'class' => array('uncas-link'),
904
        );
905

    
906
        $form['cas_links'] = array(
907
          '#theme' => 'item_list',
908
          '#items' => $items,
909
          '#attributes' => array('class' => array('cas-links')),
910
          '#weight' => 1,
911
        );
912

    
913
        $form['links']['#weight'] = 2;
914

    
915
        $form['cas_login_redirection_message'] = array(
916
          '#type' => 'item',
917
          '#markup' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
918
          '#weight' => -1,
919
        );
920

    
921
        $form['cas_identifier'] = array(
922
          '#type' => 'checkbox',
923
          '#title' => t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)),
924
          '#default_value' => variable_get('cas_login_form', CAS_NO_LINK) == CAS_MAKE_DEFAULT,
925
          '#weight' => -1,
926
          '#description' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
927
        );
928
        $form['cas.return_to'] = array('#type' => 'hidden', '#value' => user_login_destination());
929
      }
930
      break;
931

    
932
    case 'user_profile_form':
933
      $account = $form['#user'];
934
      if (user_access('administer users')) {
935
        // The user is an administrator, so add fields to allow changing the
936
        // CAS username(s) associated with the account.
937
        $cas_names = $account->cas_names;
938
        $aids = array_keys($cas_names);
939

    
940
        $element = array(
941
          '#type' => 'textfield',
942
          '#title' => t('CAS username'),
943
          '#default_value' => array_shift($cas_names),
944
          '#cas_user_aid' => array_shift($aids),
945
          '#description' => t('<a href="@url">Create, edit or delete</a> additional CAS usernames associated with this account.', array('@url' => url('user/' . $account->uid . '/cas'))),
946
          '#element_validate' => array('_cas_name_element_validate'),
947
          '#weight' => -9,
948
        );
949

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

    
976
    case 'user_pass':
977
      if (!user_access('administer users') && variable_get('cas_changePasswordURL', '') != '') {
978
        drupal_goto(variable_get('cas_changePasswordURL', ''));
979
      }
980
      break;
981

    
982
    case 'user_register_form':
983
      if (user_access('administer users')) {
984
        $form['account']['cas_name'] = array(
985
          '#type' => 'textfield',
986
          '#title' => t('CAS username'),
987
          '#default_value' => '',
988
          '#description' => t('If necessary, additional CAS usernames can be added after the account is created.'),
989
          '#element_validate' => array('_cas_name_element_validate'),
990
          '#weight' => -9,
991
        );
992
      }
993
      elseif (variable_get('cas_registerURL', '') != '') {
994
        drupal_goto(variable_get('cas_registerURL', ''));
995
      }
996
      break;
997

    
998
    case 'user_admin_account':
999
      // Insert the CAS username into the second column.
1000
      _cas_array_insert($form['accounts']['#header'], 1, array(
1001
        'cas' => array(
1002
          'data' => 'CAS usernames',
1003
        ),
1004
      ));
1005
      foreach ($form['accounts']['#options'] as $uid => &$row) {
1006
        $cas_usernames = db_query('SELECT cas_name FROM {cas_user} WHERE uid = :uid', array(':uid' => $uid))->fetchCol();
1007
        $row['cas'] = theme('item_list', array('items' => $cas_usernames));
1008
      }
1009
      break;
1010
  }
1011
}
1012

    
1013
/**
1014
 * Form element 'cas_name' validator.
1015
 *
1016
 * If the element is disaplying an existing {cas_user} entry, set
1017
 * #cas_user_aid to the corresponing authmap id to avoid spurious
1018
 * validation errors.
1019
 */
1020
function _cas_name_element_validate($element, &$form_state) {
1021
  if (empty($element['#value'])) {
1022
    // Nothing to validate if the name is empty.
1023
    return;
1024
  }
1025

    
1026
  $query = db_select('cas_user')
1027
    ->fields('cas_user', array('uid'))
1028
    ->condition('cas_name', $element['#value']);
1029

    
1030
  // If set, we ignore entries with a specified authmap id. This is used on
1031
  // the user/%user/edit page to not throw validation errors when we do not
1032
  // change the CAS username.
1033
  if (isset($element['#cas_user_aid'])) {
1034
    $query->condition('aid', $element['#cas_user_aid'], '<>');
1035
  }
1036

    
1037
  $uid = $query->execute()->fetchField();
1038

    
1039
  if ($uid !== FALSE) {
1040
    // Another user is using this CAS username.
1041
    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'))));
1042
  }
1043
}
1044

    
1045
/**
1046
 * Login form _validate hook
1047
 */
1048
function cas_login_submit(&$form, &$form_state) {
1049
  if (!empty($form_state['values']['persistent_login'])) {
1050
    $_SESSION['cas_remember'] = 1;
1051
  }
1052
  // Force redirection.
1053
  unset($_GET['destination']);
1054
  drupal_goto('cas', array('query' => $form_state['values']['cas.return_to']));
1055
}
1056

    
1057
function _cas_single_sign_out_check() {
1058
  if (isset($_POST["logoutRequest"])) {
1059
    $cas_logout_request_xml_string = utf8_encode(urldecode($_POST["logoutRequest"]));
1060
    $cas_logout_request_xml = new SimpleXMLElement($cas_logout_request_xml_string);
1061
    if (is_object($cas_logout_request_xml)) {
1062
      $namespaces = $cas_logout_request_xml->getNameSpaces();
1063
      $xsearch = 'SessionIndex';
1064
      if (isset($namespaces['samlp'])) {
1065
        $cas_session_indexes = $cas_logout_request_xml->children($namespaces['samlp'])->SessionIndex;
1066
      }
1067
      else {
1068
        $cas_session_indexes = $cas_logout_request_xml->xpath($xsearch);
1069
      }
1070
      if ($cas_session_indexes) {
1071
        $cas_session_index = (string)$cas_session_indexes[0];
1072
        // Log them out now.
1073
        // first lets find out who we want to log off
1074

    
1075

    
1076
        $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();
1077
        if ($record) {
1078
          watchdog('user', 'Session closed for %name by CAS logout request.', array('%name' => $record->name));
1079
          //remove all entry for user id in cas_login_data
1080
          db_delete('cas_login_data')
1081
            ->condition('uid', $record->uid)
1082
            ->execute();
1083

    
1084
          // remove their session
1085
          db_delete('sessions')
1086
            ->condition('uid', $record->uid)
1087
            ->execute();
1088
        }
1089
      }
1090
    }
1091
    // This request is done, so just exit.
1092
    exit();
1093
  }
1094
}
1095

    
1096
/**
1097
 * Return the current CAS username.
1098
 */
1099
function cas_current_user() {
1100
  return isset($_SESSION['cas_name']) ? $_SESSION['cas_name'] : FALSE;
1101
}
1102

    
1103
/**
1104
 * Determine whether the specified user is an "external" CAS user.
1105
 * When settings are set to use drupal as the user repository, then this
1106
 * function will always return true.
1107
 *
1108
 * @param $account
1109
 *   The user object for the user to query. If omitted, the current user is
1110
 *   used.
1111
 *
1112
 * @return
1113
 *   TRUE if the user is logged in via CAS.
1114
 */
1115
function cas_is_external_user($account = NULL) {
1116
  if (!isset($account)) {
1117
    $account = $GLOBALS['user'];
1118
  }
1119
  return in_array(cas_current_user(), $account->cas_names);
1120
}
1121

    
1122

    
1123
function _cas_single_sign_out_save_token($user) {
1124
  // Ok lets save the CAS service ticket to DB so
1125
  // we can handle CAS logoutRequests when they come
1126
  if ($user->uid && $user->uid > 0 && !empty($_SESSION['cas_ticket'])) {
1127
    db_insert('cas_login_data')
1128
      ->fields(array(
1129
        'cas_session_id' => $_SESSION['cas_ticket'],
1130
        'uid' => $user->uid,
1131
        ))
1132
      ->execute();
1133
    unset($_SESSION['cas_ticket']);
1134
  }
1135
}
1136

    
1137
/**
1138
 * Make sure that we persist ticket because of redirects performed by CAS.
1139
 *
1140
 */
1141
function _cas_single_sign_out_save_ticket() {
1142
  if (isset($_GET['ticket'])) {
1143
    $_SESSION['cas_ticket'] = $_GET['ticket'];
1144
  }
1145
}
1146

    
1147
/**
1148
 * Determine whether a CAS user is blocked.
1149
 *
1150
 * @param $cas_name
1151
 *   The CAS username.
1152
 *
1153
 * @return
1154
 *   Boolean TRUE if the user is blocked, FALSE if the user is active.
1155
 */
1156
function _cas_external_user_is_blocked($cas_name) {
1157
  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();
1158
}
1159

    
1160
/**
1161
 * Invokes hook_cas_user_TYPE() in every module.
1162
 *
1163
 * We cannot use module_invoke() because the arguments need to be passed by
1164
 * reference.
1165
 */
1166
function cas_user_module_invoke($type, &$edit, $account) {
1167
  foreach (module_implements('cas_user_' . $type) as $module) {
1168
    $function = $module . '_cas_user_' . $type;
1169
    $function($edit, $account);
1170
  }
1171
}
1172

    
1173
/**
1174
 * Roles which should be granted to all CAS users.
1175
 *
1176
 * @return
1177
 *   An associative array with the role id as the key and the role name as value.
1178
 */
1179
function cas_roles() {
1180
  $cas_roles = &drupal_static(__FUNCTION__);
1181
  if (!isset($cas_roles)) {
1182
    $cas_roles = array_intersect_key(user_roles(), array_filter(variable_get('cas_auto_assigned_role', array(DRUPAL_AUTHENTICATED_RID => TRUE))));
1183
  }
1184
  return $cas_roles;
1185
}
1186

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

    
1217
  // See if the user name is already taken.
1218
  if ((bool) db_select('users')->fields('users', array('name'))->condition('name', db_like($edit['name']), 'LIKE')->range(0, 1)->execute()->fetchField()) {
1219
    return FALSE;
1220
  }
1221

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

    
1226
  if (!empty($options['invoke_cas_user_presave'])) {
1227
    // Populate $edit with some basic properties.
1228
    $edit = array(
1229
      'cas_user' => array(
1230
        'name' => $cas_name,
1231
      ),
1232
    );
1233

    
1234
    // Allow other modules to make their own custom changes.
1235
    cas_user_module_invoke('presave', $edit, $account);
1236

    
1237
    // Clean up extra variables before saving.
1238
    unset($edit['cas_user']);
1239

    
1240
    $account = user_save($account, $edit);
1241
  }
1242

    
1243
  // Reload to ensure that we have a fully populated user object.
1244
  return user_load($account->uid);
1245
}
1246

    
1247
/**
1248
 * Get the CAS attributes of the current CAS user.
1249
 *
1250
 * Ensures that phpCAS is properly initialized before getting the attributes.
1251
 * @see phpCAS::getAttributes()
1252
 *
1253
 * @param $cas_name
1254
 *   If provided, ensure that the currently logged in CAS user matches this
1255
 *   CAS username.
1256
 *
1257
 * @return
1258
 *   An associative array of CAS attributes.
1259
 */
1260
function cas_phpcas_attributes($cas_name = NULL) {
1261
  if (isset($cas_name) && $cas_name != cas_current_user()) {
1262
    // Attributes cannot be extracted for other users, since they are
1263
    // stored in the session variable.
1264
    return array();
1265
  }
1266

    
1267
  cas_phpcas_init();
1268
  if (phpCAS::isAuthenticated()) {
1269
    if (method_exists('phpCAS', 'getAttributes')) {
1270
      return phpCAS::getAttributes();
1271
    }
1272
  }
1273

    
1274
  return array();
1275
}
1276

    
1277

    
1278
/**
1279
 * Insert an array into the specified position of another array.
1280
 *
1281
 * Preserves keys in associative arrays.
1282
 * @see http://www.php.net/manual/en/function.array-splice.php#56794
1283
 */
1284
function _cas_array_insert(&$array, $position, $insert_array) {
1285
  $first_array = array_splice($array, 0, $position);
1286
  $array = array_merge($first_array, $insert_array, $array);
1287
}
1288

    
1289
/**
1290
 * Implements hook_views_api().
1291
 */
1292
function cas_views_api() {
1293
  return array(
1294
    'api' => 3,
1295
    'path' => drupal_get_path('module', 'cas') . '/includes/views',
1296
  );
1297
}
1298

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