| 1 |
<?php |
<?php |
| 2 |
// $Id: singlesignon.module,v 1.31 2008/05/05 13:38:20 wayland76 Exp $ |
// $Id$ |
| 3 |
|
|
|
|
|
| 4 |
/** |
/** |
| 5 |
* @file |
* @file |
| 6 |
* Enables "Single Sign-Ons" between related Drupal sites on one server |
* Enables "Single Sign-Ons" between related Drupal sites on one server |
| 7 |
* with a shared database. |
* with a shared database. |
|
* |
|
|
* Installation and activation are simple: |
|
|
* + Put this file in your modules directory. |
|
|
* + If you haven't done so already, create site sub-directories in the |
|
|
* "sites" directory for each of your domains. Put "settings.php" |
|
|
* files in each of those directories. Edit each of the settings.php |
|
|
* files to values appropriate for the given domain. |
|
|
* + Make sure the "$db_prefix" variable is set correctly in each |
|
|
* settings.php file (see details below). |
|
|
* + Create the site specific and shared database tables defined in the |
|
|
* "$db_prefix" variable (see details below). |
|
|
* + On each site, starting with the master site: |
|
|
* + Log in as administrator. |
|
|
* + Enable the "singlesignon" module |
|
|
* (via the checkbox in the administer | modules interface). |
|
|
* + Enter the URL of the master site |
|
|
* (via administer | settings | singlesignon). |
|
|
* + Delete cookies from all of your Drupal sites. |
|
|
* |
|
|
* This module relies on several tables being shared between the master and |
|
|
* slave sites. Here is an example of the "$db_prefix" variable you need to |
|
|
* establish in all of your "settings.php" files in the "sites" directory. |
|
|
* Each site should have a unique name in place of "somesitename_". While |
|
|
* you can rename "shared_" to something else, the prefixes must be the |
|
|
* same for all sites. |
|
|
* |
|
|
* @verbatim |
|
|
* $db_prefix = array( |
|
|
* 'default' => 'somesitename_', |
|
|
* 'authmap' => 'shared_', |
|
|
* 'profile_fields' => 'shared_', |
|
|
* 'profile_values' => 'shared_', |
|
|
* 'role' => 'shared_', |
|
|
* 'sequences' => 'shared_', |
|
|
* 'sessions' => 'shared_', |
|
|
* 'users' => 'shared_', |
|
|
* 'users_roles' => 'shared_', |
|
|
* 'users_uid_seq' => 'shared_', // for pgsql |
|
|
* ); |
|
|
* @endverbatim |
|
|
* |
|
|
* @link http://drupal.org/project/singlesignon |
|
|
* @author Primary Author: Daniel Convissor <danielc@analysisandsolutions.com> |
|
|
* @author Maintainer: Tim Nelson <wayland@wayland.id.au> |
|
|
* @version $Revision: 1.31 $ |
|
| 8 |
*/ |
*/ |
| 9 |
|
|
| 10 |
// {{{ core functions |
/** |
| 11 |
|
* @defgroup singlesignon_core Core functions. |
| 12 |
|
* @{ |
| 13 |
|
*/ |
| 14 |
|
|
| 15 |
include_once('sessions_extra.inc'); |
include_once('sessions_extra.inc'); |
| 16 |
|
|
| 40 |
} |
} |
| 41 |
$_singlesignon_bot_matches = variable_get('singlesignon_bot_matches', $variable_defaults); |
$_singlesignon_bot_matches = variable_get('singlesignon_bot_matches', $variable_defaults); |
| 42 |
|
|
| 43 |
if ( |
// If no master URL is set or we are serving a bot, do nothing. |
| 44 |
// If the Master URL isn't set, we can't know what to do, so do nothing |
if (!$master_url || _singlesignon_is_bot()) { |
| 45 |
(!$master_url) |
return; |
|
// Likewise, bots don't sign on |
|
|
|| _singlesignon_is_bot() |
|
|
) { |
|
|
return null; |
|
| 46 |
} |
} |
| 47 |
|
|
| 48 |
$extra_base_url = _singlesignon_base_url(); |
$extra_base_url = _singlesignon_base_url(); |
| 54 |
drupal_set_message(t('Cookies are required.'), 'error'); |
drupal_set_message(t('Cookies are required.'), 'error'); |
| 55 |
return; |
return; |
| 56 |
} |
} |
|
|
|
| 57 |
// This is the user's first hit to a slave site. Take note of their |
// This is the user's first hit to a slave site. Take note of their |
| 58 |
// session ID, since that's how we tell if they've been here or not. |
// session ID, since that's how we tell if they've been here or not. |
| 59 |
// Then go to the master site to see if they are logged in over there. |
// Then go to the master site to see if they are logged in over there. |
| 66 |
// arg() only available if bootstrap has reached PATH. |
// arg() only available if bootstrap has reached PATH. |
| 67 |
drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH); |
drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH); |
| 68 |
|
|
| 69 |
$arg0 = arg(0); |
switch (arg(0)) { |
|
|
|
|
switch ($arg0) { |
|
| 70 |
case 'logout': |
case 'logout': |
| 71 |
if ($user->uid) { |
if ($user->uid) { |
| 72 |
_singlesignon_session_logout($user->uid); |
_singlesignon_session_logout($user->uid); |
| 75 |
|
|
| 76 |
case 'singlesignon': |
case 'singlesignon': |
| 77 |
if ($extra_base_url == $master_url) { |
if ($extra_base_url == $master_url) { |
| 78 |
_singlesignon_master($master_url, $arg0, arg(1)); |
_singlesignon_master($master_url, arg(0), arg(1)); |
| 79 |
} |
} |
| 80 |
return; |
return; |
| 81 |
|
|
| 86 |
// checking yet because the login process happens after this module |
// checking yet because the login process happens after this module |
| 87 |
// called. Set a flag telling us to do the master/slave checking |
// called. Set a flag telling us to do the master/slave checking |
| 88 |
// once the login process is done. |
// once the login process is done. |
| 89 |
$_SESSION['singlesignon_just_loggged_in'] = true; |
$_SESSION['singlesignon_just_loggged_in'] = TRUE; |
| 90 |
return; |
return; |
| 91 |
} |
} |
| 92 |
} |
} |
| 117 |
global $user; |
global $user; |
| 118 |
|
|
| 119 |
if (empty($_GET['singlesignon_dest']) || is_array($_GET['singlesignon_dest']) || empty($_GET['slave_session']) || is_array($_GET['slave_session']) || !_singlesignon_validate_sid($_GET['slave_session'])) { |
if (empty($_GET['singlesignon_dest']) || is_array($_GET['singlesignon_dest']) || empty($_GET['slave_session']) || is_array($_GET['slave_session']) || !_singlesignon_validate_sid($_GET['slave_session'])) { |
| 120 |
echo t('Thank you for hacking.'); |
echo 'Invalid request.'; |
| 121 |
exit; |
exit; |
| 122 |
} |
} |
| 123 |
|
|
| 147 |
} |
} |
| 148 |
} |
} |
| 149 |
|
|
| 150 |
|
/** |
| 151 |
|
* @} End of "defgroup singlesignon_core". |
| 152 |
|
*/ |
| 153 |
|
|
| 154 |
// }}} |
/** |
| 155 |
// {{{ helper functions |
* @defgroup singlesignon_helpers Helper functions. |
| 156 |
|
* @{ |
| 157 |
|
*/ |
| 158 |
|
|
| 159 |
/** |
/** |
| 160 |
* Sets up the URL and goes to it |
* Sets up the URL and goes to it |
| 161 |
*/ |
*/ |
| 162 |
function _singlesignon_goto_url($master_url, $url) { |
function _singlesignon_goto_url($master_url, $url) { |
| 163 |
// url() only available if bootstrap has reached FULL. |
// url() only available if bootstrap has reached FULL. |
| 164 |
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); |
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); |
| 165 |
|
|
| 166 |
$query = 'slave_session='. session_id() .'&singlesignon_dest='. _singlesignon_get_dest(); |
$query = 'slave_session='. session_id() .'&singlesignon_dest='. _singlesignon_get_dest(); |
| 167 |
_singlesignon_goto($master_url . url($url, array('query' => $query))); |
_singlesignon_goto($master_url . url($url, $query)); |
| 168 |
} |
} |
| 169 |
|
|
| 170 |
/** |
/** |
| 171 |
* Gets the base url and fixess it up a bit |
* Gets the base url and fixess it up a bit |
| 172 |
*/ |
*/ |
| 173 |
function _singlesignon_base_url() { |
function _singlesignon_base_url() { |
| 174 |
global $base_url; |
return preg_replace('@^(https?://[^/]+).*@', '\\1', $GLOBALS['base_url']); |
|
|
|
|
$scheme_authority = preg_replace('@^(https?://[^/]+).*@', '\\1', $base_url); |
|
|
return ($scheme_authority); |
|
| 175 |
} |
} |
| 176 |
|
|
|
|
|
| 177 |
/** |
/** |
| 178 |
* Combines $base_url and request_uri() in a safe, portable way. |
* Combines $base_url and request_uri() in a safe, portable way. |
| 179 |
*/ |
*/ |
| 180 |
function _singlesignon_get_dest() { |
function _singlesignon_get_dest() { |
| 181 |
global $base_url; |
$scheme_authority = preg_replace('@^(https?://[^/]+).*@', '\\1', $GLOBALS['base_url']); |
|
|
|
|
$scheme_authority = preg_replace('@^(https?://[^/]+).*@', '\\1', $base_url); |
|
| 182 |
return rawurlencode($scheme_authority . request_uri()); |
return rawurlencode($scheme_authority . request_uri()); |
| 183 |
} |
} |
| 184 |
|
|
| 201 |
function _singlesignon_validate_sid($sid) { |
function _singlesignon_validate_sid($sid) { |
| 202 |
if (is_array($sid)) { |
if (is_array($sid)) { |
| 203 |
if (count($sid) > 100) { |
if (count($sid) > 100) { |
| 204 |
return false; |
return FALSE; |
| 205 |
} |
} |
| 206 |
foreach ($sid as $value) { |
foreach ($sid as $value) { |
| 207 |
if (!ereg('^[A-Za-z0-9]{1,100}$', $value)) { |
if (!ereg('^[A-Za-z0-9]{1,100}$', $value)) { |
| 208 |
return false; |
return FALSE; |
| 209 |
} |
} |
| 210 |
} |
} |
| 211 |
} |
} |
| 212 |
else { |
else { |
| 213 |
if (!ereg('^[A-Za-z0-9]{1,100}$', $sid)) { |
if (!ereg('^[A-Za-z0-9]{1,100}$', $sid)) { |
| 214 |
return false; |
return FALSE; |
| 215 |
} |
} |
| 216 |
} |
} |
| 217 |
return true; |
return TRUE; |
| 218 |
} |
} |
| 219 |
|
|
| 220 |
/** |
/** |
| 239 |
return $domain['scheme'] .'://'. $domain['subdomain']; |
return $domain['scheme'] .'://'. $domain['subdomain']; |
| 240 |
} |
} |
| 241 |
|
|
| 242 |
// }}} |
+ /** |
| 243 |
// {{{ other hook functions |
+ * @} End of "defgroup singlesignon_helpers". |
| 244 |
|
+ */ |
| 245 |
|
|
| 246 |
/** |
/** |
| 247 |
* Implementation of hook_menu(). |
* Implementation of hook_menu(). |
| 263 |
* Provides user interface necessary to administer this module's settings. |
* Provides user interface necessary to administer this module's settings. |
| 264 |
*/ |
*/ |
| 265 |
function singlesignon_admin_settings() { |
function singlesignon_admin_settings() { |
|
if (!user_access('access administration pages')) { |
|
|
return drupal_access_denied(); |
|
|
} |
|
|
|
|
| 266 |
$form = array(); |
$form = array(); |
| 267 |
$use_domain = variable_get('singlesignon_use_domain_module', 0); |
$use_domain = variable_get('singlesignon_use_domain_module', 0); |
| 268 |
if (module_exists('domain')) { |
if (module_exists('domain')) { |
| 290 |
'#collapsible' => TRUE, |
'#collapsible' => TRUE, |
| 291 |
'#collapsed' => TRUE, |
'#collapsed' => TRUE, |
| 292 |
'#tree' => TRUE, |
'#tree' => TRUE, |
| 293 |
'#description' => <<<EOT |
'#description' => t('Single sign-on does not play well with bots (ie. search engines). |
|
Single sign-on does not play well with bots (ie. search engines). |
|
| 294 |
The data below will hopefully help the single sign-on module to |
The data below will hopefully help the single sign-on module to |
| 295 |
recognise bots and let them through (ie. it plays nicely with the recognised bots). |
recognise bots and let them through (ie. it plays nicely with the recognised bots).'), |
|
EOT |
|
| 296 |
); |
); |
| 297 |
$form['singlesignon_bot_matches']['useragents_case'] = array( |
$form['singlesignon_bot_matches']['useragents_case'] = array( |
| 298 |
'#type' => 'textarea', |
'#type' => 'textarea', |
| 299 |
'#title' => 'Case-sensitive User Agents', |
'#title' => t('Case-sensitive User Agents'), |
| 300 |
'#rows' => 5, |
'#rows' => 5, |
| 301 |
'#cols' => 40, |
'#cols' => 40, |
| 302 |
'#default_value' => _singlesignon_get_bm_variable('useragents_case'), |
'#default_value' => _singlesignon_get_bm_variable('useragents_case'), |
| 304 |
); |
); |
| 305 |
$form['singlesignon_bot_matches']['useragents_nocase'] = array( |
$form['singlesignon_bot_matches']['useragents_nocase'] = array( |
| 306 |
'#type' => 'textarea', |
'#type' => 'textarea', |
| 307 |
'#title' => 'Case-insensitive User Agents', |
'#title' => t('Case-insensitive User Agents'), |
| 308 |
'#rows' => 5, |
'#rows' => 5, |
| 309 |
'#cols' => 40, |
'#cols' => 40, |
| 310 |
'#default_value' => _singlesignon_get_bm_variable('useragents_nocase'), |
'#default_value' => _singlesignon_get_bm_variable('useragents_nocase'), |
| 312 |
); |
); |
| 313 |
$form['singlesignon_bot_matches']['client_IP'] = array( |
$form['singlesignon_bot_matches']['client_IP'] = array( |
| 314 |
'#type' => 'textarea', |
'#type' => 'textarea', |
| 315 |
'#title' => 'Client IP', |
'#title' => t('Client IP'), |
| 316 |
'#rows' => 5, |
'#rows' => 5, |
| 317 |
'#cols' => 40, |
'#cols' => 40, |
| 318 |
'#default_value' => _singlesignon_get_bm_variable('client_IP'), |
'#default_value' => _singlesignon_get_bm_variable('client_IP'), |
| 320 |
); |
); |
| 321 |
$form['singlesignon_bot_matches']['target_url'] = array( |
$form['singlesignon_bot_matches']['target_url'] = array( |
| 322 |
'#type' => 'textarea', |
'#type' => 'textarea', |
| 323 |
'#title' => 'Target URL', |
'#title' => t('Target URL'), |
| 324 |
'#rows' => 5, |
'#rows' => 5, |
| 325 |
'#cols' => 40, |
'#cols' => 40, |
| 326 |
'#default_value' => _singlesignon_get_bm_variable('target_url'), |
'#default_value' => _singlesignon_get_bm_variable('target_url'), |
| 327 |
'#description' => t('A list of case-sensitive strings that might match a referrer. <b>Not recommended</b> (in general; we have a few specific cases here)'), |
'#description' => t('A list of case-sensitive strings that might match a referrer. <b>Not recommended</b> (in general; we have a few specific cases here)'), |
| 328 |
); |
); |
|
|
|
| 329 |
return system_settings_form($form); |
return system_settings_form($form); |
| 330 |
} |
} |
| 331 |
|
|
| 332 |
/** |
/** |
| 333 |
* Internal function for use of singlesignon_admin_settings; turns | separated string into \n separated string. |
* Internal function for use of singlesignon_admin_settings; turns | separated string into \n separated string. |
| 334 |
* |
* |
| 335 |
* @param $variable: The short name of the singlesignon bot matching variable |
* @param $variable |
| 336 |
* @param $text: The default text for the variable |
* The short name of the singlesignon bot matching variable |
| 337 |
|
* @param $text |
| 338 |
|
* The default text for the variable |
| 339 |
*/ |
*/ |
| 340 |
function _singlesignon_get_bm_variable($variable) { |
function _singlesignon_get_bm_variable($variable) { |
|
global $_singlesignon_bot_matches; |
|
|
|
|
| 341 |
return (preg_replace( |
return (preg_replace( |
| 342 |
array("/^\/(.*?)\/i?$/", "/\|/"), |
array("/^\/(.*?)\/i?$/", "/\|/"), |
| 343 |
array("$1", "\n"), |
array("$1", "\n"), |
| 344 |
$_singlesignon_bot_matches[$variable] |
$GLOBALS['_singlesignon_bot_matches'][$variable] |
| 345 |
)); |
)); |
| 346 |
} |
} |
| 347 |
|
|
| 349 |
* Hook for validating a form; verifies the values for singlesignon bot recognition. |
* Hook for validating a form; verifies the values for singlesignon bot recognition. |
| 350 |
*/ |
*/ |
| 351 |
function singlesignon_admin_settings_validate($form, &$form_state) { |
function singlesignon_admin_settings_validate($form, &$form_state) { |
| 352 |
|
$s = array(); |
| 353 |
$s['useragents_case'] = _singlesignon_verify_value($form_state['values'], 'useragents_case'); |
$s['useragents_case'] = _singlesignon_verify_value($form_state['values'], 'useragents_case'); |
| 354 |
$s['useragents_nocase'] = _singlesignon_verify_value($form_state['values'], 'useragents_nocase', '', 'i'); |
$s['useragents_nocase'] = _singlesignon_verify_value($form_state['values'], 'useragents_nocase', '', 'i'); |
| 355 |
$s['client_IP'] = _singlesignon_verify_value($form_state['values'], 'client_IP', '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'); |
$s['client_IP'] = _singlesignon_verify_value($form_state['values'], 'client_IP', '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'); |
| 356 |
$s['target_url'] = _singlesignon_verify_value($form_state['values'], 'target_url', '\\\\\/[A-Za-z0-9_\.\*\/\\\\-]*\$$'); |
$s['target_url'] = _singlesignon_verify_value($form_state['values'], 'target_url', '\\\\\/[A-Za-z0-9_\.\*\/\\\\-]*\$$'); |
| 357 |
|
|
| 358 |
$form_state['values']['singlesignon_bot_matches'] = $s; |
$form_state['values']['singlesignon_bot_matches'] = $s; |
| 359 |
} |
} |
| 361 |
/** |
/** |
| 362 |
* Internal function: Verifies one singlesignon bot recognition value. |
* Internal function: Verifies one singlesignon bot recognition value. |
| 363 |
* |
* |
| 364 |
* @param $form_values: The values we're validating |
* @param $form_values: |
| 365 |
* @param $value: The name of the value we're validation |
* The values we're validating |
| 366 |
* @param $allowed: A regex specifying what values are allowed |
* @param $value: |
| 367 |
* @param $extras: The regex parameters (ie. 'i' is case insensitive) |
* The name of the value we're validation |
| 368 |
|
* @param $allowed: |
| 369 |
|
* A regex specifying what values are allowed |
| 370 |
|
* @param $extras: |
| 371 |
|
* The regex parameters (ie. 'i' is case insensitive) |
| 372 |
*/ |
*/ |
| 373 |
function _singlesignon_verify_value($form_values, $value, $allowed = '', $extras = '') { |
function _singlesignon_verify_value($form_values, $value, $allowed = '', $extras = '') { |
| 374 |
if ($allowed == '') { |
if ($allowed == '') { |
| 382 |
$rvals[] = $val; |
$rvals[] = $val; |
| 383 |
} |
} |
| 384 |
else { |
else { |
| 385 |
form_set_error('', t("The strings in $value contain non-word characters (we allow $allowed at the moment, and '". $val ."' is a problem)")); |
form_set_error('', t("The strings in @value contain non-word characters (we allow @allowed at the moment, and '@wrong-value' is a problem)", array('@value' => $value, '@allowed' => $allowed, '@wrong-value' => $val))); |
| 386 |
} |
} |
| 387 |
} |
} |
| 388 |
return ('/'. join('|', $rvals) ."/$extras"); |
return ('/'. join('|', $rvals) ."/$extras"); |
| 390 |
return ($form_values['singlesignon_bot_matches'][$value]); |
return ($form_values['singlesignon_bot_matches'][$value]); |
| 391 |
} |
} |
| 392 |
|
|
|
// }}} |
|
| 393 |
|
|