Projet

Général

Profil

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

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

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
/**
19
 * Implements hook_init().
20
 *
21
 * Traps a page load to see if authentication is required.
22
 */
23
function cas_init() {
24
  global $user;
25

    
26
  if (module_exists('cas_test') && arg(0) == 'cas_test') {
27
    // We are destined for a page handled by the cas_test module, so do not
28
    // do any processing here. Necessary for CAS gateway tests.
29
    return;
30
  }
31

    
32
  // Process a single-sign out request.
33
  _cas_single_sign_out_check();
34

    
35
  // If a user is not logged in, consider using CAS authentication.
36
  if (!$user->uid) {
37
    $force_authentication = _cas_force_login();
38
    $check_authentication = _cas_allow_check_for_login();
39
    if ($force_authentication || $check_authentication) {
40
      cas_login_check($force_authentication);
41
    }
42
  }
43
}
44

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

    
60
  if (!cas_phpcas_load()) {
61
    // No need to print a message, as the user will already see the failed
62
    // include_once calls.
63
    return;
64
  }
65

    
66
  // Start a drupal session
67
  drupal_session_start();
68
  _cas_single_sign_out_save_ticket();  // We use this later for CAS 3 logoutRequests
69

    
70
  // Initialize phpCAS.
71
  cas_phpcas_init();
72

    
73
  // We're going to try phpCAS auth test
74
  if ($force_authentication) {
75
    phpCAS::forceAuthentication();
76
  }
77
  else {
78
    $logged_in = phpCAS::checkAuthentication();
79
    // Set the login tested cookie
80
    setcookie('cas_login_checked', 'true');
81

    
82
    // We're done cause we're not logged in.
83
    if (!$logged_in) {
84
      return;
85
    }
86
  }
87

    
88
  // Build the cas_user object and allow modules to alter it.
89
  $cas_user = array(
90
    'name' => phpCAS::getUser(),
91
    'login' => TRUE,
92
    'register' => variable_get('cas_user_register', TRUE),
93
    'attributes' => cas_phpcas_attributes(),
94
  );
95
  drupal_alter('cas_user', $cas_user);
96

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

    
107
  // Proceed with the login process, using the altered CAS username.
108
  $cas_name = $cas_user['name'];
109

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

    
121
  if ($blocked) {
122
    // Only display error messages only if the user intended to log in.
123
    if ($force_authentication) {
124
      watchdog('cas', $blocked, array('%cas_name' => $cas_name), WATCHDOG_WARNING);
125
      drupal_set_message(t($blocked, array('%cas_name' => $cas_name)), 'error');
126
    }
127
    return;
128
  }
129

    
130
  $account = cas_user_load_by_name($cas_name);
131

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

    
146
  // final check to make sure we have a good user
147
  if ($account && $account->uid > 0) {
148
    // Save the altered CAS name for future use.
149
    $_SESSION['cas_name'] = $cas_name;
150

    
151
    $cas_first_login = !$account->login;
152

    
153
    // Save single sign out information
154
    if (!empty($_SESSION['cas_ticket'])) {
155
      _cas_single_sign_out_save_token($account);
156
    }
157

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

    
167
    // Save the user account and log the user in.
168
    $user = user_save($account, $edit);
169
    user_login_finalize($edit);
170

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

    
176
    cas_login_page($cas_first_login);
177
  }
178
  else {
179
    $user = drupal_anonymous_user();
180
    // Only display error messages only if the user intended to log in.
181
    if ($force_authentication) {
182
      drupal_set_message(t('No account found for %cas_name.', array('%cas_name' => $cas_name)), 'error');
183
    }
184
  }
185
}
186

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

    
207
  // Build the name of the file to load.
208
  if ($path != '') {
209
    $path = rtrim($path, '/') . '/';
210
  }
211
  $filename = $path . 'CAS.php';
212

    
213
  include_once($filename);
214

    
215
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
216
    // The file could not be loaded successfully.
217
    return FALSE;
218
  }
219
  return PHPCAS_VERSION;
220
}
221

    
222
/**
223
 * Initialize phpCAS.
224
 *
225
 * Will load phpCAS if necessary.
226
 */
