session_limit.module 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. <?php
  2. /**
  3. * @file
  4. * Limits multiple sessions per user.
  5. */
  6. /**
  7. * Do nothing if the session limit is exceeded.
  8. */
  9. define('SESSION_LIMIT_DO_NOTHING', 0);
  10. /**
  11. * Automatically drop sessions that would exceed the limit.
  12. */
  13. define('SESSION_LIMIT_DROP', 1);
  14. /**
  15. * Disallow sessions that would exceed the limit.
  16. */
  17. define('SESSION_LIMIT_DISALLOW_NEW', 2);
  18. /**
  19. * Default drupal_set_message type for logout message.
  20. */
  21. define('SESSION_LIMIT_LOGGED_OUT_MESSAGE_SEVERITY', 'error');
  22. /**
  23. * Default message displayed when a user is logged out.
  24. */
  25. define('SESSION_LIMIT_LOGGED_OUT_MESSAGE', 'You have been automatically logged out. Someone else has logged in with your username and password and the maximum number of @number simultaneous sessions was exceeded. This may indicate that your account has been compromised or that account sharing is not allowed on this site. Please contact the site administrator if you suspect your account has been compromised.');
  26. /**
  27. * Default message displayed for a user on a new machine when sesison limit has been reached.
  28. */
  29. define('SESSION_LIMIT_HIT_MESSAGE', 'The maximum number of simultaneous sessions (@number) for your account has been reached. You did not log off from a previous session or someone else is logged on to your account. This may indicate that your account has been compromised or that account sharing is limited on this site. Please contact the site administrator if you suspect your account has been compromised.');
  30. /**
  31. * Implements hook_help().
  32. */
  33. function session_limit_help($path, $args) {
  34. switch ($path) {
  35. case 'admin/help#session_limit':
  36. $output = '<p>' . t("The two major notice messages to users are passed through Drupal's t() function. This maintains locale support, allowing you to override strings in any language, but such text is also available to be changed through other modules like !stringoverridesurl.", array('!stringoverridesurl' => l('String Overrides', 'http://drupal.org/project/stringoverrides'))) . '</p>';
  37. $output .= '<p>' . t("The two major strings are as follows:") . '</p>';
  38. $output .= '<p><blockquote>';
  39. $output .= variable_get('session_limit_limit_hit_message', SESSION_LIMIT_HIT_MESSAGE);
  40. $output .= '</blockquote></p><p><blockquote>';
  41. $output .= 'You have been automatically logged out. Someone else has logged in with your username and password and the maximum number of @number simultaneous sessions was exceeded. This may indicate that your account has been compromised or that account sharing is not allowed on this site. Please contact the site administrator if you suspect your account has been compromised.';
  42. $output .= '</blockquote></p>';
  43. return $output;
  44. case 'session/limit':
  45. return t(variable_get('session_limit_limit_hit_message', SESSION_LIMIT_HIT_MESSAGE), array('@number' => session_limit_user_max_sessions()));
  46. }
  47. }
  48. /**
  49. * Implements hook_permission().
  50. */
  51. function session_limit_permission() {
  52. return array(
  53. 'administer session limits by role' => array(
  54. 'title' => t('Administer session limits by role'),
  55. 'description' => t(''),
  56. ),
  57. 'administer session limits per user' => array(
  58. 'title' => t('Administer session limits by user'),
  59. 'description' => t(''),
  60. ),
  61. );
  62. }
  63. /**
  64. * Implements hook_menu().
  65. */
  66. function session_limit_menu() {
  67. $items['session/limit'] = array(
  68. 'title' => 'Session limit exceeded',
  69. 'page callback' => 'drupal_get_form',
  70. 'page arguments' => array('session_limit_page'),
  71. 'access callback' => 'user_is_logged_in',
  72. 'type' => MENU_CALLBACK,
  73. );
  74. $items['mysessions'] = array(
  75. 'title' => 'My sessions',
  76. 'page callback' => 'drupal_get_form',
  77. 'page arguments' => array('session_limit_page'),
  78. 'access callback' => 'user_is_logged_in',
  79. 'type' => MENU_SUGGESTED_ITEM,
  80. );
  81. $items['admin/config/people/session-limit'] = array(
  82. 'title' => 'Session limit',
  83. 'description' => 'Configure session limits.',
  84. 'page callback' => 'drupal_get_form',
  85. 'page arguments' => array('session_limit_settings'),
  86. 'access arguments' => array('administer site configuration'),
  87. 'type' => MENU_NORMAL_ITEM,
  88. );
  89. $items['admin/config/people/session-limit/defaults'] = array(
  90. 'title' => 'Defaults',
  91. 'page callback' => 'drupal_get_form',
  92. 'page arguments' => array('session_limit_settings'),
  93. 'access arguments' => array('administer site configuration'),
  94. 'type' => MENU_DEFAULT_LOCAL_TASK,
  95. );
  96. $items['admin/config/people/session-limit/roles'] = array(
  97. 'title' => 'Role limits',
  98. 'description' => 'Configure session limits by role.',
  99. 'page callback' => 'drupal_get_form',
  100. 'page arguments' => array('session_limit_settings_byrole'),
  101. 'access arguments' => array('administer session limits by role'),
  102. 'type' => MENU_LOCAL_TASK,
  103. );
  104. $items['user/%user/session-limit'] = array(
  105. 'title' => 'Session limit',
  106. 'description' => 'Configure session limit for one user.',
  107. 'page callback' => 'drupal_get_form',
  108. 'page arguments' => array('session_limit_user_settings', 1),
  109. 'access arguments' => array('administer session limits per user'),
  110. 'type' => MENU_LOCAL_TASK,
  111. );
  112. return $items;
  113. }
  114. function session_limit_settings() {
  115. $form['session_limit_max'] = array(
  116. '#type' => 'textfield',
  117. '#title' => t('Default maximum number of active sessions'),
  118. '#default_value' => variable_get('session_limit_max', 1),
  119. '#size' => 2,
  120. '#maxlength' => 3,
  121. '#description' => t('The maximum number of active sessions a user can have. 0 implies unlimited sessions.'),
  122. );
  123. $form['session_limit_include_root_user'] = array(
  124. '#type' => 'checkbox',
  125. '#title' => t('Apply limit to root admin user.'),
  126. '#description' => t('By default session limits do not apply to user #1'),
  127. '#default_value' => variable_get('session_limit_include_root_user', FALSE),
  128. );
  129. $limit_behaviours = array(
  130. SESSION_LIMIT_DO_NOTHING => t('Do nothing.'),
  131. SESSION_LIMIT_DROP => t('Automatically drop the oldest sessions without prompting.'),
  132. SESSION_LIMIT_DISALLOW_NEW => t('Prevent new session.'),
  133. );
  134. $form['session_limit_behaviour'] = array(
  135. '#type' => 'radios',
  136. '#title' => t('When the session limit is exceeded'),
  137. '#default_value' => variable_get('session_limit_behaviour', SESSION_LIMIT_DO_NOTHING),
  138. '#options' => $limit_behaviours,
  139. );
  140. if (module_exists('masquerade')) {
  141. $form['session_limit_masquerade_ignore'] = array(
  142. '#type' => 'checkbox',
  143. '#title' => t('Ignore masqueraded sessions.'),
  144. '#description' => t("When a user administrator uses the masquerade module to impersonate a different user, it won't count against the session limit counter"),
  145. '#default_value' => variable_get('session_limit_masquerade_ignore', FALSE),
  146. );
  147. }
  148. $form['session_limit_limit_hit_message'] = array(
  149. '#type' => 'textarea',
  150. '#title' => t('Session limit has been hit message'),
  151. '#default_value' => variable_get('session_limit_limit_hit_message', SESSION_LIMIT_HIT_MESSAGE),
  152. '#description' => t('The message that is displayed to a user on the current workstation if the session limit has been reached.<br />
  153. @number is replaced with the maximum number of simultaneous sessions.'),
  154. );
  155. $form['session_limit_logged_out_message_severity'] = array(
  156. '#type' => 'select',
  157. '#title' => t('Logged out message severity'),
  158. '#default_value' => variable_get('session_limit_logged_out_message_severity', SESSION_LIMIT_LOGGED_OUT_MESSAGE_SEVERITY),
  159. '#options' => array(
  160. 'error' => t('Error'),
  161. 'warning' => t('Warning'),
  162. 'status' => t('Status'),
  163. '_none' => t('No Message'),
  164. ),
  165. '#description' => t('The Drupal message type. Defaults to Error.'),
  166. );
  167. $form['session_limit_logged_out_message'] = array(
  168. '#type' => 'textarea',
  169. '#title' => t('Logged out message'),
  170. '#default_value' => variable_get('session_limit_logged_out_message', SESSION_LIMIT_LOGGED_OUT_MESSAGE),
  171. '#description' => t('The message that is displayed to a user if the workstation has been logged out.<br />
  172. @number is replaced with the maximum number of simultaneous sessions.'),
  173. );
  174. return system_settings_form($form);
  175. }
  176. /**
  177. * Settings validation form.
  178. */
  179. function session_limit_settings_validate($form, &$form_state) {
  180. $maxsessions = $form_state['values']['session_limit_max'];
  181. if (!is_numeric($maxsessions)) {
  182. form_set_error('session_limit_max', t('You must enter a number for the maximum number of active sessions'));
  183. }
  184. elseif ($maxsessions < 0) {
  185. form_set_error('session_limit_max', t('Maximum number of active sessions must be positive'));
  186. }
  187. }
  188. /**
  189. * Settings by role form.
  190. */
  191. function session_limit_settings_byrole() {
  192. $result = db_select('variable', 'v')
  193. ->fields('v', array('name', 'value'))
  194. ->condition('name', 'session_limit_rid_%', 'LIKE')
  195. ->orderBy('name')
  196. ->execute();
  197. foreach ($result as $setting) {
  198. $role_limits[$setting->name] = unserialize($setting->value);
  199. }
  200. $roles = user_roles(TRUE);
  201. foreach ($roles as $rid => $role) {
  202. $form["session_limit_rid_$rid"] = array(
  203. '#type' => 'select',
  204. '#options' => _session_limit_user_options(),
  205. '#title' => check_plain($role),
  206. '#default_value' => empty($role_limits["session_limit_rid_$rid"]) ? 0 : $role_limits["session_limit_rid_$rid"],
  207. );
  208. }
  209. $form['submit'] = array(
  210. '#type' => 'submit',
  211. '#value' => t('Save permissions'),
  212. );
  213. return $form;
  214. }
  215. /**
  216. * Set session limits by role form submission.
  217. */
  218. function session_limit_settings_byrole_submit($form, &$form_state) {
  219. db_delete('variable')
  220. ->condition('name', 'session_limit_rid_%', 'LIKE')
  221. ->execute();
  222. foreach ($form_state['values'] as $setting_name => $setting_limit) {
  223. variable_set($setting_name, $setting_limit);
  224. }
  225. drupal_set_message(t('Role settings updated.'), 'status');
  226. watchdog('session_limit', 'Role limits updated.', array(), WATCHDOG_INFO);
  227. }
  228. /**
  229. * Implements hook_init().
  230. *
  231. * Determine whether session has been verified. Redirect user if over session
  232. * limit. Established Sessions do NOT need to verify every page load. The new
  233. * session must deal w/ determining which connection is cut.
  234. *
  235. * This intentionally doesn't use hook_user()'s login feature because that's
  236. * only really useful if the login event always boots off at least one other
  237. * active session. Doing it this way makes sure that the newest session can't
  238. * browse to a different page after their login has validated.
  239. */
  240. function session_limit_init() {
  241. global $user;
  242. $user_match = variable_get('session_limit_include_root_user', FALSE) ? 0 : 1;
  243. if ($user->uid > $user_match && !isset($_SESSION['session_limit'])) {
  244. if (_session_limit_bypass()) {
  245. // Bypass the session limitation on this page callback.
  246. return;
  247. }
  248. $query = db_select('sessions', 's')
  249. // Use distict so that HTTP and HTTPS sessions
  250. // are considered a single session.
  251. ->distinct()
  252. ->fields('s', array('sid'))
  253. ->condition('s.uid', $user->uid);
  254. if (module_exists('masquerade') && variable_get('session_limit_masquerade_ignore', FALSE)) {
  255. $query->leftJoin('masquerade', 'm', 's.uid = m.uid_as AND s.sid = m.sid');
  256. $query->isNull('m.sid');
  257. }
  258. $active_sessions = $query->countQuery()->execute()->fetchField();
  259. $max_sessions = session_limit_user_max_sessions();
  260. if (!empty($max_sessions) && $active_sessions > $max_sessions) {
  261. session_limit_invoke_session_limit(session_id(), 'collision');
  262. }
  263. else {
  264. // force checking this twice as there's a race condition around session creation.
  265. // see issue #1176412
  266. if (!isset($_SESSION['session_limit_checkonce'])) {
  267. $_SESSION['session_limit_checkonce'] = TRUE;
  268. }
  269. else {
  270. // mark session as verified to bypass this in future.
  271. $_SESSION['session_limit'] = TRUE;
  272. }
  273. }
  274. }
  275. }
  276. /**
  277. * Implements hook_action_info_alter().
  278. */
  279. function session_limit_action_info_alter(&$info) {
  280. if (module_exists('token_actions')) {
  281. foreach ($info as $type => $data) {
  282. if (stripos($type, "token_actions_") === 0 || stripos($type, "system_") === 0) {
  283. if (isset($info[$type]['hooks']['session_limit'])) {
  284. array_merge($info[$type]['hooks']['session_limit'], array('collision', 'disconnect'));
  285. }
  286. else {
  287. $info[$type]['hooks']['session_limit'] = array('collision', 'disconnect');
  288. }
  289. }
  290. }
  291. }
  292. }
  293. /**
  294. * Implements hook_field_extra_fields().
  295. */
  296. function session_limit_field_extra_fields() {
  297. $extra['user']['user']['display'] = array(
  298. 'session_limit' => array(
  299. 'label' => t('Session limit'),
  300. 'description' => t('Session limit.'),
  301. 'weight' => 10,
  302. ),
  303. );
  304. return $extra;
  305. }
  306. /**
  307. * Implements hook_user_view().
  308. */
  309. function session_limit_user_view($account, $view_mode) {
  310. if (user_access('administer session limits per user')) {
  311. $account->content['session_limit'] = array(
  312. '#title' => t('Session limit'),
  313. '#type' => 'user_profile_category',
  314. 'session_limit' => array(
  315. '#markup' => empty($account->data['session_limit']) ? t('Default') : $account->data['session_limit'],
  316. )
  317. );
  318. }
  319. }
  320. /**
  321. * Session limit user settings form.
  322. */
  323. function session_limit_user_settings($form, $form_state, $account) {
  324. $form['account'] = array(
  325. '#type' => 'value',
  326. '#value' => $account,
  327. );
  328. $form['session_limit'] = array(
  329. '#type' => 'select',
  330. '#title' => t('Maximum sessions'),
  331. '#description' => t('Total number simultaneous active sessions this user may have at one time. The default defers to the limits that apply to each of the user\'s roles.'),
  332. '#required' => FALSE,
  333. '#default_value' => empty($account->data['session_limit']) ? 0 : $account->data['session_limit'],
  334. '#options' => _session_limit_user_options(),
  335. );
  336. if ($account->uid == 1) {
  337. $form['session_limit']['#states'] = array(
  338. 'enabled' => array(
  339. ':input[name="session_limit_include_root_user"]' => array('checked' => TRUE),
  340. ),
  341. );
  342. $form['session_limit_include_root_user'] = array(
  343. '#type' => 'checkbox',
  344. '#title' => t('Apply limit to root admin user.'),
  345. '#description' => t('By default session limits do not apply to user #1'),
  346. '#default_value' => variable_get('session_limit_include_root_user', FALSE),
  347. );
  348. }
  349. $form['submit'] = array(
  350. '#type' => 'submit',
  351. '#value' => t('Save configuration'),
  352. );
  353. return $form;
  354. }
  355. /**
  356. * Session limit user settings form validation.
  357. */
  358. function session_limit_user_settings_validate($form, &$form_state) {
  359. if (!is_numeric($form_state['values']['session_limit'])) {
  360. form_set_error('session_limit', t('Only numeric submissions are valid.'));
  361. watchdog('session_limit', 'Invalid session limit submission for @user.', array('@user' => $form_state['values']['account']->name), WATCHDOG_DEBUG);
  362. }
  363. }
  364. /**
  365. * Session limit user settings form submission.
  366. */
  367. function session_limit_user_settings_submit($form, &$form_state) {
  368. watchdog('session_limit', 'Maximum sessions for @user updated to @count.', array('@user' => $form_state['values']['account']->name, '@count' => $form_state['values']['session_limit']), WATCHDOG_INFO, l(t('view'), "user/{$form_state['values']['account']->uid}"));
  369. if (empty($form_state['values']['session_limit'])) {
  370. $form_state['values']['session_limit'] = NULL;
  371. }
  372. if ($form_state['values']['account']->uid == 1) {
  373. variable_set('session_limit_include_root_user', !empty($form_state['values']['session_limit_include_root_user']));
  374. }
  375. user_save($form_state['values']['account'], array('data' => array('session_limit' => $form_state['values']['session_limit'])));
  376. drupal_set_message(t('Session limit updated for %user.', array('%user' => $form_state['values']['account']->name)), 'status', TRUE);
  377. }
  378. /**
  379. * Display or delete sessions form.
  380. */
  381. function session_limit_page() {
  382. global $user;
  383. $form = array();
  384. if (variable_get('session_limit_behaviour', SESSION_LIMIT_DO_NOTHING) == SESSION_LIMIT_DISALLOW_NEW) {
  385. session_destroy();
  386. $user = drupal_anonymous_user();
  387. return;
  388. }
  389. $result = db_query('SELECT * FROM {sessions} WHERE uid = :uid', array(':uid' => $user->uid));
  390. $active_sessions = array();
  391. $session_references = array();
  392. foreach ($result as $session_reference => $obj) {
  393. $active_sessions[$session_reference] = $obj->sid;
  394. $message = $user->sid == $obj->sid ? t('Your current session.') : '';
  395. $session_references[$session_reference] = t('<strong>Host:</strong> %host (idle: %time) <b>@message</b>',
  396. array(
  397. '%host' => $obj->hostname,
  398. '@message' => $message,
  399. '%time' => format_interval(time() - $obj->timestamp))
  400. );
  401. }
  402. $form['active_sessions'] = array(
  403. '#type' => 'value',
  404. '#value' => $active_sessions,
  405. );
  406. $form['session_reference'] = array(
  407. '#type' => 'radios',
  408. '#title' => t('Select a session to disconnect.'),
  409. '#options' => $session_references,
  410. );
  411. $form['submit'] = array(
  412. '#type' => 'submit',
  413. '#value' => t('Disconnect session'),
  414. );
  415. return $form;
  416. }
  417. /**
  418. * Handler for submissions from session_limit_page().
  419. */
  420. function session_limit_page_submit($form, &$form_state) {
  421. global $user;
  422. $session_reference = $form_state['values']['session_reference'];
  423. $sid = $form['active_sessions']['#value'][$session_reference];
  424. if ($user->sid == $sid) {
  425. drupal_goto('user/logout');
  426. }
  427. else {
  428. session_limit_invoke_session_limit($sid, 'disconnect');
  429. drupal_goto();
  430. }
  431. }
  432. /**
  433. * Helper function for populating the values of the settings form select.
  434. */
  435. function _session_limit_user_options() {
  436. $options = drupal_map_assoc(range(0, 250));
  437. $options[0] = t('Default');
  438. $options['999999'] = t('Disabled');
  439. return $options;
  440. }
  441. /**
  442. * Get the maximum number of sessions for a user.
  443. *
  444. * @param user $account
  445. * (optional) The user account to check. If not
  446. * supplied the active user account is used.
  447. */
  448. function session_limit_user_max_sessions($account = NULL) {
  449. $limits = &drupal_static(__FUNCTION__, array());
  450. if (empty($account)) {
  451. $account = $GLOBALS['user'];
  452. }
  453. if (!isset($limits[$account->uid])) {
  454. $limits[$account->uid] = (int) variable_get('session_limit_max', 1);
  455. $limit_account = session_limit_user_max_sessions_byuser($account);
  456. $limit_role = session_limit_user_max_sessions_byrole($account);
  457. if ($limit_account > 0) {
  458. $limits[$account->uid] = $limit_account;
  459. }
  460. elseif ($limit_role > 0) {
  461. $limits[$account->uid] = $limit_role;
  462. }
  463. $limits[$account->uid] = (int) $limits[$account->uid];
  464. }
  465. return $limits[$account->uid];
  466. }
  467. /**
  468. * Get user specified session limit.
  469. *
  470. * @param user $account
  471. * The user account to get the session limit for
  472. *
  473. * @return int
  474. * Maximum number of sessions.
  475. * A value of 0 means that no user limit is set for the current user
  476. * and so the role limit should be used (or default if no role limit either).
  477. */
  478. function session_limit_user_max_sessions_byuser($account) {
  479. return (int) empty($account->data['session_limit']) ? 0 : $account->data['session_limit'];
  480. }
  481. /**
  482. * Get the maximum number of sessions allowed by the roles of an account.
  483. *
  484. * @param user $account
  485. * The account to check the roles of.
  486. *
  487. * @return int
  488. * The maximum number of sessions the user is allowed by their roles.
  489. * A value of 0 means that no role limit exists for this user and so
  490. * the default should be used.
  491. */
  492. function session_limit_user_max_sessions_byrole($account) {
  493. $limits = array();
  494. foreach ($account->roles as $rid => $name) {
  495. $role_limit = variable_get("session_limit_rid_$rid", FALSE);
  496. if (!empty($role_limit)) {
  497. $limits[] = (int) $role_limit;
  498. }
  499. }
  500. return empty($limits) ? 0 : max($limits);
  501. }
  502. /**
  503. * Implements hook_trigger_info().
  504. */
  505. function session_limit_trigger_info() {
  506. return array(
  507. 'session_limit' => array(
  508. 'session_limit_collision' => array(
  509. 'label' => t('User logs in and has too many active sessions'),
  510. ),
  511. 'session_limit_disconnect' => array(
  512. 'label' => t('When an active user is logged out by a newer session'),
  513. ),
  514. ),
  515. );
  516. }
  517. /**
  518. * Limit a users access to the sites based on the current session.
  519. *
  520. * @param string $session
  521. * The session id string which identifies the current session.
  522. * @param string $op
  523. * The action which caused the session limitation event. This is
  524. * either 'collision' or 'disconnect'.
  525. *
  526. * @return array
  527. * The results of all hook_session_limit functions.
  528. * Note that in a collision event, a Drupal goto is executed so
  529. * this function does not return.
  530. */
  531. function session_limit_invoke_session_limit($session, $op) {
  532. $return = array();
  533. // Execute the hook_session_limit().
  534. foreach (module_implements('session_limit') as $name) {
  535. $function = $name . '_session_limit';
  536. $result = $function($session, $op);
  537. if (isset($result) && is_array($result)) {
  538. $return = array_merge($return, $result);
  539. }
  540. elseif (isset($result)) {
  541. $return[] = $result;
  542. }
  543. }
  544. // In the event of a collision, redirect to session handler.
  545. if ($op == 'collision') {
  546. if (variable_get('session_limit_behaviour', SESSION_LIMIT_DO_NOTHING) == SESSION_LIMIT_DROP) {
  547. global $user;
  548. // Get the number of sessions that should be removed.
  549. $limit = db_query("SELECT COUNT(DISTINCT(sid)) - :max_sessions FROM {sessions} WHERE uid = :uid", array(
  550. ':max_sessions' => session_limit_user_max_sessions($user),
  551. ':uid' => $user->uid,
  552. ))->fetchField();
  553. if ($limit > 0) {
  554. // Secure session ids are seperate rows in the database, but we don't want to kick
  555. // the user off there http session and not there https session or vice versa. This
  556. // is why this query is DISTINCT.
  557. $result = db_select('sessions', 's')
  558. ->distinct()
  559. ->fields('s', array('sid'))
  560. ->condition('s.uid', $user->uid)
  561. ->orderBy('timestamp', 'ASC')
  562. ->range(0, $limit)
  563. ->execute();
  564. foreach ($result as $session) {
  565. session_limit_invoke_session_limit($session->sid, 'disconnect');
  566. }
  567. }
  568. }
  569. else {
  570. // Otherwise re-direct to the session handler page so the user can
  571. // choose which action they would like to take.
  572. drupal_goto('session/limit');
  573. }
  574. }
  575. return $return;
  576. }
  577. /**
  578. * Implements hook_session_limit().
  579. */
  580. function session_limit_session_limit($sid, $op) {
  581. switch ($op) {
  582. case 'collision':
  583. watchdog('session_limit', 'Exceeded maximum allowed active sessions.', array(), WATCHDOG_INFO);
  584. break;
  585. case 'disconnect':
  586. $message = variable_get('session_limit_logged_out_message', SESSION_LIMIT_LOGGED_OUT_MESSAGE);
  587. $message_severity = variable_get('session_limit_logged_out_message_severity', SESSION_LIMIT_LOGGED_OUT_MESSAGE_SEVERITY);
  588. $fields['session'] = '';
  589. if ($message_severity != '_none' && !empty($message)) {
  590. $logout_message = t($message, array('@number' => session_limit_user_max_sessions()));
  591. $logout_message = 'messages|' . serialize(array($message_severity => array($logout_message)));
  592. $fields['session'] = $logout_message;
  593. }
  594. $fields['uid'] = 0;
  595. db_update('sessions')
  596. ->fields($fields)
  597. ->condition('sid', $sid)
  598. ->execute();
  599. watchdog('session_limit', 'Disconnected for excess active sessions.', array(), WATCHDOG_NOTICE);
  600. break;
  601. }
  602. }
  603. /**
  604. * Implements hook_session_limit().
  605. *
  606. * This implements the hook on behalf of the trigger module.
  607. */
  608. function trigger_session_limit($sid, $op) {
  609. // Find all the actions against this $op.
  610. // Note: this notation requires the $op to match the bit in the trigger info keys after session_limit!
  611. $aids = trigger_get_assigned_actions('session_limit_' . $op);
  612. $context = array(
  613. 'hook' => 'session_limit',
  614. 'op' => $op,
  615. 'sid' => $sid,
  616. );
  617. actions_do(array_keys($aids), $GLOBALS['user'], $context);
  618. }
  619. /**
  620. * Implements hook_session_limit().
  621. *
  622. * This implements the hook on behalf of the rules module.
  623. */
  624. function rules_session_limit($sid, $op) {
  625. global $user;
  626. rules_invoke_event('session_limit_' . $op, $user, $sid);
  627. }
  628. /**
  629. * Implements hook_session_limit_bypass().
  630. *
  631. * @return bool
  632. * TRUE if the page request should bypass session limitation restrictions.
  633. */
  634. function session_limit_session_limit_bypass() {
  635. if ((arg(0) == 'session' && arg(1) == 'limit') || (arg(0) == 'user' && arg(1) == 'logout')) {
  636. return TRUE;
  637. }
  638. }
  639. /**
  640. * Execute the session limit bypass hook.
  641. *
  642. * Allow other modules to prevent session limits in their own requirements.
  643. *
  644. * @return bool
  645. * TRUE if session limitation should be bypassed.
  646. */
  647. function _session_limit_bypass() {
  648. foreach (module_invoke_all('session_limit_bypass') as $bypass) {
  649. if (!empty($bypass)) {
  650. return TRUE;
  651. }
  652. }
  653. return FALSE;
  654. }
  655. /**
  656. * Implements hook_variable_group_info().
  657. */
  658. function session_limit_variable_group_info() {
  659. $groups['session_limit'] = array(
  660. 'title' => t('Session limit'),
  661. 'access' => 'administer site configuration',
  662. 'path' => array('admin/config/people/session_limit/defaults'),
  663. );
  664. return $groups;
  665. }
  666. /**
  667. * Implements hook_variable_info().
  668. */
  669. function session_limit_variable_info($options) {
  670. $variables['session_limit_logged_out_message'] = array(
  671. 'title' => t('Logged out message', array(), $options),
  672. 'description' => t('The message that is displayed to a user if the workstation has been logged out.<br />
  673. @number is replaced with the maximum number of simultaneous sessions.', array(), $options),
  674. 'type' => 'string',
  675. 'localize' => TRUE,
  676. 'group' => 'session_limit',
  677. );
  678. return $variables;
  679. }