session.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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 (!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.
  84. if ($is_https) {
  85. $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject();
  86. if (!$user) {
  87. if (isset($_COOKIE[$insecure_session_name])) {
  88. $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(
  89. ':sid' => $_COOKIE[$insecure_session_name]))
  90. ->fetchObject();
  91. }
  92. }
  93. }
  94. else {
  95. $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject();
  96. }
  97. // We found the client's session record and they are an authenticated,
  98. // active user.
  99. if ($user && $user->uid > 0 && $user->status == 1) {
  100. // This is done to unserialize the data member of $user.
  101. $user->data = unserialize($user->data);
  102. // Add roles element to $user.
  103. $user->roles = array();
  104. $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
  105. $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);
  106. }
  107. elseif ($user) {
  108. // The user is anonymous or blocked. Only preserve two fields from the
  109. // {sessions} table.
  110. $account = drupal_anonymous_user();
  111. $account->session = $user->session;
  112. $account->timestamp = $user->timestamp;
  113. $user = $account;
  114. }
  115. else {
  116. // The session has expired.
  117. $user = drupal_anonymous_user();
  118. $user->session = '';
  119. }
  120. // Store the session that was read for comparison in _drupal_session_write().
  121. $last_read = &drupal_static('drupal_session_last_read');
  122. $last_read = array(
  123. 'sid' => $sid,
  124. 'value' => $user->session,
  125. );
  126. return $user->session;
  127. }
  128. /**
  129. * Writes an entire session to the database (internal use only).
  130. *
  131. * This function is registered with session_set_save_handler() to support
  132. * database-backed sessions.
  133. *
  134. * This function is an internal function and must not be called directly.
  135. * Doing so may result in corrupted session data or other unexpected behavior.
  136. * Session data must always be accessed via the $_SESSION superglobal.
  137. *
  138. * @param $sid
  139. * The session ID of the session to write to.
  140. * @param $value
  141. * Session data to write as a serialized string.
  142. *
  143. * @return
  144. * Always returns TRUE.
  145. */
  146. function _drupal_session_write($sid, $value) {
  147. global $user, $is_https;
  148. // The exception handler is not active at this point, so we need to do it
  149. // manually.
  150. try {
  151. if (!drupal_save_session()) {
  152. // We don't have anything to do if we are not allowed to save the session.
  153. return;
  154. }
  155. // Check whether $_SESSION has been changed in this request.
  156. $last_read = &drupal_static('drupal_session_last_read');
  157. $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
  158. // For performance reasons, do not update the sessions table, unless
  159. // $_SESSION has changed or more than 180 has passed since the last update.
  160. if ($is_changed || !isset($user->timestamp) || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
  161. // Either ssid or sid or both will be added from $key below.
  162. $fields = array(
  163. 'uid' => $user->uid,
  164. 'cache' => isset($user->cache) ? $user->cache : 0,
  165. 'hostname' => ip_address(),
  166. 'session' => $value,
  167. 'timestamp' => REQUEST_TIME,
  168. );
  169. // Use the session ID as 'sid' and an empty string as 'ssid' by default.
  170. // _drupal_session_read() does not allow empty strings so that's a safe
  171. // default.
  172. $key = array('sid' => $sid, 'ssid' => '');
  173. // On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
  174. if ($is_https) {
  175. $key['ssid'] = $sid;
  176. // The "secure pages" setting allows a site to simultaneously use both
  177. // secure and insecure session cookies. If enabled and both cookies are
  178. // presented then use both keys.
  179. if (variable_get('https', FALSE)) {
  180. $insecure_session_name = substr(session_name(), 1);
  181. if (isset($_COOKIE[$insecure_session_name])) {
  182. $key['sid'] = $_COOKIE[$insecure_session_name];
  183. }
  184. }
  185. }
  186. elseif (variable_get('https', FALSE)) {
  187. unset($key['ssid']);
  188. }
  189. db_merge('sessions')
  190. ->key($key)
  191. ->fields($fields)
  192. ->execute();
  193. }
  194. // Likewise, do not update access time more than once per 180 seconds.
  195. if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
  196. db_update('users')
  197. ->fields(array(
  198. 'access' => REQUEST_TIME
  199. ))
  200. ->condition('uid', $user->uid)
  201. ->execute();
  202. }
  203. return TRUE;
  204. }
  205. catch (Exception $exception) {
  206. require_once DRUPAL_ROOT . '/includes/errors.inc';
  207. // If we are displaying errors, then do so with no possibility of a further
  208. // uncaught exception being thrown.
  209. if (error_displayable()) {
  210. print '<h1>Uncaught exception thrown in session handler.</h1>';
  211. print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
  212. }
  213. return FALSE;
  214. }
  215. }
  216. /**
  217. * Initializes the session handler, starting a session if needed.
  218. */
  219. function drupal_session_initialize() {
  220. global $user, $is_https;
  221. session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
  222. // We use !empty() in the following check to ensure that blank session IDs
  223. // are not valid.
  224. if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) {
  225. // If a session cookie exists, initialize the session. Otherwise the
  226. // session is only started on demand in drupal_session_commit(), making
  227. // anonymous users not use a session cookie unless something is stored in
  228. // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
  229. drupal_session_start();
  230. if (!empty($user->uid) || !empty($_SESSION)) {
  231. drupal_page_is_cacheable(FALSE);
  232. }
  233. }
  234. else {
  235. // Set a session identifier for this request. This is necessary because
  236. // we lazily start sessions at the end of this request, and some
  237. // processes (like drupal_get_token()) needs to know the future
  238. // session ID in advance.
  239. $GLOBALS['lazy_session'] = TRUE;
  240. $user = drupal_anonymous_user();
  241. // Less random sessions (which are much faster to generate) are used for
  242. // anonymous users than are generated in drupal_session_regenerate() when
  243. // a user becomes authenticated.
  244. session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE)));
  245. if ($is_https && variable_get('https', FALSE)) {
  246. $insecure_session_name = substr(session_name(), 1);
  247. $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE));
  248. $_COOKIE[$insecure_session_name] = $session_id;
  249. }
  250. }
  251. date_default_timezone_set(drupal_get_user_timezone());
  252. }
  253. /**
  254. * Starts a session forcefully, preserving already set session data.
  255. *
  256. * @ingroup php_wrappers
  257. */
  258. function drupal_session_start() {
  259. // Command line clients do not support cookies nor sessions.
  260. if (!drupal_session_started() && !drupal_is_cli()) {
  261. // Save current session data before starting it, as PHP will destroy it.
  262. $session_data = isset($_SESSION) ? $_SESSION : NULL;
  263. session_start();
  264. drupal_session_started(TRUE);
  265. // Restore session data.
  266. if (!empty($session_data)) {
  267. $_SESSION += $session_data;
  268. }
  269. }
  270. }
  271. /**
  272. * Commits the current session, if necessary.
  273. *
  274. * If an anonymous user already have an empty session, destroy it.
  275. */
  276. function drupal_session_commit() {
  277. global $user, $is_https;
  278. if (!drupal_save_session()) {
  279. // We don't have anything to do if we are not allowed to save the session.
  280. return;
  281. }
  282. if (empty($user->uid) && empty($_SESSION)) {
  283. // There is no session data to store, destroy the session if it was
  284. // previously started.
  285. if (drupal_session_started()) {
  286. session_destroy();
  287. }
  288. }
  289. else {
  290. // There is session data to store. Start the session if it is not already
  291. // started.
  292. if (!drupal_session_started()) {
  293. drupal_session_start();
  294. if ($is_https && variable_get('https', FALSE)) {
  295. $insecure_session_name = substr(session_name(), 1);
  296. $params = session_get_cookie_params();
  297. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  298. setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
  299. }
  300. }
  301. // Write the session data.
  302. session_write_close();
  303. }
  304. }
  305. /**
  306. * Returns whether a session has been started.
  307. */
  308. function drupal_session_started($set = NULL) {
  309. static $session_started = FALSE;
  310. if (isset($set)) {
  311. $session_started = $set;
  312. }
  313. return $session_started && session_id();
  314. }
  315. /**
  316. * Called when an anonymous user becomes authenticated or vice-versa.
  317. *
  318. * @ingroup php_wrappers
  319. */
  320. function drupal_session_regenerate() {
  321. global $user, $is_https;
  322. // Nothing to do if we are not allowed to change the session.
  323. if (!drupal_save_session()) {
  324. return;
  325. }
  326. if ($is_https && variable_get('https', FALSE)) {
  327. $insecure_session_name = substr(session_name(), 1);
  328. if (!isset($GLOBALS['lazy_session']) && isset($_COOKIE[$insecure_session_name])) {
  329. $old_insecure_session_id = $_COOKIE[$insecure_session_name];
  330. }
  331. $params = session_get_cookie_params();
  332. $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55));
  333. // If a session cookie lifetime is set, the session will expire
  334. // $params['lifetime'] seconds from the current request. If it is not set,
  335. // it will expire when the browser is closed.
  336. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  337. setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
  338. $_COOKIE[$insecure_session_name] = $session_id;
  339. }
  340. if (drupal_session_started()) {
  341. $old_session_id = session_id();
  342. }
  343. session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)));
  344. if (isset($old_session_id)) {
  345. $params = session_get_cookie_params();
  346. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  347. setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  348. $fields = array('sid' => session_id());
  349. if ($is_https) {
  350. $fields['ssid'] = session_id();
  351. // If the "secure pages" setting is enabled, use the newly-created
  352. // insecure session identifier as the regenerated sid.
  353. if (variable_get('https', FALSE)) {
  354. $fields['sid'] = $session_id;
  355. }
  356. }
  357. db_update('sessions')
  358. ->fields($fields)
  359. ->condition($is_https ? 'ssid' : 'sid', $old_session_id)
  360. ->execute();
  361. }
  362. elseif (isset($old_insecure_session_id)) {
  363. // If logging in to the secure site, and there was no active session on the
  364. // secure site but a session was active on the insecure site, update the
  365. // insecure session with the new session identifiers.
  366. db_update('sessions')
  367. ->fields(array('sid' => $session_id, 'ssid' => session_id()))
  368. ->condition('sid', $old_insecure_session_id)
  369. ->execute();
  370. }
  371. else {
  372. // Start the session when it doesn't exist yet.
  373. // Preserve the logged in user, as it will be reset to anonymous
  374. // by _drupal_session_read.
  375. $account = $user;
  376. drupal_session_start();
  377. $user = $account;
  378. }
  379. date_default_timezone_set(drupal_get_user_timezone());
  380. }
  381. /**
  382. * Session handler assigned by session_set_save_handler().
  383. *
  384. * Cleans up a specific session.
  385. *
  386. * @param $sid
  387. * Session ID.
  388. */
  389. function _drupal_session_destroy($sid) {
  390. global $user, $is_https;
  391. // Nothing to do if we are not allowed to change the session.
  392. if (!drupal_save_session()) {
  393. return;
  394. }
  395. // Delete session data.
  396. db_delete('sessions')
  397. ->condition($is_https ? 'ssid' : 'sid', $sid)
  398. ->execute();
  399. // Reset $_SESSION and $user to prevent a new session from being started
  400. // in drupal_session_commit().
  401. $_SESSION = array();
  402. $user = drupal_anonymous_user();
  403. // Unset the session cookies.
  404. _drupal_session_delete_cookie(session_name());
  405. if ($is_https) {
  406. _drupal_session_delete_cookie(substr(session_name(), 1), FALSE);
  407. }
  408. elseif (variable_get('https', FALSE)) {
  409. _drupal_session_delete_cookie('S' . session_name(), TRUE);
  410. }
  411. }
  412. /**
  413. * Deletes the session cookie.
  414. *
  415. * @param $name
  416. * Name of session cookie to delete.
  417. * @param boolean $secure
  418. * Force the secure value of the cookie.
  419. */
  420. function _drupal_session_delete_cookie($name, $secure = NULL) {
  421. global $is_https;
  422. if (isset($_COOKIE[$name]) || (!$is_https && $secure === TRUE)) {
  423. $params = session_get_cookie_params();
  424. if ($secure !== NULL) {
  425. $params['secure'] = $secure;
  426. }
  427. setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  428. unset($_COOKIE[$name]);
  429. }
  430. }
  431. /**
  432. * Ends a specific user's session(s).
  433. *
  434. * @param $uid
  435. * User ID.
  436. */
  437. function drupal_session_destroy_uid($uid) {
  438. // Nothing to do if we are not allowed to change the session.
  439. if (!drupal_save_session()) {
  440. return;
  441. }
  442. db_delete('sessions')
  443. ->condition('uid', $uid)
  444. ->execute();
  445. }
  446. /**
  447. * Session handler assigned by session_set_save_handler().
  448. *
  449. * Cleans up stalled sessions.
  450. *
  451. * @param $lifetime
  452. * The value of session.gc_maxlifetime, passed by PHP.
  453. * Sessions not updated for more than $lifetime seconds will be removed.
  454. */
  455. function _drupal_session_garbage_collection($lifetime) {
  456. // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
  457. // value. For example, if you want user sessions to stay in your database
  458. // for three weeks before deleting them, you need to set gc_maxlifetime
  459. // to '1814400'. At that value, only after a user doesn't log in after
  460. // three weeks (1814400 seconds) will his/her session be removed.
  461. db_delete('sessions')
  462. ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
  463. ->execute();
  464. return TRUE;
  465. }
  466. /**
  467. * Determines whether to save session data of the current request.
  468. *
  469. * This function allows the caller to temporarily disable writing of
  470. * session data, should the request end while performing potentially
  471. * dangerous operations, such as manipulating the global $user object.
  472. * See http://drupal.org/node/218104 for usage.
  473. *
  474. * @param $status
  475. * Disables writing of session data when FALSE, (re-)enables
  476. * writing when TRUE.
  477. *
  478. * @return
  479. * FALSE if writing session data has been disabled. Otherwise, TRUE.
  480. */
  481. function drupal_save_session($status = NULL) {
  482. // PHP session ID, session, and cookie handling happens in the global scope.
  483. // This value has to persist across calls to drupal_static_reset(), since a
  484. // potentially wrong or disallowed session would be written otherwise.
  485. static $save_session = TRUE;
  486. if (isset($status)) {
  487. $save_session = $status;
  488. }
  489. return $save_session;
  490. }