227
function cas_phpcas_init() {
228
  if (!defined('PHPCAS_VERSION') || !class_exists('phpCAS')) {
229
    cas_phpcas_load();
230
  }
231

    
232
  $initialized = &drupal_static(__FUNCTION__, FALSE);
233
  if ($initialized) {
234
    // phpCAS cannot be initialized twice. If you need to force this function
235
    // to run again, call drupal_static_reset('cas_phpcas_init') first.
236
    return;
237
  }
238
  $initialized = TRUE;
239

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

    
268
  //Add CAS proxy lists allowed
269
  $proxy_list = variable_get('cas_proxy_list', '');
270
  if ($proxy_list) {
271
    $proxy_list = explode("\n", $proxy_list);
272
    phpCAS::allowProxyChain(new CAS_ProxyChain($proxy_list));
273
  }
274

    
275
  // force CAS authentication
276
  if ($cas_cert = variable_get('cas_cert', '')) {
277
    phpCAS::setCasServerCACert($cas_cert);
278
  }
279
  else {
280
    phpCAS::setNoCasServerValidation();
281
  }
282

    
283
  $service = isset($_GET['q']) ? $_GET['q'] : 'cas';
284
  phpCAS::setFixedServiceURL(url($service, array('query' => cas_login_destination(), 'absolute' => TRUE)));
285

    
286
  // Allow other modules to call phpCAS routines. We do not call
287
  // drupal_alter() since there are no parameters to pass.
288
  module_invoke_all('cas_phpcas_alter');
289
}
290

    
291

    
292
/**
293
 * Implements hook_permission().
294
 */
295
function cas_permission() {
296
  return array(
297
    'administer cas' => array(
298
      'title' => t('Administer CAS'),
299
      'description' => t('Configure CAS server, default CAS user roles, login/logout redirection, and other settings.'),
300
      'restrict access' => TRUE,
301
    )
302
  );
303
}
304

    
305
/**
306
 * Implements hook_help().
307
 */
308
function cas_help($section) {
309
  switch ($section) {
310
    case 'admin/help#cas':
311
      return t("Allows users to authenticate via a Central Authentication Service.");
312
  }
313
}
314

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

    
379
function cas_user_is_logged_in() {
380
  return user_is_logged_in() || !empty($_SESSION['phpCAS']['user']);
381
}
382

    
383
/**
384
 * Implements hook_menu_site_status_alter().
385
 */
386
function cas_menu_site_status_alter(&$menu_site_status, $path) {
387
  if (user_is_logged_in() && $path == 'cas') {
388
    // If user is logged in, redirect to '<front>' instead of giving 403.
389
    drupal_goto('');
390
  }
391
}
392

    
393
/**
394
 * Implements hook_menu_link_alter().
395
 *
396
 * Flag this link as needing alter at display time.
397
 * @see cas_translated_menu_link_alter()
398
 */
399
function cas_menu_link_alter(&$item) {
400
  if ($item['link_path'] == 'cas' || $item['link_path'] == 'caslogout') {
401
    $item['options']['alter'] = TRUE;
402
  }
403
}
404

    
405
/**
406
 * Implements hook_translated_menu_item_alter().
407
 *
408
 * Append dynamic query 'destination' to several menu items.
409
 */
410
function cas_translated_menu_link_alter(&$item) {
411
  if ($item['href'] == 'cas') {
412
    $item['localized_options']['query'] = drupal_get_destination();
413
  }
414
  elseif ($item['href'] == 'caslogout' && !variable_get('cas_logout_destination', '')) {
415
    $item['localized_options']['query'] = drupal_get_destination();
416
  }
417
}
418

    
419
/**
420
 * Helper function to rewrite the destination to avoid redirecting to login page after login.
421
 *
422
 * Instead of the login page, we redirect to the front page.
423
 */
