session.inc 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <?php
  2. /**
  3. * @file
  4. * User session handling functions.
  5. *
  6. * The user-level session storage handlers:
  7. * - _drupal_session_open()
  8. * - _drupal_session_close()
  9. * - _drupal_session_read()
  10. * - _drupal_session_write()
  11. * - _drupal_session_destroy()
  12. * - _drupal_session_garbage_collection()
  13. * are assigned by session_set_save_handler() in bootstrap.inc and are called
  14. * automatically by PHP. These functions should not be called directly. Session
  15. * data should instead be accessed via the $_SESSION superglobal.
  16. */
  17. /**
  18. * Session handler assigned by session_set_save_handler().
  19. *
  20. * This function is used to handle any initialization, such as file paths or
  21. * database connections, that is needed before accessing session data. Drupal
  22. * does not need to initialize anything in this function.
  23. *
  24. * This function should not be called directly.
  25. *
  26. * @return
  27. * This function will always return TRUE.
  28. */
  29. function _drupal_session_open() {
  30. return TRUE;
  31. }
  32. /**
  33. * Session handler assigned by session_set_save_handler().
  34. *
  35. * This function is used to close the current session. Because Drupal stores
  36. * session data in the database immediately on write, this function does
  37. * not need to do anything.
  38. *
  39. * This function should not be called directly.
  40. *
  41. * @return
  42. * This function will always return TRUE.
  43. */
  44. function _drupal_session_close() {
  45. return TRUE;
  46. }
  47. /**
  48. * Reads an entire session from the database (internal use only).
  49. *
  50. * Also initializes the $user object for the user associated with the session.
  51. * This function is registered with session_set_save_handler() to support
  52. * database-backed sessions. It is called on every page load when PHP sets
  53. * up the $_SESSION superglobal.
  54. *
  55. * This function is an internal function and must not be called directly.
  56. * Doing so may result in logging out the current user, corrupting session data
  57. * or other unexpected behavior. Session data must always be accessed via the
  58. * $_SESSION superglobal.
  59. *
  60. * @param $sid
  61. * The session ID of the session to retrieve.
  62. *
  63. * @return
  64. * The user's session, or an empty string if no session exists.
  65. */
  66. function _drupal_session_read($sid) {
  67. global $user, $is_https;
  68. // Write and Close handlers are called after destructing objects
  69. // since PHP 5.0.5.
  70. // Thus destructors can use sessions but session handler can't use objects.
  71. // So we are moving session closure before destructing objects.
  72. drupal_register_shutdown_function('session_write_close');
  73. // Handle the case of first time visitors and clients that don't store
  74. // cookies (eg. web crawlers).
  75. $insecure_session_name = substr(session_name(), 1);
  76. if (empty($sid) || (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name]))) {
  77. $user = drupal_anonymous_user();
  78. return '';
  79. }
  80. // Otherwise, if the session is still active, we have a record of the
  81. // client's session in the database. If it's HTTPS then we are either have
  82. // a HTTPS session or we are about to log in so we check the sessions table
  83. // for an anonymous session with the non-HTTPS-only cookie. The session ID
  84. // that is in the user's cookie is hashed before being stored in the database
  85. // as a security measure. Thus, we have to hash it to match the database.
  86. if ($is_https) {
  87. $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => drupal_session_id($sid)))->fetchObject();
  88. if (!$user) {
  89. if (isset($_COOKIE[$insecure_session_name])) {
  90. $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
  91. ':sid' => drupal_session_id($_COOKIE[$insecure_session_name])))
  92. ->fetchObject();
  93. }
  94. }
  95. }
  96. else {
  97. $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => drupal_session_id($sid)))->fetchObject();
  98. }
  99. // We found the client's session record and they are an authenticated,
  100. // active user.
  101. if ($user && $user->uid > 0 && $user->status == 1) {
  102. // This is done to unserialize the data member of $user.
  103. $user->data = unserialize((string) $user->data);
  104. // Add roles element to $user.
  105. $user->roles = array();
  106. $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
  107. $user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1);
  108. }
  109. elseif ($user) {
  110. // The user is anonymous or blocked. Only preserve two fields from the
  111. // {sessions} table.
  112. $account = drupal_anonymous_user();
  113. $account->session = $user->session;
  114. $account->timestamp = $user->timestamp;
  115. $user = $account;
  116. }
  117. else {
  118. // The session has expired.
  119. $user = drupal_anonymous_user();
  120. $user->session = '';
  121. }
  122. // Store the session that was read for comparison in _drupal_session_write().
  123. $last_read = &drupal_static('drupal_session_last_read');
  124. $last_read = array(
  125. 'sid' => $sid,
  126. 'value' => $user->session,
  127. );
  128. return $user->session;
  129. }
  130. /**
  131. * Writes an entire session to the database (internal use only).
  132. *
  133. * This function is registered with session_set_save_handler() to support
  134. * database-backed sessions.
  135. *
  136. * This function is an internal function and must not be called directly.
  137. * Doing so may result in corrupted session data or other unexpected behavior.
  138. * Session data must always be accessed via the $_SESSION superglobal.
  139. *
  140. * @param $sid
  141. * The session ID of the session to write to.
  142. * @param $value
  143. * Session data to write as a serialized string.
  144. *
  145. * @return
  146. * Always returns TRUE.
  147. */
  148. function _drupal_session_write($sid, $value) {
  149. global $user, $is_https;
  150. // The exception handler is not active at this point, so we need to do it
  151. // manually.
  152. try {
  153. if (!drupal_save_session()) {
  154. // We don't have anything to do if we are not allowed to save the session.
  155. return TRUE;
  156. }
  157. // Check whether $_SESSION has been changed in this request.
  158. $last_read = &drupal_static('drupal_session_last_read');
  159. $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
  160. // For performance reasons, do not update the sessions table, unless
  161. // $_SESSION has changed or more than 180 has passed since the last update.
  162. if ($is_changed || !isset($user->timestamp) || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
  163. // Either ssid or sid or both will be added from $key below.
  164. $fields = array(
  165. 'uid' => $user->uid,
  166. 'cache' => isset($user->cache) ? $user->cache : 0,
  167. 'hostname' => ip_address(),
  168. 'session' => $value,
  169. 'timestamp' => REQUEST_TIME,
  170. );
  171. // Use the session ID as 'sid' and an empty string as 'ssid' by default.
  172. // _drupal_session_read() does not allow empty strings so that's a safe
  173. // default.
  174. $key = array('sid' => drupal_session_id($sid), 'ssid' => '');
  175. // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
  176. if ($is_https) {
  177. $key['ssid'] = drupal_session_id($sid);
  178. // The "secure pages" setting allows a site to simultaneously use both
  179. // secure and insecure session cookies. If enabled and both cookies are
  180. // presented then use both keys. The session ID from the cookie is
  181. // hashed before being stored in the database as a security measure.
  182. if (variable_get('https', FALSE)) {
  183. $insecure_session_name = substr(session_name(), 1);
  184. if (isset($_COOKIE[$insecure_session_name])) {
  185. $key['sid'] = drupal_session_id($_COOKIE[$insecure_session_name]);
  186. }
  187. }
  188. }
  189. elseif (variable_get('https', FALSE)) {
  190. unset($key['ssid']);
  191. }
  192. db_merge('sessions')
  193. ->key($key)
  194. ->fields($fields)
  195. ->execute();
  196. }
  197. // Likewise, do not update access time more than once per 180 seconds.
  198. if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
  199. db_update('users')
  200. ->fields(array(
  201. 'access' => REQUEST_TIME
  202. ))
  203. ->condition('uid', $user->uid)
  204. ->execute();
  205. }
  206. return TRUE;
  207. }
  208. catch (Exception $exception) {
  209. require_once DRUPAL_ROOT . '/includes/errors.inc';
  210. // If we are displaying errors, then do so with no possibility of a further
  211. // uncaught exception being thrown.
  212. if (error_displayable()) {
  213. print '<h1>Uncaught exception thrown in session handler.</h1>';
  214. print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
  215. }
  216. return FALSE;
  217. }
  218. }
  219. /**
  220. * Initializes the session handler, starting a session if needed.
  221. */
  222. function drupal_session_initialize() {
  223. global $user, $is_https;
  224. session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
  225. // We use !empty() in the following check to ensure that blank session IDs
  226. // are not valid.
  227. if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) {
  228. // If a session cookie exists, initialize the session. Otherwise the
  229. // session is only started on demand in drupal_session_commit(), making
  230. // anonymous users not use a session cookie unless something is stored in
  231. // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
  232. drupal_session_start();
  233. if (!empty($user->uid) || !empty($_SESSION)) {
  234. drupal_page_is_cacheable(FALSE);
  235. }
  236. }
  237. else {
  238. // Set a session identifier for this request. This is necessary because
  239. // we lazily start sessions at the end of this request, and some
  240. // processes (like drupal_get_token()) needs to know the future
  241. // session ID in advance.
  242. $GLOBALS['lazy_session'] = TRUE;
  243. $user = drupal_anonymous_user();
  244. // Less random sessions (which are much faster to generate) are used for
  245. // anonymous users than are generated in drupal_session_regenerate() when
  246. // a user becomes authenticated.
  247. session_id(drupal_random_key());
  248. if ($is_https && variable_get('https', FALSE)) {
  249. $insecure_session_name = substr(session_name(), 1);
  250. $session_id = drupal_random_key();
  251. $_COOKIE[$insecure_session_name] = $session_id;
  252. }
  253. }
  254. date_default_timezone_set(drupal_get_user_timezone());
  255. }
  256. /**
  257. * Starts a session forcefully, preserving already set session data.
  258. *
  259. * @ingroup php_wrappers
  260. */
  261. function drupal_session_start() {
  262. // Command line clients do not support cookies nor sessions.
  263. if (!drupal_session_started() && !drupal_is_cli()) {
  264. // Save current session data before starting it, as PHP will destroy it.
  265. $session_data = isset($_SESSION) ? $_SESSION : NULL;
  266. // Apply any overrides to the session cookie params.
  267. $params = $original_params = session_get_cookie_params();
  268. // PHP settings for samesite will be handled by _drupal_cookie_params().
  269. unset($params['samesite']);
  270. $params = _drupal_cookie_params($params);
  271. if ($params !== $original_params) {
  272. if (\PHP_VERSION_ID >= 70300) {
  273. session_set_cookie_params($params);
  274. }
  275. else {
  276. session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  277. }
  278. }
  279. session_start();
  280. drupal_session_started(TRUE);
  281. // Restore session data.
  282. if (!empty($session_data)) {
  283. $_SESSION += $session_data;
  284. }
  285. }
  286. }
  287. /**
  288. * Commits the current session, if necessary.
  289. *
  290. * If an anonymous user already have an empty session, destroy it.
  291. */
  292. function drupal_session_commit() {
  293. global $user, $is_https;
  294. if (!drupal_save_session()) {
  295. // We don't have anything to do if we are not allowed to save the session.
  296. return;
  297. }
  298. if (empty($user->uid) && empty($_SESSION)) {
  299. // There is no session data to store, destroy the session if it was
  300. // previously started.
  301. if (drupal_session_started()) {
  302. session_destroy();
  303. }
  304. }
  305. else {
  306. // There is session data to store. Start the session if it is not already
  307. // started.
  308. if (!drupal_session_started()) {
  309. drupal_session_start();
  310. if ($is_https && variable_get('https', FALSE)) {
  311. $insecure_session_name = substr(session_name(), 1);
  312. $params = session_get_cookie_params();
  313. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  314. $options = array(
  315. 'expires' => $expire,
  316. 'path' => $params['path'],
  317. 'domain' => $params['domain'],
  318. 'secure' => FALSE,
  319. 'httponly' => $params['httponly'],
  320. );
  321. drupal_setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $options);
  322. }
  323. }
  324. // Write the session data.
  325. session_write_close();
  326. }
  327. }
  328. /**
  329. * Returns whether a session has been started.
  330. */
  331. function drupal_session_started($set = NULL) {
  332. static $session_started = FALSE;
  333. if (isset($set)) {
  334. $session_started = $set;
  335. }
  336. return $session_started && session_id();
  337. }
  338. /**
  339. * Called when an anonymous user becomes authenticated or vice-versa.
  340. *
  341. * @ingroup php_wrappers
  342. */
  343. function drupal_session_regenerate() {
  344. global $user, $is_https;
  345. // Nothing to do if we are not allowed to change the session.
  346. if (!drupal_save_session()) {
  347. return;
  348. }
  349. if ($is_https && variable_get('https', FALSE)) {
  350. $insecure_session_name = substr(session_name(), 1);
  351. if (!isset($GLOBALS['lazy_session']) && isset($_COOKIE[$insecure_session_name])) {
  352. $old_insecure_session_id = $_COOKIE[$insecure_session_name];
  353. }
  354. $params = session_get_cookie_params();
  355. $session_id = drupal_random_key();
  356. // If a session cookie lifetime is set, the session will expire
  357. // $params['lifetime'] seconds from the current request. If it is not set,
  358. // it will expire when the browser is closed.
  359. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  360. $options = array(
  361. 'expires' => $expire,
  362. 'path' => $params['path'],
  363. 'domain' => $params['domain'],
  364. 'secure' => FALSE,
  365. 'httponly' => $params['httponly'],
  366. );
  367. drupal_setcookie($insecure_session_name, $session_id, $options);
  368. $_COOKIE[$insecure_session_name] = $session_id;
  369. }
  370. if (drupal_session_started()) {
  371. $old_session_id = session_id();
  372. _drupal_session_regenerate_existing();
  373. }
  374. else {
  375. session_id(drupal_random_key());
  376. }
  377. if (isset($old_session_id)) {
  378. $params = session_get_cookie_params();
  379. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  380. $options = array(
  381. 'expires' => $expire,
  382. 'path' => $params['path'],
  383. 'domain' => $params['domain'],
  384. 'secure' => $params['secure'],
  385. 'httponly' => $params['httponly'],
  386. );
  387. drupal_setcookie(session_name(), session_id(), $options);
  388. $fields = array('sid' => drupal_session_id(session_id()));
  389. if ($is_https) {
  390. $fields['ssid'] = drupal_session_id(session_id());
  391. // If the "secure pages" setting is enabled, use the newly-created
  392. // insecure session identifier as the regenerated sid.
  393. if (variable_get('https', FALSE)) {
  394. $fields['sid'] = drupal_session_id($session_id);
  395. }
  396. }
  397. db_update('sessions')
  398. ->fields($fields)
  399. ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($old_session_id))
  400. ->execute();
  401. }
  402. elseif (isset($old_insecure_session_id)) {
  403. // If logging in to the secure site, and there was no active session on the
  404. // secure site but a session was active on the insecure site, update the
  405. // insecure session with the new session identifiers.
  406. db_update('sessions')
  407. ->fields(array('sid' => drupal_session_id($session_id), 'ssid' => drupal_session_id(session_id())))
  408. ->condition('sid', drupal_session_id($old_insecure_session_id))
  409. ->execute();
  410. }
  411. else {
  412. // Start the session when it doesn't exist yet.
  413. // Preserve the logged in user, as it will be reset to anonymous
  414. // by _drupal_session_read.
  415. $account = $user;
  416. drupal_session_start();
  417. $user = $account;
  418. }
  419. date_default_timezone_set(drupal_get_user_timezone());
  420. }
  421. /**
  422. * Regenerates an existing session.
  423. */
  424. function _drupal_session_regenerate_existing() {
  425. global $user;
  426. // Preserve existing settings for the saving of sessions.
  427. $original_save_session_status = drupal_save_session();
  428. // Turn off saving of sessions.
  429. drupal_save_session(FALSE);
  430. session_write_close();
  431. drupal_session_started(FALSE);
  432. // Preserve the user object, as starting a new session will reset it.
  433. $original_user = $user;
  434. session_id(drupal_random_key());
  435. drupal_session_start();
  436. $user = $original_user;
  437. // Restore the original settings for the saving of sessions.
  438. drupal_save_session($original_save_session_status);
  439. }
  440. /**
  441. * Session handler assigned by session_set_save_handler().
  442. *
  443. * Cleans up a specific session.
  444. *
  445. * @param $sid
  446. * Session ID.
  447. */
  448. function _drupal_session_destroy($sid) {
  449. global $user, $is_https;
  450. // Nothing to do if we are not allowed to change the session.
  451. if (!drupal_save_session()) {
  452. return TRUE;
  453. }
  454. // Delete session data.
  455. db_delete('sessions')
  456. ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($sid))
  457. ->execute();
  458. // Reset $_SESSION and $user to prevent a new session from being started
  459. // in drupal_session_commit().
  460. $_SESSION = array();
  461. $user = drupal_anonymous_user();
  462. // Unset the session cookies.
  463. _drupal_session_delete_cookie(session_name());
  464. if ($is_https) {
  465. _drupal_session_delete_cookie(substr(session_name(), 1), FALSE);
  466. }
  467. elseif (variable_get('https', FALSE)) {
  468. _drupal_session_delete_cookie('S' . session_name(), TRUE);
  469. }
  470. return TRUE;
  471. }
  472. /**
  473. * Deletes the session cookie.
  474. *
  475. * @param $name
  476. * Name of session cookie to delete.
  477. * @param boolean $secure
  478. * Force the secure value of the cookie.
  479. */
  480. function _drupal_session_delete_cookie($name, $secure = NULL) {
  481. global $is_https;
  482. if (isset($_COOKIE[$name]) || (!$is_https && $secure === TRUE)) {
  483. $params = session_get_cookie_params();
  484. if ($secure !== NULL) {
  485. $params['secure'] = $secure;
  486. }
  487. $options = array(
  488. 'expires' => REQUEST_TIME - 3600,
  489. 'path' => $params['path'],
  490. 'domain' => $params['domain'],
  491. 'secure' => $params['secure'],
  492. 'httponly' => $params['httponly'],
  493. );
  494. drupal_setcookie($name, '', $options);
  495. unset($_COOKIE[$name]);
  496. }
  497. }
  498. /**
  499. * Ends a specific user's session(s).
  500. *
  501. * @param $uid
  502. * User ID.
  503. */
  504. function drupal_session_destroy_uid($uid) {
  505. // Nothing to do if we are not allowed to change the session.
  506. if (!drupal_save_session()) {
  507. return;
  508. }
  509. db_delete('sessions')
  510. ->condition('uid', $uid)
  511. ->execute();
  512. }
  513. /**
  514. * Session handler assigned by session_set_save_handler().
  515. *
  516. * Cleans up stalled sessions.
  517. *
  518. * @param $lifetime
  519. * The value of session.gc_maxlifetime, passed by PHP.
  520. * Sessions not updated for more than $lifetime seconds will be removed.
  521. */
  522. function _drupal_session_garbage_collection($lifetime) {
  523. // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
  524. // value. For example, if you want user sessions to stay in your database
  525. // for three weeks before deleting them, you need to set gc_maxlifetime
  526. // to '1814400'. At that value, only after a user doesn't log in after
  527. // three weeks (1814400 seconds) will his/her session be removed.
  528. db_delete('sessions')
  529. ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
  530. ->execute();
  531. return TRUE;
  532. }
  533. /**
  534. * Determines whether to save session data of the current request.
  535. *
  536. * This function allows the caller to temporarily disable writing of
  537. * session data, should the request end while performing potentially
  538. * dangerous operations, such as manipulating the global $user object.
  539. * See http://drupal.org/node/218104 for usage.
  540. *
  541. * @param $status
  542. * Disables writing of session data when FALSE, (re-)enables
  543. * writing when TRUE.
  544. *
  545. * @return
  546. * FALSE if writing session data has been disabled. Otherwise, TRUE.
  547. */
  548. function drupal_save_session($status = NULL) {
  549. // PHP session ID, session, and cookie handling happens in the global scope.
  550. // This value has to persist across calls to drupal_static_reset(), since a
  551. // potentially wrong or disallowed session would be written otherwise.
  552. static $save_session = TRUE;
  553. if (isset($status)) {
  554. $save_session = $status;
  555. }
  556. return $save_session;
  557. }
  558. /**
  559. * Session ids are hashed by default before being stored in the database.
  560. *
  561. * This should only be done if any existing sessions have been updated, as
  562. * reflected by the hash_session_ids variable.
  563. *
  564. * @param $id
  565. * A session id.
  566. *
  567. * @return
  568. * The session id which may have been hashed.
  569. */
  570. function drupal_session_id($id) {
  571. if (variable_get('hashed_session_ids_supported', FALSE) && !variable_get('do_not_hash_session_ids', FALSE)) {
  572. $id = drupal_hash_base64($id);
  573. }
  574. return $id;
  575. }