424
function cas_login_destination() {
425
  $destination = user_login_destination();
426
  if ($destination['destination'] == 'cas') {
427
    $destination['destination'] = '';
428
  }
429
  return $destination;
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
 * Redirects to appropriate page based on user settings.
635
 *
636
 * @param $cas_first_login
637
 *   TRUE if the user was just registered and they should be redirected to the
638
 *   configured 'Initial login landing page'.
639
 */
640
function cas_login_page($cas_first_login = FALSE) {
641
  global $user;
642
  $destination = '';
643
  $query = array();
644
  // If it is the user's first CAS login and initial login redirection is enabled, go to the set page
645
  if ($cas_first_login && variable_get('cas_first_login_destination', '')) {
646
    $destination = variable_get('cas_first_login_destination', '');
647
    if (isset($_GET['destination']))
648
      $query['destination'] = $_GET['destination'];
649
    unset($_GET['destination']);
650
  }
651

    
652
  // Respect the query string, if transmitted.
653
  drupal_goto($destination, array('query' => $query));
654
}
655

    
656
/**
657
 * Logs a user out of Drupal and then out of CAS.
658
 *
659
 * This function does not return, but instead immediately redirects the user
660
 * to the CAS server to complete the CAS logout process.
661
 *
662
 * Other modules intending to call this from their implementation of
663
 * hook_user_logout() will need to pass $invoke_hook = FALSE to avoid an
664
 * infinite recursion. WARNING: since this function does not return, any
665
 * later implementations of hook_user_logout() will not run. You may wish to
666
 * adjust the hook execution order using hook_module_implements_alter().
667
 *
668
 * @param $invoke_hook
669
 *   If TRUE, invoke hook_user_logout() and save a watchdog mesage indicating
670
 *   that the user has logged out.
671
 */
672
function cas_logout($invoke_hook = TRUE) {
673
  global $user;
674

    
675
  // Build the logout URL.
676
  cas_phpcas_init();
677

    
678
  if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
679
    // Add destination override so that a destination can be specified on the
680
    // logout link, e.g., caslogout?desination=http://foo.bar.com/foobar. We do
681
    // not allow absolute URLs to be passed via $_GET, as this can be an attack
682
    // vector.
683
    $destination = $_GET['destination'];
684
  }
685
  else {
686
    $destination = variable_get('cas_logout_destination', '');
687
  }
688

    
689
  //Make it an absolute url.  This will also convert <front> to the front page.
690
  if ($destination) {
691
    $destination_url = url($destination, array('absolute' => TRUE));
692
    $options = array(
693
      'service' => $destination_url,
694
      'url' => $destination_url,
695
    );
696
  }
697
  else {
698
    $options = array();
699
  }
700

    
701
  // Mimic user_logout().
702
  if ($invoke_hook) {
703
    watchdog('user', 'Session closed for %name.', array('%name' => $user->name));
704
    module_invoke_all('user_logout', $user);
705
  }
706

    
707
  // phpCAS automatically calls session_destroy().
708
  phpCAS::logout($options);
709
}
710

    
711
/**
712
 * Implements hook_block_info().
713
 */
714
function cas_block_info() {
715
  $blocks['login']['info'] = t('CAS login');
716
  // Not worth caching.
717
  $blocks['login']['cache'] = DRUPAL_NO_CACHE;
718

    
719
  return $blocks;
720
}
721

    
722
/**
723
 * Implements hook_block_view().
724
 */
725
function cas_block_view($delta = '') {
726
  global $user;
727

    
728
  $block = array();
729

    
730
  switch ($delta) {
731
    case 'login':
732
      // For usability's sake, avoid showing two login forms on one page.
733
      if (!$user->uid && !(arg(0) == 'user' && !is_numeric(arg(1)))) {
734
        $block['subject'] = t('User login');
735
        $block['content'] = drupal_get_form('cas_login_block');
736
      }
737
      return $block;
738
  }
739
}
740

    
741
/**
742
 * Login form for the CAS login block.
743
 */
744
function cas_login_block($form) {
745
  $form['#action'] = url('cas', array('query' => drupal_get_destination()));
746
  $form['#id'] = 'cas-login-form';
747

    
748
  $form['cas_login_redirection_message'] = array(
749
    '#type' => 'item',
750
    '#markup' => t(variable_get('cas_login_redir_message', CAS_LOGIN_REDIR_MESSAGE)),
751
    '#weight' => -1,
752
  );
753
  $form['actions'] = array('#type' => 'actions');
754
  $form['actions']['submit'] = array(
755
    '#type' => 'submit',
756
    '#value' => t(variable_get('cas_login_invite', CAS_LOGIN_INVITE_DEFAULT)),
757
  );
758

    
759
  return $form;
760
}
761

    
762
/**
763
 * Determine if we should automatically check if the user is authenticated.
764
 *
765
 * This implements part of the CAS gateway feature.
766
 * @see phpCAS::checkAuthentication()
767
 *
768
 * @return
769
 *   TRUE if we should query the CAS server to see if the user is already
770
 *   authenticated, FALSE otherwise.
771
 */
772
function _cas_allow_check_for_login() {
773
  if (!variable_get('cas_check_first', 0)) {
774
    // The user has disabled the feature.
775
    return FALSE;
776
  }
777

    
778
  // Check to see if we already have.
779
  if (!empty($_COOKIE['cas_login_checked'])) {
780
    return FALSE;
781
  }
782

    
783
  // Check to see if we've got a search bot.
784
  if (isset($_SERVER['HTTP_USER_AGENT'])) {
785
    $crawlers = array(
786
      'Google',
787
      'msnbot',
788
      'Rambler',
789
      'Yahoo',
790
      'AbachoBOT',
791
      'accoona',
792
      'AcoiRobot',
793
      'ASPSeek',
794
      'CrocCrawler',
795
      'Dumbot',
796
      'FAST-WebCrawler',
797
      'GeonaBot',
798
      'Gigabot',
799
      'Lycos',
800
      'MSRBOT',
801
      'Scooter',
802
      'AltaVista',
803
      'IDBot',
804
      'eStyle',
805
      'Scrubby',
806
      'gsa-crawler',
807
      );
808
    // Return on the first find.
809
    foreach ($crawlers as $c) {
810
      if (stripos($_SERVER['HTTP_USER_AGENT'], $c) !== FALSE) {
811
        return FALSE;
812
      }
813
    }
814
  }
815

    
816
  // Do not force login for XMLRPC, Cron, or Drush.
817
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'xmlrpc.php')) {
818
    return FALSE;
819
  }
820
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'cron.php')) {
821
    return FALSE;
822
  }
823
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'drush')) {
824
    return FALSE;
825
  }
826
  if (!empty($_SERVER['argv'][0]) && stristr($_SERVER['argv'][0], 'drush')) {
827
    return FALSE;
828
  }
829

    
830
  // Test against exclude pages.
831
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
832
    $path = drupal_get_path_alias($_GET['q']);
833
    if (drupal_match_path($path, $pages)) {
834
      return FALSE;
835
    }
836
  }
837

    
838
  return TRUE;
839
}
840

    
841
/**
842
 * Determine if we should require the user be authenticated.
843
 *
844
 * @return
845
 *   TRUE if we should require the user be authenticated, FALSE otherwise.
846
 */
847
function _cas_force_login() {
848
  // The 'cas' page is a shortcut to force authentication.
849
  if (arg(0) == 'cas') {
850
    return TRUE;
851
  }
852

    
853
  // Do not force login for XMLRPC, Cron, or Drush.
854
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'xmlrpc.php')) {
855
    return FALSE;
856
  }
857
  if (stristr($_SERVER['SCRIPT_FILENAME'], 'cron.php')) {
858
    return FALSE;
859
  }
860
  if (function_exists('drush_verify_cli') && drush_verify_cli()) {
861
    return FALSE;
862
  }
863

    
864
  // Excluded page do not need login.
865
  if ($pages = variable_get('cas_exclude', CAS_EXCLUDE)) {
866
    $path = drupal_get_path_alias($_GET['q']);
867
    if (drupal_match_path($path, $pages)) {
868
      return FALSE;
869
    }
870
  }
871

    
872
  // Set the default behavior.
873
  $force_login = variable_get('cas_access', 0);
874

    
875
  // If we match the speficied paths, reverse the behavior.
876
  if ($pages = variable_get('cas_pages', '')) {
877
    $path = drupal_get_path_alias($_GET['q']);
878
    if (drupal_match_path($path, $pages)) {
879
      $force_login = !$force_login;
880
    }
881
  }
882

    
883
  return $force_login;
884
}
885
/**
886
 * Implements hook_form_alter().
887
 *
888
 * Overrides specific from settings based on user policy.
889
 */
890
function cas_form_alter(&$form, &$form_state, $form_id) {
891

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1050
  if ($uid !== FALSE) {
1051
    // Another user is using this CAS username.
1052
    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'))));
1053
  }
1054
}
1055

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

    
1068
function _cas_single_sign_out_check() {
1069
  if (isset($_POST["logoutRequest"])) {
1070
    $cas_logout_request_xml_string = utf8_encode(urldecode($_POST["logoutRequest"]));
1071
    $cas_logout_request_xml = new SimpleXMLElement($cas_logout_request_xml_string);
1072
    if (is_object($cas_logout_request_xml)) {
1073
      $namespaces = $cas_logout_request_xml->getNameSpaces();
1074
      $xsearch = 'SessionIndex';
1075
      if (isset($namespaces['samlp'])) {
1076
        $cas_logout_request_xml->registerXPathNamespace('samlp', $namespaces['samlp']);
1077
        $xsearch = 'samlp:SessionIndex';
1078
      }
1079
      $cas_session_indexes = $cas_logout_request_xml->xpath($xsearch);
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
}